Skip to content
38 changes: 38 additions & 0 deletions components/User/ResponsiveDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Drawer, useMediaQuery, useTheme } from '@mui/material';
import { FC, PropsWithChildren } from 'react';

const DESKTOP_DRAWER_STYLES = {
position: 'sticky',
top: '5rem',
height: 'calc(100vh - 5rem)',
border: 'none',
boxShadow: 'none',
} as const;

export interface ResponsiveDrawerProps extends PropsWithChildren {
open: boolean;
onClose: () => void;
}

export const ResponsiveDrawer: FC<ResponsiveDrawerProps> = ({ open, onClose, children }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));

return (
<Drawer
anchor="left"
open={isMobile ? open : true}
variant={isMobile ? 'temporary' : 'permanent'}
onClose={onClose}
sx={{
display: { xs: isMobile ? 'block' : 'none', md: isMobile ? 'none' : 'block' },
'& .MuiDrawer-paper': {
width: 250,
...(isMobile ? {} : DESKTOP_DRAWER_STYLES),
},
}}
>
{children}
</Drawer>
);
};
90 changes: 61 additions & 29 deletions components/User/SessionBox.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { User } from '@idea2app/data-server';
import { Box, List, ListItem, ListItemButton, ListItemText, Modal } from '@mui/material';
import {
Box,
IconButton,
List,
ListItem,
ListItemButton,
ListItemText,
Modal,
} from '@mui/material';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import Link from 'next/link';
import { JWTProps } from 'next-ssr-middleware';
import { Component, HTMLAttributes, JSX } from 'react';

import { PageHead } from '../PageHead';
import { SymbolIcon } from '../Icon';
import { ResponsiveDrawer } from './ResponsiveDrawer';
import { SessionForm } from './SessionForm';

export type MenuItem = Pick<JSX.IntrinsicElements['a'], 'href' | 'title'>;
Expand All @@ -21,46 +30,69 @@ export class SessionBox extends Component<SessionBoxProps> {
@observable
accessor modalShown = false;

@observable
accessor drawerOpen = false;

componentDidMount() {
this.modalShown = !this.props.jwtPayload;
}

toggleDrawer = () => (this.drawerOpen = !this.drawerOpen);

closeDrawer = () => (this.drawerOpen = false);

renderMenuItems() {
const { path, menu = [] } = this.props;

return (
<List component="nav" className="px-2">
{menu.map(({ href, title }) => (
<ListItem key={href} disablePadding>
<ListItemButton
component={Link}
href={href || '#'}
selected={path?.split('?')[0].startsWith(href || '')}
className="rounded"
onClick={this.closeDrawer}
>
<ListItemText primary={title} />
</ListItemButton>
</ListItem>
))}
</List>
);
}

render() {
const { className = '', title, children, path, menu = [], jwtPayload, ...props } = this.props;
const { className = '', children, jwtPayload, ...props } = this.props;

return (
<div className={`flex ${className}`} {...props}>
<div>
<List
component="nav"
className="sticky-top flex-col px-3"
style={{ top: '5rem', minWidth: '200px' }}
<div className={`flex flex-col md:flex-row ${className}`} {...props}>
{/* Mobile Menu Button */}
<div className="sticky top-0 z-[1100] flex border-b p-1 md:hidden bg-background-paper border-divider">
<IconButton
edge="start"
color="inherit"
aria-label="menu"
onClick={this.toggleDrawer}
>
{menu.map(({ href, title }) => (
<ListItem key={href} disablePadding>
<ListItemButton
component={Link}
href={href || '#'}
selected={path?.split('?')[0].startsWith(href || '')}
className="rounded"
>
<ListItemText primary={title} />
</ListItemButton>
</ListItem>
))}
</List>
<SymbolIcon name="menu" />
</IconButton>
</div>
<main className="flex-1 pb-3">

{/* Unified Responsive Drawer */}
<ResponsiveDrawer open={this.drawerOpen} onClose={this.closeDrawer}>
<div className="w-[250px]">{this.renderMenuItems()}</div>
</ResponsiveDrawer>

{/* Main Content */}
<main className="flex-1 px-2 pb-3 sm:px-3">
{children}

<Modal open={this.modalShown}>
<Box
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded p-4 shadow-lg"
sx={{
width: '400px',
maxWidth: '90vw',
bgcolor: 'background.paper',
}}
className="absolute left-1/2 top-1/2 w-[90vw] -translate-x-1/2 -translate-y-1/2 rounded p-4 shadow-2xl sm:w-[400px] bg-background-paper"
sx={{ boxShadow: 24 }}
>
<SessionForm onSignIn={() => (this.modalShown = false)} />
</Box>
Expand Down
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import './.next/dev/types/routes.d.ts';
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
32 changes: 20 additions & 12 deletions pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,18 @@ const DashboardPage: FC<DashboardPageProps> = observer(({ route, jwtPayload }) =

return (
<SessionBox title={t('backend_management')} path={route.resolvedUrl} {...{ menu, jwtPayload }}>
<Container maxWidth="lg" className="py-8">
<Typography variant="h3" component="h1" gutterBottom>
<Container maxWidth="lg" className="py-3 md:py-8">
<Typography
variant="h3"
component="h1"
gutterBottom
className="text-[1.75rem] sm:text-[2.5rem] md:text-[3rem]"
>
{t('welcome_use')}
</Typography>

<Box
component="form"
sx={{ display: 'flex', gap: 2, alignItems: 'center', mt: 2, mb: 4 }}
<form
className="mb-4 mt-2 flex flex-col items-stretch gap-2 sm:flex-row sm:items-center"
onSubmit={handleCreateProject}
>
<TextField
Expand All @@ -56,16 +60,20 @@ const DashboardPage: FC<DashboardPageProps> = observer(({ route, jwtPayload }) =
defaultValue={route.query.name}
/>
<Button
className="text-nowrap"
type="submit"
className="min-w-full whitespace-nowrap sm:min-w-0"
variant="contained"
type="submit"
disabled={projectStore.uploading > 0}
>
{t('create_new_project')}
</Button>
</Box>
</form>

<Typography variant="h5" component="h2" sx={{ mt: 4, mb: 3 }}>
<Typography
variant="h5"
component="h2"
className="mb-3 mt-4 text-[1.25rem] sm:text-[1.5rem]"
>
{t('recent_projects')}
</Typography>

Expand All @@ -76,16 +84,16 @@ const DashboardPage: FC<DashboardPageProps> = observer(({ route, jwtPayload }) =
jwtPayload?.roles.includes(2 as UserRole.Client) ? { createdBy: jwtPayload.id } : {}
}
renderList={allItems => (
<Grid container spacing={3}>
<Grid container spacing={{ xs: 2, sm: 3 }}>
{allItems[0] ? (
allItems.map(project => (
<Grid key={project.id} size={{ xs: 12, md: 4 }}>
<Grid key={project.id} size={{ xs: 12, sm: 6, md: 4 }}>
<ProjectCard {...project} />
</Grid>
))
) : (
<Grid size={{ xs: 12 }}>
<Typography color="textSecondary" sx={{ textAlign: 'center', mt: 4 }}>
<Typography color="textSecondary" className="mt-4 text-center">
{t('no_project_data')}
</Typography>
</Grid>
Expand Down
65 changes: 30 additions & 35 deletions pages/dashboard/project/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,44 +80,35 @@ export default class ProjectEvaluationPage extends ObservedComponent<
const name = isBot ? `${t('ai_assistant')} 🤖` : createdBy?.name || 'User';

return (
<Box
<div
key={id}
id={index + 1 === length ? 'last-message' : undefined}
sx={{
display: 'flex',
justifyContent: isBot ? 'flex-start' : 'flex-end',
mb: 2,
width: '100%',
}}
className={`mb-2 flex w-full ${isBot ? 'justify-start' : 'justify-end'}`}
>
<Box
sx={{
display: 'flex',
flexDirection: isBot ? 'row' : 'row-reverse',
alignItems: 'flex-start',
maxWidth: '80%',
gap: 1,
}}
<div
className={`flex items-start gap-1 max-w-[95%] sm:max-w-[80%] ${isBot ? 'flex-row' : 'flex-row-reverse'}`}
>
<Avatar src={avatarSrc} alt={name} sx={{ width: 32, height: 32 }} />
<Avatar src={avatarSrc} alt={name} className="h-7 w-7 sm:h-8 sm:w-8" />
<Paper
elevation={1}
className="rounded-[16px_16px_4px_16px] p-1.5 sm:p-2 bg-primary-light text-primary-contrast"
sx={{
p: 2,
backgroundColor: 'primary.light',
color: 'primary.contrastText',
borderRadius: '16px 16px 4px 16px',
}}
>
<Typography variant="caption" display="block" sx={{ mb: 0.5, opacity: 0.8 }}>
<Typography
variant="caption"
display="block"
className="mb-0.5 text-[0.7rem] opacity-80 sm:text-[0.75rem]"
>
{name}
</Typography>

{content && (
<Typography
className="prose"
className="prose mb-1 text-[0.875rem] sm:text-base"
variant="body2"
sx={{ mb: 1 }}
dangerouslySetInnerHTML={{ __html: marked(content) }}
/>
)}
Expand All @@ -130,13 +121,13 @@ export default class ProjectEvaluationPage extends ObservedComponent<
/>
)}
{createdAt && (
<Typography variant="caption" sx={{ opacity: 0.6, fontSize: '0.75rem' }}>
<Typography variant="caption" className="text-[0.65rem] opacity-60 sm:text-[0.75rem]">
{new Date(createdAt).toLocaleTimeString()}
</Typography>
)}
</Paper>
</Box>
</Box>
</div>
</div>
);
};

Expand All @@ -154,31 +145,36 @@ export default class ProjectEvaluationPage extends ObservedComponent<

<Container
maxWidth="md"
sx={{ height: '85vh', display: 'flex', flexDirection: 'column', gap: '1rem' }}
className="flex h-[calc(100vh-120px)] flex-col gap-2 px-0 sm:gap-4 sm:px-2 md:h-[85vh]"
>
<h1 className="mt-20 text-3xl font-bold">{title}</h1>
<Typography
component="h1"
className="mb-1 mt-2 px-2 text-2xl font-bold sm:mb-2 sm:mt-4 sm:px-0 sm:text-3xl md:mt-20 md:text-5xl"
>
{title}
</Typography>
{/* Chat Messages Area */}
<Box sx={{ flex: 1, overflow: 'auto', mb: 2 }}>
<div className="mb-2 flex-1 overflow-auto">
<ScrollList
translator={i18n}
store={messageStore}
filter={{ project: projectId }}
renderList={allItems => (
<Box sx={{ height: '100%', overflowY: 'auto', p: 1 }}>
<div className="h-full overflow-y-auto p-1 sm:p-2">
{allItems.map(this.renderChatMessage)}
</Box>
</div>
)}
/>
</Box>
</div>

{/* Message Input Form */}
<Paper
component="form"
elevation={1}
sx={{ p: 2, mt: 'auto' }}
className="mx-1 mb-1 mt-auto p-1.5 sm:mx-0 sm:mb-0 sm:p-2"
onSubmit={this.handleMessageSubmit}
>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
<div className="flex flex-col items-end gap-1 sm:flex-row">
<TextField
name="content"
placeholder={t('type_your_message')}
Expand All @@ -191,14 +187,13 @@ export default class ProjectEvaluationPage extends ObservedComponent<
/>
<Button
type="submit"
className="text-nowrap"
variant="contained"
sx={{ minWidth: 'auto', px: 2 }}
className="min-w-full whitespace-nowrap px-2 sm:min-w-0"
disabled={messageStore.uploading > 0}
>
{t('send')}
</Button>
</Box>
</div>
</Paper>
</Container>
</SessionBox>
Expand Down
Loading