feat: split big file and update agents.md

This commit is contained in:
2026-06-18 23:15:19 +03:00
parent 2578e79cf5
commit b5e7cbd572
20 changed files with 536 additions and 465 deletions

View File

@@ -5,8 +5,55 @@ Orbit Academy — cohort-based education шаблон: сохраняй neo-brut
## Project Specifics ## Project Specifics
- Программы, модули, расписание, outcomes, student work, mentors, admissions и community лежат в `src/entities/site-content.ts`. - Программы, модули, расписание, outcomes, student work, mentors, admissions и community лежат в `src/entities/site-content.ts`.
- `src/app` — только route wrappers; композиция страниц находится в `src/widgets/template-ui.tsx`. - `src/app` — только route wrappers; композиция каждой страницы живёт в отдельном widget (`src/widgets/<page>-page.tsx`). См. File Map.
- Mock-интеракции поступления держи в `src/features/*/ui`; не добавляй реальные формы, auth, LMS или платежи без запроса. - Mock-интеракции поступления держи в `src/features/*/ui`; не добавляй реальные формы, auth, LMS или платежи без запроса.
- Не превращай страницы в обычный SaaS-лендинг или курс с лекциями: каждая страница должна показывать образовательный процесс, артефакт, поток, critique или результат. - Не превращай страницы в обычный SaaS-лендинг или курс с лекциями: каждая страница должна показывать образовательный процесс, артефакт, поток, critique или результат.
- Громкий brutalist стиль должен поддерживать доверие: добавляй proof, менторов, работы студентов и конкретные deliverables, а не декоративный шум. - Громкий brutalist стиль должен поддерживать доверие: добавляй proof, менторов, работы студентов и конкретные deliverables, а не декоративный шум.
- Проверка после правок: `pnpm lint` и `pnpm build`. - Проверка после правок: `pnpm lint` и `pnpm build`.
## Design System
Источник токенов — `src/app/globals.css` (`@theme` + `:root`/`.dark`). Шрифт — Inter (`--font-orbit`), один гарнитур на sans и mono. Работай через семантические классы Tailwind (`bg-primary`, `text-foreground`, `border-foreground`), не хардкодь hex/oklch.
Личность: **neo-brutalist «cream / ink / acid»** — тёплый кремовый фон, near-black «ink» как граница и текст, кислотно-зелёный secondary как сигнальный акцент, глубокий indigo primary, оранжевый accent для редких всплесков.
| Роль | Light | Характер |
|---|---|---|
| `background` | тёплый кремовый | основной фон страниц |
| `foreground` | near-black ink | текст + **границы** (`border-2 border-foreground`) |
| `primary` | глубокий indigo | заголовочные плашки, активные строки |
| `secondary` | кислотный лайм | бейджи, сигнальные акценты, hover |
| `accent` | оранжевый | редкие точечные всплески |
| `card` | почти белый | карточки на кремовом фоне |
Узнаваемые приёмы (держи их, это и есть «лицо» проекта):
- **Жёсткие углы:** `--radius` = 0.125rem; большинство элементов — `rounded-none`.
- **Толстые границы:** `border-2 border-foreground` повсюду; это структура, а не декор.
- **Hard offset shadow:** `shadow-[8px_8px_0_var(--foreground)]` (карточки, hover усиливает до `12px`).
- **Громкая типографика:** `font-black uppercase`, очень плотный `leading` (`leading-none`/`leading-[0.9]`), крупные размеры (до `text-8xl`).
- **Утилитарные классы:** `.orbit-board` — сетка-миллиметровка для «board»-секций; `.marker-highlight` — маркерное подчёркивание лаймом.
Do / Don't:
- **Do:** расширяй существующий язык — сетки, плашки, board-эстетику; держи контент-first (артефакт, proof, метрика).
- **Don't:** мягкие тени, скруглённые карточки, пастель, generic-SaaS hero с градиентом — это ломает личность шаблона.
## File Map
| Route | Widget |
|---|---|
| `/` | `src/widgets/home-page.tsx` (`HomePage`) |
| `/programs` | `src/widgets/programs-page.tsx` (`ProgramsPage`) |
| `/programs/product-management` | `src/widgets/program-detail-page.tsx` (`ProgramDetailPage`) |
| `/schedule` | `src/widgets/schedule-page.tsx` (`SchedulePage`) |
| `/outcomes` | `src/widgets/outcomes-page.tsx` (`OutcomesPage`) |
| `/admissions` | `src/widgets/admissions-page.tsx` (`AdmissionsPage`) |
| `/community` | `src/widgets/community-page.tsx` (`CommunityPage`) |
Переиспользуемые блоки:
- `src/widgets/site-shell.tsx``SiteShell` (header + nav + footer, обёртка всех страниц).
- `src/widgets/program-card.tsx``ProgramCard` (Home, Programs).
- `src/widgets/student-work-card.tsx``StudentWorkCard` (Home, Outcomes).
- `src/shared/ui/loud-title.tsx``LoudTitle` (заголовочная секция внутренних страниц).
- `src/features/admission-board/ui/admission-board.tsx` — mock admissions board.
Одноразовые блоки колоцированы со своей страницей (напр. `HeroStudio`/`OutcomeStrip` в `home-page.tsx`, `MentorRoster` в `programs-page.tsx`).

View File

@@ -1,4 +1,4 @@
import { AdmissionsPage } from "@/widgets/template-ui"; import { AdmissionsPage } from "@/widgets/admissions-page";
export default function Page() { export default function Page() {
return <AdmissionsPage />; return <AdmissionsPage />;

View File

@@ -1,4 +1,4 @@
import { CommunityPage } from "@/widgets/template-ui"; import { CommunityPage } from "@/widgets/community-page";
export default function Page() { export default function Page() {
return <CommunityPage />; return <CommunityPage />;

View File

@@ -1,4 +1,4 @@
import { OutcomesPage } from "@/widgets/template-ui"; import { OutcomesPage } from "@/widgets/outcomes-page";
export default function Page() { export default function Page() {
return <OutcomesPage />; return <OutcomesPage />;

View File

@@ -1,4 +1,4 @@
import { HomePage } from "@/widgets/template-ui"; import { HomePage } from "@/widgets/home-page";
export default function Page() { export default function Page() {
return <HomePage />; return <HomePage />;

View File

@@ -1,4 +1,4 @@
import { ProgramsPage } from "@/widgets/template-ui"; import { ProgramsPage } from "@/widgets/programs-page";
export default function Page() { export default function Page() {
return <ProgramsPage />; return <ProgramsPage />;

View File

@@ -1,4 +1,4 @@
import { ProgramDetailPage } from "@/widgets/template-ui"; import { ProgramDetailPage } from "@/widgets/program-detail-page";
export default function Page() { export default function Page() {
return <ProgramDetailPage />; return <ProgramDetailPage />;

View File

@@ -1,4 +1,4 @@
import { SchedulePage } from "@/widgets/template-ui"; import { SchedulePage } from "@/widgets/schedule-page";
export default function Page() { export default function Page() {
return <SchedulePage />; return <SchedulePage />;

View File

@@ -0,0 +1,13 @@
import { Badge } from "@/shared/ui/badge";
export function LoudTitle({ label, title, text }: { label: string; title: string; text: string }) {
return (
<section className="orbit-board border-b-2 border-foreground px-4 py-12 md:px-6 md:py-20">
<div className="mx-auto max-w-7xl">
<Badge className="mb-5 rounded-none border-2 border-foreground bg-secondary text-foreground">{label}</Badge>
<h1 className="max-w-5xl break-words text-5xl font-black uppercase leading-[0.92] md:text-8xl">{title}</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">{text}</p>
</div>
</section>
);
}

View File

@@ -0,0 +1,29 @@
import { AdmissionBoard } from "@/features/admission-board/ui/admission-board";
import { admissions } from "@/entities/site-content";
import { LoudTitle } from "@/shared/ui/loud-title";
import { SiteShell } from "@/widgets/site-shell";
export function AdmissionsPage() {
return (
<SiteShell>
<LoudTitle
label="Admissions"
title="Поступление без иллюзии элитарности"
text="Нужна не мотивационная анкета, а проверка цели, уровня и готовности показывать работу группе каждую неделю."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-8 lg:grid-cols-[1fr_430px]">
<div className="grid gap-4">
{admissions.map((item, index) => (
<div key={item} className="grid gap-4 border-2 border-foreground bg-card p-5 md:grid-cols-[72px_1fr]">
<div className="text-4xl font-black text-primary">0{index + 1}</div>
<h2 className="text-3xl font-black uppercase leading-none">{item}</h2>
</div>
))}
</div>
<AdmissionBoard />
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,54 @@
import { MessageSquareTextIcon, PlaySquareIcon, Rows3Icon } from "lucide-react";
import { communityNotes } from "@/entities/site-content";
import { LoudTitle } from "@/shared/ui/loud-title";
import { SiteShell } from "@/widgets/site-shell";
export function CommunityPage() {
return (
<SiteShell>
<LoudTitle
label="Community"
title="Комьюнити работает после последней защиты"
text="Сильная онлайн-школа продает не только программу, но и среду, где продолжают показывать работу и получать обратную связь."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-3">
{communityNotes.map((note) => (
<article key={note.title} className="flex min-h-80 flex-col justify-between border-2 border-foreground bg-card p-6">
<div>
<MessageSquareTextIcon className="mb-16 size-8 text-primary" />
<h2 className="text-3xl font-black uppercase leading-none">{note.title}</h2>
<p className="mt-4 leading-7 text-muted-foreground">{note.text}</p>
</div>
<div className="mt-8 border-t-2 border-foreground pt-4 text-sm font-black uppercase">{note.cadence}</div>
</article>
))}
</div>
</section>
<section className="px-4 pb-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.8fr_1.2fr]">
<div className="border-2 border-foreground bg-secondary p-6">
<PlaySquareIcon className="mb-10 size-8" />
<h2 className="text-4xl font-black uppercase leading-none">Demo archive</h2>
<p className="mt-4 leading-7">Записи защит, шаблоны артефактов и разборы доступны выпускникам после курса.</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
{[
["artifact library", "problem briefs, research boards, metric trees"],
["hiring rooms", "портфолио-ревью и разбор офферов"],
["peer circles", "мини-группы по роли и домену"],
["mentor threads", "асинхронные вопросы после live-сессий"],
].map(([title, text]) => (
<div key={title} className="border-2 border-foreground bg-card p-5">
<Rows3Icon className="mb-8 size-7 text-primary" />
<h3 className="text-2xl font-black uppercase">{title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{text}</p>
</div>
))}
</div>
</div>
</section>
</SiteShell>
);
}

136
src/widgets/home-page.tsx Normal file
View File

@@ -0,0 +1,136 @@
import Image from "next/image";
import Link from "next/link";
import { ArrowRightIcon } from "lucide-react";
import { heroStats, outcomes, programs, site, studentWork } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { ProgramCard } from "@/widgets/program-card";
import { SiteShell } from "@/widgets/site-shell";
import { StudentWorkCard } from "@/widgets/student-work-card";
function HeroStudio() {
return (
<div className="overflow-hidden border-2 border-foreground bg-card shadow-[8px_8px_0_var(--foreground)]">
<div className="relative h-72 border-b-2 border-foreground">
<Image
src="https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&w=1400&q=82"
alt="Команда разбирает продуктовые артефакты на воркшопе"
fill
priority
className="object-cover"
sizes="(min-width: 1024px) 38vw, 100vw"
/>
<div className="absolute left-4 top-4 bg-background px-3 py-2 text-xs font-black uppercase">
critique room
</div>
</div>
<div className="orbit-board p-5">
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-xs font-black uppercase text-muted-foreground">week 04 board</div>
<h2 className="mt-1 text-3xl font-black uppercase leading-none">Metric architecture</h2>
</div>
<Badge className="rounded-none bg-secondary text-foreground">{site.capacity}</Badge>
</div>
<div className="mt-5 grid gap-3">
{[
["19:00", "live teardown", "разбор metric tree"],
["20:10", "peer room", "критика по группам"],
["21:00", "mentor notes", "правки до пятницы"],
].map(([time, title, text]) => (
<div key={time} className="grid grid-cols-[64px_1fr] gap-3 border-2 border-foreground bg-background p-3">
<div className="font-black text-primary">{time}</div>
<div>
<div className="font-black uppercase">{title}</div>
<div className="text-sm text-muted-foreground">{text}</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
function OutcomeStrip() {
return (
<section className="border-y-2 border-foreground bg-primary px-4 py-10 text-primary-foreground md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-4">
{outcomes.map((item) => (
<div key={item.value}>
<div className="text-5xl font-black">{item.value}</div>
<p className="mt-2 max-w-sm text-sm font-bold opacity-80">{item.label}</p>
</div>
))}
</div>
</section>
);
}
export function HomePage() {
return (
<SiteShell>
<section className="orbit-board px-4 py-10 md:px-6 md:py-14">
<div className="mx-auto grid max-w-7xl gap-8 lg:grid-cols-[0.95fr_1.05fr] lg:items-center">
<div>
<Badge className="mb-5 rounded-none border-2 border-foreground bg-secondary text-foreground">
Следующий поток: {site.nextCohort}
</Badge>
<h1 className="hidden text-5xl font-black uppercase leading-[0.86] md:block md:text-8xl">
Не лекции. Еженедельный продуктовый разбор.
</h1>
<h1 className="text-[2.65rem] font-black uppercase leading-[0.9] md:hidden">
Не лекции. Работа, разбор, demo day.
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">
Orbit Academy запускает cohort-based программы, где студенты каждую неделю
приносят рабочий артефакт: problem brief, research board, metric tree или roadmap defense.
</p>
<div className="mt-7 flex flex-wrap gap-3">
<Button asChild className="rounded-none" size="lg">
<Link href="/programs">Выбрать программу <ArrowRightIcon className="size-4" /></Link>
</Button>
<Button asChild variant="outline" className="rounded-none border-2" size="lg">
<Link href="/schedule">Расписание</Link>
</Button>
</div>
<div className="mt-8 grid gap-3 md:grid-cols-3">
{heroStats.map((item) => (
<div key={item.value} className="border-2 border-foreground bg-card p-3">
<div className="text-2xl font-black">{item.value}</div>
<div className="mt-1 text-xs font-bold text-muted-foreground">{item.label}</div>
</div>
))}
</div>
</div>
<HeroStudio />
</div>
</section>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-3">
{programs.map((program, index) => (
<ProgramCard key={program.title} program={program} index={index} />
))}
</div>
</section>
<OutcomeStrip />
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.7fr_1.3fr]">
<div>
<div className="marker-highlight w-fit text-sm font-black uppercase">работы студентов</div>
<h2 className="mt-5 text-4xl font-black uppercase leading-none md:text-6xl">Proof вместо обещаний</h2>
</div>
<div className="grid gap-5 md:grid-cols-3">
{studentWork.map((work) => (
<StudentWorkCard key={work.title} work={work} />
))}
</div>
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,37 @@
import { TrophyIcon } from "lucide-react";
import { outcomes, studentWork } from "@/entities/site-content";
import { LoudTitle } from "@/shared/ui/loud-title";
import { SiteShell } from "@/widgets/site-shell";
import { StudentWorkCard } from "@/widgets/student-work-card";
export function OutcomesPage() {
return (
<SiteShell>
<LoudTitle
label="Outcomes"
title="Результаты показываются через работу, а не обещания"
text="Шаблон отделяет образовательный маркетинг от реальных outcome-блоков: кейсы, метрики, демо и роли выпускников."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-2 lg:grid-cols-4">
{outcomes.map((item) => (
<article key={item.value} className="border-2 border-foreground bg-card p-6 shadow-[8px_8px_0_var(--foreground)]">
<TrophyIcon className="mb-10 size-8 text-primary" />
<div className="text-5xl font-black">{item.value}</div>
<p className="mt-4 font-bold leading-7">{item.label}</p>
<p className="mt-3 text-sm text-muted-foreground">{item.proof}</p>
</article>
))}
</div>
</section>
<section className="px-4 pb-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-5 md:grid-cols-3">
{studentWork.map((work) => (
<StudentWorkCard key={work.title} work={work} />
))}
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,40 @@
import Link from "next/link";
import { ArrowRightIcon } from "lucide-react";
import { programs } from "@/entities/site-content";
type Program = (typeof programs)[number];
export function ProgramCard({ program, index }: { program: Program; index: number }) {
const href = program.slug === "product-management" ? "/programs/product-management" : "/programs";
return (
<Link
href={href}
className="group flex min-h-[360px] flex-col justify-between border-2 border-foreground bg-card p-5 shadow-[8px_8px_0_var(--foreground)] transition hover:-translate-y-1 hover:shadow-[12px_12px_0_var(--foreground)]"
>
<div className="flex justify-between gap-4 text-sm font-black uppercase">
<span>0{index + 1}</span>
<span>{program.duration}</span>
</div>
<div>
<h3 className="text-4xl font-black uppercase leading-none">{program.title}</h3>
<p className="mt-4 font-bold leading-7">{program.signal}</p>
<p className="mt-3 leading-7 text-muted-foreground">{program.outcome}</p>
</div>
<div>
<div className="mb-4 flex flex-wrap gap-2">
{program.deliverables.slice(0, 3).map((item) => (
<span key={item} className="border-2 border-foreground bg-secondary px-2 py-1 text-xs font-black uppercase">
{item}
</span>
))}
</div>
<div className="flex items-center justify-between border-t-2 border-foreground pt-4 text-sm font-bold">
<span>{program.level} / {program.seats} / {program.price}</span>
<ArrowRightIcon className="size-5 transition group-hover:translate-x-1" />
</div>
</div>
</Link>
);
}

View File

@@ -0,0 +1,32 @@
import { modules, programs } from "@/entities/site-content";
import { LoudTitle } from "@/shared/ui/loud-title";
import { SiteShell } from "@/widgets/site-shell";
export function ProgramDetailPage() {
const program = programs[0];
return (
<SiteShell>
<LoudTitle
label="Product Management"
title="От проблемы к roadmap, который можно защищать"
text={`${program.duration}, ${program.format}. Итог - ${program.outcome}.`}
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-5">
{modules.map((module) => (
<article key={module.week} className="grid gap-4 border-2 border-foreground bg-card p-5 md:grid-cols-[100px_0.75fr_1fr_0.8fr] md:items-start">
<div className="text-5xl font-black">{module.week}</div>
<div>
<h2 className="text-3xl font-black uppercase leading-none">{module.title}</h2>
<div className="mt-3 text-sm font-black text-primary">{module.critique}</div>
</div>
<p className="leading-7 text-muted-foreground">{module.task}</p>
<div className="border-2 border-foreground bg-secondary p-3 text-sm font-black">{module.artifact}</div>
</article>
))}
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,45 @@
import { UsersIcon } from "lucide-react";
import { mentors, programs } from "@/entities/site-content";
import { LoudTitle } from "@/shared/ui/loud-title";
import { ProgramCard } from "@/widgets/program-card";
import { SiteShell } from "@/widgets/site-shell";
function MentorRoster() {
return (
<div className="grid gap-4 md:grid-cols-3">
{mentors.map((mentor) => (
<article key={mentor.name} className="border-2 border-foreground bg-card p-5">
<UsersIcon className="mb-10 size-7 text-primary" />
<h3 className="text-2xl font-black uppercase leading-none">{mentor.name}</h3>
<p className="mt-3 font-bold">{mentor.role}</p>
<p className="mt-2 text-sm text-muted-foreground">{mentor.focus}</p>
</article>
))}
</div>
);
}
export function ProgramsPage() {
return (
<SiteShell>
<LoudTitle
label="Programs"
title="Три программы для разных точек роста"
text="Каждая программа имеет outcome, расписание, формат обратной связи, deliverables и ограниченное количество мест в группе."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-3">
{programs.map((program, index) => (
<ProgramCard key={program.title} program={program} index={index} />
))}
</div>
</section>
<section className="px-4 pb-12 md:px-6">
<div className="mx-auto max-w-7xl">
<MentorRoster />
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,34 @@
import { CalendarDaysIcon } from "lucide-react";
import { schedule } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { LoudTitle } from "@/shared/ui/loud-title";
import { SiteShell } from "@/widgets/site-shell";
export function SchedulePage() {
return (
<SiteShell>
<LoudTitle
label="Schedule"
title="Потоки видны как операционный календарь"
text="Страница помогает выбрать момент входа: дата старта, формат, статус набора и реальная интенсивность."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto max-w-7xl divide-y-2 divide-foreground border-2 border-foreground bg-card">
{schedule.map((item) => (
<div key={`${item.date}-${item.program}`} className="grid gap-3 p-5 md:grid-cols-[150px_1fr_190px_190px_150px] md:items-center">
<div className="flex items-center gap-2 font-black">
<CalendarDaysIcon className="size-5" />
{item.date}
</div>
<div className="text-3xl font-black uppercase leading-none">{item.program}</div>
<div className="font-bold">{item.mode}</div>
<div className="text-sm text-muted-foreground">{item.intensity}</div>
<Badge className="w-fit rounded-none bg-secondary text-foreground">{item.status}</Badge>
</div>
))}
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,43 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { RocketIcon } from "lucide-react";
import { navItems, site } from "@/entities/site-content";
import { Button } from "@/shared/ui/button";
export function SiteShell({ children }: { children: ReactNode }) {
return (
<main className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-30 border-b-2 border-foreground bg-background/95 backdrop-blur">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 md:px-6">
<Link href="/" className="flex items-center gap-3 text-xl font-black uppercase">
<span className="grid size-10 place-items-center border-2 border-foreground bg-secondary">
<RocketIcon className="size-5" />
</span>
{site.name}
</Link>
<nav className="hidden items-center gap-5 text-sm font-black md:flex">
{navItems.map((item) => (
<Link key={item.href} href={item.href} className="hover:underline">
{item.label}
</Link>
))}
</nav>
<Button asChild className="rounded-none bg-secondary text-foreground hover:bg-secondary/80">
<Link href="/admissions">Подать заявку</Link>
</Button>
</div>
</header>
{children}
<footer className="border-t-2 border-foreground px-4 py-8 md:px-6">
<div className="mx-auto flex max-w-7xl flex-col gap-3 text-sm md:flex-row md:items-center md:justify-between">
<div>
<div className="font-black uppercase">{site.name}</div>
<div className="mt-1 text-muted-foreground">{site.tagline}</div>
</div>
<div className="font-bold">{site.email}</div>
</div>
</footer>
</main>
);
}

View File

@@ -0,0 +1,18 @@
import Image from "next/image";
import { studentWork } from "@/entities/site-content";
export function StudentWorkCard({ work }: { work: (typeof studentWork)[number] }) {
return (
<article className="overflow-hidden border-2 border-foreground bg-card">
<div className="relative h-56 border-b-2 border-foreground">
<Image src={work.image} alt={work.title} fill className="object-cover" sizes="(min-width: 768px) 33vw, 100vw" />
</div>
<div className="p-5">
<div className="text-sm font-black uppercase text-primary">{work.role}</div>
<h3 className="mt-3 text-3xl font-black uppercase leading-none">{work.title}</h3>
<div className="mt-5 border-t-2 border-foreground pt-4 text-xl font-black">{work.result}</div>
</div>
</article>
);
}

View File

@@ -1,457 +0,0 @@
import type { ReactNode } from "react";
import Image from "next/image";
import Link from "next/link";
import {
ArrowRightIcon,
CalendarDaysIcon,
MessageSquareTextIcon,
PlaySquareIcon,
RocketIcon,
Rows3Icon,
TrophyIcon,
UsersIcon,
} from "lucide-react";
import { AdmissionBoard } from "@/features/admission-board/ui/admission-board";
import {
admissions,
communityNotes,
heroStats,
mentors,
modules,
navItems,
outcomes,
programs,
schedule,
site,
studentWork,
} from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
type Program = (typeof programs)[number];
function Shell({ children }: { children: ReactNode }) {
return (
<main className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-30 border-b-2 border-foreground bg-background/95 backdrop-blur">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 md:px-6">
<Link href="/" className="flex items-center gap-3 text-xl font-black uppercase">
<span className="grid size-10 place-items-center border-2 border-foreground bg-secondary">
<RocketIcon className="size-5" />
</span>
{site.name}
</Link>
<nav className="hidden items-center gap-5 text-sm font-black md:flex">
{navItems.map((item) => (
<Link key={item.href} href={item.href} className="hover:underline">
{item.label}
</Link>
))}
</nav>
<Button asChild className="rounded-none bg-secondary text-foreground hover:bg-secondary/80">
<Link href="/admissions">Подать заявку</Link>
</Button>
</div>
</header>
{children}
<footer className="border-t-2 border-foreground px-4 py-8 md:px-6">
<div className="mx-auto flex max-w-7xl flex-col gap-3 text-sm md:flex-row md:items-center md:justify-between">
<div>
<div className="font-black uppercase">{site.name}</div>
<div className="mt-1 text-muted-foreground">{site.tagline}</div>
</div>
<div className="font-bold">{site.email}</div>
</div>
</footer>
</main>
);
}
function LoudTitle({ label, title, text }: { label: string; title: string; text: string }) {
return (
<section className="orbit-board border-b-2 border-foreground px-4 py-12 md:px-6 md:py-20">
<div className="mx-auto max-w-7xl">
<Badge className="mb-5 rounded-none border-2 border-foreground bg-secondary text-foreground">{label}</Badge>
<h1 className="max-w-5xl break-words text-5xl font-black uppercase leading-[0.92] md:text-8xl">{title}</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">{text}</p>
</div>
</section>
);
}
function HeroStudio() {
return (
<div className="overflow-hidden border-2 border-foreground bg-card shadow-[8px_8px_0_var(--foreground)]">
<div className="relative h-72 border-b-2 border-foreground">
<Image
src="https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&w=1400&q=82"
alt="Команда разбирает продуктовые артефакты на воркшопе"
fill
priority
className="object-cover"
sizes="(min-width: 1024px) 38vw, 100vw"
/>
<div className="absolute left-4 top-4 bg-background px-3 py-2 text-xs font-black uppercase">
critique room
</div>
</div>
<div className="orbit-board p-5">
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-xs font-black uppercase text-muted-foreground">week 04 board</div>
<h2 className="mt-1 text-3xl font-black uppercase leading-none">Metric architecture</h2>
</div>
<Badge className="rounded-none bg-secondary text-foreground">{site.capacity}</Badge>
</div>
<div className="mt-5 grid gap-3">
{[
["19:00", "live teardown", "разбор metric tree"],
["20:10", "peer room", "критика по группам"],
["21:00", "mentor notes", "правки до пятницы"],
].map(([time, title, text]) => (
<div key={time} className="grid grid-cols-[64px_1fr] gap-3 border-2 border-foreground bg-background p-3">
<div className="font-black text-primary">{time}</div>
<div>
<div className="font-black uppercase">{title}</div>
<div className="text-sm text-muted-foreground">{text}</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
function ProgramCard({ program, index }: { program: Program; index: number }) {
const href = program.slug === "product-management" ? "/programs/product-management" : "/programs";
return (
<Link
href={href}
className="group flex min-h-[360px] flex-col justify-between border-2 border-foreground bg-card p-5 shadow-[8px_8px_0_var(--foreground)] transition hover:-translate-y-1 hover:shadow-[12px_12px_0_var(--foreground)]"
>
<div className="flex justify-between gap-4 text-sm font-black uppercase">
<span>0{index + 1}</span>
<span>{program.duration}</span>
</div>
<div>
<h3 className="text-4xl font-black uppercase leading-none">{program.title}</h3>
<p className="mt-4 font-bold leading-7">{program.signal}</p>
<p className="mt-3 leading-7 text-muted-foreground">{program.outcome}</p>
</div>
<div>
<div className="mb-4 flex flex-wrap gap-2">
{program.deliverables.slice(0, 3).map((item) => (
<span key={item} className="border-2 border-foreground bg-secondary px-2 py-1 text-xs font-black uppercase">
{item}
</span>
))}
</div>
<div className="flex items-center justify-between border-t-2 border-foreground pt-4 text-sm font-bold">
<span>{program.level} / {program.seats} / {program.price}</span>
<ArrowRightIcon className="size-5 transition group-hover:translate-x-1" />
</div>
</div>
</Link>
);
}
function OutcomeStrip() {
return (
<section className="border-y-2 border-foreground bg-primary px-4 py-10 text-primary-foreground md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-4">
{outcomes.map((item) => (
<div key={item.value}>
<div className="text-5xl font-black">{item.value}</div>
<p className="mt-2 max-w-sm text-sm font-bold opacity-80">{item.label}</p>
</div>
))}
</div>
</section>
);
}
function StudentWorkCard({ work }: { work: (typeof studentWork)[number] }) {
return (
<article className="overflow-hidden border-2 border-foreground bg-card">
<div className="relative h-56 border-b-2 border-foreground">
<Image src={work.image} alt={work.title} fill className="object-cover" sizes="(min-width: 768px) 33vw, 100vw" />
</div>
<div className="p-5">
<div className="text-sm font-black uppercase text-primary">{work.role}</div>
<h3 className="mt-3 text-3xl font-black uppercase leading-none">{work.title}</h3>
<div className="mt-5 border-t-2 border-foreground pt-4 text-xl font-black">{work.result}</div>
</div>
</article>
);
}
function MentorRoster() {
return (
<div className="grid gap-4 md:grid-cols-3">
{mentors.map((mentor) => (
<article key={mentor.name} className="border-2 border-foreground bg-card p-5">
<UsersIcon className="mb-10 size-7 text-primary" />
<h3 className="text-2xl font-black uppercase leading-none">{mentor.name}</h3>
<p className="mt-3 font-bold">{mentor.role}</p>
<p className="mt-2 text-sm text-muted-foreground">{mentor.focus}</p>
</article>
))}
</div>
);
}
export function HomePage() {
return (
<Shell>
<section className="orbit-board px-4 py-10 md:px-6 md:py-14">
<div className="mx-auto grid max-w-7xl gap-8 lg:grid-cols-[0.95fr_1.05fr] lg:items-center">
<div>
<Badge className="mb-5 rounded-none border-2 border-foreground bg-secondary text-foreground">
Следующий поток: {site.nextCohort}
</Badge>
<h1 className="hidden text-5xl font-black uppercase leading-[0.86] md:block md:text-8xl">
Не лекции. Еженедельный продуктовый разбор.
</h1>
<h1 className="text-[2.65rem] font-black uppercase leading-[0.9] md:hidden">
Не лекции. Работа, разбор, demo day.
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">
Orbit Academy запускает cohort-based программы, где студенты каждую неделю
приносят рабочий артефакт: problem brief, research board, metric tree или roadmap defense.
</p>
<div className="mt-7 flex flex-wrap gap-3">
<Button asChild className="rounded-none" size="lg">
<Link href="/programs">Выбрать программу <ArrowRightIcon className="size-4" /></Link>
</Button>
<Button asChild variant="outline" className="rounded-none border-2" size="lg">
<Link href="/schedule">Расписание</Link>
</Button>
</div>
<div className="mt-8 grid gap-3 md:grid-cols-3">
{heroStats.map((item) => (
<div key={item.value} className="border-2 border-foreground bg-card p-3">
<div className="text-2xl font-black">{item.value}</div>
<div className="mt-1 text-xs font-bold text-muted-foreground">{item.label}</div>
</div>
))}
</div>
</div>
<HeroStudio />
</div>
</section>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-3">
{programs.map((program, index) => (
<ProgramCard key={program.title} program={program} index={index} />
))}
</div>
</section>
<OutcomeStrip />
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.7fr_1.3fr]">
<div>
<div className="marker-highlight w-fit text-sm font-black uppercase">работы студентов</div>
<h2 className="mt-5 text-4xl font-black uppercase leading-none md:text-6xl">Proof вместо обещаний</h2>
</div>
<div className="grid gap-5 md:grid-cols-3">
{studentWork.map((work) => (
<StudentWorkCard key={work.title} work={work} />
))}
</div>
</div>
</section>
</Shell>
);
}
export function ProgramsPage() {
return (
<Shell>
<LoudTitle
label="Programs"
title="Три программы для разных точек роста"
text="Каждая программа имеет outcome, расписание, формат обратной связи, deliverables и ограниченное количество мест в группе."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-3">
{programs.map((program, index) => (
<ProgramCard key={program.title} program={program} index={index} />
))}
</div>
</section>
<section className="px-4 pb-12 md:px-6">
<div className="mx-auto max-w-7xl">
<MentorRoster />
</div>
</section>
</Shell>
);
}
export function ProgramDetailPage() {
const program = programs[0];
return (
<Shell>
<LoudTitle
label="Product Management"
title="От проблемы к roadmap, который можно защищать"
text={`${program.duration}, ${program.format}. Итог - ${program.outcome}.`}
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-5">
{modules.map((module) => (
<article key={module.week} className="grid gap-4 border-2 border-foreground bg-card p-5 md:grid-cols-[100px_0.75fr_1fr_0.8fr] md:items-start">
<div className="text-5xl font-black">{module.week}</div>
<div>
<h2 className="text-3xl font-black uppercase leading-none">{module.title}</h2>
<div className="mt-3 text-sm font-black text-primary">{module.critique}</div>
</div>
<p className="leading-7 text-muted-foreground">{module.task}</p>
<div className="border-2 border-foreground bg-secondary p-3 text-sm font-black">{module.artifact}</div>
</article>
))}
</div>
</section>
</Shell>
);
}
export function SchedulePage() {
return (
<Shell>
<LoudTitle
label="Schedule"
title="Потоки видны как операционный календарь"
text="Страница помогает выбрать момент входа: дата старта, формат, статус набора и реальная интенсивность."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto max-w-7xl divide-y-2 divide-foreground border-2 border-foreground bg-card">
{schedule.map((item) => (
<div key={`${item.date}-${item.program}`} className="grid gap-3 p-5 md:grid-cols-[150px_1fr_190px_190px_150px] md:items-center">
<div className="flex items-center gap-2 font-black">
<CalendarDaysIcon className="size-5" />
{item.date}
</div>
<div className="text-3xl font-black uppercase leading-none">{item.program}</div>
<div className="font-bold">{item.mode}</div>
<div className="text-sm text-muted-foreground">{item.intensity}</div>
<Badge className="w-fit rounded-none bg-secondary text-foreground">{item.status}</Badge>
</div>
))}
</div>
</section>
</Shell>
);
}
export function OutcomesPage() {
return (
<Shell>
<LoudTitle
label="Outcomes"
title="Результаты показываются через работу, а не обещания"
text="Шаблон отделяет образовательный маркетинг от реальных outcome-блоков: кейсы, метрики, демо и роли выпускников."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-2 lg:grid-cols-4">
{outcomes.map((item) => (
<article key={item.value} className="border-2 border-foreground bg-card p-6 shadow-[8px_8px_0_var(--foreground)]">
<TrophyIcon className="mb-10 size-8 text-primary" />
<div className="text-5xl font-black">{item.value}</div>
<p className="mt-4 font-bold leading-7">{item.label}</p>
<p className="mt-3 text-sm text-muted-foreground">{item.proof}</p>
</article>
))}
</div>
</section>
<section className="px-4 pb-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-5 md:grid-cols-3">
{studentWork.map((work) => (
<StudentWorkCard key={work.title} work={work} />
))}
</div>
</section>
</Shell>
);
}
export function AdmissionsPage() {
return (
<Shell>
<LoudTitle
label="Admissions"
title="Поступление без иллюзии элитарности"
text="Нужна не мотивационная анкета, а проверка цели, уровня и готовности показывать работу группе каждую неделю."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-8 lg:grid-cols-[1fr_430px]">
<div className="grid gap-4">
{admissions.map((item, index) => (
<div key={item} className="grid gap-4 border-2 border-foreground bg-card p-5 md:grid-cols-[72px_1fr]">
<div className="text-4xl font-black text-primary">0{index + 1}</div>
<h2 className="text-3xl font-black uppercase leading-none">{item}</h2>
</div>
))}
</div>
<AdmissionBoard />
</div>
</section>
</Shell>
);
}
export function CommunityPage() {
return (
<Shell>
<LoudTitle
label="Community"
title="Комьюнити работает после последней защиты"
text="Сильная онлайн-школа продает не только программу, но и среду, где продолжают показывать работу и получать обратную связь."
/>
<section className="px-4 py-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-3">
{communityNotes.map((note) => (
<article key={note.title} className="flex min-h-80 flex-col justify-between border-2 border-foreground bg-card p-6">
<div>
<MessageSquareTextIcon className="mb-16 size-8 text-primary" />
<h2 className="text-3xl font-black uppercase leading-none">{note.title}</h2>
<p className="mt-4 leading-7 text-muted-foreground">{note.text}</p>
</div>
<div className="mt-8 border-t-2 border-foreground pt-4 text-sm font-black uppercase">{note.cadence}</div>
</article>
))}
</div>
</section>
<section className="px-4 pb-12 md:px-6">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.8fr_1.2fr]">
<div className="border-2 border-foreground bg-secondary p-6">
<PlaySquareIcon className="mb-10 size-8" />
<h2 className="text-4xl font-black uppercase leading-none">Demo archive</h2>
<p className="mt-4 leading-7">Записи защит, шаблоны артефактов и разборы доступны выпускникам после курса.</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
{[
["artifact library", "problem briefs, research boards, metric trees"],
["hiring rooms", "портфолио-ревью и разбор офферов"],
["peer circles", "мини-группы по роли и домену"],
["mentor threads", "асинхронные вопросы после live-сессий"],
].map(([title, text]) => (
<div key={title} className="border-2 border-foreground bg-card p-5">
<Rows3Icon className="mb-8 size-7 text-primary" />
<h3 className="text-2xl font-black uppercase">{title}</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{text}</p>
</div>
))}
</div>
</div>
</section>
</Shell>
);
}