Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion pages/api/Lark/document/copy/[...slug].ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ router.post('/:type/:id', safeAPI, verifyJWT, async (context: Context) => {
? await lark.copyFile(`${type as 'wiki'}/${id}`, name, parentToken)
: await lark.copyFile(`${type as LarkDocumentPathType}/${id}`, name, parentToken);

const newId = 'token' in copiedFile ? copiedFile.token : copiedFile.obj_token;
const newId = 'token' in copiedFile ? copiedFile.token : copiedFile.node_token;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (ownerType && ownerId)
try {
Expand Down
279 changes: 212 additions & 67 deletions pages/hackathon/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import { BiTableSchema, TableCellLocation, TableCellUser } from 'mobx-lark';
import { BiTableSchema, TableCellLocation, TableCellUser, TableFormView } from 'mobx-lark';
import { observer } from 'mobx-react';
import Link from 'next/link';
import { cache, compose, errorLogger } from 'next-ssr-middleware';
import { FC, useContext } from 'react';
import {
Badge,
Button,
ButtonGroup,
Card,
Col,
Container,
Dropdown,
DropdownButton,
Row,
} from 'react-bootstrap';
import { Badge, Button, Card, Col, Container, Row } from 'react-bootstrap';
import { text2color, UserRankView } from 'idea-react';
import { formatDate } from 'web-utility';

Expand Down Expand Up @@ -45,7 +35,7 @@ export const getServerSideProps = compose<{ id: string }>(
async ({ params }) => {
const activity = await new ActivityModel().getOne(params!.id);

const { appId, tableIdMap } = activity.databaseSchema as BiTableSchema;
const { appId, tableIdMap } = (activity.databaseSchema || {}) as BiTableSchema;

const [people, organizations, agenda, prizes, templates, projects] = await Promise.all([
new PersonModel(appId, tableIdMap.Person).getAll(),
Expand Down Expand Up @@ -77,78 +67,236 @@ interface HackathonDetailProps {
};
}

const FormButtonBar = ['Person', 'Project', 'Product', 'Evaluation'];
const FormButtonBar = ['Person', 'Project', 'Product', 'Evaluation'] as const;

type FormGroupKey = (typeof FormButtonBar)[number];
type FormGroupMeta = Record<'title' | 'description' | 'eyebrow', I18nKey>;

interface FormGroup {
key: FormGroupKey;
list: TableFormView[];
meta: FormGroupMeta;
}

const FormSectionMeta: Record<FormGroupKey, FormGroupMeta> = {
Person: {
eyebrow: 'participants',
title: 'hackathon_participant_registration',
description: 'hackathon_participant_registration_description',
},
Project: {
eyebrow: 'hackathon_team_lead',
title: 'hackathon_project_registration',
description: 'hackathon_project_registration_description',
},
Product: {
eyebrow: 'hackathon_submission',
title: 'product_submission',
description: 'hackathon_product_submission_description',
},
Evaluation: {
eyebrow: 'hackathon_review',
title: 'hackathon_evaluation_entry',
description: 'hackathon_evaluation_entry_description',
},
};

const isPublicForm = ({ shared_limit }: TableFormView) =>
['anyone_editable'].includes(shared_limit as string);

const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackathon }) => {
const { t } = useContext(I18nContext);

const { name, summary, location, startTime, endTime, databaseSchema } = activity,
const {
name,
summary,
location,
startTime,
endTime,
databaseSchema,
host,
image,
type: activityType,
} = activity,
{ people, organizations, agenda, prizes, templates, projects } = hackathon;
const { forms } = databaseSchema as BiTableSchema;
const { forms } = (databaseSchema || {}) as BiTableSchema;

const formGroups = FormButtonBar.flatMap<FormGroup>(key => {
const list = forms[key]?.filter(isPublicForm) || [];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

return list[0] ? [{ key, list, meta: FormSectionMeta[key] }] : [];
});
const primaryForm =
formGroups.find(({ key }) => key === 'Person') ||
formGroups.find(({ key }) => key === 'Project') ||
formGroups[0];
const secondaryForm =
formGroups.find(({ key }) => key === 'Project' && key !== primaryForm?.key) ||
formGroups.find(({ key }) => key !== primaryForm?.key);
const heroStats = [
{ label: t('participants'), value: people.length },
{ label: t('projects'), value: projects.length },
{ label: t('templates'), value: templates.length },
{ label: t('organizations'), value: organizations.length },
];
const hostTags = (host as string[] | undefined)?.slice(0, 3) || [];
const agendaPreview = agenda.slice(0, 3);

return (
<>
<PageHead title={name as string} />

{/* Hero Section */}
<section className={styles.hero}>
<Container>
<h1 className={`text-center ${styles.title}`}>{name as string}</h1>
<p className={`text-center ${styles.description}`}>{summary as string}</p>
<Row className="align-items-center g-4">
<Col lg={7}>
<ul className="list-unstyled d-flex flex-wrap gap-3 mb-3">
<li className={styles.heroTag}>{(activityType as string) || t('hackathon')}</li>
{hostTags.map(tag => (
<li key={tag} className={styles.heroTag}>
{tag}
</li>
))}
</ul>

<Row className="mt-4 justify-content-center">
<Col md={4}>
<Card className={styles.infoCard}>
<Card.Body>
<h5 className="text-white mb-2">📍 {t('event_location')}</h5>
<p className="text-white-50 mb-0">
{(location as TableCellLocation)?.full_address}
</p>
</Card.Body>
</Card>
<h1 className={styles.title}>{name as string}</h1>
<p className={styles.description}>{summary as string}</p>

<ul className="list-unstyled d-flex flex-wrap gap-3 mt-4 mb-0">
{heroStats.map(({ label, value }) => (
<li key={label} className={styles.statChip}>
{value} {label}
</li>
))}
</ul>

<nav className="d-flex flex-wrap gap-3 mt-4">
{primaryForm && (
<Button href={primaryForm.list[0].shared_url} target="_blank" rel="noreferrer">
{t(primaryForm.meta.title)}
</Button>
)}
{secondaryForm && (
<Button
href={secondaryForm.list[0].shared_url}
target="_blank"
rel="noreferrer"
variant="light"
>
{t(secondaryForm.meta.title)}
</Button>
)}
{formGroups[0] && (
<Button href="#entry-hub" variant="outline-light">
{t('hackathon_view_all_entries')}
</Button>
)}
</nav>

<Row className="mt-4 g-3" md={2}>
<Col>
<Card className={styles.infoCard} body>
<h5 className="text-white mb-2">📍 {t('event_location')}</h5>
<address className="text-white-50 mb-0 fst-normal">
{(location as TableCellLocation)?.full_address}
</address>
</Card>
</Col>
<Col>
<Card className={styles.infoCard} body>
<h5 className="text-white mb-2">⏰ {t('event_duration')}</h5>
<p className="text-white-50 mb-0">
<time dateTime={startTime as string}>{formatDate(startTime as string)}</time>{' '}
- <time dateTime={endTime as string}>{formatDate(endTime as string)}</time>
</p>
</Card>
</Col>
</Row>
</Col>
<Col md={4}>
<Card className={styles.infoCard}>

<Col lg={5}>
<Card className={styles.heroVisualCard}>
{image && (
<div className={styles.heroImageWrap}>
<LarkImage
src={image}
alt={name as string}
className="w-100 h-100 object-fit-cover"
/>
</div>
)}
<Card.Body>
<h5 className="text-white mb-2">⏰ {t('event_duration')}</h5>
<p className="text-white-50 mb-0">
{formatDate(startTime as string)} - {formatDate(endTime as string)}
</p>
{agendaPreview[0] && (
<dl className="d-grid gap-3 mb-0">
<div className={`${styles.heroVisualHead} ${styles.heroVisualItem}`}>
<dt className={styles.heroVisualKicker}>{t('hackathon_agenda_preview')}</dt>
<dd className="fw-semibold">{agendaPreview[0].name as string}</dd>
</div>

{agendaPreview.map(({ name, startedAt, endedAt }) => (
<div
key={`${name as string}-${startedAt as string}`}
className={styles.heroVisualItem}
>
<dt className="fw-semibold">{name as string}</dt>
<dd>
<time dateTime={startedAt as string}>
{formatDate(startedAt as string)}
</time>{' '}
-{' '}
<time dateTime={endedAt as string}>
{formatDate(endedAt as string)}
</time>
</dd>
</div>
))}
</dl>
)}
</Card.Body>
</Card>
</Col>
</Row>

<ButtonGroup className="d-flex mt-3">
{FormButtonBar.map((key, index) => {
const list = forms[key]?.filter(
// @ts-expect-error Upstream types bug
({ shared_limit }) => shared_limit === 'anyone_editable',
);

return !list?.[0] ? null : list.length < 2 ? (
<Button href={list[0].shared_url} target="_blank" rel="noreferrer">
{index + 1}. {list[0].name}
</Button>
) : (
<DropdownButton
as={ButtonGroup}
title={`${index + 1}. ${t('product_submission')}`}
id={`dropdown-${key}`}
>
{list.map(({ name, shared_url }) => (
<Dropdown.Item key={name} href={shared_url} target="_blank" rel="noreferrer">
{name}
</Dropdown.Item>
))}
</DropdownButton>
);
})}
</ButtonGroup>
</Container>
</section>

<Container className="my-5">
{formGroups[0] && (
<section id="entry-hub" className={styles.section}>
<hgroup className="mb-0">
<p className={styles.sectionEyebrow}>{t('hackathon_action_hub')}</p>
<h2 className={styles.sectionTitle}>{t('hackathon_entry_flow')}</h2>
<p className={styles.sectionLead}>{t('hackathon_entry_flow_description')}</p>
</hgroup>

<Row as="ol" className="list-unstyled mt-4 g-3" md={2} xl={4}>
{formGroups.map(({ key, list, meta }, index) => (
<Col as="li" key={key}>
<Card className={styles.entryCard} body>
<span className={styles.entryStep}>
{t('hackathon_step')} {String(index + 1).padStart(2, '0')} · {t(meta.eyebrow)}
</span>
<h3 className="h5 text-white mt-2">{t(meta.title)}</h3>
<p className="text-white-50 mb-3">{t(meta.description)}</p>
<nav className="d-grid gap-2">
{list.map(({ name, shared_url }) => (
<Button
key={name}
href={shared_url}
target="_blank"
rel="noreferrer"
variant="light"
>
{name}
</Button>
))}
</nav>
</Card>
</Col>
))}
</Row>
</section>
)}

<section className={`${styles.section} ${styles.prizeSection}`}>
<h2 className={styles.sectionTitle}>🏆 {t('prizes')}</h2>
<div className="mt-4">
Expand Down Expand Up @@ -187,7 +335,6 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
</ol>
</section>

{/* Mid-front: Organizations - Horizontal logo layout */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>🏢 {t('organizations')}</h2>
<nav className={styles.orgContainer}>
Expand All @@ -205,7 +352,6 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
</nav>
</section>

{/* Mid-back: Templates - Using GitCard, 3-4 per row */}
<section className={`${styles.section} ${styles.templateSection}`}>
<h2 className={styles.sectionTitle}>🛠️ {t('templates')}</h2>
<Row className="mt-4 g-3" md={2} lg={3} xl={4}>
Expand All @@ -224,7 +370,6 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
</Row>
</section>

{/* Mid-back: Projects - Narrow cards, 3-4 per row */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>💡 {t('projects')}</h2>

Expand All @@ -251,15 +396,15 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
</a>
</div>
<div className="text-white-50 small">
<strong>{t('members')}:</strong> {(members as string[]).join(', ')}
<strong>{t('members')}:</strong>{' '}
{(members as string[] | undefined)?.join(', ') || '-'}
</div>
</Card>
</Col>
))}
</Row>
</section>

{/* Footer: Participants - Circular avatars only */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>👥 {t('participants')}</h2>
<nav className={styles.participantCloud}>
Expand Down
Loading