abstracted navigation logic
This commit is contained in:
@@ -15,6 +15,32 @@ import Profile from './components/Profile';
|
|||||||
import { useArticles } from './providers/Article';
|
import { useArticles } from './providers/Article';
|
||||||
import { useAuth } from './providers/Author';
|
import { useAuth } from './providers/Author';
|
||||||
import { View, useViewRouter } from "./types/views";
|
import { View, useViewRouter } from "./types/views";
|
||||||
|
import { ArticleModel } from "./types/models";
|
||||||
|
import { ArticleViewProps, ArticleEditorProps } from "./types/props";
|
||||||
|
|
||||||
|
function HomeView({ currentUser, open_login, open_profile, open_create, articles, openArticle }: any) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "flex-end", mb: 2, gap: 1 }}>
|
||||||
|
{!currentUser ? (
|
||||||
|
<Button variant="outlined" onClick={open_login}>Login</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button variant="outlined" onClick={open_profile}>
|
||||||
|
{currentUser.username}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={open_create}>
|
||||||
|
New Article
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<MainContent articles={articles} onSelectArticle={openArticle} />
|
||||||
|
<Latest articles={articles} onSelectArticle={openArticle} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Blog(props: { disableCustomTheme?: boolean }) {
|
export default function Blog(props: { disableCustomTheme?: boolean }) {
|
||||||
const { articles, loading, error } = useArticles();
|
const { articles, loading, error } = useArticles();
|
||||||
@@ -27,72 +53,86 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
goBack,
|
goBack,
|
||||||
openLogin,
|
navigateToChildren,
|
||||||
openRegister,
|
|
||||||
openProfile,
|
|
||||||
openCreate,
|
|
||||||
openEditor,
|
|
||||||
openArticle,
|
openArticle,
|
||||||
} = useViewRouter(setUI);
|
} = useViewRouter(setUI);
|
||||||
|
|
||||||
|
type RouterContext = {
|
||||||
|
ui: any;
|
||||||
|
articles: ArticleModel[];
|
||||||
|
currentUser: any;
|
||||||
|
openArticle: (index: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ViewComponentEntry<P> = {
|
||||||
|
component: React.ComponentType<P>;
|
||||||
|
extraProps?: (ctx: RouterContext) => Partial<P>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VIEW_COMPONENTS: Record<View, ViewComponentEntry<any>> = {
|
||||||
|
home: {
|
||||||
|
component: HomeView,
|
||||||
|
},
|
||||||
|
|
||||||
|
login: {
|
||||||
|
component: Login,
|
||||||
|
},
|
||||||
|
|
||||||
|
register: {
|
||||||
|
component: Register,
|
||||||
|
},
|
||||||
|
|
||||||
|
profile: {
|
||||||
|
component: Profile,
|
||||||
|
},
|
||||||
|
|
||||||
|
article: {
|
||||||
|
component: ArticleView,
|
||||||
|
extraProps: ({ ui, articles }) => ({
|
||||||
|
article: articles[ui.selectedArticle!],
|
||||||
|
}) satisfies Partial<ArticleViewProps>,
|
||||||
|
},
|
||||||
|
|
||||||
|
editor: {
|
||||||
|
component: ArticleEditor,
|
||||||
|
extraProps: ({ ui, articles }) => ({
|
||||||
|
article: ui.selectedArticle !== null ? articles[ui.selectedArticle] : null,
|
||||||
|
}) satisfies Partial<ArticleEditorProps>,
|
||||||
|
},
|
||||||
|
|
||||||
|
create: {
|
||||||
|
component: ArticleEditor,
|
||||||
|
extraProps: () => ({
|
||||||
|
article: null,
|
||||||
|
}) satisfies Partial<ArticleEditorProps>,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const renderView = () => {
|
const renderView = () => {
|
||||||
switch (ui.view) {
|
const entry = VIEW_COMPONENTS[ui.view];
|
||||||
case "login":
|
const ViewComponent = entry.component;
|
||||||
return <Login onBack={() => goBack(ui.view)} onRegister={openRegister} />;
|
|
||||||
|
|
||||||
case "register":
|
const childNav = navigateToChildren(ui.view);
|
||||||
return <Register onBack={() => goBack(ui.view)} />;
|
|
||||||
|
|
||||||
case "profile":
|
const ctx: RouterContext = {
|
||||||
return <Profile onBack={() => goBack(ui.view)} />;
|
ui,
|
||||||
|
articles,
|
||||||
|
currentUser,
|
||||||
|
|
||||||
case "article":
|
openArticle,
|
||||||
return (
|
};
|
||||||
<ArticleView
|
|
||||||
article={articles[ui.selectedArticle!]}
|
|
||||||
onBack={() => goBack(ui.view)}
|
|
||||||
onEdit={openEditor}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "editor":
|
const extraProps = entry.extraProps ? entry.extraProps(ctx) : {};
|
||||||
return (
|
|
||||||
<ArticleEditor
|
|
||||||
article={ui.selectedArticle !== null ? articles[ui.selectedArticle] : null}
|
|
||||||
onBack={() => goBack(ui.view)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "create":
|
return (
|
||||||
return (
|
<ViewComponent
|
||||||
<ArticleEditor
|
{...ctx}
|
||||||
onBack={() => goBack(ui.view)}
|
{...childNav}
|
||||||
/>
|
onBack={() => goBack(ui.view)}
|
||||||
);
|
{...extraProps}
|
||||||
|
/>
|
||||||
default:
|
);
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mb: 2, gap: 1 }}>
|
|
||||||
{!currentUser ? (
|
|
||||||
<Button variant="outlined" onClick={openLogin}>Login</Button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Button variant="outlined" onClick={openProfile}>
|
|
||||||
{currentUser.username}
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" onClick={openCreate}>
|
|
||||||
New Article
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<MainContent articles={articles} onSelectArticle={openArticle} />
|
|
||||||
<Latest articles={articles} onSelectArticle={openArticle} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { styled } from '@mui/material/styles';
|
|||||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||||
import { ArticleMeta } from "../ArticleMeta";
|
import { ArticleMeta } from "../ArticleMeta";
|
||||||
import { ArticleProps } from '../../types/props';
|
import { ArticleViewProps } from '../../types/props';
|
||||||
import {useAuth} from "../../providers/Author";
|
import {useAuth} from "../../providers/Author";
|
||||||
|
|
||||||
const ArticleContainer = styled(Box)(({ theme }) => ({
|
const ArticleContainer = styled(Box)(({ theme }) => ({
|
||||||
@@ -28,10 +28,12 @@ const CoverImage = styled('img')({
|
|||||||
export default function ArticleView({
|
export default function ArticleView({
|
||||||
article,
|
article,
|
||||||
onBack,
|
onBack,
|
||||||
onEdit,
|
open_editor,
|
||||||
}: ArticleProps) {
|
}: ArticleViewProps) {
|
||||||
|
|
||||||
const { currentUser } = useAuth();
|
const { currentUser } = useAuth();
|
||||||
|
const onEdit = open_editor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArticleContainer>
|
<ArticleContainer>
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ export interface LatestProps {
|
|||||||
onLoadMore?: (offset: number, limit: number) => Promise<void>; // optional async callback
|
onLoadMore?: (offset: number, limit: number) => Promise<void>; // optional async callback
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArticleProps {
|
export interface ArticleViewProps {
|
||||||
article: ArticleModel;
|
article: ArticleModel;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onEdit: () => void;
|
open_editor?: () => void; // optional because home → article must still work
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ArticleEditorProps {
|
export interface ArticleEditorProps {
|
||||||
article?: ArticleModel | null;
|
article?: ArticleModel | null;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// types.ts
|
|
||||||
export type View =
|
export type View =
|
||||||
| "home"
|
| "home"
|
||||||
| "login"
|
| "login"
|
||||||
@@ -40,17 +39,16 @@ export const VIEW_TREE: Record<View, ViewNode> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useViewRouter(setUI: React.Dispatch<any>) {
|
export function useViewRouter(setUI: any) {
|
||||||
const navigate = (view: View) => {
|
const navigate = (view: View) => {
|
||||||
setUI((prev: any) => ({ ...prev, view }));
|
setUI((prev: any) => ({ ...prev, view }));
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const goBack = (currentView: View) => {
|
// auto back logic from parent
|
||||||
const parent = VIEW_TREE[currentView].parent;
|
const goBack = (view: View) => {
|
||||||
if (parent) {
|
const parent = VIEW_TREE[view].parent;
|
||||||
setUI((prev: any) => ({ ...prev, view: parent }));
|
if (parent) navigate(parent);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openArticle = (i: number) => {
|
const openArticle = (i: number) => {
|
||||||
@@ -58,14 +56,17 @@ export function useViewRouter(setUI: React.Dispatch<any>) {
|
|||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
// auto child navigators from children[]
|
||||||
navigate,
|
const navigateToChildren = (view: View) => {
|
||||||
goBack,
|
const node = VIEW_TREE[view];
|
||||||
openLogin: () => navigate("login"),
|
const funcs: Record<string, () => void> = {};
|
||||||
openRegister: () => navigate("register"),
|
|
||||||
openProfile: () => navigate("profile"),
|
node.children?.forEach((child) => {
|
||||||
openCreate: () => navigate("create"),
|
funcs[`open_${child}`] = () => navigate(child);
|
||||||
openEditor: () => navigate("editor"),
|
});
|
||||||
openArticle,
|
|
||||||
|
return funcs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return { navigate, goBack, openArticle, navigateToChildren };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user