diff --git a/components/LarkImage.tsx b/components/LarkImage.tsx new file mode 100644 index 0000000..28da59f --- /dev/null +++ b/components/LarkImage.tsx @@ -0,0 +1,31 @@ +import { TableCellValue } from 'mobx-lark'; +import { ImageProps } from 'next/image'; +import { FC } from 'react'; + +import { blobURLOf } from '../models/Base'; +import { DefaultImage, fileURLOf } from '../pages/api/Lark/file/[id]'; + +export interface LarkImageProps extends Omit { + src?: TableCellValue; +} + +export const LarkImage: FC = ({ src = DefaultImage, alt, ...props }) => ( + {alt} { + const path = fileURLOf(src), + errorURL = decodeURI(image.src); + + if (!path) return; + + if (errorURL.endsWith(path)) { + if (!alt) image.src = DefaultImage; + } else if (!errorURL.endsWith(DefaultImage)) { + image.src = path; + } + }} + /> +); diff --git a/components/Layout/ColorModeDropdown.tsx b/components/Layout/ColorModeDropdown.tsx index 0259e2d..95677ef 100644 --- a/components/Layout/ColorModeDropdown.tsx +++ b/components/Layout/ColorModeDropdown.tsx @@ -17,7 +17,13 @@ export default function ColorModeIconDropdown() { const toggleMode = () => setMode(resolvedMode === 'light' ? 'dark' : 'light'); return ( - + {icon} ); diff --git a/components/Layout/Footer.tsx b/components/Layout/Footer.tsx index 90a1e3f..4165615 100644 --- a/components/Layout/Footer.tsx +++ b/components/Layout/Footer.tsx @@ -1,3 +1,27 @@ export const Footer = () => ( -
idea2app
+
+ © 2024 idea2app + +
); diff --git a/components/Layout/Frame.tsx b/components/Layout/Frame.tsx new file mode 100644 index 0000000..ee4dc57 --- /dev/null +++ b/components/Layout/Frame.tsx @@ -0,0 +1,46 @@ +import { DataObject, Filter, ListModel } from 'mobx-restful'; +import { FC, ReactNode } from 'react'; + +import { i18n } from '../../models/Translation'; +import { PageHead } from '../PageHead'; +import { ScrollList } from '../ScrollList'; + +export interface FrameProps = Filter> { + store: ListModel; + filter?: F; + defaultData?: D[]; + title: string; + header: string; + className?: string; + scrollList?: boolean; + children?: ReactNode; + Layout: FC<{ defaultData: D[]; className?: string }>; +} + +/** + * @todo remove ScrollList and use children instead? + */ +export const Frame = = Filter>({ + className = '', + scrollList = true, + children, + title, + header, + Layout, + ...rest +}: FrameProps) => ( +
+ +

{header}

+ + {scrollList ? ( + } + {...rest} + /> + ) : ( + children + )} +
+); diff --git a/components/Layout/MainNavigator.tsx b/components/Layout/MainNavigator.tsx index 06b0d13..8821a60 100644 --- a/components/Layout/MainNavigator.tsx +++ b/components/Layout/MainNavigator.tsx @@ -1,13 +1,23 @@ -import { AppBar, Drawer, IconButton, PopoverProps, Toolbar } from '@mui/material'; +import { + AppBar, + Button, + Drawer, + IconButton, + Menu, + MenuItem, + PopoverProps, + Toolbar +} from '@mui/material'; import { observable } from 'mobx'; import { observer } from 'mobx-react'; import Image from 'next/image'; import Link from 'next/link'; -import { Component, SyntheticEvent } from 'react'; +import { Component } from 'react'; -import { i18n } from '../../models/Translation'; +import { i18n, LanguageName } from '../../models/Translation'; import { SymbolIcon } from '../Icon'; import ColorModeIconDropdown from './ColorModeDropdown'; +import { GithubIcon } from './Svg'; const { t } = i18n; @@ -21,10 +31,10 @@ export const mainNavLinks = () => [ export class MainNavigator extends Component { @observable accessor menuExpand = false; @observable accessor menuAnchor: PopoverProps['anchorEl'] = null; - @observable accessor eventKey = 0; - handleChange = (event: SyntheticEvent, newValue: number) => { - this.eventKey = newValue; + switchI18n = (key: string) => { + i18n.changeLanguage(key as keyof typeof LanguageName); + this.menuAnchor = null; }; renderLinks = () => @@ -34,6 +44,49 @@ export class MainNavigator extends Component { )); + renderI18nSwitch = () => { + const { currentLanguage } = i18n, + { menuAnchor } = this; + + return ( + <> + + (this.menuAnchor = null)} + > + {Object.entries(LanguageName).map(([key, name]) => ( + this.switchI18n(key)} + > + {name} + + ))} + + + ); + }; + renderDrawer = () => ( -
+
+ + + + {this.renderI18nSwitch()}
diff --git a/components/Section.tsx b/components/Layout/Section.tsx similarity index 89% rename from components/Section.tsx rename to components/Layout/Section.tsx index dbc726c..6e14c6c 100644 --- a/components/Section.tsx +++ b/components/Layout/Section.tsx @@ -2,7 +2,7 @@ import { Button } from '@mui/material'; import Link from 'next/link'; import { FC, PropsWithChildren } from 'react'; -import { i18n } from '../models/Translation'; +import { i18n } from '../../models/Translation'; export type SectionProps = PropsWithChildren< Partial> @@ -10,7 +10,7 @@ export type SectionProps = PropsWithChildren< const { t } = i18n; -export const Section: FC = ({ id, title, children, link, className }) => ( +export const Section: FC = ({ id, title, children, link, className = '' }) => (

{title} diff --git a/components/ScrollList.tsx b/components/ScrollList.tsx index 8d40e11..19f46b0 100644 --- a/components/ScrollList.tsx +++ b/components/ScrollList.tsx @@ -63,7 +63,7 @@ export class ScrollList<
{renderList(allItems)} -
+
{noMore || !allItems.length ? t('no_more') : t('load_more')}
diff --git a/models/Base.ts b/models/Base.ts index 905d442..d32f4ab 100644 --- a/models/Base.ts +++ b/models/Base.ts @@ -1,4 +1,5 @@ import { HTTPClient } from 'koajax'; +import { TableCellValue } from 'mobx-lark'; export const isServer = () => typeof window === 'undefined'; @@ -11,7 +12,21 @@ export const API_Host = isServer() : 'http://localhost:3000' : globalThis.location.origin; +export const blobClient = new HTTPClient({ + baseURI: 'https://ows.blob.core.chinacloudapi.cn/$web/', + responseType: 'arraybuffer' +}); + +export const fileBaseURI = blobClient.baseURI + 'file'; + export const larkClient = new HTTPClient({ baseURI: `${API_Host}/api/Lark/`, - responseType: 'json', + responseType: 'json' }); + +export const blobURLOf = (value: TableCellValue) => + value instanceof Array + ? typeof value[0] === 'object' && ('file_token' in value[0] || 'attachmentToken' in value[0]) + ? `${fileBaseURI}/${value[0].name}` + : '' + : value + ''; diff --git a/pages/_app.tsx b/pages/_app.tsx index c88a9eb..26e3271 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -57,13 +57,13 @@ const AppShell = observer(({ Component, pageProps }: AppProps<{}>) => ( * @see {@link https://mui.com/material-ui/integrations/interoperability/#tailwind-css} */} - +
+ -
-
-
+
+
diff --git a/pages/api/Lark/bitable/v1/[...slug].ts b/pages/api/Lark/bitable/v1/[...slug].ts index 96b8aa9..7bd1e18 100644 --- a/pages/api/Lark/bitable/v1/[...slug].ts +++ b/pages/api/Lark/bitable/v1/[...slug].ts @@ -5,12 +5,10 @@ import { proxyLark } from '../../core'; export default proxyLark((path, data) => { if (path.split('?')[0].endsWith('/records')) { - const items = - (data as LarkPageData>).data!.items || []; + const items = (data as LarkPageData>).data!.items || []; for (const { fields } of items) - for (const key of Object.keys(fields)) - if (!/^\w+$/.test(key)) delete fields[key]; + for (const key of Object.keys(fields)) if (!/^\w+$/.test(key)) delete fields[key]; } return data; }); diff --git a/pages/api/Lark/file/[id].ts b/pages/api/Lark/file/[id].ts index cbdbc28..725ff0c 100644 --- a/pages/api/Lark/file/[id].ts +++ b/pages/api/Lark/file/[id].ts @@ -4,6 +4,8 @@ import { TableCellMedia, TableCellValue } from 'mobx-lark'; import { safeAPI } from '../../core'; import { lark } from '../core'; +export const DefaultImage = '/idea2app.svg'; + export const fileURLOf = (field: TableCellValue) => field instanceof Array ? field[0] diff --git a/pages/index.tsx b/pages/index.tsx index 7d08a51..dfb26f3 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -9,31 +9,23 @@ import { FC } from 'react'; import { PartnerOverview } from '../components/Client/Partner'; import { GitListLayout } from '../components/Git'; import { SymbolIcon } from '../components/Icon'; +import { Section } from '../components/Layout/Section'; import { MemberCard } from '../components/Member/Card'; import { PageHead } from '../components/PageHead'; -import { Section } from '../components/Section'; import { MEMBER_VIEW, MemberModel } from '../models/Member'; import { GitRepositoryModel } from '../models/Repository'; import { i18n } from '../models/Translation'; import { PARTNERS_INFO, service } from './api/home'; export const getServerSideProps = compose(cache(), errorLogger, translator(i18n), async () => { - const [ - // projects, - repositories, - // partners - members - ] = await Promise.all([ - // new ProjectModel().getList({}, 1, 9), + const [repositories, members] = await Promise.all([ new GitRepositoryModel('idea2app').getList({ relation: [] }, 1, 9), new MemberModel().getViewList(MEMBER_VIEW) ]); return { props: { - // projects: JSON.parse(JSON.stringify(projects)) as Project[], repositories: JSON.parse(JSON.stringify(repositories)) as GitRepository[], - members: members.filter(({ github, position, summary }) => github && position && summary) } }; @@ -46,7 +38,7 @@ const HomePage: FC> = obs <> -
+
idea2app logo @@ -105,10 +97,6 @@ const HomePage: FC> = obs
- {/*
- -
*/} -
( ]); return { - props: { member, leaderProjects, memberProjects } + props: JSON.parse( + JSON.stringify({ + member, + leaderProjects, + memberProjects + }) + ) }; } ); +@observer +class MemberDetailPage extends Component { + @observable accessor eventKey = '0'; -const MemberDetailPage: FC = observer( - ({ member, leaderProjects, memberProjects }) => ( -
- + handleChange = (event: SyntheticEvent, newValue: string) => (this.eventKey = newValue); -
-
- -
-
- - {Object.entries({ - [t('projects_as_leader')]: leaderProjects, - [t('projects_as_member')]: memberProjects - }).map(([label, list]) => ( - - {label} - -
- } - /> - ))} - -
+ render() { + const { member, leaderProjects, memberProjects } = this.props; + + const entries = Object.entries({ + [t('projects_as_leader')]: leaderProjects, + [t('projects_as_member')]: memberProjects + }); + return ( +
+ + +
+
    + +
+ +
+ + + {entries.map(([label, list], index) => ( + + {label} + + } + value={index + ''} + /> + ))} + + {entries.map(([label, list], index) => ( + + + + ))} + +
-
- ) -); + ); + } +} export default MemberDetailPage; diff --git a/pages/member/index.tsx b/pages/member/index.tsx index 8dc4e96..f3f9ba4 100644 --- a/pages/member/index.tsx +++ b/pages/member/index.tsx @@ -3,9 +3,8 @@ import { InferGetServerSidePropsType } from 'next'; import { cache, compose, errorLogger, translator } from 'next-ssr-middleware'; import { FC } from 'react'; +import { Frame } from '../../components/Layout/Frame'; import { MemberListLayout } from '../../components/Member/List'; -import { PageHead } from '../../components/PageHead'; -import { ScrollList } from '../../components/ScrollList'; import memberStore, { Member, MemberModel } from '../../models/Member'; import { i18n } from '../../models/Translation'; @@ -19,18 +18,13 @@ const { t } = i18n; const MemberListPage: FC> = observer( ({ list }) => ( -
- - -

{t('member')}

- - } - defaultData={list} - /> -
+ ) ); diff --git a/pages/open-source.tsx b/pages/open-source.tsx index c374e7c..640aa32 100644 --- a/pages/open-source.tsx +++ b/pages/open-source.tsx @@ -4,8 +4,7 @@ import { cache, compose, errorLogger, translator } from 'next-ssr-middleware'; import { FC } from 'react'; import { GitListLayout } from '../components/Git'; -import { PageHead } from '../components/PageHead'; -import { ScrollList } from '../components/ScrollList'; +import { Frame } from '../components/Layout/Frame'; import repositoryStore, { GitRepositoryModel } from '../models/Repository'; import { i18n } from '../models/Translation'; @@ -15,22 +14,16 @@ export const getServerSideProps = compose(cache(), errorLogger, translator(i18n) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment return { props: JSON.parse(JSON.stringify({ list })) }; }); - const { t } = i18n; const GitListPage: FC<{ list: GitRepository[] }> = observer(({ list }) => ( -
- - -

{t('open_source_project')}

- - } - defaultData={list} - /> -
+ )); export default GitListPage; diff --git a/pages/project/[id].tsx b/pages/project/[id].tsx index 3e077dc..cbcdd09 100644 --- a/pages/project/[id].tsx +++ b/pages/project/[id].tsx @@ -31,31 +31,32 @@ export const getServerSideProps = compose< return { props: { project: JSON.parse(JSON.stringify(project)) as Project, - repositories + repositories: JSON.parse(JSON.stringify(repositories)) } }; }); const ProjectDetailPage = observer( ({ project, repositories }: InferGetServerSidePropsType) => ( -
+
-
- +
+ + {/** + * @todo replace with LarkImage after R2 is ready + */} {String(project.name)} -
+

-

{t('open_source_project')}

+

{t('open_source_project')}

-
    +
      {repositories.map(repository => ( -
    • - -
    • + ))}
diff --git a/pages/project/index.tsx b/pages/project/index.tsx index ec267ba..26b9869 100644 --- a/pages/project/index.tsx +++ b/pages/project/index.tsx @@ -3,9 +3,8 @@ import { InferGetServerSidePropsType } from 'next'; import { cache, compose, errorLogger, translator } from 'next-ssr-middleware'; import { FC } from 'react'; -import { PageHead } from '../../components/PageHead'; +import { Frame } from '../../components/Layout/Frame'; import { ProjectListLayout } from '../../components/Project'; -import { ScrollList } from '../../components/ScrollList'; import projectStore, { Project, ProjectModel } from '../../models/Project'; import { i18n } from '../../models/Translation'; @@ -19,18 +18,13 @@ export const getServerSideProps = compose(cache(), errorLogger, translator(i18n) const ProjectListPage: FC> = observer( ({ list }) => ( -
- - -

{t('custom_software_development')}

- - } - defaultData={list} - /> -
+ ) );