diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..e587469
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,12 @@
+# 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`.
diff --git a/SITEMAP.md b/SITEMAP.md
new file mode 100644
index 0000000..459bc00
--- /dev/null
+++ b/SITEMAP.md
@@ -0,0 +1,23 @@
+# 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`.
diff --git a/next.config.mjs b/next.config.mjs
index f713a74..b1e31a1 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,7 +1,5 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
- /* config options here */
- // Оптимизация бандла
experimental: {
optimizePackageImports: [
"lucide-react",
@@ -21,11 +19,12 @@ const nextConfig = {
"@radix-ui/react-tooltip",
],
},
- // Компрессия и оптимизация
compress: true,
- // Оптимизация изображений
images: {
formats: ["image/avif", "image/webp"],
+ remotePatterns: [
+ { protocol: "https", hostname: "images.unsplash.com" },
+ ],
},
};
diff --git a/package.json b/package.json
index 9354352..7db64ba 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "tungulov-space",
+ "name": "template-commonground-impact-shadcn",
"version": "0.1.0",
"private": true,
"scripts": {
diff --git a/src/app/campaigns/clean-water/page.tsx b/src/app/campaigns/clean-water/page.tsx
new file mode 100644
index 0000000..fc93de0
--- /dev/null
+++ b/src/app/campaigns/clean-water/page.tsx
@@ -0,0 +1,5 @@
+import { CampaignDetailPage } from "@/widgets/template-ui";
+
+export default function Page() {
+ return ;
+}
diff --git a/src/app/campaigns/page.tsx b/src/app/campaigns/page.tsx
new file mode 100644
index 0000000..fffb1b3
--- /dev/null
+++ b/src/app/campaigns/page.tsx
@@ -0,0 +1,5 @@
+import { CampaignsPage } from "@/widgets/template-ui";
+
+export default function Page() {
+ return ;
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index dc98be7..0f9c55b 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -6,8 +6,8 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
+ --font-sans: var(--font-commonground);
+ --font-mono: var(--font-commonground);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -44,79 +44,100 @@
}
:root {
- --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);
+ --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);
}
.dark {
- --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);
+ --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);
}
@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);
+}
diff --git a/src/app/impact/page.tsx b/src/app/impact/page.tsx
new file mode 100644
index 0000000..ff2ca98
--- /dev/null
+++ b/src/app/impact/page.tsx
@@ -0,0 +1,5 @@
+import { ImpactPage } from "@/widgets/template-ui";
+
+export default function Page() {
+ return ;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index fa5d047..010e9c3 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,18 +1,18 @@
import type { Metadata } from "next";
-import { Roboto_Flex } from "next/font/google";
+import { Noto_Serif } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/shared/hooks/theme-provider";
import { ThemeMessageListener } from "@/shared/hooks/theme-message-listener";
-const robotoFlex = Roboto_Flex({
- variable: "--font-roboto-flex",
- weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
+const notoSerif = Noto_Serif({
+ variable: "--font-commonground",
+ weight: ["300", "400", "500", "600", "700", "800", "900"],
subsets: ["latin", "cyrillic"],
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Common Ground - кампании и открытая отчетность",
+ description: "Editorial impact шаблон для НКО: кампании, пожертвования, истории людей, волонтерские смены и прозрачный бюджет.",
};
export default function RootLayout({
@@ -22,10 +22,10 @@ export default function RootLayout({
}>) {
return (
-
+
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 5d915f1..2794f3c 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,99 +1,5 @@
-"use client";
+import { HomePage } from "@/widgets/template-ui";
-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 (
-
-
-
- );
-}
-
-function StatusPill() {
- return (
-
-
- Проект готов к работе
-
- );
-}
-
-export default function Home() {
- return (
-
-
-
-
-
-
-
-
-
-
-
- Fluw template
-
-
- Пустой шаблон приложения для быстрого старта
-
-
-
-
-
-
-
- Начните с чистой основы. Доведите идею до живого продукта.
-
-
-
- Это пустой шаблон Fluw для нового веб-приложения: чистая
- структура, live runtime, визуальные правки, AI-контекст и кодовая
- база, которую можно развивать.
-
-
-
- {technologyBadges.map((badge) => (
-
- {badge}
-
- ))}
-
-
-
-
-
-
-
- );
+export default function Page() {
+ return ;
}
diff --git a/src/app/stories/page.tsx b/src/app/stories/page.tsx
new file mode 100644
index 0000000..3a8e767
--- /dev/null
+++ b/src/app/stories/page.tsx
@@ -0,0 +1,5 @@
+import { StoriesPage } from "@/widgets/template-ui";
+
+export default function Page() {
+ return ;
+}
diff --git a/src/app/transparency/page.tsx b/src/app/transparency/page.tsx
new file mode 100644
index 0000000..e61c20b
--- /dev/null
+++ b/src/app/transparency/page.tsx
@@ -0,0 +1,5 @@
+import { TransparencyPage } from "@/widgets/template-ui";
+
+export default function Page() {
+ return ;
+}
diff --git a/src/app/volunteer/page.tsx b/src/app/volunteer/page.tsx
new file mode 100644
index 0000000..6716f5d
--- /dev/null
+++ b/src/app/volunteer/page.tsx
@@ -0,0 +1,5 @@
+import { VolunteerPage } from "@/widgets/template-ui";
+
+export default function Page() {
+ return ;
+}
diff --git a/src/entities/site-content.ts b/src/entities/site-content.ts
new file mode 100644
index 0000000..3766c84
--- /dev/null
+++ b/src/entities/site-content.ts
@@ -0,0 +1,133 @@
+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;
diff --git a/src/features/donation-panel/ui/donation-panel.tsx b/src/features/donation-panel/ui/donation-panel.tsx
new file mode 100644
index 0000000..d56f467
--- /dev/null
+++ b/src/features/donation-panel/ui/donation-panel.tsx
@@ -0,0 +1,99 @@
+"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 = {
+ 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(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 (
+
+
+
+
+
+
+
Поддержать сбор
+
+ Mock-панель без реальных платежей. Показывает будущую механику выбора суммы и назначения.
+
+
+
+
+
+ {modes.map((item) => (
+ 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}
+
+ ))}
+
+
+
+ {amounts.map((item) => (
+ 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)} ₽
+
+ ))}
+
+
+
+
Что изменит сумма
+
{impactByAmount[amount]}.
+
+
+
+ {receiptRows.map((row) => (
+
+ {row.label}
+ {row.value}
+
+ ))}
+
+
+
+
+ Продолжить без оплаты
+
+
+ );
+}
diff --git a/src/widgets/template-ui.tsx b/src/widgets/template-ui.tsx
new file mode 100644
index 0000000..dcf317c
--- /dev/null
+++ b/src/widgets/template-ui.tsx
@@ -0,0 +1,496 @@
+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 (
+
+
+ {children}
+
+
+ );
+}
+
+function EditorialTitle({ label, title, text }: { label: string; title: string; text: string }) {
+ return (
+
+
+
{label}
+
{title}
+
{text}
+
+
+ );
+}
+
+function CampaignCard({ campaign, featured = false }: { campaign: Campaign; featured?: boolean }) {
+ const href = campaign.slug === "clean-water" ? "/campaigns/clean-water" : "/campaigns";
+
+ return (
+
+
+
+
+
+ {campaign.deadline}
+
+
+
+
+
+ {campaign.region}
+
+ {campaignProgress(campaign)}% цели
+
+
{campaign.title}
+
{campaign.lead}
+
{campaign.text}
+
+
+ {formatRub(campaign.raised)} ₽ собрано
+ {formatRub(campaign.goal)} ₽ цель
+
+
+
+
+ );
+}
+
+function MetricCard({ metric }: { metric: (typeof impactMetrics)[number] }) {
+ return (
+
+ {metric.value}
+ {metric.label}
+ {metric.detail}
+
+ );
+}
+
+function LedgerTable() {
+ return (
+
+ {ledger.map((row) => (
+
+
{row.date}
+
{row.source}
+
{row.amount}
+
{row.note}
+
+ ))}
+
+ );
+}
+
+function StoryCard({ story }: { story: (typeof stories)[number] }) {
+ return (
+
+
+
+
+
+
+
+ {story.place}
+
+
{story.title}
+
{story.text}
+
{story.result}
+
+
+ );
+}
+
+function DocumentCard({ item }: { item: (typeof documents)[number] }) {
+ return (
+
+
+ {item.title}
+ {item.status}
+ {item.owner}
+
+ );
+}
+
+export function HomePage() {
+ const featured = campaigns[0];
+
+ return (
+
+
+
+
+
{site.issue} / {site.tagline}
+
+ Помощь, которую видно по документам и людям
+
+
+ Common Ground - шаблон для НКО и impact-платформы, где каждая кампания показывает
+ цель, бюджет, полевой прогресс, истории и открытый реестр расходов.
+
+
+
+ Смотреть кампании
+
+
+ Открыть отчетность
+
+
+
+
+
+ полевой выпуск
+
+ НКО продает доверие. Поэтому первый экран сразу дает цель, факт, ответственного и путь к документам.
+
+
+
+ открытых кампаний
+ 3
+
+
+ обновление реестра
+ еженедельно
+
+
+ полевой desk
+ {site.email}
+
+
+
+
+
+
+
+
+
+
+ {impactMetrics.map((item) => (
+
+
{item.value}
+
{item.label}
+
+ ))}
+
+
+
+
+
+
+
живой реестр
+
Последние движения средств
+
+ Не прячьте доверие в PDF. Покажите свежие поступления, закупки и основание расходов прямо на сайте.
+
+
+
+
+
+
+ );
+}
+
+export function CampaignsPage() {
+ return (
+
+
+
+
+ {campaigns.map((campaign) => (
+
+ ))}
+
+
+
+ );
+}
+
+export function CampaignDetailPage() {
+ const campaign = campaigns[0];
+
+ return (
+
+
+
+
+ {campaign.region}
+ {campaign.deadline}
+
+
{campaign.title}
+
{campaign.lead}
+
{campaign.text}
+
+
+
+ {formatRub(campaign.raised)} ₽ собрано
+ {formatRub(campaign.goal)} ₽ цель
+
+
+
+ {campaign.checkpoints.map((point, index) => (
+
+
0{index + 1}
+
{point}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
бюджет кампании
+
Куда уйдет каждый рубль
+
+
+ {campaign.allocation.map((item) => (
+
+
+ {item.label}
+ {item.value}%
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
+export function ImpactPage() {
+ return (
+
+
+
+
+ {impactMetrics.map((item) => (
+
+ ))}
+
+
+
+
+
+
+
Методика подтверждения
+
+ Каждая цифра связывается с документом: актом, фото, письмом партнера, платежом или сменой координатора.
+
+
+
+
+
+
+ );
+}
+
+export function StoriesPage() {
+ return (
+
+
+
+
+ {stories.map((story) => (
+
+ ))}
+
+
+
+ );
+}
+
+export function VolunteerPage() {
+ return (
+
+
+
+
+ {volunteerShifts.map((shift) => (
+
+
+
{shift.date}
+
{shift.time}
+
+ {shift.title}
+
+
+ {shift.spots}
+
+
+
+ {shift.location}
+
+
+ {shift.role}
+
+
+ ))}
+
+
+
+ );
+}
+
+export function TransparencyPage() {
+ return (
+
+
+
+
+
+
+
Структура расходов
+
+ {transparency.map((item) => (
+
+
+ {item.label}
+ {item.value}%
+
+
+
{item.detail}
+
+ ))}
+
+
+
+
+ Партнерская проверка
+
+ Отчеты, акты, фотофиксация и письма партнеров должны иметь отдельные блоки, а не прятаться в футере.
+
+
+
+ Скачать mock-отчет
+
+
+
+
+
+
+
+
документы
+
Что можно проверить
+
+ {partners.map((partner) => (
+
+ {partner}
+
+ ))}
+
+
+
+ {documents.map((item) => (
+
+ ))}
+
+
+
+
+ );
+}