feat: split big file and update agents.md
This commit is contained in:
53
AGENTS.md
53
AGENTS.md
@@ -5,8 +5,59 @@ Common Ground — editorial impact/NKO шаблон: сохраняй newsroom-
|
|||||||
## Project Specifics
|
## Project Specifics
|
||||||
|
|
||||||
- Кампании, метрики, ledger, истории, смены, документы, партнеры и бюджет описаны в `src/entities/site-content.ts`.
|
- Кампании, метрики, ledger, истории, смены, документы, партнеры и бюджет описаны в `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`; не добавляй реальные платежи, CRM, donor accounts или API без запроса.
|
- Mock-пожертвования держи в `src/features/*/ui`; не добавляй реальные платежи, CRM, donor accounts или API без запроса.
|
||||||
- Не делай generic charity landing: каждая страница должна усиливать доверие через цель, прогресс, документы, ответственного, место или конкретную смену.
|
- Не делай generic charity landing: каждая страница должна усиливать доверие через цель, прогресс, документы, ответственного, место или конкретную смену.
|
||||||
- Избегай мягкой декоративной благотворительности; стиль должен ощущаться как редакция, полевой desk и публичная отчетность.
|
- Избегай мягкой декоративной благотворительности; стиль должен ощущаться как редакция, полевой desk и публичная отчетность.
|
||||||
- Проверка после правок: `pnpm lint` и `pnpm build`.
|
- Проверка после правок: `pnpm lint` и `pnpm build`.
|
||||||
|
|
||||||
|
## Design System
|
||||||
|
|
||||||
|
Источник токенов — `src/app/globals.css` (`@theme` + `:root`/`.dark`). Шрифт — Noto Serif (`--font-commonground`), один serif-гарнитур на sans и mono, с включёнными `ss01`/`cv01`/`tnum` (табличные цифры для ledger). Работай через семантические классы Tailwind (`bg-card`, `text-foreground`, `text-primary`, `border-foreground`), не хардкодь hex/oklch.
|
||||||
|
|
||||||
|
Личность: **newsroom / полевой выпуск** — тёплый газетный кремовый фон с лёгкой бумажной зернистостью, тёмно-коричневый «ink» как текст и границы, глубокий teal primary как «редакционный» сигнал, мягкий зелёный secondary (надежда/поле) и тёплый терракотовый accent. Это не charity-пастель: ощущение печатной редакции и публичной отчётности.
|
||||||
|
|
||||||
|
| Роль | Light | Характер |
|
||||||
|
|---|---|---|
|
||||||
|
| `background` | газетный кремовый (oklch 0.96 0.018 83) | бумажный фон с зерном-сеткой |
|
||||||
|
| `foreground` | тёмно-коричневый ink (oklch 0.17 0.025 65) | текст + **границы** (`border-2 border-foreground`) |
|
||||||
|
| `primary` | глубокий teal (oklch 0.43 0.095 218) | акценты, цифры ledger, кнопки, иконки |
|
||||||
|
| `secondary` | мягкий зелёный (oklch 0.83 0.09 145) | бейджи региона/роли, hero-glow |
|
||||||
|
| `muted` / `muted-foreground` | тёплый серо-бежевый | вторичный текст |
|
||||||
|
| `accent` | терракота (oklch 0.73 0.105 50) | редкий тёплый сигнал |
|
||||||
|
| `card` | почти белый кремовый | карточки кампаний, ledger, документы |
|
||||||
|
|
||||||
|
Узнаваемые приёмы (держи их, это и есть «лицо» проекта):
|
||||||
|
- **Серифная типографика:** Noto Serif + `font-black` — газетные заголовки, очень плотный `leading` (`leading-[0.88]`/`leading-[0.96]`), крупные размеры (до `text-8xl`). Тело текста тоже serif.
|
||||||
|
- **Толстые границы:** `border-2 border-foreground`, `divide-y-2 divide-foreground` — карточки, таблицы ledger и доски смен очерчены, а не растворены.
|
||||||
|
- **Hard offset shadow:** `shadow-[8px_8px_0_var(--foreground)]` на ключевых aside-блоках (hero-выпуск, партнёрская проверка).
|
||||||
|
- **Double-rule плашки:** утилита `.impact-rule` — двойная типографская линейка сверху/снизу для рубрик-капителей (`text-sm font-black uppercase`).
|
||||||
|
- **Газетная подложка:** `.newsprint` — тонкая сетка-растр для hero/title-секций; глобальный фон body тоже несёт зелёный radial-glow + вертикальный растр.
|
||||||
|
- **Скромный radius:** `--radius` = 0.18rem; элементы почти прямоугольные, но не абсолютно острые.
|
||||||
|
- **Ledger-first доверие:** табличные цифры (`tnum`), прогресс-бары целей и процентов бюджета, проверяемые блоки документов.
|
||||||
|
|
||||||
|
Do / Don't:
|
||||||
|
- **Do:** держи serif-заголовки, толстые границы, double-rule рубрики, публичный ledger и прогресс-бары; цифры — через `text-primary`.
|
||||||
|
- **Don't:** sans-заголовки, мягкие тени, скруглённые «pill»-карточки, пастельные градиенты, generic-charity hero — это ломает newsroom-личность.
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
| Route | Widget |
|
||||||
|
|---|---|
|
||||||
|
| `/` | `src/widgets/home-page.tsx` (`HomePage`) |
|
||||||
|
| `/campaigns` | `src/widgets/campaigns-page.tsx` (`CampaignsPage`) |
|
||||||
|
| `/campaigns/clean-water` | `src/widgets/campaign-detail-page.tsx` (`CampaignDetailPage`) |
|
||||||
|
| `/impact` | `src/widgets/impact-page.tsx` (`ImpactPage`) |
|
||||||
|
| `/stories` | `src/widgets/stories-page.tsx` (`StoriesPage`) |
|
||||||
|
| `/volunteer` | `src/widgets/volunteer-page.tsx` (`VolunteerPage`) |
|
||||||
|
| `/transparency` | `src/widgets/transparency-page.tsx` (`TransparencyPage`) |
|
||||||
|
|
||||||
|
Переиспользуемые блоки:
|
||||||
|
- `src/widgets/site-shell.tsx` — `SiteShell` (header + nav + footer, обёртка всех страниц).
|
||||||
|
- `src/widgets/campaign-card.tsx` — `CampaignCard` (Home featured + Campaigns grid).
|
||||||
|
- `src/widgets/ledger-table.tsx` — `LedgerTable` (Home + Impact).
|
||||||
|
- `src/shared/ui/editorial-title.tsx` — `EditorialTitle` (заголовочная секция внутренних страниц: Campaigns, Impact, Stories, Volunteer, Transparency).
|
||||||
|
- `src/shared/lib/campaign.ts` — `formatRub`, `campaignProgress` (CampaignCard + Campaign Detail).
|
||||||
|
- `src/features/donation-panel/ui/donation-panel.tsx` — mock-панель пожертвования.
|
||||||
|
|
||||||
|
Одноразовые блоки колоцированы со своей страницей: `MetricCard` в `impact-page.tsx`, `StoryCard` в `stories-page.tsx`, `DocumentCard` в `transparency-page.tsx`.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CampaignDetailPage } from "@/widgets/template-ui";
|
import { CampaignDetailPage } from "@/widgets/campaign-detail-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <CampaignDetailPage />;
|
return <CampaignDetailPage />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CampaignsPage } from "@/widgets/template-ui";
|
import { CampaignsPage } from "@/widgets/campaigns-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <CampaignsPage />;
|
return <CampaignsPage />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ImpactPage } from "@/widgets/template-ui";
|
import { ImpactPage } from "@/widgets/impact-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <ImpactPage />;
|
return <ImpactPage />;
|
||||||
|
|||||||
@@ -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 />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { StoriesPage } from "@/widgets/template-ui";
|
import { StoriesPage } from "@/widgets/stories-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <StoriesPage />;
|
return <StoriesPage />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TransparencyPage } from "@/widgets/template-ui";
|
import { TransparencyPage } from "@/widgets/transparency-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <TransparencyPage />;
|
return <TransparencyPage />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { VolunteerPage } from "@/widgets/template-ui";
|
import { VolunteerPage } from "@/widgets/volunteer-page";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <VolunteerPage />;
|
return <VolunteerPage />;
|
||||||
|
|||||||
11
src/shared/lib/campaign.ts
Normal file
11
src/shared/lib/campaign.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { campaigns } from "@/entities/site-content";
|
||||||
|
|
||||||
|
type Campaign = (typeof campaigns)[number];
|
||||||
|
|
||||||
|
export function formatRub(value: number) {
|
||||||
|
return new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 }).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function campaignProgress(campaign: Campaign) {
|
||||||
|
return Math.round((campaign.raised / campaign.goal) * 100);
|
||||||
|
}
|
||||||
11
src/shared/ui/editorial-title.tsx
Normal file
11
src/shared/ui/editorial-title.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export function EditorialTitle({ label, title, text }: { label: string; title: string; text: string }) {
|
||||||
|
return (
|
||||||
|
<section className="newsprint border-b-2 border-foreground px-4 py-12 md:px-6 md:py-20">
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<div className="impact-rule mb-6 py-2 text-sm font-black uppercase">{label}</div>
|
||||||
|
<h1 className="max-w-5xl text-5xl font-black leading-[0.95] md:text-8xl">{title}</h1>
|
||||||
|
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground md:text-xl">{text}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
src/widgets/campaign-card.tsx
Normal file
50
src/widgets/campaign-card.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { campaigns } from "@/entities/site-content";
|
||||||
|
import { campaignProgress, formatRub } from "@/shared/lib/campaign";
|
||||||
|
import { Badge } from "@/shared/ui/badge";
|
||||||
|
import { Progress } from "@/shared/ui/progress";
|
||||||
|
|
||||||
|
type Campaign = (typeof campaigns)[number];
|
||||||
|
|
||||||
|
export function CampaignCard({ campaign, featured = false }: { campaign: Campaign; featured?: boolean }) {
|
||||||
|
const href = campaign.slug === "clean-water" ? "/campaigns/clean-water" : "/campaigns";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className={`overflow-hidden border-2 border-foreground bg-card ${featured ? "md:grid md:grid-cols-[1.05fr_0.95fr]" : ""}`}>
|
||||||
|
<Link href={href} className={featured ? "contents" : "block"}>
|
||||||
|
<div className={`relative border-b-2 border-foreground ${featured ? "min-h-[360px] md:border-b-0 md:border-r-2" : "h-72"}`}>
|
||||||
|
<Image
|
||||||
|
src={campaign.image}
|
||||||
|
alt={campaign.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes={featured ? "(min-width: 768px) 50vw, 100vw" : "(min-width: 768px) 33vw, 100vw"}
|
||||||
|
/>
|
||||||
|
<div className="absolute left-4 top-4 bg-background px-3 py-2 text-xs font-black uppercase">
|
||||||
|
{campaign.deadline}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-5 md:p-6">
|
||||||
|
<div className="mb-5 flex flex-wrap items-center gap-2">
|
||||||
|
<Badge variant="outline" className="border-foreground bg-secondary font-black">
|
||||||
|
{campaign.region}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm font-bold text-muted-foreground">{campaignProgress(campaign)}% цели</span>
|
||||||
|
</div>
|
||||||
|
<h2 className={`${featured ? "text-4xl md:text-6xl" : "text-3xl"} font-black leading-[0.96]`}>{campaign.title}</h2>
|
||||||
|
<p className="mt-4 text-lg font-bold leading-7">{campaign.lead}</p>
|
||||||
|
<p className="mt-3 leading-7 text-muted-foreground">{campaign.text}</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Progress value={campaignProgress(campaign)} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex justify-between gap-4 text-sm font-black">
|
||||||
|
<span>{formatRub(campaign.raised)} ₽ собрано</span>
|
||||||
|
<span>{formatRub(campaign.goal)} ₽ цель</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
src/widgets/campaign-detail-page.tsx
Normal file
63
src/widgets/campaign-detail-page.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { DonationPanel } from "@/features/donation-panel/ui/donation-panel";
|
||||||
|
import { campaigns } from "@/entities/site-content";
|
||||||
|
import { campaignProgress, formatRub } from "@/shared/lib/campaign";
|
||||||
|
import { Badge } from "@/shared/ui/badge";
|
||||||
|
import { Progress } from "@/shared/ui/progress";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
export function CampaignDetailPage() {
|
||||||
|
const campaign = campaigns[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<section className="grid border-b-2 border-foreground lg:grid-cols-[1fr_430px]">
|
||||||
|
<div className="newsprint p-4 md:p-6">
|
||||||
|
<div className="mb-5 flex flex-wrap gap-2">
|
||||||
|
<Badge className="font-black">{campaign.region}</Badge>
|
||||||
|
<Badge variant="outline" className="border-foreground bg-background font-black">{campaign.deadline}</Badge>
|
||||||
|
</div>
|
||||||
|
<h1 className="max-w-4xl text-5xl font-black leading-[0.96] md:text-8xl">{campaign.title}</h1>
|
||||||
|
<p className="mt-6 max-w-2xl text-xl font-bold leading-8">{campaign.lead}</p>
|
||||||
|
<p className="mt-4 max-w-2xl leading-8 text-muted-foreground">{campaign.text}</p>
|
||||||
|
<div className="mt-8 max-w-3xl">
|
||||||
|
<Progress value={campaignProgress(campaign)} />
|
||||||
|
<div className="mt-3 flex justify-between gap-4 font-black">
|
||||||
|
<span>{formatRub(campaign.raised)} ₽ собрано</span>
|
||||||
|
<span>{formatRub(campaign.goal)} ₽ цель</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 grid max-w-4xl gap-3 md:grid-cols-4">
|
||||||
|
{campaign.checkpoints.map((point, index) => (
|
||||||
|
<div key={point} className="border-2 border-foreground bg-card p-4">
|
||||||
|
<div className="text-3xl font-black text-primary">0{index + 1}</div>
|
||||||
|
<div className="mt-6 font-black">{point}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-t-2 border-foreground p-4 md:p-6 lg:border-l-2 lg:border-t-0">
|
||||||
|
<DonationPanel />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="px-4 py-12 md:px-6">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.75fr_1.25fr]">
|
||||||
|
<div>
|
||||||
|
<div className="impact-rule py-2 text-sm font-black uppercase">бюджет кампании</div>
|
||||||
|
<h2 className="mt-5 text-4xl font-black leading-none">Куда уйдет каждый рубль</h2>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-5">
|
||||||
|
{campaign.allocation.map((item) => (
|
||||||
|
<div key={item.label}>
|
||||||
|
<div className="mb-2 flex justify-between font-black">
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<span>{item.value}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={item.value} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/widgets/campaigns-page.tsx
Normal file
23
src/widgets/campaigns-page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { campaigns } from "@/entities/site-content";
|
||||||
|
import { EditorialTitle } from "@/shared/ui/editorial-title";
|
||||||
|
import { CampaignCard } from "@/widgets/campaign-card";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
export function CampaignsPage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<EditorialTitle
|
||||||
|
label="Кампании"
|
||||||
|
title="Каждый сбор объясняет цель, регион, срок и бюджет"
|
||||||
|
text="Страница помогает быстро понять, где нужна помощь, сколько уже закрыто и какие шаги будут проверены после сбора."
|
||||||
|
/>
|
||||||
|
<section className="px-4 py-12 md:px-6">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-3">
|
||||||
|
{campaigns.map((campaign) => (
|
||||||
|
<CampaignCard key={campaign.title} campaign={campaign} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
90
src/widgets/home-page.tsx
Normal file
90
src/widgets/home-page.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowRightIcon, NewspaperIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { campaigns, impactMetrics, site } from "@/entities/site-content";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { CampaignCard } from "@/widgets/campaign-card";
|
||||||
|
import { LedgerTable } from "@/widgets/ledger-table";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
export function HomePage() {
|
||||||
|
const featured = campaigns[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<section className="newsprint border-b-2 border-foreground px-4 py-10 md:px-6 md:py-14">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_420px]">
|
||||||
|
<div>
|
||||||
|
<div className="impact-rule mb-5 py-2 text-sm font-black uppercase">{site.issue} / {site.tagline}</div>
|
||||||
|
<h1 className="max-w-5xl text-5xl font-black leading-[0.88] md:text-8xl">
|
||||||
|
Помощь, которую видно по документам и людям
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">
|
||||||
|
Common Ground - шаблон для НКО и impact-платформы, где каждая кампания показывает
|
||||||
|
цель, бюджет, полевой прогресс, истории и открытый реестр расходов.
|
||||||
|
</p>
|
||||||
|
<div className="mt-7 flex flex-wrap gap-3">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link href="/campaigns">Смотреть кампании <ArrowRightIcon className="size-4" /></Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline" size="lg">
|
||||||
|
<Link href="/transparency">Открыть отчетность</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<aside className="border-2 border-foreground bg-card p-5 shadow-[8px_8px_0_var(--foreground)]">
|
||||||
|
<NewspaperIcon className="mb-10 size-8 text-primary" />
|
||||||
|
<div className="impact-rule py-2 text-sm font-black uppercase">полевой выпуск</div>
|
||||||
|
<p className="mt-5 text-xl font-bold leading-8">
|
||||||
|
НКО продает доверие. Поэтому первый экран сразу дает цель, факт, ответственного и путь к документам.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 grid gap-3 text-sm">
|
||||||
|
<div className="flex items-center justify-between border-b border-foreground/25 pb-2">
|
||||||
|
<span className="text-muted-foreground">открытых кампаний</span>
|
||||||
|
<span className="font-black">3</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between border-b border-foreground/25 pb-2">
|
||||||
|
<span className="text-muted-foreground">обновление реестра</span>
|
||||||
|
<span className="font-black">еженедельно</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground">полевой desk</span>
|
||||||
|
<span className="font-black">{site.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="px-4 py-12 md:px-6">
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<CampaignCard campaign={featured} featured />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<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-4 md:grid-cols-4">
|
||||||
|
{impactMetrics.map((item) => (
|
||||||
|
<div key={item.value}>
|
||||||
|
<div className="text-5xl font-black">{item.value}</div>
|
||||||
|
<p className="mt-2 text-sm font-bold opacity-85">{item.label}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="px-4 py-12 md:px-6">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.85fr_1.15fr]">
|
||||||
|
<div>
|
||||||
|
<div className="impact-rule py-2 text-sm font-black uppercase">живой реестр</div>
|
||||||
|
<h2 className="mt-5 text-4xl font-black leading-none md:text-6xl">Последние движения средств</h2>
|
||||||
|
<p className="mt-4 leading-7 text-muted-foreground">
|
||||||
|
Не прячьте доверие в PDF. Покажите свежие поступления, закупки и основание расходов прямо на сайте.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<LedgerTable />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
src/widgets/impact-page.tsx
Normal file
47
src/widgets/impact-page.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { ClipboardCheckIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { impactMetrics } from "@/entities/site-content";
|
||||||
|
import { EditorialTitle } from "@/shared/ui/editorial-title";
|
||||||
|
import { LedgerTable } from "@/widgets/ledger-table";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
function MetricCard({ metric }: { metric: (typeof impactMetrics)[number] }) {
|
||||||
|
return (
|
||||||
|
<article className="border-2 border-foreground bg-card p-5">
|
||||||
|
<div className="text-5xl font-black md:text-6xl">{metric.value}</div>
|
||||||
|
<p className="mt-4 font-black leading-6">{metric.label}</p>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-muted-foreground">{metric.detail}</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImpactPage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<EditorialTitle
|
||||||
|
label="Отчет о влиянии"
|
||||||
|
title="Impact измеряется людьми, сменами и подтвержденными расходами"
|
||||||
|
text="Страница заменяет красивую декларацию на отчетные блоки, которые можно развивать в полноценный годовой отчет."
|
||||||
|
/>
|
||||||
|
<section className="px-4 py-12 md:px-6">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-5 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{impactMetrics.map((item) => (
|
||||||
|
<MetricCard key={item.value} metric={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="px-4 pb-12 md:px-6">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_1fr]">
|
||||||
|
<div className="border-2 border-foreground bg-card p-6">
|
||||||
|
<ClipboardCheckIcon className="mb-10 size-8 text-primary" />
|
||||||
|
<h2 className="text-4xl font-black leading-none">Методика подтверждения</h2>
|
||||||
|
<p className="mt-5 leading-7 text-muted-foreground">
|
||||||
|
Каждая цифра связывается с документом: актом, фото, письмом партнера, платежом или сменой координатора.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<LedgerTable />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/widgets/ledger-table.tsx
Normal file
16
src/widgets/ledger-table.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ledger } from "@/entities/site-content";
|
||||||
|
|
||||||
|
export function LedgerTable() {
|
||||||
|
return (
|
||||||
|
<div className="divide-y-2 divide-foreground border-2 border-foreground bg-card">
|
||||||
|
{ledger.map((row) => (
|
||||||
|
<div key={`${row.date}-${row.source}`} className="grid gap-2 p-4 text-sm md:grid-cols-[110px_1fr_130px_1.1fr] md:items-center">
|
||||||
|
<div className="font-black">{row.date}</div>
|
||||||
|
<div>{row.source}</div>
|
||||||
|
<div className="font-black text-primary">{row.amount}</div>
|
||||||
|
<div className="text-muted-foreground">{row.note}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/widgets/site-shell.tsx
Normal file
46
src/widgets/site-shell.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { NewspaperIcon } 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 bg-foreground text-background">
|
||||||
|
<NewspaperIcon 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 size="sm">
|
||||||
|
<Link href="/campaigns/clean-water">Помочь</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{children}
|
||||||
|
<footer className="border-t-2 border-foreground px-4 py-8 md:px-6">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-4 text-sm md:grid-cols-[1fr_auto] md:items-center">
|
||||||
|
<div>
|
||||||
|
<div className="font-black uppercase">{site.name}</div>
|
||||||
|
<div className="mt-1 text-muted-foreground">{site.tagline}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-4 font-bold">
|
||||||
|
<span>{site.email}</span>
|
||||||
|
<span>{site.phone}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/widgets/stories-page.tsx
Normal file
44
src/widgets/stories-page.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { MapPinIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { stories } from "@/entities/site-content";
|
||||||
|
import { EditorialTitle } from "@/shared/ui/editorial-title";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
function StoryCard({ story }: { story: (typeof stories)[number] }) {
|
||||||
|
return (
|
||||||
|
<article className="overflow-hidden border-2 border-foreground bg-card">
|
||||||
|
<div className="relative h-64 border-b-2 border-foreground">
|
||||||
|
<Image src={story.image} alt={story.title} fill className="object-cover" sizes="(min-width: 768px) 33vw, 100vw" />
|
||||||
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="mb-4 flex items-center gap-2 text-sm font-black uppercase text-primary">
|
||||||
|
<MapPinIcon className="size-4" />
|
||||||
|
{story.place}
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-black leading-tight">{story.title}</h2>
|
||||||
|
<p className="mt-4 leading-7 text-muted-foreground">{story.text}</p>
|
||||||
|
<div className="mt-5 border-t-2 border-foreground pt-4 text-lg font-black">{story.result}</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StoriesPage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<EditorialTitle
|
||||||
|
label="Истории"
|
||||||
|
title="Истории показывают, что изменилось после кампании"
|
||||||
|
text="Вместо generic отзывов здесь короткие заметки с местом, ситуацией, фотографией и проверяемым результатом."
|
||||||
|
/>
|
||||||
|
<section className="px-4 py-12 md:px-6">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-3">
|
||||||
|
{stories.map((story) => (
|
||||||
|
<StoryCard key={story.title} story={story} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,496 +0,0 @@
|
|||||||
import type { ReactNode } from "react";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
ArrowRightIcon,
|
|
||||||
ClipboardCheckIcon,
|
|
||||||
FileTextIcon,
|
|
||||||
LandmarkIcon,
|
|
||||||
MapPinIcon,
|
|
||||||
NewspaperIcon,
|
|
||||||
ReceiptTextIcon,
|
|
||||||
ShieldCheckIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import { DonationPanel } from "@/features/donation-panel/ui/donation-panel";
|
|
||||||
import {
|
|
||||||
campaigns,
|
|
||||||
documents,
|
|
||||||
impactMetrics,
|
|
||||||
ledger,
|
|
||||||
navItems,
|
|
||||||
partners,
|
|
||||||
site,
|
|
||||||
stories,
|
|
||||||
transparency,
|
|
||||||
volunteerShifts,
|
|
||||||
} from "@/entities/site-content";
|
|
||||||
import { Badge } from "@/shared/ui/badge";
|
|
||||||
import { Button } from "@/shared/ui/button";
|
|
||||||
import { Progress } from "@/shared/ui/progress";
|
|
||||||
|
|
||||||
type Campaign = (typeof campaigns)[number];
|
|
||||||
|
|
||||||
function formatRub(value: number) {
|
|
||||||
return new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 }).format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function campaignProgress(campaign: Campaign) {
|
|
||||||
return Math.round((campaign.raised / campaign.goal) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 bg-foreground text-background">
|
|
||||||
<NewspaperIcon 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 size="sm">
|
|
||||||
<Link href="/campaigns/clean-water">Помочь</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
{children}
|
|
||||||
<footer className="border-t-2 border-foreground px-4 py-8 md:px-6">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-4 text-sm md:grid-cols-[1fr_auto] md:items-center">
|
|
||||||
<div>
|
|
||||||
<div className="font-black uppercase">{site.name}</div>
|
|
||||||
<div className="mt-1 text-muted-foreground">{site.tagline}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-4 font-bold">
|
|
||||||
<span>{site.email}</span>
|
|
||||||
<span>{site.phone}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditorialTitle({ label, title, text }: { label: string; title: string; text: string }) {
|
|
||||||
return (
|
|
||||||
<section className="newsprint border-b-2 border-foreground px-4 py-12 md:px-6 md:py-20">
|
|
||||||
<div className="mx-auto max-w-7xl">
|
|
||||||
<div className="impact-rule mb-6 py-2 text-sm font-black uppercase">{label}</div>
|
|
||||||
<h1 className="max-w-5xl text-5xl font-black leading-[0.95] md:text-8xl">{title}</h1>
|
|
||||||
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground md:text-xl">{text}</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CampaignCard({ campaign, featured = false }: { campaign: Campaign; featured?: boolean }) {
|
|
||||||
const href = campaign.slug === "clean-water" ? "/campaigns/clean-water" : "/campaigns";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article className={`overflow-hidden border-2 border-foreground bg-card ${featured ? "md:grid md:grid-cols-[1.05fr_0.95fr]" : ""}`}>
|
|
||||||
<Link href={href} className={featured ? "contents" : "block"}>
|
|
||||||
<div className={`relative border-b-2 border-foreground ${featured ? "min-h-[360px] md:border-b-0 md:border-r-2" : "h-72"}`}>
|
|
||||||
<Image
|
|
||||||
src={campaign.image}
|
|
||||||
alt={campaign.title}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
sizes={featured ? "(min-width: 768px) 50vw, 100vw" : "(min-width: 768px) 33vw, 100vw"}
|
|
||||||
/>
|
|
||||||
<div className="absolute left-4 top-4 bg-background px-3 py-2 text-xs font-black uppercase">
|
|
||||||
{campaign.deadline}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-5 md:p-6">
|
|
||||||
<div className="mb-5 flex flex-wrap items-center gap-2">
|
|
||||||
<Badge variant="outline" className="border-foreground bg-secondary font-black">
|
|
||||||
{campaign.region}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-sm font-bold text-muted-foreground">{campaignProgress(campaign)}% цели</span>
|
|
||||||
</div>
|
|
||||||
<h2 className={`${featured ? "text-4xl md:text-6xl" : "text-3xl"} font-black leading-[0.96]`}>{campaign.title}</h2>
|
|
||||||
<p className="mt-4 text-lg font-bold leading-7">{campaign.lead}</p>
|
|
||||||
<p className="mt-3 leading-7 text-muted-foreground">{campaign.text}</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<Progress value={campaignProgress(campaign)} />
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 flex justify-between gap-4 text-sm font-black">
|
|
||||||
<span>{formatRub(campaign.raised)} ₽ собрано</span>
|
|
||||||
<span>{formatRub(campaign.goal)} ₽ цель</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MetricCard({ metric }: { metric: (typeof impactMetrics)[number] }) {
|
|
||||||
return (
|
|
||||||
<article className="border-2 border-foreground bg-card p-5">
|
|
||||||
<div className="text-5xl font-black md:text-6xl">{metric.value}</div>
|
|
||||||
<p className="mt-4 font-black leading-6">{metric.label}</p>
|
|
||||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{metric.detail}</p>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LedgerTable() {
|
|
||||||
return (
|
|
||||||
<div className="divide-y-2 divide-foreground border-2 border-foreground bg-card">
|
|
||||||
{ledger.map((row) => (
|
|
||||||
<div key={`${row.date}-${row.source}`} className="grid gap-2 p-4 text-sm md:grid-cols-[110px_1fr_130px_1.1fr] md:items-center">
|
|
||||||
<div className="font-black">{row.date}</div>
|
|
||||||
<div>{row.source}</div>
|
|
||||||
<div className="font-black text-primary">{row.amount}</div>
|
|
||||||
<div className="text-muted-foreground">{row.note}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StoryCard({ story }: { story: (typeof stories)[number] }) {
|
|
||||||
return (
|
|
||||||
<article className="overflow-hidden border-2 border-foreground bg-card">
|
|
||||||
<div className="relative h-64 border-b-2 border-foreground">
|
|
||||||
<Image src={story.image} alt={story.title} fill className="object-cover" sizes="(min-width: 768px) 33vw, 100vw" />
|
|
||||||
</div>
|
|
||||||
<div className="p-5">
|
|
||||||
<div className="mb-4 flex items-center gap-2 text-sm font-black uppercase text-primary">
|
|
||||||
<MapPinIcon className="size-4" />
|
|
||||||
{story.place}
|
|
||||||
</div>
|
|
||||||
<h2 className="text-3xl font-black leading-tight">{story.title}</h2>
|
|
||||||
<p className="mt-4 leading-7 text-muted-foreground">{story.text}</p>
|
|
||||||
<div className="mt-5 border-t-2 border-foreground pt-4 text-lg font-black">{story.result}</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DocumentCard({ item }: { item: (typeof documents)[number] }) {
|
|
||||||
return (
|
|
||||||
<article className="border-2 border-foreground bg-card p-4">
|
|
||||||
<ReceiptTextIcon className="mb-8 size-7 text-primary" />
|
|
||||||
<h3 className="text-2xl font-black leading-tight">{item.title}</h3>
|
|
||||||
<p className="mt-3 font-bold">{item.status}</p>
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">{item.owner}</p>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HomePage() {
|
|
||||||
const featured = campaigns[0];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<section className="newsprint border-b-2 border-foreground px-4 py-10 md:px-6 md:py-14">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_420px]">
|
|
||||||
<div>
|
|
||||||
<div className="impact-rule mb-5 py-2 text-sm font-black uppercase">{site.issue} / {site.tagline}</div>
|
|
||||||
<h1 className="max-w-5xl text-5xl font-black leading-[0.88] md:text-8xl">
|
|
||||||
Помощь, которую видно по документам и людям
|
|
||||||
</h1>
|
|
||||||
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">
|
|
||||||
Common Ground - шаблон для НКО и impact-платформы, где каждая кампания показывает
|
|
||||||
цель, бюджет, полевой прогресс, истории и открытый реестр расходов.
|
|
||||||
</p>
|
|
||||||
<div className="mt-7 flex flex-wrap gap-3">
|
|
||||||
<Button asChild size="lg">
|
|
||||||
<Link href="/campaigns">Смотреть кампании <ArrowRightIcon className="size-4" /></Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="outline" size="lg">
|
|
||||||
<Link href="/transparency">Открыть отчетность</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<aside className="border-2 border-foreground bg-card p-5 shadow-[8px_8px_0_var(--foreground)]">
|
|
||||||
<NewspaperIcon className="mb-10 size-8 text-primary" />
|
|
||||||
<div className="impact-rule py-2 text-sm font-black uppercase">полевой выпуск</div>
|
|
||||||
<p className="mt-5 text-xl font-bold leading-8">
|
|
||||||
НКО продает доверие. Поэтому первый экран сразу дает цель, факт, ответственного и путь к документам.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6 grid gap-3 text-sm">
|
|
||||||
<div className="flex items-center justify-between border-b border-foreground/25 pb-2">
|
|
||||||
<span className="text-muted-foreground">открытых кампаний</span>
|
|
||||||
<span className="font-black">3</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between border-b border-foreground/25 pb-2">
|
|
||||||
<span className="text-muted-foreground">обновление реестра</span>
|
|
||||||
<span className="font-black">еженедельно</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground">полевой desk</span>
|
|
||||||
<span className="font-black">{site.email}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="px-4 py-12 md:px-6">
|
|
||||||
<div className="mx-auto max-w-7xl">
|
|
||||||
<CampaignCard campaign={featured} featured />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<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-4 md:grid-cols-4">
|
|
||||||
{impactMetrics.map((item) => (
|
|
||||||
<div key={item.value}>
|
|
||||||
<div className="text-5xl font-black">{item.value}</div>
|
|
||||||
<p className="mt-2 text-sm font-bold opacity-85">{item.label}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="px-4 py-12 md:px-6">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.85fr_1.15fr]">
|
|
||||||
<div>
|
|
||||||
<div className="impact-rule py-2 text-sm font-black uppercase">живой реестр</div>
|
|
||||||
<h2 className="mt-5 text-4xl font-black leading-none md:text-6xl">Последние движения средств</h2>
|
|
||||||
<p className="mt-4 leading-7 text-muted-foreground">
|
|
||||||
Не прячьте доверие в PDF. Покажите свежие поступления, закупки и основание расходов прямо на сайте.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<LedgerTable />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CampaignsPage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<EditorialTitle
|
|
||||||
label="Кампании"
|
|
||||||
title="Каждый сбор объясняет цель, регион, срок и бюджет"
|
|
||||||
text="Страница помогает быстро понять, где нужна помощь, сколько уже закрыто и какие шаги будут проверены после сбора."
|
|
||||||
/>
|
|
||||||
<section className="px-4 py-12 md:px-6">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-3">
|
|
||||||
{campaigns.map((campaign) => (
|
|
||||||
<CampaignCard key={campaign.title} campaign={campaign} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CampaignDetailPage() {
|
|
||||||
const campaign = campaigns[0];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<section className="grid border-b-2 border-foreground lg:grid-cols-[1fr_430px]">
|
|
||||||
<div className="newsprint p-4 md:p-6">
|
|
||||||
<div className="mb-5 flex flex-wrap gap-2">
|
|
||||||
<Badge className="font-black">{campaign.region}</Badge>
|
|
||||||
<Badge variant="outline" className="border-foreground bg-background font-black">{campaign.deadline}</Badge>
|
|
||||||
</div>
|
|
||||||
<h1 className="max-w-4xl text-5xl font-black leading-[0.96] md:text-8xl">{campaign.title}</h1>
|
|
||||||
<p className="mt-6 max-w-2xl text-xl font-bold leading-8">{campaign.lead}</p>
|
|
||||||
<p className="mt-4 max-w-2xl leading-8 text-muted-foreground">{campaign.text}</p>
|
|
||||||
<div className="mt-8 max-w-3xl">
|
|
||||||
<Progress value={campaignProgress(campaign)} />
|
|
||||||
<div className="mt-3 flex justify-between gap-4 font-black">
|
|
||||||
<span>{formatRub(campaign.raised)} ₽ собрано</span>
|
|
||||||
<span>{formatRub(campaign.goal)} ₽ цель</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-10 grid max-w-4xl gap-3 md:grid-cols-4">
|
|
||||||
{campaign.checkpoints.map((point, index) => (
|
|
||||||
<div key={point} className="border-2 border-foreground bg-card p-4">
|
|
||||||
<div className="text-3xl font-black text-primary">0{index + 1}</div>
|
|
||||||
<div className="mt-6 font-black">{point}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="border-t-2 border-foreground p-4 md:p-6 lg:border-l-2 lg:border-t-0">
|
|
||||||
<DonationPanel />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className="px-4 py-12 md:px-6">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.75fr_1.25fr]">
|
|
||||||
<div>
|
|
||||||
<div className="impact-rule py-2 text-sm font-black uppercase">бюджет кампании</div>
|
|
||||||
<h2 className="mt-5 text-4xl font-black leading-none">Куда уйдет каждый рубль</h2>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-5">
|
|
||||||
{campaign.allocation.map((item) => (
|
|
||||||
<div key={item.label}>
|
|
||||||
<div className="mb-2 flex justify-between font-black">
|
|
||||||
<span>{item.label}</span>
|
|
||||||
<span>{item.value}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={item.value} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ImpactPage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<EditorialTitle
|
|
||||||
label="Отчет о влиянии"
|
|
||||||
title="Impact измеряется людьми, сменами и подтвержденными расходами"
|
|
||||||
text="Страница заменяет красивую декларацию на отчетные блоки, которые можно развивать в полноценный годовой отчет."
|
|
||||||
/>
|
|
||||||
<section className="px-4 py-12 md:px-6">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-5 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{impactMetrics.map((item) => (
|
|
||||||
<MetricCard key={item.value} metric={item} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className="px-4 pb-12 md:px-6">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_1fr]">
|
|
||||||
<div className="border-2 border-foreground bg-card p-6">
|
|
||||||
<ClipboardCheckIcon className="mb-10 size-8 text-primary" />
|
|
||||||
<h2 className="text-4xl font-black leading-none">Методика подтверждения</h2>
|
|
||||||
<p className="mt-5 leading-7 text-muted-foreground">
|
|
||||||
Каждая цифра связывается с документом: актом, фото, письмом партнера, платежом или сменой координатора.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<LedgerTable />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StoriesPage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<EditorialTitle
|
|
||||||
label="Истории"
|
|
||||||
title="Истории показывают, что изменилось после кампании"
|
|
||||||
text="Вместо generic отзывов здесь короткие заметки с местом, ситуацией, фотографией и проверяемым результатом."
|
|
||||||
/>
|
|
||||||
<section className="px-4 py-12 md:px-6">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-3">
|
|
||||||
{stories.map((story) => (
|
|
||||||
<StoryCard key={story.title} story={story} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VolunteerPage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<EditorialTitle
|
|
||||||
label="Волонтерские смены"
|
|
||||||
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">
|
|
||||||
{volunteerShifts.map((shift) => (
|
|
||||||
<article key={`${shift.date}-${shift.title}`} className="grid gap-3 p-5 md:grid-cols-[130px_1fr_150px_150px_140px] md:items-center">
|
|
||||||
<div>
|
|
||||||
<div className="font-black">{shift.date}</div>
|
|
||||||
<div className="text-sm text-muted-foreground">{shift.time}</div>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-black leading-tight">{shift.title}</h2>
|
|
||||||
<div className="flex items-center gap-2 text-sm font-bold">
|
|
||||||
<UsersIcon className="size-4 text-primary" />
|
|
||||||
{shift.spots}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm font-bold">
|
|
||||||
<MapPinIcon className="size-4 text-primary" />
|
|
||||||
{shift.location}
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="w-fit border-foreground bg-secondary font-black">
|
|
||||||
{shift.role}
|
|
||||||
</Badge>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TransparencyPage() {
|
|
||||||
return (
|
|
||||||
<Shell>
|
|
||||||
<EditorialTitle
|
|
||||||
label="Прозрачность"
|
|
||||||
title="Открытая структура расходов до доверия, а не после"
|
|
||||||
text="Шаблон дает НКО готовую страницу для бюджета, документов, партнеров, подтверждений и годового отчета."
|
|
||||||
/>
|
|
||||||
<section className="px-4 py-12 md:px-6">
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_420px]">
|
|
||||||
<div className="border-2 border-foreground bg-card p-6">
|
|
||||||
<LandmarkIcon className="mb-10 size-8 text-primary" />
|
|
||||||
<h2 className="mb-6 text-4xl font-black leading-none">Структура расходов</h2>
|
|
||||||
<div className="grid gap-5">
|
|
||||||
{transparency.map((item) => (
|
|
||||||
<div key={item.label}>
|
|
||||||
<div className="mb-2 flex justify-between gap-4 font-black">
|
|
||||||
<span>{item.label}</span>
|
|
||||||
<span>{item.value}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={item.value} />
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">{item.detail}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<aside className="border-2 border-foreground bg-card p-6 shadow-[8px_8px_0_var(--foreground)]">
|
|
||||||
<ShieldCheckIcon className="mb-10 size-8 text-primary" />
|
|
||||||
<h2 className="text-4xl font-black leading-none">Партнерская проверка</h2>
|
|
||||||
<p className="mt-5 leading-7 text-muted-foreground">
|
|
||||||
Отчеты, акты, фотофиксация и письма партнеров должны иметь отдельные блоки, а не прятаться в футере.
|
|
||||||
</p>
|
|
||||||
<Button className="mt-7 w-full" size="lg">
|
|
||||||
<FileTextIcon className="size-4" />
|
|
||||||
Скачать mock-отчет
|
|
||||||
</Button>
|
|
||||||
</aside>
|
|
||||||
</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.72fr_1.28fr]">
|
|
||||||
<div>
|
|
||||||
<div className="impact-rule py-2 text-sm font-black uppercase">документы</div>
|
|
||||||
<h2 className="mt-5 text-4xl font-black leading-none">Что можно проверить</h2>
|
|
||||||
<div className="mt-8 flex flex-wrap gap-2">
|
|
||||||
{partners.map((partner) => (
|
|
||||||
<Badge key={partner} variant="outline" className="border-foreground bg-secondary px-3 py-1 font-black">
|
|
||||||
{partner}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
|
||||||
{documents.map((item) => (
|
|
||||||
<DocumentCard key={item.title} item={item} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
82
src/widgets/transparency-page.tsx
Normal file
82
src/widgets/transparency-page.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { FileTextIcon, LandmarkIcon, ReceiptTextIcon, ShieldCheckIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { documents, partners, transparency } from "@/entities/site-content";
|
||||||
|
import { Badge } from "@/shared/ui/badge";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { Progress } from "@/shared/ui/progress";
|
||||||
|
import { EditorialTitle } from "@/shared/ui/editorial-title";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
function DocumentCard({ item }: { item: (typeof documents)[number] }) {
|
||||||
|
return (
|
||||||
|
<article className="border-2 border-foreground bg-card p-4">
|
||||||
|
<ReceiptTextIcon className="mb-8 size-7 text-primary" />
|
||||||
|
<h3 className="text-2xl font-black leading-tight">{item.title}</h3>
|
||||||
|
<p className="mt-3 font-bold">{item.status}</p>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">{item.owner}</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TransparencyPage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<EditorialTitle
|
||||||
|
label="Прозрачность"
|
||||||
|
title="Открытая структура расходов до доверия, а не после"
|
||||||
|
text="Шаблон дает НКО готовую страницу для бюджета, документов, партнеров, подтверждений и годового отчета."
|
||||||
|
/>
|
||||||
|
<section className="px-4 py-12 md:px-6">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[1fr_420px]">
|
||||||
|
<div className="border-2 border-foreground bg-card p-6">
|
||||||
|
<LandmarkIcon className="mb-10 size-8 text-primary" />
|
||||||
|
<h2 className="mb-6 text-4xl font-black leading-none">Структура расходов</h2>
|
||||||
|
<div className="grid gap-5">
|
||||||
|
{transparency.map((item) => (
|
||||||
|
<div key={item.label}>
|
||||||
|
<div className="mb-2 flex justify-between gap-4 font-black">
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<span>{item.value}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={item.value} />
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">{item.detail}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<aside className="border-2 border-foreground bg-card p-6 shadow-[8px_8px_0_var(--foreground)]">
|
||||||
|
<ShieldCheckIcon className="mb-10 size-8 text-primary" />
|
||||||
|
<h2 className="text-4xl font-black leading-none">Партнерская проверка</h2>
|
||||||
|
<p className="mt-5 leading-7 text-muted-foreground">
|
||||||
|
Отчеты, акты, фотофиксация и письма партнеров должны иметь отдельные блоки, а не прятаться в футере.
|
||||||
|
</p>
|
||||||
|
<Button className="mt-7 w-full" size="lg">
|
||||||
|
<FileTextIcon className="size-4" />
|
||||||
|
Скачать mock-отчет
|
||||||
|
</Button>
|
||||||
|
</aside>
|
||||||
|
</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.72fr_1.28fr]">
|
||||||
|
<div>
|
||||||
|
<div className="impact-rule py-2 text-sm font-black uppercase">документы</div>
|
||||||
|
<h2 className="mt-5 text-4xl font-black leading-none">Что можно проверить</h2>
|
||||||
|
<div className="mt-8 flex flex-wrap gap-2">
|
||||||
|
{partners.map((partner) => (
|
||||||
|
<Badge key={partner} variant="outline" className="border-foreground bg-secondary px-3 py-1 font-black">
|
||||||
|
{partner}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{documents.map((item) => (
|
||||||
|
<DocumentCard key={item.title} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/widgets/volunteer-page.tsx
Normal file
42
src/widgets/volunteer-page.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { MapPinIcon, UsersIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { volunteerShifts } from "@/entities/site-content";
|
||||||
|
import { Badge } from "@/shared/ui/badge";
|
||||||
|
import { EditorialTitle } from "@/shared/ui/editorial-title";
|
||||||
|
import { SiteShell } from "@/widgets/site-shell";
|
||||||
|
|
||||||
|
export function VolunteerPage() {
|
||||||
|
return (
|
||||||
|
<SiteShell>
|
||||||
|
<EditorialTitle
|
||||||
|
label="Волонтерские смены"
|
||||||
|
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">
|
||||||
|
{volunteerShifts.map((shift) => (
|
||||||
|
<article key={`${shift.date}-${shift.title}`} className="grid gap-3 p-5 md:grid-cols-[130px_1fr_150px_150px_140px] md:items-center">
|
||||||
|
<div>
|
||||||
|
<div className="font-black">{shift.date}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{shift.time}</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-black leading-tight">{shift.title}</h2>
|
||||||
|
<div className="flex items-center gap-2 text-sm font-bold">
|
||||||
|
<UsersIcon className="size-4 text-primary" />
|
||||||
|
{shift.spots}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm font-bold">
|
||||||
|
<MapPinIcon className="size-4 text-primary" />
|
||||||
|
{shift.location}
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="w-fit border-foreground bg-secondary font-black">
|
||||||
|
{shift.role}
|
||||||
|
</Badge>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</SiteShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user