diff --git a/components/Activity/Hackathon/ActionHub.module.less b/components/Activity/Hackathon/ActionHub.module.less new file mode 100644 index 0000000..270acd4 --- /dev/null +++ b/components/Activity/Hackathon/ActionHub.module.less @@ -0,0 +1,177 @@ +@import './theme.less'; + +.section { + padding-top: clamp(3rem, 5vw, 4rem); +} + +.registerWrap { + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + align-items: start; + gap: 1.5rem; + + @media (max-width: 1199px) { + grid-template-columns: 1fr; + } +} + +.registerCard { + ; + background: linear-gradient(135deg, rgba(44, 232, 255, 0.08), rgba(123, 97, 255, 0.14)); +} + +.registerCardInner, +.entryHub { + padding: 1.55rem; +} + +.regEyebrow, +.entryEyebrow { + margin: 0; + color: @cyan; + font-weight: 700; + font-size: 0.75rem; + font-family: @heading; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.entryStep { + color: @cyan; + font-weight: 700; + font-size: 0.72rem; + font-family: @heading; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.regTitle, +.entryTitle { + margin: 0; + color: #fff; + font-weight: 800; + font-size: clamp(1.55rem, 3vw, 2.1rem); + line-height: 1.3; + font-family: @heading; +} + +.regTitle { + margin-top: 0; +} + +.regDesc, +.entryCard p { + color: @muted; + line-height: 1.75; +} + +.regDesc { + margin: 0; +} + +.regActions, +.entryLinks { + min-width: 0; +} + +.actionButton { + ; +} + +.actionButtonGhost { + ; + border-color: rgba(255, 255, 255, 0.18); + color: @muted; + + &:hover { + border-color: rgba(44, 232, 255, 0.36); + color: @cyan; + } +} + +.entryLink { + border: 1px solid rgba(44, 232, 255, 0.24); + border-radius: 8px; + background: rgba(44, 232, 255, 0.06); + padding: 0.42rem 0.85rem; + color: @cyan; + font-size: 0.8rem; + font-family: @heading; + letter-spacing: 0.06em; + text-decoration: none; + text-transform: uppercase; + + &:hover { + border-color: rgba(44, 232, 255, 0.48); + background: rgba(44, 232, 255, 0.12); + color: @cyan; + text-decoration: none; + } +} + +.regFacts { + margin: 0; + + li { + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 999px; + background: rgba(255, 255, 255, 0.05); + padding: 0.44rem 0.9rem; + color: @copy; + font-size: 0.82rem; + } +} + +.entryHubHead { + margin-bottom: 0; +} + +.entryTitle { + margin-top: 0; + font-size: 1.55rem; +} + +.entryCard { + ; + transition: + transform 0.22s ease, + border-color 0.22s ease; + padding: 1.3rem; + height: 100%; + + &:hover { + transform: translateY(-3px); + border-color: rgba(44, 232, 255, 0.26); + } +} + +.entryStep { + color: @cyan; +} + +.entryCard h4 { + margin: 0; + color: #fff; + font-size: 1.02rem; + font-family: @heading; +} + +.entryCard p { + margin: 0; +} + +.entryMetaRow { + margin: 0; +} + +.entryMeta { + border-radius: 999px; + background: rgba(255, 201, 77, 0.12); + padding: 0.28rem 0.7rem; + color: @gold; + font-weight: 700; + font-size: 0.72rem; + font-family: @heading; + letter-spacing: 0.08em; + text-transform: uppercase; +} diff --git a/components/Activity/Hackathon/ActionHub.tsx b/components/Activity/Hackathon/ActionHub.tsx new file mode 100644 index 0000000..b471a8b --- /dev/null +++ b/components/Activity/Hackathon/ActionHub.tsx @@ -0,0 +1,117 @@ +import type { FC, PropsWithChildren } from 'react'; +import { Col, Container, Row } from 'react-bootstrap'; + +import { HackathonHeroAction } from './Hero'; +import styles from './ActionHub.module.less'; + +export interface HackathonActionHubEntry { + count: number; + description: string; + eyebrow: string; + links: HackathonHeroAction[]; + title: string; +} + +export interface HackathonActionHubProps { + entries: HackathonActionHubEntry[]; + facts: string[]; + primaryAction?: HackathonHeroAction; + primaryDescription: string; + primaryTitle: string; + subtitle: string; + title: string; +} + +export const HackathonActionHubLink: FC<{ + action: HackathonHeroAction; + variant: 'ghost' | 'primary'; +}> = ({ action, variant }) => ( + + {action.label} + +); + +const ActionEntryCard: FC<{ entry: HackathonActionHubEntry; step: string }> = ({ entry, step }) => ( +
+ + {step} · {entry.eyebrow} + +

{entry.title}

+

{entry.description}

+ +
+ {entry.count} + + {entry.eyebrow} + +
+ + +
+); + +export const HackathonActionHub: FC> = ({ + children, + entries, + facts, + primaryAction, + primaryDescription, + primaryTitle, + subtitle, + title, +}) => ( +
+ +
+
+
+

{title}

+

{primaryTitle}

+

{primaryDescription}

+ + + +
    + {facts.map(fact => ( +
  • {fact}
  • + ))} +
+
+
+ +
+
+

{subtitle}

+

{title}

+
+ + + {entries.map((entry, index) => ( + + + + ))} + +
+
+
+
+); diff --git a/components/Activity/Hackathon/Awards.module.less b/components/Activity/Hackathon/Awards.module.less new file mode 100644 index 0000000..1451dc4 --- /dev/null +++ b/components/Activity/Hackathon/Awards.module.less @@ -0,0 +1,231 @@ +@import './theme.less'; + +; + +.supportEyebrow { + margin: 0 0 0.4rem; + color: @cyan; + font-weight: 700; + font-size: 0.75rem; + font-family: @heading; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.supportCopy { + min-width: 0; +} + +.awardsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1rem; +} + +.judgingCard { + margin-top: 0; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 22px; + background: + radial-gradient(circle at top right, rgba(255, 201, 77, 0.08), transparent 30%), + linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.015)), + rgba(8, 18, 39, 0.82); + padding: 1.4rem; +} + +.judgingTitle { + margin: 0; + color: #fff; + font-size: 1rem; + font-family: @heading; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.criteriaGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.9rem; + + @media (max-width: 991px) { + grid-template-columns: 1fr; + } +} + +.criterion { + grid-template-columns: auto 1fr; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 14px; + background: rgba(255, 255, 255, 0.03); + padding: 0.9rem; +} + +.criterionWeight { + color: @cyan; + font-size: 1.15rem; + font-family: @heading; +} + +.criterionTitle { + display: block; + color: #fff; + font-size: 0.92rem; + line-height: 1.45; +} + +.criterionDescription { + margin: 0; + color: @muted; + font-size: 0.87rem; + line-height: 1.6; +} + +.badgeTile { + ; + transition: + transform 0.22s ease, + border-color 0.22s ease; + overflow: hidden; + + &:hover { + transform: translateY(-4px); + border-color: rgba(255, 201, 77, 0.32); + } +} + +.badgeArtWrap { + padding: 1.1rem 1.1rem 0; +} + +.badgeArt { + border-radius: 22px; + background: rgba(255, 255, 255, 0.04); + aspect-ratio: 1; + width: 100%; + object-fit: cover; +} + +.badgeTileBody { + padding: 1.15rem 1.2rem 1.3rem; +} + +.badgeTierLabel { + color: @gold; + font-size: 0.74rem; + font-family: @heading; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.badgeTileTitle, +.supportTitle { + margin: 0; + color: #fff; + font-weight: 800; + font-family: @heading; +} + +.badgeTileTitle { + font-size: 1.05rem; +} + +.badgeTileCopy, +.supportDescription { + color: @muted; + line-height: 1.75; +} + +.badgeTileCopy { + margin: 0.7rem 0 1rem; +} + +.prizeMeta { + display: grid; + gap: 0.7rem; + margin: 0; + + div { + display: grid; + grid-template-columns: auto 1fr; + align-items: start; + gap: 0.8rem; + } + + dt, + dd { + margin: 0; + } + + dt { + color: @muted; + font-size: 0.72rem; + font-family: @heading; + letter-spacing: 0.08em; + text-transform: uppercase; + } + + dd { + color: #fff; + line-height: 1.5; + } +} + +.supportCard { + ; + display: grid; + grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.25fr); + gap: 1.25rem; + margin-top: 0; + padding: 1.55rem; + + @media (max-width: 1199px) { + grid-template-columns: 1fr; + } +} + +.supportTitle { + margin-top: 0; + font-size: 1.45rem; +} + +.partnerGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); + align-items: center; + gap: 0.8rem; +} + +.supportLink { + justify-self: start; + margin-top: 0; + border: 1px solid rgba(44, 232, 255, 0.3); + border-radius: 10px; + background: rgba(44, 232, 255, 0.08); + padding: 0.65rem 1rem; + color: #fff; + font-size: 0.82rem; + font-family: @heading; + letter-spacing: 0.06em; + text-decoration: none; + text-transform: uppercase; + + &:hover { + border-color: rgba(44, 232, 255, 0.52); + color: #fff; + text-decoration: none; + } +} + +.partnerLink { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px; + background: rgba(255, 255, 255, 0.04); + padding: 1rem; + min-height: 104px; +} + +.partnerLogo { + max-width: 100%; + max-height: 58px; + object-fit: contain; +} diff --git a/components/Activity/Hackathon/Awards.tsx b/components/Activity/Hackathon/Awards.tsx new file mode 100644 index 0000000..c8b2ef5 --- /dev/null +++ b/components/Activity/Hackathon/Awards.tsx @@ -0,0 +1,166 @@ +import { TableCellValue } from 'mobx-lark'; +import { FC } from 'react'; +import { Container } from 'react-bootstrap'; + +import { LarkImage } from '../../LarkImage'; +import type { HackathonHeroAction } from './Hero'; +import styles from './Awards.module.less'; + +export interface HackathonAwardsMeta { + label: string; + value: Value; +} + +export interface HackathonPrizeItem { + description: string; + id: string; + image?: TableCellValue; + meta: HackathonAwardsMeta[]; + tier: string; + title: string; +} + +export interface HackathonOrganizationItem { + href?: string; + id: string; + logo?: TableCellValue; + name: string; +} + +export type HackathonJudgingCriterion = Record<'id' | 'title' | 'weight' | 'description', string>; + +export interface HackathonAwardsProps { + criteria?: HackathonJudgingCriterion[]; + organizations: HackathonOrganizationItem[]; + prizes: HackathonPrizeItem[]; + subtitle: string; + supportAction?: HackathonHeroAction; + supportDescription: string; + supportEyebrow: string; + supportTitle: string; + title: string; +} + +const PrizeCard: FC = ({ description, image, meta, tier, title }) => ( +
+ {image && ( +
+ +
+ )} + +
+ {tier} +

{title}

+

{description}

+ +
+ {meta.map(({ label, value }) => ( +
+
{label}
+
{value}
+
+ ))} +
+
+
+); + +const OrganizationLogo: FC = ({ href, logo, name }) => { + const imageNode = ; + + return href ? ( + + {imageNode} + + ) : ( +
+ {imageNode} +
+ ); +}; + +export const HackathonAwards: FC = ({ + criteria, + organizations, + prizes, + subtitle, + supportAction, + supportDescription, + supportEyebrow, + supportTitle, + title, +}) => ( +
+ + {prizes[0] && ( + <> +
+

{title}

+

{subtitle}

+
+
+ +
+ {prizes.map(prize => ( + + ))} +
+ + {criteria?.[0] && ( +
+

{subtitle}

+ +
    + {criteria.map(({ description, id, title, weight }) => ( +
  • + {weight} +
    + {title} +

    {description}

    +
    +
  • + ))} +
+
+ )} + + )} + + {organizations[0] && ( +
+
+

{supportEyebrow}

+

{supportTitle}

+

{supportDescription}

+
+ + + + {supportAction && ( + + {supportAction.label} + + )} +
+ )} +
+
+); diff --git a/components/Activity/Hackathon/FAQ.module.less b/components/Activity/Hackathon/FAQ.module.less new file mode 100644 index 0000000..3afdf74 --- /dev/null +++ b/components/Activity/Hackathon/FAQ.module.less @@ -0,0 +1,74 @@ +@import './theme.less'; + +; + +.faqGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (max-width: 991px) { + grid-template-columns: 1fr; + } +} + +.faqItem { + transition: + border-color 0.24s ease, + transform 0.24s ease, + box-shadow 0.24s ease; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 20px; + background: + radial-gradient(circle at top right, rgba(44, 232, 255, 0.08), transparent 36%), + linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.015)), + rgba(8, 18, 39, 0.8); + padding: 1.35rem 1.5rem; + + &:hover { + transform: translateY(-3px); + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.2); + border-color: rgba(44, 232, 255, 0.24); + } + + &[open] { + border-color: rgba(44, 232, 255, 0.28); + } +} + +.faqQuestion { + position: relative; + cursor: pointer; + padding-right: 2rem; + color: #fff; + font-weight: 700; + font-size: 1rem; + line-height: 1.6; + list-style: none; + + &::marker, + &::-webkit-details-marker { + display: none; + } + + &::after { + position: absolute; + top: 0; + right: 0; + transition: transform 0.24s ease; + content: '+'; + color: @cyan; + font-weight: 300; + font-size: 1.5rem; + line-height: 1; + } +} + +.faqItem[open] .faqQuestion::after { + transform: rotate(45deg); +} + +.faqAnswer { + margin: 0; + color: @muted; + font-size: 0.95rem; + line-height: 1.8; +} diff --git a/components/Activity/Hackathon/FAQ.tsx b/components/Activity/Hackathon/FAQ.tsx new file mode 100644 index 0000000..8f9676d --- /dev/null +++ b/components/Activity/Hackathon/FAQ.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; +import { Container } from 'react-bootstrap'; + +import styles from './FAQ.module.less'; + +export type HackathonFAQItem = Record<'id' | 'question' | 'answer', string>; + +export interface HackathonFAQProps { + items: HackathonFAQItem[]; + subtitle: string; + title: string; +} + +export const HackathonFAQ: FC = ({ items, subtitle, title }) => ( +
+ +
+

{title}

+

{subtitle}

+
+
+ +
+ {items.map(({ answer, id, question }) => ( +
+ {question} +

{answer}

+
+ ))} +
+
+
+); diff --git a/components/Activity/Hackathon/Hero.module.less b/components/Activity/Hackathon/Hero.module.less new file mode 100644 index 0000000..c04735b --- /dev/null +++ b/components/Activity/Hackathon/Hero.module.less @@ -0,0 +1,457 @@ +@import './theme.less'; + +.hero { + position: relative; + padding: 7.75rem 0 clamp(3.5rem, 6vw, 5rem); + min-height: clamp(720px, 92vh, 900px); + overflow: hidden; + + scroll-margin-top: 5rem; + color: @copy; + + &::before, + &::after { + position: absolute; + inset: 0; + pointer-events: none; + content: ''; + } + + &::before { + opacity: 0.45; + mask-image: radial-gradient(circle at center, black 42%, transparent 100%); + background-image: + linear-gradient(rgba(44, 232, 255, 0.08) 1px, transparent 1px), + linear-gradient(90deg, rgba(44, 232, 255, 0.08) 1px, transparent 1px); + background-size: 54px 54px; + } + + &::after { + inset: auto 0 0; + background: linear-gradient(180deg, transparent, rgba(5, 8, 20, 0.95)); + height: 140px; + } +} + +.siteHeader { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 20; + backdrop-filter: blur(20px) saturate(1.4); + border-bottom: 1px solid rgba(44, 232, 255, 0.12); + background: rgba(4, 4, 15, 0.78); +} + +.headerContainer { + min-width: 0; +} + +.logoText { + color: #fff; + font-weight: 900; + font-size: clamp(1.25rem, 2vw, 1.75rem); + font-family: @heading; + letter-spacing: 0.1em; + text-decoration: none; + text-transform: uppercase; + + &:hover { + color: #fff; + text-decoration: none; + } +} + +.headerNav { + min-width: 0; +} + +.headerNavLink { + color: rgba(255, 255, 255, 0.72); + font-size: 0.95rem; + font-family: @heading; + letter-spacing: 0.08em; + text-decoration: none; + + &:hover { + color: #fff; + text-decoration: none; + } +} + +.heroInner { + display: grid; + position: relative; + grid-template-columns: minmax(0, 0.92fr) minmax(320px, 440px); + align-items: start; + gap: clamp(2rem, 4vw, 4rem); + z-index: 1; +} + +.heroContent { + gap: 1.5rem; + max-width: 680px; +} + +.heroBadge { + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 999px; + background: rgba(255, 255, 255, 0.06); + padding: 0.42rem 0.9rem; + color: @copy; + font-weight: 700; + font-size: 0.78rem; + font-family: @heading; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.heroBadgeCyan { + box-shadow: 0 0 18px rgba(44, 232, 255, 0.22); + border-color: rgba(44, 232, 255, 0.28); + color: @cyan; +} + +.heroBadgeGold { + box-shadow: 0 0 18px rgba(255, 201, 77, 0.18); + border-color: rgba(255, 201, 77, 0.24); + color: @gold; +} + +.heroBadgeGreen { + box-shadow: 0 0 18px rgba(72, 241, 168, 0.2); + border-color: rgba(72, 241, 168, 0.24); + color: @green; +} + +.heroBadgeRose { + box-shadow: 0 0 18px rgba(255, 120, 186, 0.18); + border-color: rgba(255, 120, 186, 0.24); + color: @rose; +} + +.title { + gap: 0.35rem; + line-height: 1.02; + font-family: @heading; + letter-spacing: 0.03em; +} + +.heroTitlePrimary { + display: block; + color: #fff; + font-weight: 900; + font-size: clamp(3rem, 6vw, 5.25rem); +} + +.heroTitleAccent { + display: block; + background: linear-gradient(135deg, @gold 0%, #ff9a1a 100%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + font-weight: 900; + font-size: clamp(2.65rem, 5.5vw, 4.75rem); +} + +.heroTitleSecondary { + display: block; + background: linear-gradient(90deg, @cyan, @gold 78%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + font-size: clamp(1rem, 1.7vw, 1.45rem); + letter-spacing: 0.32em; + text-transform: none; +} + +.description { + margin: 0; + max-width: 62ch; + color: @muted; + font-size: 1.08rem; + line-height: 1.8; + font-family: @body; + + strong { + color: #fff; + } +} + +.countdownWrap { + display: grid; + gap: 0.75rem; + max-width: 520px; +} + +.countdownLabel { + color: @muted; + font-size: 0.72rem; + font-family: @heading; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.countdownGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.8rem; +} + +.countdownCell { + gap: 0.7rem; + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.03), + 0 0 26px rgba(44, 232, 255, 0.08); + border: 1px solid rgba(44, 232, 255, 0.26); + border-radius: 18px; + background: linear-gradient(180deg, rgba(44, 232, 255, 0.08), rgba(44, 232, 255, 0.03)); + min-height: 120px; + + strong { + color: #fff; + font-size: clamp(2.3rem, 4vw, 3.8rem); + line-height: 1; + font-family: @heading; + letter-spacing: 0.08em; + } + + span { + color: rgba(255, 255, 255, 0.72); + font-size: 0.82rem; + font-family: @heading; + letter-spacing: 0.2em; + text-transform: uppercase; + } +} + +.actionButton { + box-shadow: 0 0 28px rgba(44, 232, 255, 0.14); + border-color: rgba(44, 232, 255, 0.48); + background: rgba(44, 232, 255, 0.08); + color: #fff; + + &:hover { + box-shadow: 0 0 36px rgba(44, 232, 255, 0.22); + border-color: rgba(44, 232, 255, 0.72); + color: #fff; + } +} + +.actionButtonGhost { + border-color: rgba(255, 255, 255, 0.16); + background: rgba(255, 255, 255, 0.03); + color: rgba(255, 255, 255, 0.82); + + &:hover { + border-color: rgba(255, 255, 255, 0.28); + color: #fff; + } +} + +.statChip { + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 999px; + background: rgba(255, 255, 255, 0.04); + padding: 0.45rem 0.75rem; + color: rgba(255, 255, 255, 0.72); + font-size: 0.82rem; + letter-spacing: 0.3px; +} + +.heroVisual { + position: relative; + min-height: 640px; +} + +.heroVisualCard { + position: relative; + z-index: 1; + backdrop-filter: blur(18px); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 28px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02)); + overflow: hidden; +} + +.heroVisualHead { + padding: 1.15rem 1.25rem 0; +} + +.visualKicker, +.visualChip, +.floatingLabel { + font-weight: 700; + font-size: 0.75rem; + font-family: @heading; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.visualKicker, +.floatingLabel { + color: @muted; +} + +.visualChip { + color: @gold; +} + +.heroImageFrame { + position: relative; + margin: 1rem 1.25rem 0; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 28px; + background: linear-gradient(135deg, rgba(44, 232, 255, 0.08), rgba(123, 97, 255, 0.14)); + min-height: 420px; + overflow: hidden; + + :global(img) { + position: relative; + z-index: 2; + } +} + +.mascotGlow { + position: absolute; + z-index: 1; + filter: blur(28px); + inset: 12% 18%; + border-radius: 999px; + background: radial-gradient(circle, rgba(44, 232, 255, 0.35), transparent 70%); +} + +.heroImageFallback { + min-height: 420px; + color: @cyan; + font-weight: 900; + font-size: clamp(1.25rem, 3vw, 2rem); + font-family: @heading; + letter-spacing: 0.12em; + text-align: center; + text-transform: uppercase; +} + +.heroVisualFoot { + padding: 1.15rem 1.25rem 1.35rem; +} + +.heroVisualTitle { + margin: 0 0 0.35rem; + color: #fff; + font-size: 1.05rem; + font-family: @heading; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.heroVisualCopy, +.heroVisualDescription { + color: @muted; + line-height: 1.75; +} + +.heroVisualCopy { + color: #fff; + font-size: 0.95rem; +} + +.heroFloatingCard { + position: absolute; + transform: rotate(-2.5deg); + z-index: 3; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.28); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 24px; + background: rgba(8, 18, 39, 0.88); + padding: 1rem 1.1rem; + max-width: 260px; + + strong { + display: block; + margin-top: 0.35rem; + color: #fff; + font-size: 1rem; + line-height: 1.4; + } + + p { + margin: 0.45rem 0 0; + color: @muted; + font-size: 0.92rem; + line-height: 1.6; + } +} + +.heroFloatingCardTop { + top: 0.75rem; + right: -1rem; +} + +.heroFloatingCardBottom { + bottom: 1.2rem; + left: -1rem; + transform: rotate(2deg); +} + +@media (max-width: 1199px) { + .heroInner { + grid-template-columns: 1fr; + } + + .heroVisual { + min-height: 0; + } + + .heroFloatingCardTop { + right: 1rem; + } + + .heroFloatingCardBottom { + left: 1rem; + } +} + +@media (max-width: 991px) { + .title { + font-size: clamp(2.2rem, 9vw, 3.45rem); + } +} + +@media (max-width: 767px) { + .hero { + padding-top: 6rem; + min-height: 0; + } + + .heroContent { + gap: 1.25rem; + } + + .heroFloatingCard { + position: static; + transform: none; + max-width: none; + } + + .heroImageFrame { + min-height: 260px; + } + + .heroBadge { + font-size: 0.72rem; + } + + .countdownGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .countdownCell { + min-height: 96px; + + strong { + font-size: 2rem; + } + } +} diff --git a/components/Activity/Hackathon/Hero.tsx b/components/Activity/Hackathon/Hero.tsx new file mode 100644 index 0000000..855aee7 --- /dev/null +++ b/components/Activity/Hackathon/Hero.tsx @@ -0,0 +1,268 @@ +import { TableCellValue } from 'mobx-lark'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { Container } from 'react-bootstrap'; + +import { LarkImage } from '../../LarkImage'; +import styles from './Hero.module.less'; + +export type HackathonHeroNavItem = Record<'label' | 'href', string>; + +export interface HackathonHeroAction extends HackathonHeroNavItem { + external?: boolean; +} + +export type HackathonHeroCard = Record<'title' | 'description' | 'eyebrow', string>; + +export interface HackathonHeroProps extends Record< + | `visual${'Kicker' | 'Title' | 'Copy' | 'Chip'}` + | 'name' + | 'subtitle' + | 'description' + | 'locationText' + | 'imageFallback', + string +> { + badges: string[]; + bottomCard?: HackathonHeroCard; + chips?: string[]; + countdownLabel?: string; + countdownUnitLabels: string[]; + countdownTo?: string; + image?: TableCellValue; + navigation: HackathonHeroNavItem[]; + primaryAction: HackathonHeroAction; + secondaryAction: HackathonHeroAction; + topCard?: HackathonHeroCard; +} + +const BadgeToneClass = [ + styles.heroBadgeCyan, + styles.heroBadgeGold, + styles.heroBadgeGreen, + styles.heroBadgeRose, +]; + +const HeroLink: FC<{ + action: HackathonHeroAction; + variant: 'ghost' | 'primary'; +}> = ({ action, variant }) => ( + + {action.label} + +); + +const NavLink: FC<{ item: HackathonHeroNavItem }> = ({ item }) => ( + + {item.label} + +); + +const FloatingCard: FC<{ + card: HackathonHeroCard; + position: 'bottom' | 'top'; +}> = ({ card, position }) => ( +
+ {card.eyebrow} + {card.title} +

{card.description}

+
+); + +const useCountdown = (countdownTo?: string) => { + const target = useMemo(() => { + const value = countdownTo ? new Date(countdownTo).getTime() : NaN; + + return Number.isFinite(value) ? value : NaN; + }, [countdownTo]); + const [now, setNow] = useState(null); + + useEffect(() => { + if (!Number.isFinite(target)) return; + + setNow(Date.now()); + + const timer = window.setInterval(() => setNow(Date.now()), 1000); + + return () => window.clearInterval(timer); + }, [target]); + + return useMemo(() => { + if (!Number.isFinite(target) || now === null) return ['--', '--', '--', '--']; + + const rest = Math.max(0, target - now); + const totalSeconds = Math.floor(rest / 1000); + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + return [days, hours, minutes, seconds].map(value => String(value).padStart(2, '0')); + }, [now, target]); +}; + +const splitHeroTitle = (name: string, subtitle: string) => { + const segments = name.split(/\s+/).filter(Boolean); + + if (segments.length < 3) + return { + primary: name, + secondary: subtitle, + }; + + return { + primary: segments.slice(0, Math.max(1, segments.length - 2)).join(' '), + secondary: segments.slice(-2).join(' '), + }; +}; + +export const HackathonHero: FC = ({ + badges, + bottomCard, + chips, + countdownLabel, + countdownUnitLabels, + countdownTo, + description, + image, + imageFallback, + locationText, + name, + navigation, + primaryAction, + secondaryAction, + subtitle, + topCard, + visualChip, + visualCopy, + visualKicker, + visualTitle, +}) => { + const countdown = useCountdown(countdownTo); + const title = splitHeroTitle(name, subtitle); + + return ( +
+
+ +
+ + {name} + + + +
+
+
+ + +
+
+
    + {badges.map((badge, index) => ( +
  • + {badge} +
  • + ))} +
+ +

+ {title.primary} + {title.secondary} + {subtitle} +

+ +

{description}

+ + {countdownTo && ( +
+ {countdownLabel && ( +

{countdownLabel}

+ )} + +
    + {countdown.map((value, index) => ( +
  1. + {value} + {countdownUnitLabels[index]} +
  2. + ))} +
+
+ )} + + + + {chips?.[0] && ( +
    + {chips.map(chip => ( +
  • + {chip} +
  • + ))} +
+ )} +
+ +
+
+
+ {visualKicker} + {visualChip} +
+ +
+
+ + {image ? ( + + ) : ( +
+ {imageFallback} +
+ )} +
+ +
+

{locationText}

+

{visualTitle}

+

{visualCopy}

+
+
+ + {topCard && } + {bottomCard && } +
+
+
+
+ ); +}; diff --git a/components/Activity/Hackathon/Overview.module.less b/components/Activity/Hackathon/Overview.module.less new file mode 100644 index 0000000..4f9922a --- /dev/null +++ b/components/Activity/Hackathon/Overview.module.less @@ -0,0 +1,86 @@ +@import './theme.less'; + +; + +.themePanel { + position: relative; + border: 1px solid rgba(255, 201, 77, 0.16); + border-radius: 28px; + background: linear-gradient(135deg, rgba(255, 201, 77, 0.08), rgba(123, 97, 255, 0.12)); + padding: 1.5rem 1.6rem; + + &::before { + position: absolute; + top: -1.9rem; + left: 1rem; + content: '"'; + color: rgba(255, 201, 77, 0.18); + font-size: 6rem; + line-height: 1; + font-family: serif; + } +} + +.themeText { + color: #fff; + font-weight: 900; + font-size: clamp(1.35rem, 2.6vw, 2rem); + font-family: @heading; +} + +.themeSub { + margin: 0; + color: @muted; + line-height: 1.75; +} + +.trackCard { + position: relative; + transition: + transform 0.22s ease, + border-color 0.22s ease; + padding: 1.4rem; + height: 100%; + overflow: hidden; + + &::before { + position: absolute; + top: 0; + right: 0; + left: 0; + opacity: 0.75; + background: linear-gradient(90deg, @cyan, @purple); + height: 2px; + content: ''; + } + + &:hover { + transform: translateY(-4px); + border-color: rgba(44, 232, 255, 0.26); + } +} + +.trackIcon { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); + border-radius: 18px; + background: rgba(255, 255, 255, 0.06); + width: 54px; + height: 54px; + font-size: 1.35rem; +} + +.trackName { + margin: 0; + color: #fff; + font-weight: 800; + font-size: 1rem; + line-height: 1.3; + font-family: @heading; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.trackDesc { + color: @muted; + line-height: 1.75; +} diff --git a/components/Activity/Hackathon/Overview.tsx b/components/Activity/Hackathon/Overview.tsx new file mode 100644 index 0000000..7a54ff2 --- /dev/null +++ b/components/Activity/Hackathon/Overview.tsx @@ -0,0 +1,56 @@ +import { FC } from 'react'; +import { Col, Container, Row } from 'react-bootstrap'; + +import styles from './Overview.module.less'; + +export type HackathonOverviewCard = Record<'title' | 'description' | 'icon', string>; + +export type HackathonOverviewProps = Record< + 'title' | 'subtitle' | 'themeText' | 'themeSub', + string +> & { + cards: HackathonOverviewCard[]; +}; + +const OverviewCard: FC = ({ description, icon, title }) => ( +
+ + {icon} + +
{title}
+

{description}

+
+); + +export const HackathonOverview: FC = ({ + cards, + subtitle, + themeSub, + themeText, + title, +}) => ( +
+ +
+

{title}

+

{subtitle}

+
+
+ +
+
{themeText}
+

{themeSub}

+
+ + + {cards.map(card => ( + + + + ))} + +
+
+); diff --git a/components/Activity/Hackathon/Participants.module.less b/components/Activity/Hackathon/Participants.module.less new file mode 100644 index 0000000..d08e0b4 --- /dev/null +++ b/components/Activity/Hackathon/Participants.module.less @@ -0,0 +1,55 @@ +@import './theme.less'; + +; + +.participantCloud { + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + + @media (max-width: 767px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +.participantCard { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); + border-radius: 24px; + background: rgba(255, 255, 255, 0.04); + padding: 1.1rem 0.9rem; + color: #fff; + text-decoration: none; +} + +.participantCard:hover { + color: @cyan; + text-decoration: none; +} + +.avatar { + border: 2px solid rgba(44, 232, 255, 0.2); + border-radius: 50%; + width: 84px; + height: 84px; + object-fit: cover; +} + +.participantName { + font-size: 0.92rem; + line-height: 1.4; +} + +.toggleButton { + border: 1px solid rgba(44, 232, 255, 0.28); + border-radius: 10px; + background: rgba(44, 232, 255, 0.06); + padding: 0.62rem 0.95rem; + color: #fff; + font-size: 0.78rem; + font-family: @heading; + letter-spacing: 0.08em; + text-transform: uppercase; + + &:hover { + border-color: rgba(44, 232, 255, 0.48); + background: rgba(44, 232, 255, 0.11); + } +} diff --git a/components/Activity/Hackathon/Participants.tsx b/components/Activity/Hackathon/Participants.tsx new file mode 100644 index 0000000..33107da --- /dev/null +++ b/components/Activity/Hackathon/Participants.tsx @@ -0,0 +1,92 @@ +import { TableCellValue } from 'mobx-lark'; +import { FC, useMemo, useState } from 'react'; +import { Container } from 'react-bootstrap'; + +import { LarkImage } from '../../LarkImage'; +import styles from './Participants.module.less'; + +export interface HackathonParticipantItem { + avatar?: TableCellValue; + githubLink?: string; + id: string; + name: string; +} + +export interface HackathonParticipantsProps { + initialVisible?: number; + participants: HackathonParticipantItem[]; + showLessLabel?: string; + showMoreLabel?: string; + subtitle: string; + title: string; +} + +const ParticipantCard: FC = ({ avatar, githubLink, name }) => { + const content = ( + <> + + {name} + + ); + + return githubLink ? ( + + {content} + + ) : ( +
+ {content} +
+ ); +}; + +export const HackathonParticipants: FC = ({ + initialVisible = 12, + participants, + showLessLabel, + showMoreLabel, + subtitle, + title, +}) => { + const [expanded, setExpanded] = useState(false); + const visibleParticipants = useMemo( + () => (expanded ? participants : participants.slice(0, initialVisible)), + [expanded, initialVisible, participants], + ); + const hasMore = participants.length > initialVisible; + + return ( +
+ +
+

{title}

+

{subtitle}

+
+
+ +
+ {visibleParticipants.map(participant => ( + + ))} +
+ + {hasMore && ( + + )} +
+
+ ); +}; diff --git a/components/Activity/Hackathon/Resources.module.less b/components/Activity/Hackathon/Resources.module.less new file mode 100644 index 0000000..90c3ee0 --- /dev/null +++ b/components/Activity/Hackathon/Resources.module.less @@ -0,0 +1,168 @@ +@import './theme.less'; + +; + +.resourceCard, +.projectCard { + ; + transition: + transform 0.22s ease, + border-color 0.22s ease; + padding: 1.25rem; + height: 100%; + + &:hover { + transform: translateY(-3px); + border-color: rgba(44, 232, 255, 0.26); + } +} + +.resourceHead, +.projectTop { + min-width: 0; +} + +.projectHead { + margin: 0; +} + +.resourceTitle, +.projectTitle { + margin: 0; + color: #fff; + font-weight: 800; + font-size: 1.02rem; + font-family: @heading; +} + +.projectTitle a { + color: #fff; + text-decoration: none; + + &:hover { + color: @cyan; + text-decoration: none; + } +} + +.resourceDescription, +.projectSummary { + color: @muted; + line-height: 1.75; +} + +.topicList { + margin: 0; +} + +.topicChip, +.topicChipMuted { + border-radius: 999px; + padding: 0.34rem 0.72rem; + font-size: 0.76rem; + + @media (max-width: 767px) { + font-size: 0.72rem; + } +} + +.topicChip { + background: rgba(44, 232, 255, 0.12); + color: @cyan; +} + +.topicChipMuted { + background: rgba(255, 255, 255, 0.08); + color: @copy; +} + +.resourceLinks { + min-width: 0; +} + +.entryLink { + border: 1px solid rgba(44, 232, 255, 0.24); + border-radius: 8px; + background: rgba(44, 232, 255, 0.06); + padding: 0.42rem 0.85rem; + color: @cyan; + font-size: 0.8rem; + font-family: @heading; + letter-spacing: 0.06em; + text-decoration: none; + text-transform: uppercase; + + &:hover { + border-color: rgba(44, 232, 255, 0.48); + background: rgba(44, 232, 255, 0.12); + color: @cyan; + text-decoration: none; + } +} + +.toggleButton { + border: 1px solid rgba(44, 232, 255, 0.28); + border-radius: 10px; + background: rgba(44, 232, 255, 0.06); + padding: 0.62rem 0.95rem; + color: #fff; + font-size: 0.78rem; + font-family: @heading; + letter-spacing: 0.08em; + text-transform: uppercase; + + &:hover { + border-color: rgba(44, 232, 255, 0.48); + background: rgba(44, 232, 255, 0.11); + } +} + +.scoreCircle { + box-shadow: 0 0 24px rgba(44, 232, 255, 0.18); + border: 1px solid rgba(44, 232, 255, 0.2); + border-radius: 50%; + background: rgba(44, 232, 255, 0.1); + width: 62px; + height: 62px; + color: @cyan; + font-weight: 800; + font-size: 1.05rem; + font-family: @heading; + + @media (max-width: 767px) { + width: 54px; + height: 54px; + font-size: 0.92rem; + } +} + +.projectMeta { + display: grid; + gap: 0.7rem; + margin: 0; + + div { + display: grid; + grid-template-columns: auto 1fr; + align-items: start; + gap: 0.8rem; + } + + dt, + dd { + margin: 0; + } + + dt { + color: @muted; + font-size: 0.72rem; + font-family: @heading; + letter-spacing: 0.08em; + text-transform: uppercase; + } + + dd { + color: #fff; + line-height: 1.5; + } +} diff --git a/components/Activity/Hackathon/Resources.tsx b/components/Activity/Hackathon/Resources.tsx new file mode 100644 index 0000000..962e942 --- /dev/null +++ b/components/Activity/Hackathon/Resources.tsx @@ -0,0 +1,213 @@ +import { FC, useMemo, useState } from 'react'; +import { Col, Container, Row } from 'react-bootstrap'; + +import type { HackathonAwardsMeta } from './Awards'; +import styles from './Resources.module.less'; + +export interface HackathonTemplateItem { + description: string; + id: string; + languages: string[]; + previewLabel: string; + previewUrl?: string; + sourceLabel: string; + sourceUrl?: string; + tags: string[]; + title: string; +} + +export interface HackathonProjectMeta extends HackathonAwardsMeta { + valueHref?: string; +} + +export interface HackathonProjectItem { + description: string; + id: string; + link: string; + meta: HackathonProjectMeta[]; + score: string; + title: string; +} + +export interface HackathonResourcesProps extends Record< + `project${'Subtitle' | 'Title'}` | `template${'Subtitle' | 'Title'}`, + string +> { + projectInitialVisible?: number; + projectItems: HackathonProjectItem[]; + showLessLabel?: string; + showMoreLabel?: string; + templateInitialVisible?: number; + templateItems: HackathonTemplateItem[]; +} + +const TemplateCard: FC = ({ + description, + languages, + previewLabel, + previewUrl, + sourceLabel, + sourceUrl, + tags, + title, +}) => ( +
+
+

{title}

+
+

{description}

+ +
    + {languages.map(language => ( +
  • + {language} +
  • + ))} + {tags.map(tag => ( +
  • + {tag} +
  • + ))} +
+ + +
+); + +const ProjectCard: FC = ({ description, link, meta, score, title }) => ( +
+
+
+

+ {title} +

+ {description &&

{description}

} +
+
+ {score} +
+
+ +
+ {meta.map(({ label, value, valueHref }) => ( +
+
{label}
+
{valueHref ? {value} : value}
+
+ ))} +
+
+); + +export const HackathonResources: FC = ({ + projectInitialVisible = 3, + projectItems, + showLessLabel, + showMoreLabel, + templateInitialVisible = 6, + projectSubtitle, + projectTitle, + templateItems, + templateSubtitle, + templateTitle, +}) => { + const [templatesExpanded, setTemplatesExpanded] = useState(false); + const [projectsExpanded, setProjectsExpanded] = useState(false); + const visibleTemplates = useMemo( + () => (templatesExpanded ? templateItems : templateItems.slice(0, templateInitialVisible)), + [templateInitialVisible, templateItems, templatesExpanded], + ); + const visibleProjects = useMemo( + () => (projectsExpanded ? projectItems : projectItems.slice(0, projectInitialVisible)), + [projectInitialVisible, projectItems, projectsExpanded], + ); + const hasMoreTemplates = templateItems.length > templateInitialVisible; + const hasMoreProjects = projectItems.length > projectInitialVisible; + + return ( +
+ + {templateItems[0] && ( + <> +
+

{templateTitle}

+

{templateSubtitle}

+
+
+ + + {visibleTemplates.map(template => ( + + + + ))} + + + {hasMoreTemplates && ( + + )} + + )} + + {projectItems[0] && ( + <> +
+

{projectTitle}

+

{projectSubtitle}

+
+
+ + + {visibleProjects.map(project => ( + + + + ))} + + + {hasMoreProjects && ( + + )} + + )} +
+
+ ); +}; diff --git a/components/Activity/Hackathon/Schedule.module.less b/components/Activity/Hackathon/Schedule.module.less new file mode 100644 index 0000000..4b84c0c --- /dev/null +++ b/components/Activity/Hackathon/Schedule.module.less @@ -0,0 +1,310 @@ +@import './theme.less'; + +; + +.scheduleIntro { + margin-bottom: 0; +} + +.scheduleKicker { + margin: 0; + color: @cyan; + font-size: 0.8rem; + font-family: @heading; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.scheduleLead { + margin: 0; + color: #fff; + font-weight: 800; + font-size: clamp(1.45rem, 3vw, 2.05rem); + font-family: @heading; +} + +.scheduleOverview { + margin-bottom: 0; +} + +.schedulePill { + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 999px; + padding: 0.55rem 1rem; + color: @muted; + font-size: 0.74rem; + font-family: @heading; + letter-spacing: 0.06em; + text-transform: uppercase; + + @media (max-width: 767px) { + font-size: 0.72rem; + } +} + +.scheduleDays { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + + @media (max-width: 991px) { + grid-template-columns: 1fr; + } +} + +.dayCard { + ; + position: relative; + transition: + transform 0.22s ease, + border-color 0.22s ease, + box-shadow 0.22s ease; + padding: 1.35rem; + height: 100%; + overflow: hidden; + + &:hover { + transform: translateY(-4px); + box-shadow: 0 32px 80px rgba(0, 0, 0, 0.4); + border-color: rgba(44, 232, 255, 0.28); + } +} + +.formation { + background: linear-gradient(135deg, rgba(123, 97, 255, 0.12), rgba(28, 20, 67, 0.86)); + + &::before { + position: absolute; + top: 0; + right: 0; + left: 0; + background: linear-gradient(90deg, @purple, @cyan); + height: 2px; + content: ''; + } + + .dayNo { + background: rgba(123, 97, 255, 0.18); + color: @purple; + } +} + +.enrollment { + background: linear-gradient(135deg, rgba(44, 232, 255, 0.1), rgba(14, 43, 80, 0.86)); + + &::before { + position: absolute; + top: 0; + right: 0; + left: 0; + background: linear-gradient(90deg, @cyan, @purple); + height: 2px; + content: ''; + } + + .dayNo { + background: rgba(44, 232, 255, 0.14); + color: @cyan; + } +} + +.competition { + background: linear-gradient(135deg, rgba(255, 201, 77, 0.12), rgba(60, 35, 8, 0.88)); + + &::before { + position: absolute; + top: 0; + right: 0; + left: 0; + background: linear-gradient(90deg, @gold, @rose); + height: 2px; + content: ''; + } + + .dayNo { + background: rgba(255, 201, 77, 0.16); + color: @gold; + } +} + +.break { + background: linear-gradient(135deg, rgba(255, 120, 186, 0.1), rgba(72, 18, 48, 0.88)); + + &::before { + position: absolute; + top: 0; + right: 0; + left: 0; + background: linear-gradient(90deg, @rose, @purple); + height: 2px; + content: ''; + } + + .dayNo { + background: rgba(255, 120, 186, 0.16); + color: @rose; + } +} + +.evaluation { + background: linear-gradient(135deg, rgba(72, 241, 168, 0.12), rgba(14, 50, 39, 0.88)); + + &::before { + position: absolute; + top: 0; + right: 0; + left: 0; + background: linear-gradient(90deg, @green, @cyan); + height: 2px; + content: ''; + } + + .dayNo { + background: rgba(72, 241, 168, 0.16); + color: @green; + } +} + +.dayCardHead { + min-width: 0; +} + +.dayNo { + border-radius: 999px; + background: rgba(44, 232, 255, 0.12); + padding: 0.3rem 0.85rem; + color: @cyan; + font-weight: 700; + font-size: 0.72rem; + font-family: @heading; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.dayDate { + color: @muted; + font-size: 0.82rem; + font-family: @heading; + letter-spacing: 0.04em; +} + +.dayTitle { + margin: 0; + color: #fff; + font-weight: 800; + font-size: clamp(1.1rem, 2vw, 1.35rem); + font-family: @heading; + letter-spacing: 0.04em; +} + +.daySub { + margin: 0; + color: @muted; + line-height: 1.7; +} + +.dayAgenda { + display: grid; + gap: 0.7rem; + margin: 0; + + div { + display: grid; + grid-template-columns: auto 1fr; + align-items: start; + gap: 0.8rem; + } + + dt, + dd { + margin: 0; + } +} + +.timePill { + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + padding: 0.4rem 0.75rem; + min-height: 30px; + color: @copy; + font-size: 0.72rem; + font-family: @heading; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.agendaCopy { + min-width: 0; + + strong { + color: #fff; + line-height: 1.4; + } + + span { + color: @muted; + font-size: 0.92rem; + line-height: 1.55; + } +} + +.stageGoal { + margin: 0; + border-top: 1px dashed rgba(255, 255, 255, 0.16); + padding-top: 0; + + strong { + color: @cyan; + font-size: 0.78rem; + font-family: @heading; + letter-spacing: 0.08em; + text-transform: uppercase; + } + + span { + color: #fff; + font-size: 0.92rem; + line-height: 1.6; + } +} + +.keyDates { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.85rem; + margin: 0; + + @media (max-width: 991px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @media (max-width: 575px) { + grid-template-columns: 1fr; + } +} + +.keyDateCard { + border: 1px solid rgba(44, 232, 255, 0.24); + border-radius: 14px; + background: rgba(44, 232, 255, 0.08); + padding: 0.95rem 1rem; +} + +.keyDateCardWarn { + border-color: rgba(255, 201, 77, 0.26); + background: rgba(255, 201, 77, 0.08); +} + +.keyDateValue { + color: #fff; + font-size: 1.2rem; + line-height: 1.2; + font-family: @heading; + letter-spacing: 0.04em; +} + +.keyDateLabel { + color: @muted; + font-size: 0.84rem; + line-height: 1.6; +} diff --git a/components/Activity/Hackathon/Schedule.tsx b/components/Activity/Hackathon/Schedule.tsx new file mode 100644 index 0000000..ec2e678 --- /dev/null +++ b/components/Activity/Hackathon/Schedule.tsx @@ -0,0 +1,144 @@ +import { FC } from 'react'; +import { Container } from 'react-bootstrap'; + +import type { HackathonAwardsMeta } from './Awards'; +import styles from './Schedule.module.less'; + +export type HackathonScheduleTone = + | 'break' + | 'competition' + | 'enrollment' + | 'evaluation' + | 'formation'; + +export interface HackathonScheduleFact extends HackathonAwardsMeta { + meta: string; +} + +export interface HackathonScheduleItem extends Record< + 'id' | 'phase' | 'title' | 'dateText' | 'description', + string +> { + facts: HackathonScheduleFact[]; + + stageGoal?: string; + tone: HackathonScheduleTone; +} + +export interface HackathonScheduleProps extends Record< + 'title' | 'subtitle' | 'lead' | 'kicker' | 'stageGoalLabel', + string +> { + items: HackathonScheduleItem[]; + keyDates?: Record<'date' | 'label', string>[]; + overviewPills: string[]; +} + +const ScheduleCard: FC> = ({ + dateText, + description, + facts, + phase, + phaseLabel, + stageGoal, + stageGoalLabel, + title, + tone, +}) => ( +
+
+ + {phaseLabel} {phase} + + +
+ +

{title}

+

{description}

+ +
+ {facts.map(({ label, meta, value }) => ( +
+
{label}
+
+ {value} + {meta} +
+
+ ))} +
+ + {stageGoal && ( +

+ {stageGoalLabel}: + {stageGoal} +

+ )} +
+); + +export const HackathonSchedule: FC = ({ + items, + keyDates, + kicker, + lead, + overviewPills, + phaseLabel, + stageGoalLabel, + subtitle, + title, +}) => ( +
+ +
+

{kicker}

+

{title}

+

{subtitle}

+
+
+ + {lead && ( +
+

{lead}

+
+ )} + +
    + {overviewPills.map(pill => ( +
  • + {pill} +
  • + ))} +
+ +
+ {items.map(item => ( + + ))} +
+ + {keyDates?.[0] && ( +
    + {keyDates.map(({ date, label }, index) => ( +
  • + {date} + {label} +
  • + ))} +
+ )} +
+
+); diff --git a/components/Activity/Hackathon/constant.ts b/components/Activity/Hackathon/constant.ts new file mode 100644 index 0000000..c39c078 --- /dev/null +++ b/components/Activity/Hackathon/constant.ts @@ -0,0 +1,362 @@ +import { TableCellUser } from 'mobx-lark'; + +import { Activity, ActivityModel } from '../../../models/Activity'; +import { Agenda, Organization, Person, Prize, Project, Template } from '../../../models/Hackathon'; +import { i18n } from '../../../models/Translation'; +import type { HackathonAwardsMeta } from './Awards'; +import type { HackathonFAQItem } from './FAQ'; +import { HackathonHeroCard, HackathonHeroNavItem } from './Hero'; +import { + agendaToneClassOf, + agendaTypeLabelOf, + compactSummaryOf, + formatPeriod, + normalizeAgendaType, + previewText, +} from './utility'; + +export const RequiredTableKeys = [ + 'Person', + 'Organization', + 'Agenda', + 'Prize', + 'Template', + 'Project', +] as const; + +export type RequiredTableKey = (typeof RequiredTableKeys)[number]; + +export type FormGroupKey = 'Evaluation' | 'Person' | 'Product' | 'Project'; + +export interface FormGroupView extends HackathonHeroCard { + key: FormGroupKey; + links: (HackathonHeroNavItem & { external: true })[]; +} + +export const FormButtonBar = ['Person', 'Project', 'Product', 'Evaluation'] as const; + +export const buildFormSectionMeta = ({ t }: Pick) => ({ + Person: { + eyebrow: t('participants'), + title: t('hackathon_participant_registration'), + description: t('hackathon_participant_registration_description'), + }, + Project: { + eyebrow: t('hackathon_team_lead'), + title: t('hackathon_project_registration'), + description: t('hackathon_project_registration_description'), + }, + Product: { + eyebrow: t('hackathon_submission'), + title: t('product_submission'), + description: t('hackathon_product_submission_description'), + }, + Evaluation: { + eyebrow: t('hackathon_review'), + title: t('hackathon_evaluation_entry'), + description: t('hackathon_evaluation_entry_description'), + }, +}); + +export const buildScheduleReasonMap = ({ + t, +}: Pick): Partial> => ({ + enrollment: t('hackathon_schedule_reason_enrollment'), + formation: t('hackathon_schedule_reason_formation'), + competition: t('hackathon_schedule_reason_competition'), + evaluation: t('hackathon_schedule_reason_evaluation'), +}); + +export const buildScheduleFocusMap = ({ + t, +}: Pick): Partial> => ({ + enrollment: t('hackathon_schedule_focus_enrollment'), + formation: t('hackathon_schedule_focus_formation'), + competition: t('hackathon_schedule_focus_competition'), + evaluation: t('hackathon_schedule_focus_evaluation'), +}); + +export const buildScheduleGoalMap = ({ + t, +}: Pick): Partial> => ({ + enrollment: t('hackathon_schedule_goal_enrollment'), + formation: t('hackathon_schedule_goal_formation'), + competition: t('hackathon_schedule_goal_competition'), + evaluation: t('hackathon_schedule_goal_evaluation'), +}); + +export const heroNavigation = ({ t }: typeof i18n) => [ + { href: '#tracks', label: t('hackathon_highlights') }, + { href: '#schedule', label: t('agenda') }, + { href: '#awards', label: t('prizes') }, + { href: '#faq', label: t('common_questions') }, +]; + +export const buildCountdownUnitLabels = ({ t }: typeof i18n) => [ + t('countdown_days'), + t('countdown_hours'), + t('countdown_minutes'), + t('countdown_seconds'), +]; + +export const buildHighlightCards = ( + { t }: typeof i18n, + { + agendaItems, + eventRange, + organizations, + prizes, + templates, + }: { + agendaItems: Agenda[]; + eventRange: string; + organizations: Organization[]; + prizes: Prize[]; + templates: Template[]; + }, +) => [ + { + icon: '👥', + title: t('participants'), + description: buildFormSectionMeta({ t }).Person.description, + }, + { + icon: '🚀', + title: t('projects'), + description: buildFormSectionMeta({ t }).Project.description, + }, + { + icon: '🛠', + title: t('templates'), + description: previewText( + templates.map(({ name }) => name), + t('templates'), + ), + }, + { + icon: '🏆', + title: t('prizes'), + description: previewText( + prizes.map(({ name }) => name), + t('hackathon_prizes'), + ), + }, + { + icon: '🤝', + title: t('organizations'), + description: previewText( + organizations.map(({ name }) => name), + t('organizations'), + ), + }, + { + icon: '📅', + title: t('agenda'), + description: previewText( + agendaItems.map(({ name }) => name), + eventRange || t('agenda'), + ), + }, +]; + +export const buildJudgingCriteria = ({ t }: typeof i18n) => [ + { + id: 'innovation', + weight: '30%', + title: t('hackathon_criteria_innovation_title'), + description: t('hackathon_criteria_innovation_desc'), + }, + { + id: 'technical_depth', + weight: '25%', + title: t('hackathon_criteria_technical_title'), + description: t('hackathon_criteria_technical_desc'), + }, + { + id: 'completion', + weight: '25%', + title: t('hackathon_criteria_completion_title'), + description: t('hackathon_criteria_completion_desc'), + }, + { + id: 'presentation', + weight: '20%', + title: t('hackathon_criteria_presentation_title'), + description: t('hackathon_criteria_presentation_desc'), + }, +]; +export const buildScheduleItems = ( + { t }: typeof i18n, + { agendaItems, locationText }: { agendaItems: Agenda[]; locationText: string }, +) => { + const scheduleReasonMap = buildScheduleReasonMap({ t }); + const scheduleFocusMap = buildScheduleFocusMap({ t }); + const scheduleGoalMap = buildScheduleGoalMap({ t }); + + return agendaItems.map(({ id, name, type, summary, startedAt, endedAt }, index) => { + const normalizedType = normalizeAgendaType(type); + const typeLabel = agendaTypeLabelOf(type, t); + const reasonText = scheduleReasonMap[normalizedType] ?? t('hackathon_schedule_reason_default'); + const focusText = scheduleFocusMap[normalizedType] ?? t('hackathon_schedule_focus_default'); + const stageGoalText = scheduleGoalMap[normalizedType] ?? t('hackathon_schedule_goal_default'); + const description = compactSummaryOf((summary as string) || reasonText, reasonText, 120); + const windowValue = formatPeriod(startedAt, endedAt) || '-'; + const focusValue = compactSummaryOf((summary as string) || focusText, focusText, 92); + + return { + id: id as string, + phase: String(index + 1).padStart(2, '0'), + dateText: formatPeriod(startedAt, endedAt), + title: name as string, + description, + stageGoal: stageGoalText, + tone: agendaToneClassOf(type, index), + facts: [ + { + label: t('hackathon_schedule_window_label'), + value: windowValue, + meta: `${typeLabel} · ${locationText}`, + }, + { + label: t('hackathon_schedule_reason_label'), + value: reasonText, + meta: t('hackathon_schedule_reason_meta'), + }, + { + label: t('hackathon_schedule_focus_label'), + value: focusValue, + meta: t('hackathon_schedule_focus_meta'), + }, + ], + }; + }); +}; + +export const buildPrizeItems = ({ t }: typeof i18n, prizes: Prize[]) => + prizes.map(({ id, name, image, summary, level, sponsor, price, amount }, index) => ({ + id: id as string, + title: name as string, + tier: (level as string) || `#${index + 1}`, + description: (summary as string) || previewText([sponsor, price, amount], t('prizes')), + image, + meta: [ + sponsor ? { label: t('sponsor'), value: sponsor as string } : null, + price ? { label: t('price'), value: price as string } : null, + amount ? { label: t('amount'), value: amount as string } : null, + ].filter(Boolean) as HackathonAwardsMeta[], + })); + +export const buildOrganizationItems = (organizations: Organization[]) => + organizations.map(({ id, name, link, logo }) => ({ + id: id as string, + name: name as string, + href: link as string | undefined, + logo, + })); + +export const buildTemplateItems = ({ t }: typeof i18n, templates: Template[]) => + templates.map(({ id, name, languages, tags, sourceLink, summary, previewLink }) => ({ + id: id as string, + title: name as string, + description: (summary as string) || '', + languages: ((languages as string[] | undefined) || []).filter(Boolean), + tags: ((tags as string[] | undefined) || []).filter(Boolean), + sourceUrl: sourceLink as string | undefined, + sourceLabel: t('source_code'), + previewUrl: previewLink as string | undefined, + previewLabel: t('preview'), + })); + +export const buildParticipantItems = (people: Person[]) => + people.map(({ id, name, avatar, githubLink }) => ({ + id: id as string, + name: name as string, + avatar, + githubLink: githubLink as string | undefined, + })); + +export const buildFAQItems = ( + { t }: typeof i18n, + { + eventRange, + locationText, + organizationsCount, + primaryForm, + projectsCount, + resourceSummary, + scheduleOverviewPills, + secondaryForm, + templatesCount, + }: { + eventRange: string; + locationText: string; + organizationsCount: number; + primaryForm?: FormGroupView; + projectsCount: number; + resourceSummary: string; + scheduleOverviewPills: string[]; + secondaryForm?: FormGroupView; + templatesCount: number; + }, +): HackathonFAQItem[] => + [ + primaryForm + ? { + id: 'registration', + question: t('hackathon_faq_registration_question'), + answer: `${t('hackathon_faq_registration_answer_prefix')}${primaryForm.title}。${primaryForm.description}`, + } + : null, + { + id: 'schedule', + question: t('hackathon_faq_schedule_question'), + answer: [eventRange, ...scheduleOverviewPills.slice(0, 3)].filter(Boolean).join(' · '), + }, + { + id: 'location', + question: t('hackathon_faq_location_question'), + answer: `${t('event_location')}:${locationText}`, + }, + { + id: 'resources', + question: t('hackathon_faq_resources_question'), + answer: `${t('templates')} ${templatesCount} · ${t('projects')} ${projectsCount} · ${t('organizations')} ${organizationsCount}。${resourceSummary}`, + }, + secondaryForm + ? { + id: 'submission', + question: t('hackathon_faq_submission_question'), + answer: `${t('hackathon_faq_submission_answer_prefix')}${secondaryForm.title}。${secondaryForm.description}`, + } + : null, + ].filter(Boolean) as HackathonFAQItem[]; + +export const buildProjectItems = ( + { t }: typeof i18n, + { projects, activity }: { projects: Project[]; activity: Activity }, +) => + projects.map(({ id, name, score, summary, createdBy, members }) => { + const creator = createdBy as TableCellUser | undefined; + const scoreText = score === null || score === undefined || score === '' ? '—' : `${score}`; + + return { + id: id as string, + title: name as string, + link: `${ActivityModel.getLink(activity)}/team/${id}`, + score: scoreText, + description: (summary as string) || '', + meta: [ + creator + ? { + label: t('created_by'), + value: creator.name || '—', + valueHref: creator.email ? `mailto:${creator.email}` : undefined, + } + : { label: t('created_by'), value: '—' }, + { + label: t('members'), + value: (members as string[] | undefined)?.join(', ') || '—', + }, + ], + }; + }); diff --git a/components/Activity/Hackathon/theme.less b/components/Activity/Hackathon/theme.less new file mode 100644 index 0000000..e2eece2 --- /dev/null +++ b/components/Activity/Hackathon/theme.less @@ -0,0 +1,169 @@ +@bg: #050814; +@panel: rgba(8, 18, 39, 0.76); +@panel-strong: rgba(6, 13, 30, 0.92); +@border: rgba(120, 160, 255, 0.18); +@copy: #eef5ff; +@muted: rgba(220, 232, 255, 0.72); +@cyan: #2ce8ff; +@gold: #ffc94d; +@green: #48f1a8; +@rose: #ff78ba; +@purple: #7b61ff; +@shadow: 0 28px 80px rgba(0, 0, 0, 0.34); +@heading: 'Orbitron', 'Avenir Next', 'Segoe UI', sans-serif; +@body: 'Outfit', 'Avenir Next', 'Segoe UI', sans-serif; + +.panel-card() { + box-shadow: @shadow; + border: 1px solid @border; + border-radius: 28px; + background: @panel; + backdrop-filter: blur(18px); +} + +.section-shell() { + position: relative; + padding: clamp(4rem, 7vw, 6rem) 0; + background: linear-gradient( + 180deg, + rgba(5, 8, 20, 0.98), + rgba(7, 12, 26, 0.98) + ); + color: @copy; +} + +.section-frame() { + .section { + .section-shell(); + } + + .sectionHeader { + .section-header(); + } + + .sectionTitle { + .section-title(); + } + + .sectionSubtitle { + .section-subtitle(); + } + + .accentLine { + .accent-line(); + } +} + +.section-header() { + text-align: center; + margin-bottom: clamp(2.3rem, 4vw, 3rem); +} + +.section-title() { + display: inline-block; + margin: 0; + background: linear-gradient(135deg, #fff 20%, @cyan 80%); + background-clip: text; + -webkit-background-clip: text; + color: transparent; + font-family: @heading; + font-size: clamp(1.9rem, 4vw, 2.6rem); + font-weight: 900; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.section-subtitle() { + color: @muted; + font-family: @heading; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.accent-line() { + width: 72px; + height: 2px; + margin: 1rem auto 0; + border-radius: 999px; + background: linear-gradient(90deg, @cyan, @purple); + box-shadow: 0 0 14px rgba(44, 232, 255, 0.32); +} + +.button-base() { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + transition: + transform 0.22s ease, + box-shadow 0.22s ease, + border-color 0.22s ease, + background 0.22s ease, + color 0.22s ease; + border: 1px solid transparent; + border-radius: 10px; + padding: 0.9rem 1.35rem; + font-family: @heading; + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.08em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + + &:hover { + transform: translateY(-2px); + text-decoration: none; + } +} + +.button-primary() { + .button-base(); + box-shadow: 0 0 24px rgba(44, 232, 255, 0.18); + border-color: rgba(44, 232, 255, 0.3); + background: rgba(44, 232, 255, 0.12); + color: @cyan; + + &:hover { + box-shadow: 0 0 34px rgba(44, 232, 255, 0.28); + background: rgba(44, 232, 255, 0.2); + color: #fff; + } +} + +.button-ghost() { + .button-base(); + border-color: rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.04); + color: @copy; + + &:hover { + border-color: rgba(255, 201, 77, 0.24); + background: rgba(255, 201, 77, 0.1); + color: @gold; + } +} + +.eyebrow() { + color: @muted; + font-family: @heading; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.chip() { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); + border-radius: 999px; + background: rgba(255, 255, 255, 0.06); +} + +@media (max-width: 767px) { + .section-title() { + font-size: 1.65rem; + letter-spacing: 0.08em; + } +} diff --git a/components/Activity/Hackathon/utility.ts b/components/Activity/Hackathon/utility.ts new file mode 100644 index 0000000..0ba13b8 --- /dev/null +++ b/components/Activity/Hackathon/utility.ts @@ -0,0 +1,107 @@ +import { TableCellValue, TableFormView } from 'mobx-lark'; +import { formatDate } from 'web-utility'; + +import type { HackathonScheduleTone } from './Schedule'; +import { i18n, I18nKey } from '../../../models/Translation'; + +export const AgendaTypeClassMap: Partial> = { + workshop: 'formation', + formation: 'formation', + presentation: 'enrollment', + enrollment: 'enrollment', + coding: 'competition', + competition: 'competition', + break: 'break', + ceremony: 'evaluation', + evaluation: 'evaluation', +}; + +export const buildAgendaTypeLabelMap = ({ + t, +}: Pick): Partial> => ({ + workshop: t('workshop'), + presentation: t('presentation'), + coding: t('coding'), + break: t('break'), + ceremony: t('ceremony'), + enrollment: t('enrollment'), + formation: t('formation'), + competition: t('competition'), + evaluation: t('evaluation'), +}); + +export const isPublicForm = ({ shared_limit }: TableFormView) => + ['anyone_editable'].includes(shared_limit as string); + +export const formatMoment = (value?: TableCellValue) => (value ? formatDate(value as string) : ''); + +export const formatPeriod = (startedAt?: TableCellValue, endedAt?: TableCellValue) => + [formatMoment(startedAt), formatMoment(endedAt)].filter(Boolean).join(' - '); + +export const previewText = (items: TableCellValue[], fallback: string) => + items + .map(item => item?.toString()) + .filter(Boolean) + .slice(0, 2) + .join(' · ') || fallback; + +export const agendaToneClassOf = (type: TableCellValue, index: number) => { + const normalized = type?.toString().toLowerCase() || ''; + const fallbackOrder: HackathonScheduleTone[] = [ + 'formation', + 'enrollment', + 'competition', + 'break', + 'evaluation', + ]; + + return AgendaTypeClassMap[normalized] || fallbackOrder[index % fallbackOrder.length]; +}; + +export const agendaTypeLabelOf = ( + type: TableCellValue, + t: (key: I18nKey) => string, + fallback = '-', +) => { + const normalized = type?.toString().toLowerCase() || ''; + + return buildAgendaTypeLabelMap({ t })[normalized] || type?.toString() || fallback; +}; + +export const compactSummaryOf = ( + text: TableCellValue | string[] | string | undefined, + fallback: string, + limit = 96, +) => { + const source = Array.isArray(text) + ? text + .map(item => item?.toString()) + .filter(Boolean) + .join(' · ') + : text?.toString() || ''; + const normalized = source.replace(/\s+/g, ' ').trim(); + + if (!normalized) return fallback; + + return normalized.length > limit ? `${normalized.slice(0, limit).trim()}...` : normalized; +}; + +export const dateKeyOf = (value?: TableCellValue) => { + const dateText = formatMoment(value); + + return dateText ? dateText.slice(5, 10).replace(/\//g, '-') : ''; +}; + +export const compactDateKeyOf = (value?: TableCellValue) => dateKeyOf(value).replace('-', '.'); + +export const daysBetween = (startedAt?: TableCellValue, endedAt?: TableCellValue) => { + const start = new Date((startedAt as string) || '').getTime(); + const end = new Date((endedAt as string) || '').getTime(); + + if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return 0; + + return Math.max(1, Math.ceil((end - start) / (24 * 60 * 60 * 1000))); +}; + +export const normalizeAgendaType = (value?: TableCellValue) => + value?.toString().toLowerCase() || ''; diff --git a/models/Hackathon.ts b/models/Hackathon.ts index a7afb37..d01f94a 100644 --- a/models/Hackathon.ts +++ b/models/Hackathon.ts @@ -4,6 +4,7 @@ import { BiDataQueryOptions, BiDataTable, normalizeText, + normalizeTextArray, TableCellRelation, TableCellText, TableCellUser, @@ -50,11 +51,15 @@ export class PersonModel extends BiDataTable() { queryOptions: BiDataQueryOptions = { text_field_as_array: false }; - extractFields({ fields: { githubLink, ...fields }, ...meta }: TableRecord) { + extractFields({ + fields: { githubLink, organizations, ...fields }, + ...meta + }: TableRecord) { return { ...meta, ...fields, githubLink: normalizeText(githubLink as TableCellText), + organizations: normalizeTextArray(organizations as TableCellRelation[]), }; } } @@ -67,11 +72,16 @@ export class OrganizationModel extends BiDataTable() { queryOptions: BiDataQueryOptions = { text_field_as_array: false }; - extractFields({ fields: { link, ...fields }, ...meta }: TableRecord) { + extractFields({ + fields: { link, members, prizes, ...fields }, + ...meta + }: TableRecord) { return { ...meta, ...fields, link: normalizeText(link as TableCellText), + members: normalizeTextArray(members as TableCellRelation[]), + prizes: normalizeTextArray(prizes as TableCellRelation[]), }; } } @@ -93,6 +103,15 @@ export class PrizeModel extends BiDataTable() { client = larkClient; queryOptions: BiDataQueryOptions = { text_field_as_array: false }; + + extractFields({ fields: { summary, sponsor, ...fields }, ...meta }: TableRecord) { + return { + ...meta, + ...fields, + summary: (summary as TableCellText[])!.map(normalizeText), + sponsor: normalizeText(sponsor as TableCellText), + }; + } } export type Template = LarkBase & @@ -160,7 +179,7 @@ export class MemberModel extends BiDataTable() { person: (person as TableCellUser[])?.[0], summary: (summary as TableCellText[])!.map(normalizeText), skills: skills?.toString().split(/\s*,\s*/) || [], - githubAccount: normalizeText(githubAccount as TableCellText), + githubAccount: (githubAccount as TableCellText[])!.map(normalizeText), }; } } diff --git a/pages/_app.tsx b/pages/_app.tsx index e2dbf2f..b16c6f0 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -43,6 +43,8 @@ export default class CustomApp extends App { { t } = this.i18nStore; const thisFullYear = new Date().getFullYear(), { asPath } = router; + const isArticlePage = asPath.startsWith('/article/') || asPath.startsWith('/policy/'), + isActivityPage = asPath.startsWith('/hackathon'); return ( @@ -52,18 +54,23 @@ export default class CustomApp extends App { {t('open_source_bazaar')} - - -
- {asPath.startsWith('/article/') || asPath.startsWith('/policy/') ? ( - - - - ) : ( - - )} -
+ {isActivityPage ? ( + + ) : ( + <> + +
+ {isArticlePage ? ( + + + + ) : ( + + )} +
+ + )}