feat: pretify it
This commit is contained in:
5
src/app/campaigns/clean-water/page.tsx
Normal file
5
src/app/campaigns/clean-water/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CampaignDetailPage } from "@/widgets/template-ui";
|
||||
|
||||
export default function Page() {
|
||||
return <CampaignDetailPage />;
|
||||
}
|
||||
5
src/app/campaigns/page.tsx
Normal file
5
src/app/campaigns/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CampaignsPage } from "@/widgets/template-ui";
|
||||
|
||||
export default function Page() {
|
||||
return <CampaignsPage />;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
5
src/app/impact/page.tsx
Normal file
5
src/app/impact/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ImpactPage } from "@/widgets/template-ui";
|
||||
|
||||
export default function Page() {
|
||||
return <ImpactPage />;
|
||||
}
|
||||
@@ -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 (
|
||||
<html lang="ru" suppressHydrationWarning>
|
||||
<body className={`${robotoFlex.variable} antialiased`}>
|
||||
<body className={`${notoSerif.variable} antialiased`}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
defaultTheme="light"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
|
||||
100
src/app/page.tsx
100
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 (
|
||||
<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>
|
||||
);
|
||||
export default function Page() {
|
||||
return <HomePage />;
|
||||
}
|
||||
|
||||
5
src/app/stories/page.tsx
Normal file
5
src/app/stories/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { StoriesPage } from "@/widgets/template-ui";
|
||||
|
||||
export default function Page() {
|
||||
return <StoriesPage />;
|
||||
}
|
||||
5
src/app/transparency/page.tsx
Normal file
5
src/app/transparency/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TransparencyPage } from "@/widgets/template-ui";
|
||||
|
||||
export default function Page() {
|
||||
return <TransparencyPage />;
|
||||
}
|
||||
5
src/app/volunteer/page.tsx
Normal file
5
src/app/volunteer/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { VolunteerPage } from "@/widgets/template-ui";
|
||||
|
||||
export default function Page() {
|
||||
return <VolunteerPage />;
|
||||
}
|
||||
133
src/entities/site-content.ts
Normal file
133
src/entities/site-content.ts
Normal file
@@ -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;
|
||||
99
src/features/donation-panel/ui/donation-panel.tsx
Normal file
99
src/features/donation-panel/ui/donation-panel.tsx
Normal file
@@ -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<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>
|
||||
);
|
||||
}
|
||||
496
src/widgets/template-ui.tsx
Normal file
496
src/widgets/template-ui.tsx
Normal file
@@ -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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user