Skip to content

Commit 570cbc7

Browse files
authored
Frame, member and project detail layout (#67)
1 parent 144cbc7 commit 570cbc7

File tree

17 files changed

+300
-121
lines changed

17 files changed

+300
-121
lines changed

components/LarkImage.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { TableCellValue } from 'mobx-lark';
2+
import { ImageProps } from 'next/image';
3+
import { FC } from 'react';
4+
5+
import { blobURLOf } from '../models/Base';
6+
import { DefaultImage, fileURLOf } from '../pages/api/Lark/file/[id]';
7+
8+
export interface LarkImageProps extends Omit<ImageProps, 'src'> {
9+
src?: TableCellValue;
10+
}
11+
12+
export const LarkImage: FC<LarkImageProps> = ({ src = DefaultImage, alt, ...props }) => (
13+
<img
14+
loading="lazy"
15+
{...props}
16+
src={blobURLOf(src)}
17+
alt={alt}
18+
onError={({ currentTarget: image }) => {
19+
const path = fileURLOf(src),
20+
errorURL = decodeURI(image.src);
21+
22+
if (!path) return;
23+
24+
if (errorURL.endsWith(path)) {
25+
if (!alt) image.src = DefaultImage;
26+
} else if (!errorURL.endsWith(DefaultImage)) {
27+
image.src = path;
28+
}
29+
}}
30+
/>
31+
);

components/Layout/ColorModeDropdown.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ export default function ColorModeIconDropdown() {
1717
const toggleMode = () => setMode(resolvedMode === 'light' ? 'dark' : 'light');
1818

1919
return (
20-
<IconButton data-screenshot="toggle-mode" size="small" disableRipple onClick={toggleMode}>
20+
<IconButton
21+
color="inherit"
22+
data-screenshot="toggle-mode"
23+
size="small"
24+
disableRipple
25+
onClick={toggleMode}
26+
>
2127
{icon}
2228
</IconButton>
2329
);

components/Layout/Footer.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
11
export const Footer = () => (
2-
<div className="container mx-auto flex max-w-screen-xl py-12 text-center">idea2app</div>
2+
<div className="container mx-auto flex max-w-screen-xl items-center justify-between border-t-2 px-4 py-12 text-center">
3+
© 2024 idea2app
4+
<ul className="flex gap-4">
5+
<li>
6+
<a
7+
className="border-b-2 border-b-black py-1 dark:border-b-white"
8+
href="https://web-cell.dev/"
9+
target="_blank"
10+
rel="noreferrer"
11+
>
12+
Web Cell
13+
</a>
14+
</li>
15+
<li>
16+
<a
17+
className="border-b-2 border-b-black py-1 dark:border-b-white"
18+
href="https://tech-query.me/"
19+
target="_blank"
20+
rel="noreferrer"
21+
>
22+
TechQuery
23+
</a>
24+
</li>
25+
</ul>
26+
</div>
327
);

components/Layout/Frame.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { DataObject, Filter, ListModel } from 'mobx-restful';
2+
import { FC, ReactNode } from 'react';
3+
4+
import { i18n } from '../../models/Translation';
5+
import { PageHead } from '../PageHead';
6+
import { ScrollList } from '../ScrollList';
7+
8+
export interface FrameProps<D extends DataObject, F extends Filter<D> = Filter<D>> {
9+
store: ListModel<D, F>;
10+
filter?: F;
11+
defaultData?: D[];
12+
title: string;
13+
header: string;
14+
className?: string;
15+
scrollList?: boolean;
16+
children?: ReactNode;
17+
Layout: FC<{ defaultData: D[]; className?: string }>;
18+
}
19+
20+
/**
21+
* @todo remove ScrollList and use children instead?
22+
*/
23+
export const Frame = <D extends DataObject, F extends Filter<D> = Filter<D>>({
24+
className = '',
25+
scrollList = true,
26+
children,
27+
title,
28+
header,
29+
Layout,
30+
...rest
31+
}: FrameProps<D, F>) => (
32+
<div className={`container mx-auto max-w-screen-xl px-4 pb-6 pt-16 ${className}`}>
33+
<PageHead title={title} />
34+
<h1 className="my-8 text-4xl">{header}</h1>
35+
36+
{scrollList ? (
37+
<ScrollList
38+
translator={i18n}
39+
renderList={allItems => <Layout defaultData={allItems} />}
40+
{...rest}
41+
/>
42+
) : (
43+
children
44+
)}
45+
</div>
46+
);

components/Layout/MainNavigator.tsx

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1-
import { AppBar, Drawer, IconButton, PopoverProps, Toolbar } from '@mui/material';
1+
import {
2+
AppBar,
3+
Button,
4+
Drawer,
5+
IconButton,
6+
Menu,
7+
MenuItem,
8+
PopoverProps,
9+
Toolbar
10+
} from '@mui/material';
211
import { observable } from 'mobx';
312
import { observer } from 'mobx-react';
413
import Image from 'next/image';
514
import Link from 'next/link';
6-
import { Component, SyntheticEvent } from 'react';
15+
import { Component } from 'react';
716

8-
import { i18n } from '../../models/Translation';
17+
import { i18n, LanguageName } from '../../models/Translation';
918
import { SymbolIcon } from '../Icon';
1019
import ColorModeIconDropdown from './ColorModeDropdown';
20+
import { GithubIcon } from './Svg';
1121

1222
const { t } = i18n;
1323

@@ -21,10 +31,10 @@ export const mainNavLinks = () => [
2131
export class MainNavigator extends Component {
2232
@observable accessor menuExpand = false;
2333
@observable accessor menuAnchor: PopoverProps['anchorEl'] = null;
24-
@observable accessor eventKey = 0;
2534

26-
handleChange = (event: SyntheticEvent, newValue: number) => {
27-
this.eventKey = newValue;
35+
switchI18n = (key: string) => {
36+
i18n.changeLanguage(key as keyof typeof LanguageName);
37+
this.menuAnchor = null;
2838
};
2939

3040
renderLinks = () =>
@@ -34,6 +44,49 @@ export class MainNavigator extends Component {
3444
</Link>
3545
));
3646

47+
renderI18nSwitch = () => {
48+
const { currentLanguage } = i18n,
49+
{ menuAnchor } = this;
50+
51+
return (
52+
<>
53+
<Button
54+
color="inherit"
55+
aria-controls="i18n-menu"
56+
size="small"
57+
id="i18n-selector"
58+
startIcon={<SymbolIcon name="translate" />}
59+
onClick={event => (this.menuAnchor = event.currentTarget)}
60+
>
61+
{LanguageName[currentLanguage]}
62+
</Button>
63+
<Menu
64+
anchorEl={menuAnchor}
65+
id="i18n-menu"
66+
slotProps={{
67+
paper: {
68+
variant: 'outlined',
69+
sx: { my: '4px' }
70+
}
71+
}}
72+
open={Boolean(menuAnchor)}
73+
onClose={() => (this.menuAnchor = null)}
74+
>
75+
{Object.entries(LanguageName).map(([key, name]) => (
76+
<MenuItem
77+
key={key}
78+
value={key}
79+
selected={key === currentLanguage}
80+
onClick={() => this.switchI18n(key)}
81+
>
82+
{name}
83+
</MenuItem>
84+
))}
85+
</Menu>
86+
</>
87+
);
88+
};
89+
3790
renderDrawer = () => (
3891
<nav className="sm:hidden">
3992
<IconButton
@@ -77,8 +130,12 @@ export class MainNavigator extends Component {
77130

78131
<nav className="item-center hidden flex-row gap-4 sm:flex">{this.renderLinks()}</nav>
79132

80-
<div className="flex flex-row items-center gap-4">
133+
<div className="flex flex-row items-center gap-3 sm:gap-6">
134+
<Link href="https://github.com/idea2app" target="_blank" rel="noopener noreferrer">
135+
<GithubIcon />
136+
</Link>
81137
<ColorModeIconDropdown />
138+
{this.renderI18nSwitch()}
82139
</div>
83140
</div>
84141
</Toolbar>

components/Section.tsx renamed to components/Layout/Section.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import { Button } from '@mui/material';
22
import Link from 'next/link';
33
import { FC, PropsWithChildren } from 'react';
44

5-
import { i18n } from '../models/Translation';
5+
import { i18n } from '../../models/Translation';
66

77
export type SectionProps = PropsWithChildren<
88
Partial<Record<'id' | 'title' | 'link' | 'className', string>>
99
>;
1010

1111
const { t } = i18n;
1212

13-
export const Section: FC<SectionProps> = ({ id, title, children, link, className }) => (
13+
export const Section: FC<SectionProps> = ({ id, title, children, link, className = '' }) => (
1414
<section className={`mx-auto flex max-w-screen-xl flex-col gap-6 py-8 ${className}`}>
1515
<h2 className="text-center" id={id}>
1616
{title}

components/ScrollList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class ScrollList<
6363
<div>
6464
{renderList(allItems)}
6565

66-
<footer style={{ marginTop: '1.5rem' }}>
66+
<footer style={{ marginTop: '1.5rem', textAlign: 'center' }}>
6767
{noMore || !allItems.length ? t('no_more') : t('load_more')}
6868
</footer>
6969
</div>

models/Base.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { HTTPClient } from 'koajax';
2+
import { TableCellValue } from 'mobx-lark';
23

34
export const isServer = () => typeof window === 'undefined';
45

@@ -11,7 +12,21 @@ export const API_Host = isServer()
1112
: 'http://localhost:3000'
1213
: globalThis.location.origin;
1314

15+
export const blobClient = new HTTPClient({
16+
baseURI: 'https://ows.blob.core.chinacloudapi.cn/$web/',
17+
responseType: 'arraybuffer'
18+
});
19+
20+
export const fileBaseURI = blobClient.baseURI + 'file';
21+
1422
export const larkClient = new HTTPClient({
1523
baseURI: `${API_Host}/api/Lark/`,
16-
responseType: 'json',
24+
responseType: 'json'
1725
});
26+
27+
export const blobURLOf = (value: TableCellValue) =>
28+
value instanceof Array
29+
? typeof value[0] === 'object' && ('file_token' in value[0] || 'attachmentToken' in value[0])
30+
? `${fileBaseURI}/${value[0].name}`
31+
: ''
32+
: value + '';

pages/_app.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ const AppShell = observer(({ Component, pageProps }: AppProps<{}>) => (
5757
* @see {@link https://mui.com/material-ui/integrations/interoperability/#tailwind-css}
5858
*/}
5959
<ThemeProvider theme={theme} defaultMode="system" disableTransitionOnChange>
60-
<MainNavigator />
60+
<div className="flex min-h-screen flex-col justify-between">
61+
<MainNavigator />
6162

62-
<div className="pt-16">
6363
<Component {...pageProps} />
64-
</div>
6564

66-
<Footer />
65+
<Footer />
66+
</div>
6767
</ThemeProvider>
6868
</StyledEngineProvider>
6969
</>

pages/api/Lark/bitable/v1/[...slug].ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import { proxyLark } from '../../core';
55

66
export default proxyLark((path, data) => {
77
if (path.split('?')[0].endsWith('/records')) {
8-
const items =
9-
(data as LarkPageData<TableRecord<DataObject>>).data!.items || [];
8+
const items = (data as LarkPageData<TableRecord<DataObject>>).data!.items || [];
109

1110
for (const { fields } of items)
12-
for (const key of Object.keys(fields))
13-
if (!/^\w+$/.test(key)) delete fields[key];
11+
for (const key of Object.keys(fields)) if (!/^\w+$/.test(key)) delete fields[key];
1412
}
1513
return data;
1614
});

pages/api/Lark/file/[id].ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { TableCellMedia, TableCellValue } from 'mobx-lark';
44
import { safeAPI } from '../../core';
55
import { lark } from '../core';
66

7+
export const DefaultImage = '/idea2app.svg';
8+
79
export const fileURLOf = (field: TableCellValue) =>
810
field instanceof Array
911
? field[0]

pages/index.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,23 @@ import { FC } from 'react';
99
import { PartnerOverview } from '../components/Client/Partner';
1010
import { GitListLayout } from '../components/Git';
1111
import { SymbolIcon } from '../components/Icon';
12+
import { Section } from '../components/Layout/Section';
1213
import { MemberCard } from '../components/Member/Card';
1314
import { PageHead } from '../components/PageHead';
14-
import { Section } from '../components/Section';
1515
import { MEMBER_VIEW, MemberModel } from '../models/Member';
1616
import { GitRepositoryModel } from '../models/Repository';
1717
import { i18n } from '../models/Translation';
1818
import { PARTNERS_INFO, service } from './api/home';
1919

2020
export const getServerSideProps = compose(cache(), errorLogger, translator(i18n), async () => {
21-
const [
22-
// projects,
23-
repositories,
24-
// partners
25-
members
26-
] = await Promise.all([
27-
// new ProjectModel().getList({}, 1, 9),
21+
const [repositories, members] = await Promise.all([
2822
new GitRepositoryModel('idea2app').getList({ relation: [] }, 1, 9),
2923
new MemberModel().getViewList(MEMBER_VIEW)
3024
]);
3125

3226
return {
3327
props: {
34-
// projects: JSON.parse(JSON.stringify(projects)) as Project[],
3528
repositories: JSON.parse(JSON.stringify(repositories)) as GitRepository[],
36-
3729
members: members.filter(({ github, position, summary }) => github && position && summary)
3830
}
3931
};
@@ -46,7 +38,7 @@ const HomePage: FC<InferGetServerSidePropsType<typeof getServerSideProps>> = obs
4638
<>
4739
<PageHead />
4840

49-
<div className="px-2 py-6">
41+
<div className="px-4 py-6 pt-16">
5042
<section className="container mx-auto flex max-w-screen-lg flex-col gap-4">
5143
<div className="flex flex-row items-center justify-around py-12">
5244
<Image src="/idea2app.svg" width={234} height={220} alt="idea2app logo" />
@@ -105,10 +97,6 @@ const HomePage: FC<InferGetServerSidePropsType<typeof getServerSideProps>> = obs
10597
<div className="absolute right-0 top-0 z-20 block h-24 w-24 bg-gradient-to-l from-background to-transparent" />
10698
</section>
10799

108-
{/* <Section title={t('latest_projects')} link="/project">
109-
<ProjectListLayout defaultData={projects} />
110-
</Section>*/}
111-
112100
<Section title={t('member')} link="/member">
113101
<div className="relative max-h-[45rem] overflow-hidden">
114102
<Masonry

0 commit comments

Comments
 (0)