418 lines
14 KiB
TypeScript
418 lines
14 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useNavigate, useLocation, useParams } from 'react-router-dom';
|
|
import {
|
|
Box,
|
|
Drawer,
|
|
AppBar,
|
|
Toolbar,
|
|
IconButton,
|
|
Typography,
|
|
List,
|
|
ListItem,
|
|
ListItemButton,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
Paper,
|
|
Grid,
|
|
Card,
|
|
CardContent,
|
|
CardActionArea,
|
|
useTheme,
|
|
useMediaQuery
|
|
} from '@mui/material';
|
|
import MenuIcon from '@mui/icons-material/Menu';
|
|
import PersonIcon from '@mui/icons-material/Person';
|
|
import CloseIcon from '@mui/icons-material/Close';
|
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
import DescriptionIcon from '@mui/icons-material/Description';
|
|
import CodeIcon from '@mui/icons-material/Code';
|
|
import LayersIcon from '@mui/icons-material/Layers';
|
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
|
import PaletteIcon from '@mui/icons-material/Palette';
|
|
import AnalyticsIcon from '@mui/icons-material/Analytics';
|
|
import ViewQuiltIcon from '@mui/icons-material/ViewQuilt';
|
|
|
|
import { Document } from '../components/Document';
|
|
import { BackstoryPageProps } from '../components/BackstoryTab';
|
|
import { BackstoryUIOverviewPage } from 'documents/BackstoryUIOverviewPage';
|
|
import { BackstoryAppAnalysisPage } from 'documents/BackstoryAppAnalysisPage';
|
|
import { BackstoryThemeVisualizerPage } from 'documents/BackstoryThemeVisualizerPage';
|
|
import { UserManagement } from 'documents/UserManagement';
|
|
import { MockupPage } from 'documents/MockupPage';
|
|
|
|
// Sidebar navigation component using MUI components
|
|
const Sidebar: React.FC<{
|
|
currentPage: string;
|
|
onDocumentSelect: (docName: string, open: boolean) => void;
|
|
onClose?: () => void;
|
|
isMobile: boolean;
|
|
}> = ({ currentPage, onDocumentSelect, onClose, isMobile }) => {
|
|
const navigate = useNavigate();
|
|
|
|
// Document definitions
|
|
|
|
const handleItemClick = (route: string) => {
|
|
onDocumentSelect(route, true);
|
|
if (isMobile && onClose) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
<Box sx={{
|
|
p: 2,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
borderBottom: 1,
|
|
borderColor: 'divider',
|
|
}}>
|
|
<Typography variant="h6" component="h2" fontWeight="bold">
|
|
Documentation
|
|
</Typography>
|
|
{isMobile && onClose && (
|
|
<IconButton
|
|
onClick={onClose}
|
|
size="small"
|
|
aria-label="Close navigation"
|
|
>
|
|
<CloseIcon />
|
|
</IconButton>
|
|
)}
|
|
</Box>
|
|
|
|
<Box sx={{
|
|
flexGrow: 1,
|
|
overflow: 'auto',
|
|
p: 1
|
|
}}>
|
|
<List>
|
|
{documents.map((doc, index) => (
|
|
<ListItem key={index} disablePadding>
|
|
<ListItemButton
|
|
onClick={() => doc.route ? handleItemClick(doc.route) : navigate('/')}
|
|
selected={currentPage === doc.route}
|
|
sx={{
|
|
borderRadius: 1,
|
|
mb: 0.5
|
|
}}
|
|
>
|
|
<ListItemIcon sx={{
|
|
color: currentPage === doc.route ? 'primary.main' : 'text.secondary',
|
|
minWidth: 40
|
|
}}>
|
|
{getDocumentIcon(doc.title)}
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary={doc.title}
|
|
slotProps={{
|
|
primary: {
|
|
fontWeight: currentPage === doc.route ? 'medium' : 'regular',
|
|
}
|
|
}}
|
|
/>
|
|
</ListItemButton>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const getDocumentIcon = (title: string): React.ReactNode => {
|
|
const item = documents.find(d => d.title.toLocaleLowerCase() === title.toLocaleLowerCase());
|
|
if (!item) {
|
|
throw Error(`${title} does not exist in documents`);
|
|
}
|
|
return item.icon || <ViewQuiltIcon />;
|
|
}
|
|
|
|
type DocType = {
|
|
title: string;
|
|
route: string | null;
|
|
description: string;
|
|
icon?: React.ReactNode;
|
|
};
|
|
|
|
const documents : DocType[] = [
|
|
{ title: "Backstory", route: null, description: "Backstory", icon: <ArrowBackIcon /> },
|
|
{ title: "About", route: "about", description: "General information about the application and its purpose", icon: <DescriptionIcon /> },
|
|
{ title: "BETA", route: "beta", description: "Details about the current beta version and upcoming features", icon: <CodeIcon /> },
|
|
{ title: "Resume Generation Architecture", route: "resume-generation", description: "Technical overview of how resumes are processed and generated", icon: <LayersIcon /> },
|
|
{ title: "Application Architecture", route: "about-app", description: "System design and technical stack information", icon: <LayersIcon /> },
|
|
{ title: "Authentication Architecture", route: "authentication.md", description: "Complete authentication architecture", icon: <LayersIcon /> },
|
|
{ title: "UI Overview", route: "ui-overview", description: "Guide to the user interface components and interactions", icon: <DashboardIcon /> },
|
|
{ title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts", icon: <DashboardIcon /> },
|
|
{ title: "Chat Mockup", route: "mockup-chat-system", description: "Mockup of chat system", icon: <DashboardIcon /> },
|
|
{ title: "Theme Visualizer", route: "theme-visualizer", description: "Explore and customize application themes and visual styles", icon: <PaletteIcon /> },
|
|
{ title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application", icon: <AnalyticsIcon /> },
|
|
{ title: 'Text Mockups', route: "backstory-ui-mockups", description: "Early text mockups of many of the interaction points." },
|
|
{ title: 'User Management', route: "user-management", description: "User management.", icon: <PersonIcon /> },
|
|
{ title: 'Type Safety', route: "type-safety", description: "Overview of front/back-end type synchronization.", icon: <CodeIcon /> },
|
|
];
|
|
|
|
const documentFromRoute = (route: string) : DocType | null => {
|
|
const index = documents.findIndex(v => v.route === route);
|
|
if (index === -1) {
|
|
return null
|
|
}
|
|
return documents[index];
|
|
};
|
|
|
|
// Helper function to get document title from route
|
|
const documentTitleFromRoute = (route: string): string => {
|
|
const doc = documentFromRoute(route);
|
|
if (doc === null) {
|
|
return 'Documentation'
|
|
}
|
|
return doc.title;
|
|
}
|
|
|
|
const DocsPage = (props: BackstoryPageProps) => {
|
|
const { submitQuery, setSnack } = props;
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { paramPage = '' } = useParams();
|
|
const [page, setPage] = useState<string>(paramPage);
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
|
|
const theme = useTheme();
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
|
|
// Track location changes
|
|
useEffect(() => {
|
|
const parts = location.pathname.split('/');
|
|
if (parts.length > 2) {
|
|
setPage(parts[2]);
|
|
} else {
|
|
setPage('');
|
|
}
|
|
}, [location]);
|
|
|
|
// Close drawer when changing to desktop view
|
|
useEffect(() => {
|
|
if (!isMobile) {
|
|
setDrawerOpen(false);
|
|
}
|
|
}, [isMobile]);
|
|
|
|
// Handle document navigation
|
|
const onDocumentExpand = (docName: string, open: boolean) => {
|
|
console.log("Document expanded:", { docName, open, location });
|
|
if (open) {
|
|
const parts = location.pathname.split('/');
|
|
if (docName === "backstory") {
|
|
navigate('/');
|
|
return;
|
|
}
|
|
if (parts.length > 2) {
|
|
const basePath = parts.slice(0, -1).join('/');
|
|
navigate(`${basePath}/${docName}`);
|
|
} else {
|
|
navigate(docName);
|
|
}
|
|
} else {
|
|
const basePath = location.pathname.split('/').slice(0, -1).join('/');
|
|
navigate(`${basePath}`);
|
|
}
|
|
};
|
|
|
|
// Toggle mobile drawer
|
|
const toggleDrawer = () => {
|
|
setDrawerOpen(!drawerOpen);
|
|
};
|
|
|
|
// Close the drawer
|
|
const closeDrawer = () => {
|
|
setDrawerOpen(false);
|
|
};
|
|
|
|
interface DocViewProps {
|
|
page: string
|
|
};
|
|
const DocView = (props: DocViewProps) => {
|
|
const { page = 'about' } = props;
|
|
const title = documentTitleFromRoute(page);
|
|
const icon = getDocumentIcon(title);
|
|
|
|
return (
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ color: 'inherit', fontSize: "1.75rem", fontWeight: "bold", display: "flex", flexDirection: "row", gap: 1, alignItems: "center", mr: 1.5 }}>
|
|
{icon}
|
|
{title}
|
|
</Box>
|
|
{page && <Document
|
|
filepath={`/docs/${page}.md`}
|
|
submitQuery={submitQuery}
|
|
setSnack={setSnack}
|
|
/>}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
// Render the appropriate content based on current page
|
|
function renderContent() {
|
|
switch (page) {
|
|
case 'ui-overview':
|
|
return (<BackstoryUIOverviewPage />);
|
|
case 'theme-visualizer':
|
|
return (<Paper sx={{ m: 0, p: 1 }}><BackstoryThemeVisualizerPage /></Paper>);
|
|
case 'app-analysis':
|
|
return (<BackstoryAppAnalysisPage />);
|
|
case 'ui-mockup':
|
|
return (<MockupPage />);
|
|
case 'user-management':
|
|
return (<UserManagement />);
|
|
default:
|
|
if (documentFromRoute(page)) {
|
|
return <DocView page={page}/>
|
|
}
|
|
// Document grid for landing page
|
|
return (
|
|
<Paper sx={{ p: 1 }} elevation={0}>
|
|
<Box sx={{ mb: 2 }}>
|
|
<Typography variant="h4" component="h1" gutterBottom>
|
|
Documentation
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary">
|
|
Select a document from the sidebar to view detailed technical information about the application.
|
|
</Typography>
|
|
</Box>
|
|
<Grid container spacing={1}>
|
|
{documents.map((doc, index) => {
|
|
if (doc.route === null) return (<></>);
|
|
return (<Grid sx={{ minWidth: "164px" }} size={{ xs: 12, sm: 6, md: 4 }} key={index}>
|
|
<Card sx={{ minHeight: "180px" }}>
|
|
<CardActionArea onClick={() => doc.route ? onDocumentExpand(doc.route, true) : navigate('/')}>
|
|
<CardContent sx={{ display: "flex", flexDirection: "column", m: 0, p: 1 }}>
|
|
<Box sx={{ display: 'flex', flexDirection: "row", gap: 1, verticalAlign: 'top' }}>
|
|
{getDocumentIcon(doc.title)}
|
|
<Typography variant="h3" sx={{ m: "0 !important" }}>{doc.title}</Typography>
|
|
</Box>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{doc.description}
|
|
</Typography>
|
|
</CardContent>
|
|
</CardActionArea>
|
|
</Card>
|
|
</Grid>
|
|
)
|
|
})}
|
|
</Grid>
|
|
</Paper>
|
|
);
|
|
}
|
|
}
|
|
|
|
// Calculate drawer width
|
|
const drawerWidth = 240;
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex', height: '100%' }}>
|
|
{/* Mobile App Bar */}
|
|
{isMobile && (
|
|
<AppBar
|
|
position="fixed"
|
|
sx={{
|
|
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
|
ml: { sm: `${drawerWidth}px` },
|
|
display: { md: 'none' }
|
|
}}
|
|
elevation={0}
|
|
color="default"
|
|
>
|
|
<Toolbar>
|
|
<IconButton
|
|
aria-label="open drawer"
|
|
edge="start"
|
|
onClick={toggleDrawer}
|
|
sx={{ mr: 2 }}
|
|
>
|
|
<MenuIcon />
|
|
</IconButton>
|
|
<Typography variant="h6" noWrap component="div" sx={{ color: "white" }}>
|
|
{page ? documentTitleFromRoute(page) : "Documentation"}
|
|
</Typography>
|
|
</Toolbar>
|
|
</AppBar>
|
|
)}
|
|
|
|
{/* Navigation drawer */}
|
|
<Box
|
|
component="nav"
|
|
sx={{
|
|
width: { md: drawerWidth },
|
|
flexShrink: { md: 0 }
|
|
}}
|
|
>
|
|
{/* Mobile drawer (temporary) */}
|
|
{isMobile ? (
|
|
<Drawer
|
|
variant="temporary"
|
|
open={drawerOpen}
|
|
onClose={closeDrawer}
|
|
ModalProps={{
|
|
keepMounted: true, // Better open performance on mobile
|
|
}}
|
|
sx={{
|
|
display: { xs: 'block', md: 'none' },
|
|
'& .MuiDrawer-paper': {
|
|
boxSizing: 'border-box',
|
|
width: drawerWidth
|
|
},
|
|
}}
|
|
>
|
|
<Sidebar
|
|
currentPage={page}
|
|
onDocumentSelect={onDocumentExpand}
|
|
onClose={closeDrawer}
|
|
isMobile={true}
|
|
/>
|
|
</Drawer>
|
|
) : (
|
|
// Desktop drawer (permanent)
|
|
<Drawer
|
|
variant="permanent"
|
|
sx={{
|
|
display: { xs: 'none', md: 'block' },
|
|
'& .MuiDrawer-paper': {
|
|
boxSizing: 'border-box',
|
|
width: drawerWidth,
|
|
position: 'relative',
|
|
height: '100%'
|
|
},
|
|
}}
|
|
open
|
|
>
|
|
<Sidebar
|
|
currentPage={page}
|
|
onDocumentSelect={onDocumentExpand}
|
|
isMobile={false}
|
|
/>
|
|
</Drawer>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Main content */}
|
|
<Box
|
|
component="main"
|
|
sx={{
|
|
flexGrow: 1,
|
|
p: 3,
|
|
width: { md: `calc(100% - ${drawerWidth}px)` },
|
|
pt: isMobile ? { xs: 8, sm: 9 } : 3, // Add padding top on mobile to account for AppBar
|
|
height: '100%',
|
|
overflow: 'auto'
|
|
}}
|
|
>
|
|
{renderContent()}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export { DocsPage }; |