abstracted navigation logic

This commit is contained in:
2025-11-18 16:53:36 +05:30
parent f52c4a5287
commit 459fa5855c
4 changed files with 122 additions and 78 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;

View File

@@ -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 };
} }