Skip to content

Commit 9002038

Browse files
committed
[refactor] reuse Main Navigator to render Dashboard menu (fix #92)
[optimize] simplify Chat UI layout [optimize] submit Chat messages with Ctrl + Enter keys
1 parent bb16850 commit 9002038

File tree

7 files changed

+156
-206
lines changed

7 files changed

+156
-206
lines changed

components/Layout/MainNavigator.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
PopoverProps,
99
Toolbar,
1010
} from '@mui/material';
11-
import { computed, observable } from 'mobx';
11+
import { observable } from 'mobx';
1212
import { observer } from 'mobx-react';
1313
import { ObservedComponent } from 'mobx-react-helper';
1414
import Link from 'next/link';
@@ -17,34 +17,22 @@ import { i18n, I18nContext, LanguageName } from '../../models/Translation';
1717
import { SymbolIcon } from '../Icon';
1818
import { ColorModeIconDropdown } from './ColorModeDropdown';
1919
import { BrandLogo, GithubIcon } from './Svg';
20+
import { MenuLink } from './menu';
2021

2122
@observer
22-
export class MainNavigator extends ObservedComponent<{}, typeof i18n> {
23+
export class MainNavigator extends ObservedComponent<{ menu: MenuLink[] }, typeof i18n> {
2324
static contextType = I18nContext;
2425

2526
@observable accessor menuExpand = false;
2627
@observable accessor menuAnchor: PopoverProps['anchorEl'] = null;
2728

28-
@computed
29-
get links() {
30-
const { t } = this.observedContext!;
31-
32-
return [
33-
{ title: t('latest_projects'), href: '/project' },
34-
{ title: t('member'), href: '/member' },
35-
{ title: t('open_source_project'), href: '/open-source' },
36-
{ title: t('wiki'), href: '/wiki' },
37-
{ title: t('github_reward'), href: '/project/reward/issue', target: '_top' },
38-
];
39-
}
40-
4129
switchI18n = (key: string) => {
4230
this.observedContext!.loadLanguages(key as keyof typeof LanguageName);
4331
this.menuAnchor = null;
4432
};
4533

4634
renderLinks = () =>
47-
this.links.map(({ title, href, target }) => (
35+
this.props.menu.map(({ title, href, target }) => (
4836
<Button key={title} component={Link} className="py-1" href={href} target={target}>
4937
{title}
5038
</Button>

components/Layout/menu.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { JSX } from 'react';
2+
3+
import { i18n } from '../../models/Translation';
4+
5+
export type MenuLink = Pick<JSX.IntrinsicElements['a'], 'href' | 'title' | 'target'>;
6+
7+
export const PublicMenu = ({ t }: typeof i18n) => [
8+
{ title: t('latest_projects'), href: '/project' },
9+
{ title: t('member'), href: '/member' },
10+
{ title: t('open_source_project'), href: '/open-source' },
11+
{ title: t('wiki'), href: '/wiki' },
12+
{ title: t('github_reward'), href: '/project/reward/issue', target: '_top' },
13+
];
14+
15+
export const PrivateMenu = ({ t }: typeof i18n) => [{ href: '/dashboard', title: t('overview') }];

components/User/SessionBox.tsx

Lines changed: 15 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,14 @@
11
import { User } from '@idea2app/data-server';
2-
import {
3-
Box,
4-
IconButton,
5-
List,
6-
ListItem,
7-
ListItemButton,
8-
ListItemText,
9-
Modal,
10-
} from '@mui/material';
2+
import { Box, Modal } from '@mui/material';
113
import { observable } from 'mobx';
124
import { observer } from 'mobx-react';
13-
import Link from 'next/link';
145
import { JWTProps } from 'next-ssr-middleware';
15-
import { Component, HTMLAttributes, JSX } from 'react';
6+
import { Component, PropsWithChildren } from 'react';
167

17-
import { SymbolIcon } from '../Icon';
18-
import { ResponsiveDrawer } from './ResponsiveDrawer';
198
import { SessionForm } from './SessionForm';
209

21-
export type MenuItem = Pick<JSX.IntrinsicElements['a'], 'href' | 'title'>;
22-
23-
export interface SessionBoxProps extends HTMLAttributes<HTMLDivElement>, JWTProps<User> {
10+
export interface SessionBoxProps extends PropsWithChildren<JWTProps<User>> {
2411
path?: string;
25-
menu?: MenuItem[];
2612
}
2713

2814
@observer
@@ -41,64 +27,22 @@ export class SessionBox extends Component<SessionBoxProps> {
4127

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

44-
renderMenuItems() {
45-
const { path, menu = [] } = this.props;
46-
47-
return (
48-
<List component="nav" className="px-2">
49-
{menu.map(({ href, title }) => (
50-
<ListItem key={href} disablePadding>
51-
<ListItemButton
52-
component={Link}
53-
href={href || '#'}
54-
selected={path?.split('?')[0].startsWith(href || '')}
55-
className="rounded"
56-
onClick={this.closeDrawer}
57-
>
58-
<ListItemText primary={title} />
59-
</ListItemButton>
60-
</ListItem>
61-
))}
62-
</List>
63-
);
64-
}
65-
6630
render() {
67-
const { className = '', children, jwtPayload, ...props } = this.props;
31+
const { children } = this.props;
6832

6933
return (
70-
<div className={`flex flex-col md:flex-row ${className}`} {...props}>
71-
{/* Mobile Menu Button */}
72-
<div className="sticky top-0 z-[1100] flex border-b p-1 md:hidden bg-background-paper border-divider">
73-
<IconButton
74-
edge="start"
75-
color="inherit"
76-
aria-label="menu"
77-
onClick={this.toggleDrawer}
78-
>
79-
<SymbolIcon name="menu" />
80-
</IconButton>
81-
</div>
82-
83-
{/* Unified Responsive Drawer */}
84-
<ResponsiveDrawer open={this.drawerOpen} onClose={this.closeDrawer}>
85-
<div className="w-[250px]">{this.renderMenuItems()}</div>
86-
</ResponsiveDrawer>
34+
<>
35+
{children}
8736

88-
{/* Main Content */}
89-
<main className="flex-1 px-2 pb-3 sm:px-3">
90-
{children}
91-
92-
<Modal open={this.modalShown}>
93-
<Box
94-
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"
95-
sx={{ boxShadow: 24 }}
96-
>
97-
<SessionForm onSignIn={() => (this.modalShown = false)} />
98-
</Box>
99-
</Modal>
100-
</main>
101-
</div>
37+
<Modal open={this.modalShown}>
38+
<Box
39+
className="bg-background-paper absolute top-1/2 left-1/2 w-[90vw] -translate-x-1/2 -translate-y-1/2 rounded p-4 shadow-2xl sm:w-[400px]"
40+
sx={{ boxShadow: 24 }}
41+
>
42+
<SessionForm onSignIn={() => (this.modalShown = false)} />
43+
</Box>
44+
</Modal>
45+
</>
10246
);
10347
}
10448
}

pages/_app.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Head from 'next/head';
1111

1212
import { Footer } from '../components/Layout/Footer';
1313
import { MainNavigator } from '../components/Layout/MainNavigator';
14+
import { PrivateMenu, PublicMenu } from '../components/Layout/menu';
1415
import { isServer } from '../models/configuration';
1516
import { createI18nStore, I18nContext, I18nProps, loadSSRLanguage } from '../models/Translation';
1617

@@ -66,10 +67,13 @@ export default class CustomApp extends App<I18nProps & { emotionCache: EmotionCa
6667
}
6768

6869
render() {
69-
const { Component, pageProps, emotionCache = clientCache } = this.props;
70+
const { router, Component, pageProps, emotionCache = clientCache } = this.props;
71+
const { asPath } = router,
72+
{ i18nStore } = this;
73+
const menu = asPath.startsWith('/dashboard') ? PrivateMenu(i18nStore) : PublicMenu(i18nStore);
7074

7175
return (
72-
<I18nContext.Provider value={this.i18nStore}>
76+
<I18nContext.Provider value={i18nStore}>
7377
<AppCacheProvider emotionCache={emotionCache}>
7478
<GlobalStyles styles="@layer theme, base, mui, components, utilities;" />
7579
<Head>
@@ -81,7 +85,7 @@ export default class CustomApp extends App<I18nProps & { emotionCache: EmotionCa
8185
*/}
8286
<ThemeProvider theme={theme} defaultMode="system" disableTransitionOnChange>
8387
<div className="flex min-h-screen flex-col justify-between">
84-
<MainNavigator />
88+
<MainNavigator menu={menu} />
8589

8690
<Component {...pageProps} />
8791

pages/dashboard/index.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { User, UserRole } from '@idea2app/data-server';
2-
import { Box, Button, Container, Grid, TextField, Typography } from '@mui/material';
2+
import { Button, Container, Grid, TextField, Typography } from '@mui/material';
33
import { observer } from 'mobx-react';
44
import { compose, JWTProps, jwtVerifier, RouteProps, router } from 'next-ssr-middleware';
55
import { FC, FormEvent, useContext } from 'react';
66
import { formToJSON } from 'web-utility';
77

8+
import { PageHead } from '../../components/PageHead';
89
import { ProjectCard } from '../../components/Project/NewCard';
910
import { ScrollList } from '../../components/ScrollList';
1011
import { SessionBox } from '../../components/User/SessionBox';
@@ -22,8 +23,6 @@ const DashboardPage: FC<DashboardPageProps> = observer(({ route, jwtPayload }) =
2223
const i18n = useContext(I18nContext);
2324
const { t } = i18n;
2425

25-
const menu = [{ href: '/dashboard', title: t('overview') }];
26-
2726
const handleCreateProject = async (event: FormEvent<HTMLFormElement>) => {
2827
event.preventDefault();
2928
event.stopPropagation();
@@ -36,8 +35,10 @@ const DashboardPage: FC<DashboardPageProps> = observer(({ route, jwtPayload }) =
3635
};
3736

3837
return (
39-
<SessionBox title={t('backend_management')} path={route.resolvedUrl} {...{ menu, jwtPayload }}>
40-
<Container maxWidth="lg" className="py-3 md:py-8">
38+
<SessionBox path={route.resolvedUrl} {...{ jwtPayload }}>
39+
<PageHead title={t('backend_management')} />
40+
41+
<Container maxWidth="lg" className="px-4 py-6 pt-16">
4142
<Typography
4243
variant="h3"
4344
component="h1"
@@ -48,7 +49,7 @@ const DashboardPage: FC<DashboardPageProps> = observer(({ route, jwtPayload }) =
4849
</Typography>
4950

5051
<form
51-
className="mb-4 mt-2 flex flex-col items-stretch gap-2 sm:flex-row sm:items-center"
52+
className="mt-2 mb-4 flex flex-col items-stretch gap-2 sm:flex-row sm:items-center"
5253
onSubmit={handleCreateProject}
5354
>
5455
<TextField
@@ -72,7 +73,7 @@ const DashboardPage: FC<DashboardPageProps> = observer(({ route, jwtPayload }) =
7273
<Typography
7374
variant="h5"
7475
component="h2"
75-
className="mb-3 mt-4 text-[1.25rem] sm:text-[1.5rem]"
76+
className="mt-4 mb-3 text-[1.25rem] sm:text-[1.5rem]"
7677
>
7778
{t('recent_projects')}
7879
</Typography>

pages/dashboard/project/[id].tsx

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { ConsultMessage, User, UserRole } from '@idea2app/data-server';
2-
import { Avatar, Box, Button, Container, Paper, TextField, Typography } from '@mui/material';
2+
import { Avatar, Button, Container, Paper, TextField, Typography } from '@mui/material';
33
import { marked } from 'marked';
44
import { observer } from 'mobx-react';
55
import { ObservedComponent, reaction } from 'mobx-react-helper';
66
import { compose, JWTProps, jwtVerifier, RouteProps, router } from 'next-ssr-middleware';
7-
import { FormEvent } from 'react';
7+
import { FormEvent, KeyboardEventHandler } from 'react';
88
import { formToJSON, scrollTo, sleep } from 'web-utility';
99

1010
import { PageHead } from '../../../components/PageHead';
@@ -69,6 +69,13 @@ export default class ProjectEvaluationPage extends ObservedComponent<
6969
form.reset();
7070
};
7171

72+
handleQuickSubmit: KeyboardEventHandler = ({ ctrlKey, key, target }) => {
73+
if (ctrlKey && key === 'Enter')
74+
(target as HTMLTextAreaElement).form?.dispatchEvent(
75+
new SubmitEvent('submit', { cancelable: true, bubbles: true }),
76+
);
77+
};
78+
7279
renderChatMessage = (
7380
{ id, content, evaluation, prototypes, createdAt, createdBy }: ConsultMessage,
7481
index = 0,
@@ -86,12 +93,12 @@ export default class ProjectEvaluationPage extends ObservedComponent<
8693
className={`mb-2 flex w-full ${isBot ? 'justify-start' : 'justify-end'}`}
8794
>
8895
<div
89-
className={`flex items-start gap-1 max-w-[95%] sm:max-w-[80%] ${isBot ? 'flex-row' : 'flex-row-reverse'}`}
96+
className={`flex max-w-[95%] items-start gap-1 sm:max-w-[80%] ${isBot ? 'flex-row' : 'flex-row-reverse'}`}
9097
>
9198
<Avatar src={avatarSrc} alt={name} className="h-7 w-7 sm:h-8 sm:w-8" />
9299
<Paper
93100
elevation={1}
94-
className="rounded-[16px_16px_4px_16px] p-1.5 sm:p-2 bg-primary-light text-primary-contrast"
101+
className="bg-primary-light text-primary-contrast rounded-[16px_16px_4px_16px] p-1.5 sm:p-2"
95102
sx={{
96103
backgroundColor: 'primary.light',
97104
color: 'primary.contrastText',
@@ -143,16 +150,10 @@ export default class ProjectEvaluationPage extends ObservedComponent<
143150
<SessionBox {...{ jwtPayload, menu, title }} path={`/dashboard/project/${projectId}`}>
144151
<PageHead title={title} />
145152

146-
<Container
147-
maxWidth="md"
148-
className="flex h-[calc(100vh-120px)] flex-col gap-2 px-0 sm:gap-4 sm:px-2 md:h-[85vh]"
149-
>
150-
<Typography
151-
component="h1"
152-
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"
153-
>
153+
<Container maxWidth="md" className="px-4 py-6 pt-16">
154+
<h1 className="sticky top-[4rem] z-1 m-0 py-5 text-3xl font-bold backdrop-blur-md">
154155
{title}
155-
</Typography>
156+
</h1>
156157
{/* Chat Messages Area */}
157158
<div className="mb-2 flex-1 overflow-auto">
158159
<ScrollList
@@ -171,29 +172,28 @@ export default class ProjectEvaluationPage extends ObservedComponent<
171172
<Paper
172173
component="form"
173174
elevation={1}
174-
className="mx-1 mb-1 mt-auto p-1.5 sm:mx-0 sm:mb-0 sm:p-2"
175+
className="sticky bottom-0 mx-1 mt-auto mb-1 flex items-end gap-2 p-1.5 sm:mx-0 sm:mb-0 sm:p-2"
175176
onSubmit={this.handleMessageSubmit}
176177
>
177-
<div className="flex flex-col items-end gap-1 sm:flex-row">
178-
<TextField
179-
name="content"
180-
placeholder={t('type_your_message')}
181-
multiline
182-
maxRows={4}
183-
fullWidth
184-
variant="outlined"
185-
size="small"
186-
required
187-
/>
188-
<Button
189-
type="submit"
190-
variant="contained"
191-
className="min-w-full whitespace-nowrap px-2 sm:min-w-0"
192-
disabled={messageStore.uploading > 0}
193-
>
194-
{t('send')}
195-
</Button>
196-
</div>
178+
<TextField
179+
name="content"
180+
placeholder={t('type_your_message')}
181+
multiline
182+
maxRows={4}
183+
fullWidth
184+
variant="outlined"
185+
size="small"
186+
required
187+
onKeyUp={this.handleQuickSubmit}
188+
/>
189+
<Button
190+
type="submit"
191+
variant="contained"
192+
className="min-w-full px-2 whitespace-nowrap sm:min-w-0"
193+
disabled={messageStore.uploading > 0}
194+
>
195+
{t('send')}
196+
</Button>
197197
</Paper>
198198
</Container>
199199
</SessionBox>

0 commit comments

Comments
 (0)