Revert "feat: pretify it"

This reverts commit d557c4da1f.
This commit is contained in:
2026-05-08 22:44:21 +03:00
parent d557c4da1f
commit ef4f5f11c6
16 changed files with 175 additions and 894 deletions

View File

@@ -1,12 +0,0 @@
# AGENTS.md
Common Ground — editorial impact/NKO шаблон: сохраняй newsroom-структуру, публичный ledger, кампании с бюджетом, истории людей, волонтерские смены и проверяемые документы.
## Project Specifics
- Кампании, метрики, ledger, истории, смены, документы, партнеры и бюджет описаны в `src/entities/site-content.ts`.
- `src/app` — только route wrappers; композиция страниц находится в `src/widgets/template-ui.tsx`.
- Mock-пожертвования держи в `src/features/*/ui`; не добавляй реальные платежи, CRM, donor accounts или API без запроса.
- Не делай generic charity landing: каждая страница должна усиливать доверие через цель, прогресс, документы, ответственного, место или конкретную смену.
- Избегай мягкой декоративной благотворительности; стиль должен ощущаться как редакция, полевой desk и публичная отчетность.
- Проверка после правок: `pnpm lint` и `pnpm build`.

View File

@@ -1,23 +0,0 @@
# Common Ground Sitemap
Editorial impact шаблон для НКО и campaign platform. Сайт продает доверие через кампании с бюджетом, истории людей, волонтерские смены, живой реестр средств и проверяемые документы.
## Pages
| Route | Page | Blocks and copy intent |
| --- | --- | --- |
| `/` | Главная | Newspaper hero, issue label, доверительный редакционный блок, featured campaign, impact strip, свежий ledger средств. |
| `/campaigns` | Кампании | Campaign index: регион, срок, сумма, прогресс, проблема, распределение средств и переход в детальную кампанию. |
| `/campaigns/clean-water` | Кампания | Dossier-style detail: проблема, цель, checkpoints, progress, donation mock-panel и бюджет кампании. |
| `/impact` | Отчет о влиянии | Метрики, методика подтверждения, ledger, связь цифр с документами и полевыми данными. |
| `/stories` | Истории | Editorial story cards with images, place, result and field context. |
| `/volunteer` | Волонтерские смены | Операционная доска: дата, время, роль, место, свободные места и тип задачи. |
| `/transparency` | Прозрачность | Структура расходов, mock-отчет, документы, партнеры и блоки проверки. |
## Visual Direction
- Newspaper/editorial layout on clay paper with strong black rules, blue primary, green secondary and warm accent.
- Avoid generic charity softness: the visual language is newsroom, field desk and public ledger.
- Real human/field imagery from Unsplash is used for campaign and story context.
- Feature mock: `DonationPanel` in `src/features/donation-panel/ui/donation-panel.tsx`.
- Domain content lives in `src/entities/site-content.ts`.

View File

@@ -1,5 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
/* config options here */
// Оптимизация бандла
experimental: {
optimizePackageImports: [
"lucide-react",
@@ -19,12 +21,11 @@ const nextConfig = {
"@radix-ui/react-tooltip",
],
},
// Компрессия и оптимизация
compress: true,
// Оптимизация изображений
images: {
formats: ["image/avif", "image/webp"],
remotePatterns: [
{ protocol: "https", hostname: "images.unsplash.com" },
],
},
};

View File

@@ -1,5 +1,5 @@
{
"name": "template-commonground-impact-shadcn",
"name": "tungulov-space",
"version": "0.1.0",
"private": true,
"scripts": {

View File

@@ -1,5 +0,0 @@
import { CampaignDetailPage } from "@/widgets/template-ui";
export default function Page() {
return <CampaignDetailPage />;
}

View File

@@ -1,5 +0,0 @@
import { CampaignsPage } from "@/widgets/template-ui";
export default function Page() {
return <CampaignsPage />;
}

View File

@@ -6,8 +6,8 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-commonground);
--font-mono: var(--font-commonground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -44,100 +44,79 @@
}
:root {
--radius: 0.18rem;
--background: oklch(0.96 0.018 83);
--foreground: oklch(0.17 0.025 65);
--card: oklch(0.99 0.012 84);
--card-foreground: oklch(0.17 0.025 65);
--popover: oklch(0.99 0.012 84);
--popover-foreground: oklch(0.17 0.025 65);
--primary: oklch(0.43 0.095 218);
--primary-foreground: oklch(0.98 0.014 84);
--secondary: oklch(0.83 0.09 145);
--secondary-foreground: oklch(0.17 0.025 65);
--muted: oklch(0.89 0.028 80);
--muted-foreground: oklch(0.42 0.032 63);
--accent: oklch(0.73 0.105 50);
--accent-foreground: oklch(0.17 0.025 65);
--destructive: oklch(0.58 0.22 29);
--border: oklch(0.28 0.026 65);
--input: oklch(0.28 0.026 65);
--ring: oklch(0.43 0.095 218);
--chart-1: oklch(0.43 0.095 218);
--chart-2: oklch(0.73 0.105 50);
--chart-3: oklch(0.83 0.09 145);
--chart-4: oklch(0.56 0.13 20);
--chart-5: oklch(0.68 0.1 285);
--sidebar: oklch(0.99 0.012 84);
--sidebar-foreground: oklch(0.17 0.025 65);
--sidebar-primary: oklch(0.43 0.095 218);
--sidebar-primary-foreground: oklch(0.98 0.014 84);
--sidebar-accent: oklch(0.89 0.028 80);
--sidebar-accent-foreground: oklch(0.17 0.025 65);
--sidebar-border: oklch(0.28 0.026 65);
--sidebar-ring: oklch(0.43 0.095 218);
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.17 0.025 65);
--foreground: oklch(0.96 0.018 83);
--card: oklch(0.22 0.026 65);
--card-foreground: oklch(0.96 0.018 83);
--popover: oklch(0.22 0.026 65);
--popover-foreground: oklch(0.96 0.018 83);
--primary: oklch(0.73 0.105 50);
--primary-foreground: oklch(0.17 0.025 65);
--secondary: oklch(0.32 0.055 145);
--secondary-foreground: oklch(0.96 0.018 83);
--muted: oklch(0.26 0.026 65);
--muted-foreground: oklch(0.78 0.022 83);
--accent: oklch(0.61 0.09 218);
--accent-foreground: oklch(0.96 0.018 83);
--border: oklch(0.82 0.018 83);
--input: oklch(0.82 0.018 83);
--ring: oklch(0.73 0.105 50);
--sidebar: oklch(0.2 0.026 65);
--sidebar-foreground: oklch(0.96 0.018 83);
--sidebar-primary: oklch(0.73 0.105 50);
--sidebar-primary-foreground: oklch(0.17 0.025 65);
--sidebar-accent: oklch(0.26 0.026 65);
--sidebar-accent-foreground: oklch(0.96 0.018 83);
--sidebar-border: oklch(0.82 0.018 83);
--sidebar-ring: oklch(0.73 0.105 50);
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
scroll-behavior: smooth;
}
body {
@apply bg-background text-foreground;
background-image:
radial-gradient(circle at 12% 10%, oklch(0.83 0.09 145 / 0.18), transparent 24rem),
linear-gradient(90deg, oklch(0.17 0.025 65 / 0.045) 1px, transparent 1px);
background-size: auto, 22px 22px;
font-feature-settings: "ss01" 1, "cv01" 1, "tnum" 1;
}
a {
text-decoration-thickness: 1px;
text-underline-offset: 0.18em;
}
}
.newsprint {
background-image:
linear-gradient(90deg, oklch(0.17 0.025 65 / 0.06) 1px, transparent 1px),
linear-gradient(180deg, oklch(0.17 0.025 65 / 0.035) 1px, transparent 1px);
background-size: 18px 18px;
}
.impact-rule {
border-top: 3px double var(--foreground);
border-bottom: 3px double var(--foreground);
}

View File

@@ -1,5 +0,0 @@
import { ImpactPage } from "@/widgets/template-ui";
export default function Page() {
return <ImpactPage />;
}

View File

@@ -1,18 +1,18 @@
import type { Metadata } from "next";
import { Noto_Serif } from "next/font/google";
import { Roboto_Flex } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/shared/hooks/theme-provider";
import { ThemeMessageListener } from "@/shared/hooks/theme-message-listener";
const notoSerif = Noto_Serif({
variable: "--font-commonground",
weight: ["300", "400", "500", "600", "700", "800", "900"],
const robotoFlex = Roboto_Flex({
variable: "--font-roboto-flex",
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
subsets: ["latin", "cyrillic"],
});
export const metadata: Metadata = {
title: "Common Ground - кампании и открытая отчетность",
description: "Editorial impact шаблон для НКО: кампании, пожертвования, истории людей, волонтерские смены и прозрачный бюджет.",
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
@@ -22,10 +22,10 @@ export default function RootLayout({
}>) {
return (
<html lang="ru" suppressHydrationWarning>
<body className={`${notoSerif.variable} antialiased`}>
<body className={`${robotoFlex.variable} antialiased`}>
<ThemeProvider
attribute="class"
defaultTheme="light"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>

View File

@@ -1,5 +1,99 @@
import { HomePage } from "@/widgets/template-ui";
"use client";
export default function Page() {
return <HomePage />;
import { motion } from "framer-motion";
import { ArrowRightIcon } from "lucide-react";
const technologyBadges = [
"Next.js",
"React",
"Tailwind",
"shadcn",
"Gitea",
"Live runtime",
"AI context",
] as const;
function FluwSquareIcon({ className = "size-11" }: { className?: string }) {
return (
<span
className={`inline-flex items-center justify-center ${className}`}
aria-hidden="true"
>
<span className="size-full rounded-[10px] bg-[#4ADE80] shadow-[0_0_30px_rgba(74,222,128,0.28)]" />
</span>
);
}
function StatusPill() {
return (
<div className="inline-flex items-center gap-2 rounded-full border border-[#4ADE80]/30 bg-[#4ADE80]/10 px-3 py-1.5 text-xs font-medium text-[#4ADE80]">
<span className="size-1.5 rounded-full bg-[#4ADE80]" />
Проект готов к работе
</div>
);
}
export default function Home() {
return (
<main className="min-h-screen overflow-hidden bg-[#030303] text-white">
<div className="pointer-events-none fixed inset-0 bg-[linear-gradient(rgba(74,222,128,0.08)_1px,transparent_1px),linear-gradient(90deg,rgba(74,222,128,0.08)_1px,transparent_1px)] bg-size-[44px_44px] opacity-35" />
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_12%,rgba(74,222,128,0.18),transparent_34%),radial-gradient(circle_at_80%_80%,rgba(255,255,255,0.08),transparent_28%)]" />
<section className="relative mx-auto flex min-h-screen w-full max-w-[900px] items-center justify-center px-4 py-10 sm:px-6 lg:px-8">
<div className="w-full">
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, ease: "easeOut" }}
className="mx-auto flex max-w-[760px] flex-col items-center text-center"
>
<div className="mb-8 flex items-center gap-3 text-left">
<FluwSquareIcon />
<div>
<div className="text-sm font-semibold tracking-normal text-white">
Fluw template
</div>
<div className="text-xs text-white/45">
Пустой шаблон приложения для быстрого старта
</div>
</div>
</div>
<StatusPill />
<h1 className="mt-6 max-w-[760px] text-4xl font-bold leading-[1.02] tracking-normal text-white sm:text-5xl md:text-6xl">
Начните с чистой основы. Доведите идею до живого продукта.
</h1>
<p className="mt-5 max-w-[640px] text-base leading-7 text-white/62 sm:text-lg">
Это пустой шаблон Fluw для нового веб-приложения: чистая
структура, live runtime, визуальные правки, AI-контекст и кодовая
база, которую можно развивать.
</p>
<div className="mt-6 flex max-w-[620px] flex-wrap justify-center gap-2">
{technologyBadges.map((badge) => (
<span
key={badge}
className="rounded-full border border-white/10 bg-white/[0.035] px-3 py-1.5 text-xs font-medium text-white/65"
>
{badge}
</span>
))}
</div>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<a
href="https://git.fluw.ru"
className="inline-flex h-11 items-center justify-center gap-2 rounded-lg bg-[#4ADE80] px-5 text-sm font-semibold text-[#06120a] transition hover:bg-[#68ee98]"
>
Открыть Git
<ArrowRightIcon className="size-4" />
</a>
</div>
</motion.div>
</div>
</section>
</main>
);
}

View File

@@ -1,5 +0,0 @@
import { StoriesPage } from "@/widgets/template-ui";
export default function Page() {
return <StoriesPage />;
}

View File

@@ -1,5 +0,0 @@
import { TransparencyPage } from "@/widgets/template-ui";
export default function Page() {
return <TransparencyPage />;
}

View File

@@ -1,5 +0,0 @@
import { VolunteerPage } from "@/widgets/template-ui";
export default function Page() {
return <VolunteerPage />;
}

View File

@@ -1,133 +0,0 @@
export const site = {
name: "Common Ground",
tagline: "кампании, истории и отчетность без тумана",
email: "fielddesk@commonground.org",
issue: "Выпуск 04 / май",
phone: "+7 495 221-70-18",
};
export const navItems = [
{ href: "/campaigns", label: "Кампании" },
{ href: "/impact", label: "Отчет" },
{ href: "/stories", label: "Истории" },
{ href: "/volunteer", label: "Смены" },
{ href: "/transparency", label: "Прозрачность" },
] as const;
export const campaigns = [
{
slug: "clean-water",
title: "Чистая вода для северных поселков",
goal: 4200000,
raised: 2870000,
region: "Карелия",
deadline: "24 дня до закупки",
image: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=1400&q=82",
lead: "Три поселка зависят от привозной воды и старых скважин с высоким железом.",
text: "Собираем на модульные станции фильтрации, обучение операторов, сервисный запас и лабораторный контроль на первый год.",
allocation: [
{ label: "станции и монтаж", value: 61 },
{ label: "логистика", value: 18 },
{ label: "обучение", value: 9 },
{ label: "расходники", value: 12 },
],
checkpoints: ["анализ воды", "закупка фильтров", "монтаж", "обучение оператора"],
},
{
slug: "warm-classrooms",
title: "Теплые классы на зимний семестр",
goal: 1800000,
raised: 990000,
region: "Псковская область",
deadline: "до 15 августа",
image: "https://images.unsplash.com/photo-1509062522246-3755977927d7?auto=format&fit=crop&w=1400&q=82",
lead: "В двух школах температура в младших классах падает ниже нормы в ветреные дни.",
text: "Меняем окна, усиливаем радиаторы, закрываем тепловые мосты и делаем безопасные зоны для младших классов.",
allocation: [
{ label: "окна", value: 46 },
{ label: "отопление", value: 31 },
{ label: "работы", value: 17 },
{ label: "контроль", value: 6 },
],
checkpoints: ["замеры", "смета", "закупка", "приемка"],
},
{
slug: "legal-aid",
title: "Юридическая помощь семьям",
goal: 960000,
raised: 740000,
region: "онлайн и регионы",
deadline: "набор открыт",
image: "https://images.unsplash.com/photo-1521791136064-7986c2920216?auto=format&fit=crop&w=1400&q=82",
lead: "Семьи теряют месяцы на документы, потому что помощь разбросана по разным службам.",
text: "Финансируем консультации, шаблоны заявлений, сопровождение документов и горячую линию с понятным расписанием.",
allocation: [
{ label: "юристы", value: 58 },
{ label: "горячая линия", value: 19 },
{ label: "шаблоны", value: 11 },
{ label: "координация", value: 12 },
],
checkpoints: ["прием заявки", "разбор документов", "письмо", "сопровождение"],
},
] as const;
export const impactMetrics = [
{ value: "18 420", label: "людей получили доступ к программам", detail: "подтверждено партнерами на местах" },
{ value: "91%", label: "расходов имеют открытое подтверждение", detail: "акты, фото, платежные реестры" },
{ value: "312", label: "волонтерских смен закрыто за год", detail: "логистика, звонки, выезды, склад" },
{ value: "37", label: "локальных координаторов обучено", detail: "после завершения кампаний" },
] as const;
export const ledger = [
{ date: "02 мая", source: "частные доноры", amount: "+418 000 ₽", note: "132 перевода до 12 000 ₽" },
{ date: "06 мая", source: "закупка фильтров", amount: "-690 000 ₽", note: "счет CG-WTR-041" },
{ date: "10 мая", source: "корпоративный матчинг", amount: "+1 200 000 ₽", note: "партнер удвоил сбор недели" },
{ date: "14 мая", source: "логистика Карелия", amount: "-182 000 ₽", note: "доставка модулей и расходников" },
] as const;
export const stories = [
{
title: "Станция, которую обслуживает местная команда",
place: "поселок Ладожский",
image: "https://images.unsplash.com/photo-1494526585095-c41746248156?auto=format&fit=crop&w=1200&q=82",
text: "После запуска школа перестала закупать воду в канистрах. Оператор из поселка прошел обучение и ведет журнал расходников.",
result: "420 учеников и сотрудников",
},
{
title: "Волонтерская смена без хаоса",
place: "Псков",
image: "https://images.unsplash.com/photo-1559027615-cd4628902d4a?auto=format&fit=crop&w=1200&q=82",
text: "Координаторы заранее разделили задачи: замеры, доставка, прием материалов и фотофиксация. Смена закончилась актом, а не перепиской.",
result: "18 окон принято за день",
},
{
title: "Документы стали понятнее",
place: "онлайн",
image: "https://images.unsplash.com/photo-1554224155-6726b3ff858f?auto=format&fit=crop&w=1200&q=82",
text: "Семьи получили шаблоны заявлений и короткие консультации без очереди в городских центрах.",
result: "64 дела закрыто за месяц",
},
] as const;
export const volunteerShifts = [
{ date: "15 июня", time: "11:00-15:00", title: "Сортировка семейных наборов", spots: "8 мест", location: "склад Север", role: "логистика" },
{ date: "22 июня", time: "07:30-19:00", title: "Выездная фотофиксация", spots: "3 места", location: "Карелия", role: "field team" },
{ date: "29 июня", time: "10:00-14:00", title: "Горячая линия документов", spots: "6 мест", location: "онлайн", role: "операторы" },
{ date: "04 июля", time: "13:00-17:00", title: "Проверка актов и чеков", spots: "4 места", location: "офис", role: "отчетность" },
] as const;
export const transparency = [
{ label: "Программы", value: 68, detail: "закупка, монтаж, консультации, расходники" },
{ label: "Логистика", value: 14, detail: "доставка, склад, выездные смены" },
{ label: "Команда", value: 12, detail: "координация, полевые менеджеры, горячая линия" },
{ label: "Администрирование", value: 6, detail: "банк, бухгалтерия, связь, документы" },
] as const;
export const documents = [
{ title: "Реестр платежей", status: "обновлен 14 мая", owner: "финансовый координатор" },
{ title: "Акты приемки", status: "17 документов", owner: "полевые партнеры" },
{ title: "Фотофиксация", status: "84 файла", owner: "волонтерская команда" },
{ title: "Письма партнеров", status: "9 подтверждений", owner: "программный отдел" },
] as const;
export const partners = ["Северная вода", "Школа 18", "Legal Desk", "Field Notes", "Local Lab"] as const;

View File

@@ -1,99 +0,0 @@
"use client";
import { useMemo, useState } from "react";
import { HeartHandshakeIcon, ReceiptTextIcon } from "lucide-react";
import { Button } from "@/shared/ui/button";
const amounts = [1000, 2500, 5000, 12000] as const;
const modes = ["разово", "ежемесячно"] as const;
const impactByAmount: Record<number, string> = {
1000: "закрывает расходники для одного семейного набора",
2500: "оплачивает доставку фильтров до пункта выдачи",
5000: "помогает провести обучение локального оператора",
12000: "покрывает часть сервисного запаса станции на месяц",
};
function formatRub(value: number) {
return new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 }).format(value);
}
export function DonationPanel() {
const [amount, setAmount] = useState<number>(amounts[1]);
const [mode, setMode] = useState<(typeof modes)[number]>(modes[0]);
const receiptRows = useMemo(
() => [
{ label: "помощь кампании", value: `${formatRub(amount)}` },
{ label: "платежный провайдер", value: "не подключен" },
{ label: "тип поддержки", value: mode },
],
[amount, mode],
);
return (
<section className="border-2 border-foreground bg-card p-5 shadow-[8px_8px_0_var(--foreground)]">
<div className="flex items-start gap-3">
<span className="grid size-11 place-items-center bg-primary text-primary-foreground">
<HeartHandshakeIcon className="size-6" />
</span>
<div>
<h3 className="text-2xl font-black leading-none">Поддержать сбор</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
Mock-панель без реальных платежей. Показывает будущую механику выбора суммы и назначения.
</p>
</div>
</div>
<div className="mt-6 grid grid-cols-2 gap-2">
{modes.map((item) => (
<button
key={item}
type="button"
onClick={() => setMode(item)}
className={`border-2 border-foreground px-3 py-2 text-sm font-black uppercase ${
mode === item ? "bg-foreground text-background" : "bg-background"
}`}
>
{item}
</button>
))}
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
{amounts.map((item) => (
<button
key={item}
type="button"
onClick={() => setAmount(item)}
className={`border-2 border-foreground px-3 py-3 text-lg font-black ${
amount === item ? "bg-primary text-primary-foreground" : "bg-background"
}`}
>
{formatRub(item)}
</button>
))}
</div>
<div className="mt-5 border-2 border-foreground bg-secondary p-4 text-sm leading-6">
<div className="font-black uppercase">Что изменит сумма</div>
<p className="mt-2">{impactByAmount[amount]}.</p>
</div>
<div className="mt-5 grid gap-2">
{receiptRows.map((row) => (
<div key={row.label} className="flex items-center justify-between gap-4 border-b border-foreground/25 py-2 text-sm">
<span className="text-muted-foreground">{row.label}</span>
<span className="text-right font-black">{row.value}</span>
</div>
))}
</div>
<Button type="button" className="mt-5 w-full" size="lg">
<ReceiptTextIcon className="size-4" />
Продолжить без оплаты
</Button>
</section>
);
}

View File

@@ -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>
);
}