Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions components/Base/CommentBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Giscus, { GiscusProps } from '@giscus/react';
import { observer } from 'mobx-react';
import { FC } from 'react';

import { i18n } from '../../models/Translation';

export const CommentBox: FC<Partial<GiscusProps>> = observer(props => {
const { currentLanguage } = i18n;

return (
<Giscus
{...props}
repo="kaiyuanshe/kaiyuanshe.github.io"
repoId="MDEwOlJlcG9zaXRvcnkxMzEwMDg4MTI="
mapping="pathname"
reactionsEnabled="1"
emitMetadata="1"
inputPosition="bottom"
theme="light"
lang={currentLanguage?.startsWith('zh-') ? currentLanguage : currentLanguage?.split('-')[0]}
/>
);
});
27 changes: 27 additions & 0 deletions components/Base/FileList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { text2color } from 'idea-react';
import { TableCellAttachment } from 'mobx-lark';
import { observer } from 'mobx-react';
import { FilePreview } from 'mobx-restful-table';
import { FC, useContext } from 'react';
import { Badge } from 'react-bootstrap';

import { I18nContext } from '../../models/Translation';

export const FileList: FC<{ data: TableCellAttachment[] }> = observer(({ data }) => {
const { t } = useContext(I18nContext);

return (
<section>
<h2>{t('file_download')}</h2>
<ol className="mt-3 mb-5">
{data.map(({ id, name, mimeType, attachmentToken }) => (
<li key={id + ''}>
<FilePreview type={mimeType} path={`/api/lark/file/${attachmentToken}`} />

<Badge bg={text2color(name, ['light'])}>{name}</Badge>
</li>
))}
</ol>
</section>
);
});
10 changes: 3 additions & 7 deletions components/LarkImage.tsx → components/Base/LarkImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ import { TableCellValue } from 'mobx-lark';
import { FC } from 'react';
import { Image, ImageProps } from 'react-bootstrap';

import { fileURLOf } from '../models/Base';
import { DefaultImage } from '../models/configuration';
import { DefaultImage } from '../../utility/configuration';
import { fileURLOf } from '../../utility/Lark';

export interface LarkImageProps extends Omit<ImageProps, 'src'> {
src?: TableCellValue;
}

export const LarkImage: FC<LarkImageProps> = ({
src = DefaultImage,
alt,
...props
}) => (
export const LarkImage: FC<LarkImageProps> = ({ src = DefaultImage, alt, ...props }) => (
<Image
fluid
loading="lazy"
Expand Down
26 changes: 26 additions & 0 deletions components/Base/TagNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { text2color } from 'idea-react';
import { FC, HTMLAttributes } from 'react';
import { Badge } from 'react-bootstrap';

export interface TagNavProps extends HTMLAttributes<HTMLElement> {
linkOf?: (value: string) => string;
list: string[];
onCheck?: (value: string) => any;
}

export const TagNav: FC<TagNavProps> = ({ className = '', list, linkOf, onCheck, ...props }) => (
<nav className={`d-flex flex-wrap gap-2 ${className}`} {...props}>
{list.map(tag => (
<Badge
key={tag + ''}
as="a"
className={`text-decoration-none ${onCheck ? 'cursor-pointer' : ''}`}
bg={text2color(tag + '', ['light'])}
href={linkOf?.(tag)}
onClick={onCheck && (() => onCheck(tag))}
>
{tag + ''}
</Badge>
))}
</nav>
);
34 changes: 34 additions & 0 deletions components/Base/ZodiacBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Link from 'next/link';
import { FC, ReactNode } from 'react';

export const ZodiacSigns = ['🐵', '🐔', '🐶', '🐷', '🐭', '🐮', '🐯', '🐰', '🐲', '🐍', '🐴', '🐐'];

export interface ZodiacBarProps {
startYear: number;
endYear?: number;
itemOf?: (year: number, zodiac: string) => { link?: string; title?: ReactNode };
}

export const ZodiacBar: FC<ZodiacBarProps> = ({
startYear,
endYear = new Date().getFullYear(),
itemOf,
}) => (
<ol className="list-inline d-flex flex-wrap justify-content-center gap-3">
{Array.from({ length: endYear - startYear + 1 }, (_, index) => {
const year = endYear - index;
const zodiac = ZodiacSigns[year % 12];
const { link = '#', title } = itemOf?.(year, zodiac) || {};

return (
<li key={index} className="list-inline-item border rounded">
<Link className="d-inline-block p-3 text-decoration-none text-center" href={link}>
<div className="fs-1">{zodiac}</div>

{title}
</Link>
</li>
);
})}
</ol>
);
53 changes: 53 additions & 0 deletions components/Department/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { FC } from 'react';

import { Department } from '../../models/Personnel/Department';
import { LarkImage } from '../Base/LarkImage';
import { TagNav } from '../Base/TagNav';

export interface GroupCardProps
extends Pick<Department, 'name' | 'logo' | 'tags' | 'summary' | 'email'> {
className?: string;
}

export const GroupCard: FC<GroupCardProps> = ({
className = '',
name,
logo,
tags,
summary,
email,
}) => (
<div className={`d-flex flex-column align-items-center ${className}`}>
<h3 className="h5 mb-3 flex-fill">
<a className="text-decoration-none text-dark" href={`/department/${name}`}>
{name as string}
</a>
</h3>

{logo && (
<LarkImage
className="mb-3 flex-fill object-fit-contain"
style={{ maxWidth: '10rem' }}
src={logo}
alt={name as string}
/>
)}
{tags && (
<TagNav linkOf={value => `/search/department?keywords=${value}`} list={tags as string[]} />
)}
{email && (
<dl className="mt-1 d-flex align-items-start">
<dt className="me-1">E-mail:</dt>
<dd>
<a href={`mailto:${email}`}>{email as string}</a>
</dd>
</dl>
)}
<p
className="mt-3 mb-0 text-wrap text-start overflow-auto"
style={{ maxWidth: '50vw', maxHeight: '10rem' }}
>
{summary as string}
</p>
</div>
);
60 changes: 60 additions & 0 deletions components/Department/OKRCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { text2color } from 'idea-react';
import { textJoin } from 'mobx-i18n';
import { observer } from 'mobx-react';
import { FC, useContext } from 'react';
import { Badge, Card } from 'react-bootstrap';
import { formatDate } from 'web-utility';

import { OKR } from '../../models/Governance/OKR';
import { I18nContext } from '../../models/Translation';

export const OKRCard: FC<OKR> = observer(
({
createdAt,
department,
object,
firstResult,
secondResult,
thirdResult,
planQ1,
planQ2,
planQ3,
planQ4,
}) => {
const { t } = useContext(I18nContext);

return (
<Card>
<Card.Header as="h3">{object?.toString()}</Card.Header>

<Card.Body className="overflow-auto" style={{ maxHeight: '25rem' }}>
<Card.Title as="h4">{t('key_results')}</Card.Title>
<Card.Text>
<ol>
{[firstResult, secondResult, thirdResult].map(
result => result && <li key={result + ''}>{result + ''}</li>,
)}
</ol>
</Card.Text>
<Card.Title as="h4">{textJoin(t('quarterly'), t('plan'))}</Card.Title>
<Card.Text>
<ol className="list-unstyled">
{[planQ1, planQ2, planQ3, planQ4].map((plan, index) => (
<li key={index}>
<Badge bg={text2color(`Q${index + 1}`, ['light'])}>Q{index + 1}</Badge>{' '}
{plan?.toString()}
</li>
))}
</ol>
</Card.Text>
</Card.Body>
<Card.Footer className="d-flex justify-content-between align-items-center">
<time dateTime={new Date(createdAt as number).toJSON()}>
{formatDate(createdAt as number)}
</time>
<Badge bg={text2color(department + '', ['light'])}>{department + ''}</Badge>
</Card.Footer>
</Card>
);
},
);
36 changes: 36 additions & 0 deletions components/Department/ReportCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { text2color } from 'idea-react';
import { observer } from 'mobx-react';
import { FC, useContext } from 'react';
import { Badge, Card } from 'react-bootstrap';
import { formatDate } from 'web-utility';

import { Report } from '../../models/Governance/Report';
import { I18nContext } from '../../models/Translation';

export const ReportCard: FC<Report> = observer(
({ createdAt, department, plan, progress, product, problem, meeting }) => {
const { t } = useContext(I18nContext);

return (
<Card>
<Card.Header as="h3">{meeting?.toString()}</Card.Header>
<Card.Body as="dl" className="mb-0 overflow-auto" style={{ maxHeight: '25rem' }}>
<Card.Title as="dt">{t('plan')}</Card.Title>
<Card.Text as="dd" dangerouslySetInnerHTML={{ __html: plan?.toString() || '' }} />
<Card.Title as="dt">{t('progress')}</Card.Title>
<Card.Text as="dd" dangerouslySetInnerHTML={{ __html: progress?.toString() || '' }} />
<Card.Title as="dt">{t('product')}</Card.Title>
<Card.Text as="dd" dangerouslySetInnerHTML={{ __html: product?.toString() || '' }} />
<Card.Title as="dt">{t('problem')}</Card.Title>
<Card.Text as="dd" dangerouslySetInnerHTML={{ __html: problem?.toString() || '' }} />
</Card.Body>
<Card.Footer className="d-flex justify-content-between align-items-center">
<time dateTime={new Date(createdAt as number).toJSON()}>
{formatDate(createdAt as number)}
</time>
<Badge bg={text2color(department + '', ['light'])}>{department + ''}</Badge>
</Card.Footer>
</Card>
);
},
);
68 changes: 68 additions & 0 deletions components/Department/Tree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { SVGCharts, Tooltip, TreeSeries } from 'echarts-jsx';
import { Loading } from 'idea-react';
import { observer } from 'mobx-react';
import { ObservedComponent } from 'mobx-react-helper';
import { Form } from 'react-bootstrap';
import { renderToStaticMarkup } from 'react-dom/server';

import { DepartmentModel, DepartmentNode } from '../../models/Personnel/Department';
import { i18n, I18nContext } from '../../models/Translation';
import { GroupCard } from './Card';

@observer
export default class DepartmentTree extends ObservedComponent<{}, typeof i18n> {
static contextType = I18nContext;

store = new DepartmentModel();

componentDidMount() {
this.store.getAll();
}

renderGroup(name: string) {
const group = this.store.allItems.find(({ name: n }) => n === name);

return renderToStaticMarkup(group?.summary ? <GroupCard {...group} /> : <></>);
}

jumpLink({ name }: DepartmentNode) {
if (name === '理事会') {
location.href = '/department/board-of-directors';
} else {
location.href = `/department/${name}`;
}
}

render() {
const { t } = this.observedContext,
{ downloading, activeShown, tree } = this.store;

return (
<>
{downloading > 0 && <Loading />}

<label className="d-flex justify-content-center gap-3">
{t('show_active_departments')}
<Form.Switch checked={activeShown} onChange={this.store.toggleActive} />
</label>

<SVGCharts style={{ height: '80vh' }}>
<Tooltip trigger="item" triggerOn="mousemove" />

<TreeSeries
label={{
position: 'left',
verticalAlign: 'middle',
fontSize: 16,
}}
tooltip={{
formatter: ({ name }) => this.renderGroup(name),
}}
data={[tree]}
onClick={({ data }) => this.jumpLink(data as DepartmentNode)}
/>
</SVGCharts>
</>
);
}
}
Loading
Loading