feat: pretify template
This commit is contained in:
12
SITEMAP.md
12
SITEMAP.md
@@ -6,7 +6,12 @@ B2B SaaS-шаблон для AI support platform с pricing, customers, resource
|
|||||||
|
|
||||||
| Route | Page | Blocks and copy intent |
|
| Route | Page | Blocks and copy intent |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `/` | Главная | Launch-style hero, AI support workflow, platform cards, customer proof. |\n| `/product` | Product | Issue detection, agent workflow, integrations, QA and analytics. |\n| `/pricing` | Pricing | SaaS pricing tiers, token/seat messaging, FAQ. |\n| `/customers` | Customers | Case studies, logos, quotes, business outcomes. |\n| `/resources` | Resources | Articles, guides, webinars, playbooks. |\n| `/dashboard` | Dashboard | Demo support dashboard with charts, queues and health cards. |
|
| `/` | Главная | Launch-style hero, AI support workflow, platform cards, customer proof. |
|
||||||
|
| `/product` | Product | Issue detection, agent workflow, integrations, QA and analytics. |
|
||||||
|
| `/pricing` | Pricing | SaaS pricing tiers, token/seat messaging, FAQ. |
|
||||||
|
| `/customers` | Customers | Case studies, logos, quotes, business outcomes. |
|
||||||
|
| `/resources` | Resources | Articles, guides, webinars, playbooks. |
|
||||||
|
| `/dashboard` | Dashboard | Demo support dashboard with charts, queues and health cards. |
|
||||||
|
|
||||||
## Visual Direction
|
## Visual Direction
|
||||||
|
|
||||||
@@ -24,3 +29,8 @@ B2B SaaS-шаблон для AI support platform с pricing, customers, resource
|
|||||||
- Добавлены доменные production-блоки поверх базовой структуры: richer hero/product/dashboard sections, сценарии принятия решения, trust/operations blocks.
|
- Добавлены доменные production-блоки поверх базовой структуры: richer hero/product/dashboard sections, сценарии принятия решения, trust/operations blocks.
|
||||||
- Все важные CTA и пользовательские лейблы держатся в JSX или typed content arrays, чтобы визуальный редактор Fluw мог находить и менять тексты.
|
- Все важные CTA и пользовательские лейблы держатся в JSX или typed content arrays, чтобы визуальный редактор Fluw мог находить и менять тексты.
|
||||||
- Шрифты выбраны с поддержкой кириллицы; latin-only display fonts не используются.
|
- Шрифты выбраны с поддержкой кириллицы; latin-only display fonts не используются.
|
||||||
|
|
||||||
|
## Premium pass
|
||||||
|
|
||||||
|
- Переработаны hero/карточки/доменные секции: меньше generic UI, больше сценарной пользы и визуального характера.
|
||||||
|
- Сохранены node_modules и .next для следующих этапов локальной проверки.
|
||||||
|
|||||||
@@ -144,3 +144,27 @@
|
|||||||
linear-gradient(135deg, oklch(0.92 0.22 116), oklch(0.78 0.2 196));
|
linear-gradient(135deg, oklch(0.92 0.22 116), oklch(0.78 0.2 196));
|
||||||
animation: grainient-shift 12s ease-in-out infinite;
|
animation: grainient-shift 12s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grainient-field::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,.7) 1px, transparent 0);
|
||||||
|
background-size: 18px 18px;
|
||||||
|
opacity: .28;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-aurora {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 14% 12%, oklch(0.91 0.24 132 / 86%), transparent 30%),
|
||||||
|
radial-gradient(circle at 82% 18%, oklch(0.82 0.21 190 / 76%), transparent 34%),
|
||||||
|
radial-gradient(circle at 54% 82%, oklch(0.80 0.18 264 / 42%), transparent 36%),
|
||||||
|
linear-gradient(135deg, oklch(0.96 0.13 122), oklch(0.85 0.16 192));
|
||||||
|
animation: grainient-shift 14s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.signal-grid {
|
||||||
|
background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,.64) 1px, transparent 0);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
opacity: .42;
|
||||||
|
mask-image: linear-gradient(180deg, black 0%, black 68%, transparent 100%);
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import {
|
|||||||
|
|
||||||
export const site = {
|
export const site = {
|
||||||
name: "SignalDesk AI",
|
name: "SignalDesk AI",
|
||||||
tagline: "AI-агенты, real-time issue detection и QA для команд поддержки, которые растут быстрее очереди тикетов.",
|
tagline: "AI-агенты, real-time issue detection и QA для support-команд, которые растут быстрее очереди тикетов.",
|
||||||
cta: "Запустить демо",
|
cta: "Запустить демо",
|
||||||
secondaryCta: "Смотреть панель",
|
secondaryCta: "Смотреть панель",
|
||||||
heroImage: "https://images.unsplash.com/photo-1551434678-e076c223a692?auto=format&fit=crop&w=1400&q=80",
|
heroImage: "https://images.unsplash.com/photo-1551434678-e076c223a692?auto=format&fit=crop&w=1400&q=80",
|
||||||
accentImage: "https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&w=1100&q=80",
|
accentImage: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1100&q=80",
|
||||||
nav: [
|
nav: [
|
||||||
{ href: "/product", label: "Продукт" },
|
{ href: "/product", label: "Продукт" },
|
||||||
{ href: "/pricing", label: "Тарифы" },
|
{ href: "/pricing", label: "Тарифы" },
|
||||||
@@ -27,15 +27,15 @@ export const site = {
|
|||||||
|
|
||||||
export const highlights = [
|
export const highlights = [
|
||||||
{ title: "Deflection", value: "42%", text: "AI закрывает повторяющиеся обращения без потери контроля.", icon: BotIcon },
|
{ title: "Deflection", value: "42%", text: "AI закрывает повторяющиеся обращения без потери контроля.", icon: BotIcon },
|
||||||
{ title: "Detection", value: "30 сек", text: "Система замечает всплеск проблем до массовой эскалации.", icon: ActivityIcon },
|
{ title: "Detection", value: "30 сек", text: "Команда видит всплеск проблемы до массовой эскалации.", icon: ActivityIcon },
|
||||||
{ title: "QA coverage", value: "100%", text: "Проверка разговоров, тональности и compliance-сигналов.", icon: ShieldCheckIcon },
|
{ title: "QA coverage", value: "100%", text: "Проверка разговоров, тональности и compliance-сигналов.", icon: ShieldCheckIcon },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const products = [
|
export const products = [
|
||||||
{ name: "AI-помощник входящей очереди", price: "agent layer", tag: "automation", text: "Черновики ответов, summary, handoff и подсказки оператору." },
|
{ name: "AI-помощник оператора", price: "agent layer", tag: "automation", text: "Черновики ответов, summary, handoff и next best action." },
|
||||||
{ name: "Радар проблем", price: "real time", tag: "detection", text: "Кластеры проблем, topic volume и affected conversations." },
|
{ name: "Радар проблем", price: "real time", tag: "detection", text: "Кластеры тем, topic volume и affected conversations." },
|
||||||
{ name: "QA-мониторинг", price: "always on", tag: "quality", text: "Покрытие всех диалогов вместо выборочной ручной проверки." },
|
{ name: "QA-мониторинг", price: "always on", tag: "quality", text: "Оценка всех диалогов вместо ручной выборки." },
|
||||||
{ name: "Сохранение выручки", price: "retention", tag: "business", text: "Сигналы churn risk, refunds и missed SLA в одном dashboard." },
|
{ name: "Revenue risk", price: "retention", tag: "business", text: "Сигналы churn risk, refunds и missed SLA в одном dashboard." },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const dishes = products;
|
export const dishes = products;
|
||||||
@@ -47,14 +47,14 @@ export const tastingSets = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const testimonials = [
|
export const testimonials = [
|
||||||
{ name: "MintCart", text: "Мы увидели системные проблемы доставки раньше, чем они стали refund wave.", rating: "38% fewer escalations" },
|
{ name: "MintCart", text: "Мы увидели системные проблемы доставки раньше, чем они стали refund wave.", rating: "-38% escalations" },
|
||||||
{ name: "CloudDesk", text: "QA перестал быть выборкой. Теперь команда видит всю картину.", rating: "100% QA coverage" },
|
{ name: "CloudDesk", text: "QA перестал быть выборкой. Теперь команда видит всю картину.", rating: "100% QA coverage" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const eventTypes = [
|
export const eventTypes = [
|
||||||
{ title: "Connect inbox", text: "Подключите support channels и импортируйте историю тем.", icon: MessageSquareTextIcon },
|
{ title: "Подключить inbox", text: "Support channels, история тем и SLA попадают в единую очередь.", icon: MessageSquareTextIcon },
|
||||||
{ title: "Train playbooks", text: "Задайте tone, escalation rules и policy snippets.", icon: CheckCircle2Icon },
|
{ title: "Настроить playbooks", text: "Tone, escalation rules и policy snippets контролируют AI-ответы.", icon: CheckCircle2Icon },
|
||||||
{ title: "Monitor outcomes", text: "Смотрите SLA, sentiment, deflection и risk topics.", icon: BarChart3Icon },
|
{ title: "Измерять outcome", text: "SLA, sentiment, deflection и risk topics видны в dashboard.", icon: BarChart3Icon },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const contactCards = [
|
export const contactCards = [
|
||||||
|
|||||||
@@ -2,919 +2,52 @@
|
|||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSyncExternalStore } from "react";
|
import { ArrowRightIcon, CheckIcon, MenuIcon, ShieldCheckIcon, SparklesIcon } from "lucide-react";
|
||||||
import {
|
|
||||||
Area,
|
|
||||||
AreaChart,
|
|
||||||
Bar,
|
|
||||||
BarChart,
|
|
||||||
CartesianGrid,
|
|
||||||
Cell,
|
|
||||||
Pie,
|
|
||||||
PieChart,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Tooltip,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
} from "recharts";
|
|
||||||
import {
|
|
||||||
ArrowRightIcon,
|
|
||||||
BellIcon,
|
|
||||||
CheckIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
ФильтрIcon,
|
|
||||||
MenuIcon,
|
|
||||||
SearchIcon,
|
|
||||||
ShoppingCartIcon,
|
|
||||||
SlidersHorizontalIcon,
|
|
||||||
StarIcon,
|
|
||||||
UserRoundIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import { site } from "@/entities/site-content";
|
import { eventTypes, highlights, site, testimonials } from "@/entities/site-content";
|
||||||
import { Button } from "@/shared/ui/button";
|
|
||||||
import { Input } from "@/shared/ui/input";
|
|
||||||
import { Textarea } from "@/shared/ui/textarea";
|
|
||||||
import { Badge } from "@/shared/ui/badge";
|
import { Badge } from "@/shared/ui/badge";
|
||||||
import { Card, CardContent } from "@/shared/ui/card";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { Separator } from "@/shared/ui/separator";
|
import { Separator } from "@/shared/ui/separator";
|
||||||
import { Progress } from "@/shared/ui/progress";
|
|
||||||
|
|
||||||
type IconComponent = React.ComponentType<{ className?: string }>;
|
type IconComponent = React.ComponentType<{ className?: string }>;
|
||||||
|
type TextItem = { name: string; price: string; tag: string; text: string };
|
||||||
|
type TileItem = { title: string; price: string; items: readonly string[] };
|
||||||
|
type IconItem = { title: string; text: string; icon: IconComponent };
|
||||||
|
|
||||||
interface MetricItem {
|
const bars = [18, 26, 22, 42, 58, 48, 72, 80, 62, 88, 96, 76];
|
||||||
title: string;
|
|
||||||
value: string;
|
|
||||||
text: string;
|
|
||||||
icon: IconComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextItem {
|
|
||||||
name: string;
|
|
||||||
price: string;
|
|
||||||
tag: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TileItem {
|
|
||||||
title: string;
|
|
||||||
price: string;
|
|
||||||
items: readonly string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TestimonialItem {
|
|
||||||
name: string;
|
|
||||||
text: string;
|
|
||||||
rating: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IconItem {
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
icon: IconComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
const revenueData = [
|
|
||||||
{ name: "Mon", value: 28, alt: 20 },
|
|
||||||
{ name: "Tue", value: 34, alt: 24 },
|
|
||||||
{ name: "Wed", value: 31, alt: 28 },
|
|
||||||
{ name: "Thu", value: 46, alt: 32 },
|
|
||||||
{ name: "Fri", value: 52, alt: 36 },
|
|
||||||
{ name: "Sat", value: 43, alt: 31 },
|
|
||||||
{ name: "Sun", value: 58, alt: 42 },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const pieData = [
|
|
||||||
{ name: "Complete", value: 56, fill: "var(--primary)" },
|
|
||||||
{ name: "At risk", value: 24, fill: "var(--accent)" },
|
|
||||||
{ name: "Open", value: 20, fill: "var(--muted)" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
function subscribeToClient(onStoreChange: () => void) {
|
|
||||||
const frame = window.requestAnimationFrame(onStoreChange);
|
|
||||||
return () => window.cancelAnimationFrame(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClientChart({ children }: { children: React.ReactNode }) {
|
|
||||||
const mounted = useSyncExternalStore(
|
|
||||||
subscribeToClient,
|
|
||||||
() => true,
|
|
||||||
() => false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mounted) {
|
|
||||||
return <div className="h-full w-full rounded-md bg-muted" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
return (
|
return <header className="sticky top-0 z-50 border-b border-border bg-background/90 backdrop-blur-xl"><div className="mx-auto flex h-[72px] w-full max-w-[1280px] items-center justify-between px-5 sm:px-8"><Link href="/" className="flex items-center gap-3"><span className="flex size-10 items-center justify-center rounded-md bg-primary text-sm font-bold text-primary-foreground">Si</span><span className="text-xl font-semibold">{site.name}</span></Link><nav className="hidden items-center gap-7 md:flex">{site.nav.map((item) => <Link key={item.href} href={item.href} className="text-sm font-semibold text-muted-foreground hover:text-foreground">{item.label}</Link>)}</nav><div className="flex items-center gap-2"><Button asChild className="hidden h-11 rounded-md px-6 sm:inline-flex"><Link href="/dashboard">{site.cta}</Link></Button><Button variant="outline" size="icon" className="rounded-md md:hidden" aria-label="Открыть меню"><MenuIcon className="size-4" /></Button></div></div></header>;
|
||||||
<header className="sticky top-0 z-50 border-b border-border/70 bg-background/88 backdrop-blur-xl">
|
|
||||||
<div className="mx-auto flex h-16 w-full max-w-[1180px] items-center justify-between px-4 sm:px-6">
|
|
||||||
<Link href="/" className="flex items-center gap-3">
|
|
||||||
<span className="flex size-9 items-center justify-center rounded-md bg-primary text-sm font-bold text-primary-foreground">
|
|
||||||
{site.name.slice(0, 2)}
|
|
||||||
</span>
|
|
||||||
<span className="font-display text-lg font-semibold">{site.name}</span>
|
|
||||||
</Link>
|
|
||||||
<nav className="hidden items-center gap-1 md:flex">
|
|
||||||
{site.nav.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.href}
|
|
||||||
href={item.href}
|
|
||||||
className="rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button asChild className="hidden h-10 rounded-md sm:inline-flex">
|
|
||||||
<Link href={site.nav[1]?.href ?? "/"}>{site.cta}</Link>
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="icon" className="rounded-md md:hidden" aria-label="Open menu">
|
|
||||||
<MenuIcon className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SiteFooter() {
|
export function SiteFooter() { return <footer className="border-t border-border bg-card"><div className="mx-auto grid w-full max-w-[1280px] gap-8 px-5 py-12 sm:px-8 md:grid-cols-[1.4fr_1fr_1fr]"><div><div className="text-2xl font-semibold">{site.name}</div><p className="mt-4 max-w-[460px] text-sm leading-6 text-muted-foreground">{site.tagline}</p></div><div className="grid gap-2 text-sm text-muted-foreground"><div className="mb-2 font-semibold text-foreground">Страницы</div>{site.nav.slice(0,4).map((item) => <Link key={item.href} href={item.href}>{item.label}</Link>)}</div><div className="text-sm leading-6 text-muted-foreground"><div className="mb-2 font-semibold text-foreground">Шаблон</div>B2B SaaS лендинг с launch hero, workflow, pricing, customer stories и demo dashboard.</div></div></footer>; }
|
||||||
return (
|
|
||||||
<footer className="border-t border-border bg-card">
|
|
||||||
<div className="mx-auto grid w-full max-w-[1180px] gap-8 px-4 py-10 sm:px-6 md:grid-cols-[1.4fr_1fr_1fr]">
|
|
||||||
<div>
|
|
||||||
<div className="mb-3 font-display text-2xl font-semibold">{site.name}</div>
|
|
||||||
<p className="max-w-[460px] text-sm leading-6 text-muted-foreground">{site.tagline}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-3 text-sm font-semibold">Страницы</div>
|
|
||||||
<div className="grid gap-2 text-sm text-muted-foreground">
|
|
||||||
{site.nav.slice(0, 4).map((item) => (
|
|
||||||
<Link key={item.href} href={item.href} className="hover:text-foreground">
|
|
||||||
{item.label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-3 text-sm font-semibold">Для AI-правок</div>
|
|
||||||
<p className="text-sm leading-6 text-muted-foreground">
|
|
||||||
Статический production-ready шаблон: тексты, секции и UI можно менять через визуальное редактирование Fluw.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PageHero({
|
|
||||||
eyebrow,
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
primaryHref,
|
|
||||||
primaryLabel,
|
|
||||||
secondaryHref,
|
|
||||||
secondaryLabel,
|
|
||||||
image,
|
|
||||||
}: {
|
|
||||||
eyebrow: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
primaryHref: string;
|
|
||||||
primaryLabel: string;
|
|
||||||
secondaryHref: string;
|
|
||||||
secondaryLabel: string;
|
|
||||||
image: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid min-h-[calc(100vh-4rem)] w-full max-w-[1180px] items-center gap-10 px-4 py-12 sm:px-6 lg:grid-cols-[0.95fr_1.05fr]">
|
|
||||||
<div>
|
|
||||||
<Badge variant="outline" className="mb-5 rounded-md border-primary/30 bg-primary/5 text-primary">
|
|
||||||
{eyebrow}
|
|
||||||
</Badge>
|
|
||||||
<h1 className="font-display text-5xl font-semibold leading-[0.98] tracking-normal sm:text-6xl lg:text-7xl">
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
<p className="mt-6 max-w-[620px] text-base leading-7 text-muted-foreground sm:text-lg">{text}</p>
|
|
||||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
|
||||||
<Button asChild className="h-11 rounded-md px-5">
|
|
||||||
<Link href={primaryHref}>
|
|
||||||
{primaryLabel}
|
|
||||||
<ArrowRightIcon className="size-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="outline" className="h-11 rounded-md px-5">
|
|
||||||
<Link href={secondaryHref}>{secondaryLabel}</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative min-h-[420px] overflow-hidden rounded-lg border border-border bg-muted">
|
|
||||||
<Image src={image} alt="" fill priority className="object-cover" sizes="(min-width: 1024px) 560px, 100vw" />
|
|
||||||
<div className="absolute bottom-4 left-4 right-4 rounded-md border border-white/30 bg-white/82 p-4 text-foreground shadow-lg backdrop-blur">
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-semibold">Готовый шаблон</div>
|
|
||||||
<div className="text-xs text-muted-foreground">Несколько страниц, редактируемые тексты, production-логика блоков</div>
|
|
||||||
</div>
|
|
||||||
<StarIcon className="size-5 fill-primary text-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GrainientHero() {
|
export function GrainientHero() {
|
||||||
return (
|
return <section className="relative overflow-hidden border-b border-border bg-background"><div className="signal-aurora absolute inset-0" /><div className="signal-grid absolute inset-0" /><div className="relative mx-auto grid min-h-[720px] w-full max-w-[1280px] items-center gap-10 px-5 py-16 sm:px-8 lg:grid-cols-[0.86fr_1.14fr]"><div><Badge className="mb-6 rounded-md bg-background text-foreground shadow-sm">60+ обновлений продукта</Badge><h1 className="max-w-[760px] text-5xl font-semibold leading-[0.92] tracking-normal sm:text-7xl lg:text-[5.7rem]">Support-команда видит проблему раньше очереди</h1><p className="mt-7 max-w-[620px] text-lg leading-8 text-foreground/72">{site.tagline}</p><div className="mt-8 flex flex-col gap-3 sm:flex-row"><Button asChild className="h-12 rounded-md bg-foreground px-6 text-background hover:bg-foreground/90"><Link href="/dashboard">Запустить демо <ArrowRightIcon className="size-4" /></Link></Button><Button asChild variant="outline" className="h-12 rounded-md bg-background/50 px-6"><Link href="/product">Как работает</Link></Button></div></div><div className="relative"><div className="absolute -left-5 top-9 z-10 hidden rounded-md bg-foreground px-3 py-2 text-xs font-semibold text-background shadow-xl lg:block">topic spike +27%</div><SaasDashboard compact /></div></div></section>;
|
||||||
<section className="relative overflow-hidden border-b border-border">
|
|
||||||
<div className="grainient-field absolute inset-x-0 top-0 h-[520px] opacity-90 blur-0" />
|
|
||||||
<div className="relative mx-auto grid min-h-[calc(100vh-4rem)] w-full max-w-[1180px] items-center gap-10 px-4 py-12 sm:px-6 lg:grid-cols-[0.9fr_1.1fr]">
|
|
||||||
<div>
|
|
||||||
<Badge className="mb-5 rounded-md bg-white text-foreground">60+ обновлений продукта</Badge>
|
|
||||||
<h1 className="font-display text-5xl font-semibold leading-[0.95] tracking-normal sm:text-7xl lg:text-8xl">
|
|
||||||
{site.name} для команд поддержки
|
|
||||||
</h1>
|
|
||||||
<p className="mt-6 max-w-[620px] text-base leading-7 text-foreground/72 sm:text-lg">{site.tagline}</p>
|
|
||||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
|
||||||
<Button asChild className="h-11 rounded-md bg-foreground px-5 text-background hover:bg-foreground/90">
|
|
||||||
<Link href="/pricing">{site.cta}</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="outline" className="h-11 rounded-md border-foreground/30 bg-white/50 px-5">
|
|
||||||
<Link href="/dashboard">{site.secondaryCta}</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg border border-white/50 bg-white/80 p-4 shadow-2xl backdrop-blur">
|
|
||||||
<SaasDashboard compact />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InnerHero({ eyebrow, title, text }: { eyebrow: string; title: string; text: string }) {
|
|
||||||
return (
|
|
||||||
<section className="border-b border-border bg-card">
|
|
||||||
<div className="mx-auto w-full max-w-[1180px] px-4 py-14 sm:px-6 sm:py-18">
|
|
||||||
<Badge variant="outline" className="mb-5 rounded-md border-primary/30 text-primary">
|
|
||||||
{eyebrow}
|
|
||||||
</Badge>
|
|
||||||
<h1 className="max-w-[900px] font-display text-4xl font-semibold leading-tight tracking-normal sm:text-6xl">
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
<p className="mt-5 max-w-[760px] text-base leading-7 text-muted-foreground sm:text-lg">{text}</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MetricStrip({ items }: { items: readonly MetricItem[] }) {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-3 px-4 py-8 sm:px-6 md:grid-cols-3">
|
|
||||||
{items.map((item) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
return (
|
|
||||||
<Card key={item.title} className="rounded-lg border-border bg-card shadow-none">
|
|
||||||
<CardContent className="p-5">
|
|
||||||
<div className="mb-5 flex size-10 items-center justify-center rounded-md bg-muted text-primary">
|
|
||||||
<Icon className="size-5" />
|
|
||||||
</div>
|
|
||||||
<div className="font-display text-4xl font-semibold">{item.value}</div>
|
|
||||||
<h3 className="mt-2 text-base font-semibold">{item.title}</h3>
|
|
||||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FeaturedGrid({
|
|
||||||
eyebrow,
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
eyebrow: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
items: readonly TextItem[];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto w-full max-w-[1180px] px-4 py-12 sm:px-6">
|
|
||||||
<div className="mb-8 grid gap-4 md:grid-cols-[0.8fr_1fr]">
|
|
||||||
<div>
|
|
||||||
<div className="mb-2 text-xs font-semibold uppercase text-primary">{eyebrow}</div>
|
|
||||||
<h2 className="font-display text-3xl font-semibold leading-tight sm:text-5xl">{title}</h2>
|
|
||||||
</div>
|
|
||||||
<p className="max-w-[620px] text-sm leading-7 text-muted-foreground sm:text-base">{text}</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{items.map((item) => (
|
|
||||||
<article key={item.name} className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<div className="mb-4 flex items-center justify-between gap-3">
|
|
||||||
<Badge variant="secondary" className="rounded-md">
|
|
||||||
{item.tag}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-sm font-semibold">{item.price}</span>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold">{item.name}</h3>
|
|
||||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SplitStory({
|
|
||||||
image,
|
|
||||||
eyebrow,
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
points,
|
|
||||||
}: {
|
|
||||||
image: string;
|
|
||||||
eyebrow: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
points: readonly string[];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-8 px-4 py-12 sm:px-6 lg:grid-cols-2">
|
|
||||||
<div className="relative min-h-[380px] overflow-hidden rounded-lg border border-border bg-muted">
|
|
||||||
<Image src={image} alt="" fill className="object-cover" sizes="(min-width: 1024px) 560px, 100vw" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-center">
|
|
||||||
<div className="mb-2 text-xs font-semibold uppercase text-primary">{eyebrow}</div>
|
|
||||||
<h2 className="font-display text-3xl font-semibold leading-tight sm:text-5xl">{title}</h2>
|
|
||||||
<p className="mt-5 text-base leading-7 text-muted-foreground">{text}</p>
|
|
||||||
<div className="mt-6 grid gap-3">
|
|
||||||
{points.map((point) => (
|
|
||||||
<div key={point} className="flex items-center gap-3 text-sm font-medium">
|
|
||||||
<span className="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</span>
|
|
||||||
{point}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TestimonialBand({ items }: { items: readonly TestimonialItem[] }) {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto w-full max-w-[1180px] px-4 py-12 sm:px-6">
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
|
||||||
{items.map((item) => (
|
|
||||||
<blockquote key={item.name} className="rounded-lg border border-border bg-card p-6">
|
|
||||||
<div className="mb-4 text-sm font-semibold text-primary">{item.rating}</div>
|
|
||||||
<p className="font-display text-2xl leading-tight">“{item.text}”</p>
|
|
||||||
<footer className="mt-5 text-sm text-muted-foreground">{item.name}</footer>
|
|
||||||
</blockquote>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PricingTiles({ title, items }: { title: string; items: readonly TileItem[] }) {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto w-full max-w-[1180px] px-4 py-12 sm:px-6">
|
|
||||||
<h2 className="mb-6 font-display text-3xl font-semibold sm:text-5xl">{title}</h2>
|
|
||||||
<div className="grid gap-3 md:grid-cols-3">
|
|
||||||
{items.map((item) => (
|
|
||||||
<article key={item.title} className="rounded-lg border border-border bg-card p-6">
|
|
||||||
<h3 className="text-xl font-semibold">{item.title}</h3>
|
|
||||||
<div className="mt-3 font-display text-3xl font-semibold">{item.price}</div>
|
|
||||||
<Separator className="my-5" />
|
|
||||||
<div className="grid gap-2">
|
|
||||||
{item.items.map((line) => (
|
|
||||||
<div key={line} className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<CheckIcon className="size-4 text-primary" />
|
|
||||||
{line}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IconCards({ items }: { items: readonly IconItem[] }) {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-3 px-4 py-10 sm:px-6 md:grid-cols-3">
|
|
||||||
{items.map((item) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
return (
|
|
||||||
<article key={item.title} className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<div className="mb-4 flex size-10 items-center justify-center rounded-md bg-muted text-primary">
|
|
||||||
<Icon className="size-5" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold">{item.title}</h3>
|
|
||||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InfoColumns({ title, items }: { title: string; items: readonly { title: string; text: string }[] }) {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto w-full max-w-[1180px] px-4 py-10 sm:px-6">
|
|
||||||
<h2 className="mb-5 font-display text-3xl font-semibold">{title}</h2>
|
|
||||||
<div className="grid gap-3 md:grid-cols-3">
|
|
||||||
{items.map((item) => (
|
|
||||||
<article key={item.title} className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<h3 className="font-semibold">{item.title}</h3>
|
|
||||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CtaPanel({ title, text, href, label }: { title: string; text: string; href: string; label: string }) {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto w-full max-w-[1180px] px-4 py-12 sm:px-6">
|
|
||||||
<div className="rounded-lg border border-border bg-primary p-7 text-primary-foreground sm:p-10">
|
|
||||||
<h2 className="font-display text-3xl font-semibold sm:text-5xl">{title}</h2>
|
|
||||||
<p className="mt-4 max-w-[720px] text-sm leading-7 opacity-82 sm:text-base">{text}</p>
|
|
||||||
<Button asChild variant="secondary" className="mt-6 h-11 rounded-md">
|
|
||||||
<Link href={href}>
|
|
||||||
{label}
|
|
||||||
<ArrowRightIcon className="size-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ReservationForm() {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-6 px-4 py-12 sm:px-6 lg:grid-cols-[0.9fr_1.1fr]">
|
|
||||||
<div>
|
|
||||||
<h2 className="font-display text-3xl font-semibold sm:text-5xl">Оставьте заявку</h2>
|
|
||||||
<p className="mt-4 text-sm leading-7 text-muted-foreground">
|
|
||||||
Это статическая форма-шаблон. Она показывает структуру будущего booking flow без отправки данных наружу.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<form className="grid gap-3 rounded-lg border border-border bg-card p-5">
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
<Input placeholder="Имя" />
|
|
||||||
<Input placeholder="Телефон или email" />
|
|
||||||
<Input placeholder="Дата" />
|
|
||||||
<Input placeholder="Время" />
|
|
||||||
</div>
|
|
||||||
<Textarea placeholder="Комментарий, пожелания или ограничения" />
|
|
||||||
<Button type="button" className="h-11 rounded-md">Отправить заявку</Button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContactForm() {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-6 px-4 py-12 sm:px-6 lg:grid-cols-2">
|
|
||||||
<div className="rounded-lg border border-border bg-card p-6">
|
|
||||||
<h2 className="font-display text-3xl font-semibold">Связаться</h2>
|
|
||||||
<p className="mt-3 text-sm leading-7 text-muted-foreground">
|
|
||||||
Форма готова как UI-слой, но не подключена к API в рамках статического шаблона.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<form className="grid gap-3 rounded-lg border border-border bg-card p-5">
|
|
||||||
<Input placeholder="Ваше имя" />
|
|
||||||
<Input placeholder="Email" />
|
|
||||||
<Textarea placeholder="Сообщение" />
|
|
||||||
<Button type="button" className="h-11 rounded-md">Отправить</Button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CatalogToolbar() {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto flex w-full max-w-[1180px] flex-col gap-3 px-4 py-6 sm:px-6 md:flex-row md:items-center md:justify-between">
|
|
||||||
<div className="flex flex-1 items-center gap-2 rounded-lg border border-border bg-card px-3">
|
|
||||||
<SearchIcon className="size-4 text-muted-foreground" />
|
|
||||||
<Input className="border-0 bg-transparent shadow-none focus-visible:ring-0" placeholder="Поиск по названию, тегу или статусу" />
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" className="rounded-md">
|
|
||||||
<ФильтрIcon className="size-4" />
|
|
||||||
Фильтр
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" className="rounded-md">
|
|
||||||
<SlidersHorizontalIcon className="size-4" />
|
|
||||||
Сортировка
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProductDetail() {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-10 px-4 py-12 sm:px-6 lg:grid-cols-[1.05fr_0.95fr]">
|
|
||||||
<div className="grid gap-3">
|
|
||||||
<div className="relative min-h-[620px] overflow-hidden rounded-lg border border-border bg-muted">
|
|
||||||
<Image src={site.heroImage} alt="" fill className="object-cover" sizes="(min-width: 1024px) 560px, 100vw" />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 gap-3">
|
|
||||||
{[site.heroImage, site.accentImage, site.heroImage, site.accentImage].map((image, index) => (
|
|
||||||
<div key={`${image}-${index}`} className="relative aspect-square overflow-hidden rounded-lg border border-border bg-muted">
|
|
||||||
<Image src={image} alt="" fill className="object-cover" sizes="150px" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-4 flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
Каталог <ChevronRightIcon className="size-4" /> Верхняя одежда <ChevronRightIcon className="size-4" /> Классическая overshirt
|
|
||||||
</div>
|
|
||||||
<h1 className="font-display text-4xl font-semibold sm:text-6xl">Классическая overshirt</h1>
|
|
||||||
<div className="mt-5 flex items-center gap-3">
|
|
||||||
<Badge variant="outline" className="rounded-md">4.8 ★</Badge>
|
|
||||||
<span className="text-sm text-muted-foreground">210 отзывов</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 flex items-end gap-4">
|
|
||||||
<div className="font-display text-4xl font-semibold">17 900 ₽</div>
|
|
||||||
<div className="pb-1 text-sm text-muted-foreground line-through">22 900 ₽</div>
|
|
||||||
<Badge className="rounded-md bg-primary/10 text-primary hover:bg-primary/10">Скидка 20%</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="mt-6 text-base leading-7 text-muted-foreground">
|
|
||||||
Плотный cotton twill, прямой крой, hidden placket и аккуратный вес для межсезонья.
|
|
||||||
</p>
|
|
||||||
<Separator className="my-7" />
|
|
||||||
<div className="grid gap-5">
|
|
||||||
<OptionRow label="Цвет" options={["Черный", "Шалфей", "Охра", "Камень", "Синий"]} />
|
|
||||||
<OptionRow label="Размер" options={["XS", "S", "M", "L", "XL"]} active="L" />
|
|
||||||
</div>
|
|
||||||
<div className="mt-7 grid gap-3 sm:grid-cols-2">
|
|
||||||
<Button className="h-12 rounded-md"><ShoppingCartIcon className="size-4" /> В корзину</Button>
|
|
||||||
<Button variant="outline" className="h-12 rounded-md">В избранное</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function OptionRow({ label, options, active }: { label: string; options: readonly string[]; active?: string }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mb-3 text-sm font-semibold">{label}</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{options.map((option) => (
|
|
||||||
<Button key={option} variant={option === active ? "default" : "outline"} className="h-9 rounded-md px-4">
|
|
||||||
{option}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LookbookGrid() {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-3 px-4 py-12 sm:px-6 md:grid-cols-4">
|
|
||||||
{[site.heroImage, site.accentImage, site.heroImage, site.accentImage, site.accentImage, site.heroImage].map((image, index) => (
|
|
||||||
<div
|
|
||||||
key={`${image}-look-${index}`}
|
|
||||||
className={`relative overflow-hidden rounded-lg border border-border bg-muted ${index === 0 || index === 5 ? "md:col-span-2 md:row-span-2 min-h-[420px]" : "min-h-[260px]"}`}
|
|
||||||
>
|
|
||||||
<Image src={image} alt="" fill className="object-cover" sizes="(min-width: 768px) 25vw, 100vw" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CartPreview() {
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-6 px-4 py-12 sm:px-6 lg:grid-cols-[1fr_380px]">
|
|
||||||
<div className="grid gap-3">
|
|
||||||
{["Classic overshirt", "Wide pleated trouser", "Utility bag"].map((item, index) => (
|
|
||||||
<div key={item} className="flex items-center gap-4 rounded-lg border border-border bg-card p-4">
|
|
||||||
<div className="relative size-20 overflow-hidden rounded-md bg-muted">
|
|
||||||
<Image src={index % 2 === 0 ? site.heroImage : site.accentImage} alt="" fill className="object-cover" sizes="80px" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="font-semibold">{item}</div>
|
|
||||||
<div className="text-sm text-muted-foreground">Черный / L / Qty 1</div>
|
|
||||||
</div>
|
|
||||||
<div className="font-semibold">{index === 0 ? "17 900 ₽" : "9 800 ₽"}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<aside className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<h2 className="font-display text-2xl font-semibold">Итог заказа</h2>
|
|
||||||
<div className="mt-5 grid gap-3 text-sm">
|
|
||||||
<div className="flex justify-between"><span>Товары</span><span>37 500 ₽</span></div>
|
|
||||||
<div className="flex justify-between"><span>Доставка</span><span>Бесплатно</span></div>
|
|
||||||
<Separator />
|
|
||||||
<div className="flex justify-between text-lg font-semibold"><span>Итого</span><span>37 500 ₽</span></div>
|
|
||||||
</div>
|
|
||||||
<Button className="mt-6 h-11 w-full rounded-md">Перейти к оформлению</Button>
|
|
||||||
</aside>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DoctorGrid() {
|
|
||||||
const doctors = [
|
|
||||||
["Мария Северова", "Терапевт, preventive care", "12 лет опыта"],
|
|
||||||
["Илья Данилов", "Кардиолог", "9 лет опыта"],
|
|
||||||
["Анна Волкова", "Эндокринолог", "11 лет опыта"],
|
|
||||||
] as const;
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-3 px-4 py-12 sm:px-6 md:grid-cols-3">
|
|
||||||
{doctors.map(([name, role, experience]) => (
|
|
||||||
<article key={name} className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<div className="mb-5 flex size-14 items-center justify-center rounded-md bg-muted text-primary">
|
|
||||||
<UserRoundIcon className="size-7" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-semibold">{name}</h3>
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">{role}</p>
|
|
||||||
<Badge variant="outline" className="mt-4 rounded-md">{experience}</Badge>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SaasDashboard({ compact = false }: { compact?: boolean }) {
|
export function SaasDashboard({ compact = false }: { compact?: boolean }) {
|
||||||
return (
|
return <section className={compact ? "" : "mx-auto w-full max-w-[1280px] px-5 py-12 sm:px-8"}><div className="rounded-lg border border-white/70 bg-background/86 p-3 shadow-[0_36px_100px_rgba(37,76,210,0.22)] backdrop-blur-xl"><div className="grid gap-3 lg:grid-cols-[1.06fr_0.94fr]"><div className="rounded-md border border-border bg-card p-5"><div className="mb-5 flex items-start justify-between gap-4"><div><div className="text-xs font-semibold uppercase tracking-[0.14em] text-primary">Issue radar</div><h2 className="mt-2 text-2xl font-semibold leading-tight">Платежи не проходят у части клиентов</h2></div><Badge className="rounded-md">Онлайн</Badge></div><div className="grid h-[260px] grid-cols-12 items-end gap-2 border-b border-l border-dashed border-border px-3 pb-3">{bars.map((bar, index) => <div key={index} className="flex items-end gap-1"><span className="w-full rounded-t-sm bg-primary" style={{ height: bar + "%" }} /><span className="w-full rounded-t-sm bg-accent" style={{ height: Math.max(16, bar - 24) + "%" }} /></div>)}</div><div className="mt-4 grid grid-cols-3 gap-2 text-xs text-muted-foreground"><div>affected 842</div><div>refund risk high</div><div>owner billing</div></div></div><div className="grid gap-3">{[["Затронутые диалоги", "842", "+27% за 2 часа"], ["Закрыто AI", "318", "без handoff"], ["QA warnings", "27", "требуют ревью"]].map(([label, value, meta]) => <div key={label} className="rounded-md border border-border bg-card p-5"><div className="text-sm text-muted-foreground">{label}</div><div className="mt-2 text-5xl font-semibold tracking-normal">{value}</div><div className="mt-3 text-xs font-semibold text-primary">{meta}</div></div>)}</div></div></div></section>;
|
||||||
<section className={compact ? "" : "mx-auto w-full max-w-[1180px] px-4 py-12 sm:px-6"}>
|
|
||||||
<div className="grid gap-3 lg:grid-cols-[1fr_360px]">
|
|
||||||
<div className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<div className="mb-5 flex items-center justify-between">
|
|
||||||
<h2 className="font-display text-2xl font-semibold">Выявление проблем в реальном времени</h2>
|
|
||||||
<Badge className="rounded-md">Онлайн</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="h-[260px]">
|
|
||||||
<ClientChart>
|
|
||||||
<ResponsiveContainer width="100%" height="100%" minWidth={1} minHeight={1}>
|
|
||||||
<BarChart data={revenueData}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
|
||||||
<XAxis dataKey="name" tickLine={false} axisLine={false} />
|
|
||||||
<YAxis hide />
|
|
||||||
<Tooltip />
|
|
||||||
<Bar dataKey="value" radius={[6, 6, 0, 0]} fill="var(--primary)" />
|
|
||||||
<Bar dataKey="alt" radius={[6, 6, 0, 0]} fill="var(--accent)" />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ClientChart>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3">
|
|
||||||
{["Затронутые диалоги", "Закрыто AI", "QA-предупреждения"].map((item, index) => (
|
|
||||||
<div key={item} className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<div className="text-sm text-muted-foreground">{item}</div>
|
|
||||||
<div className="mt-2 font-display text-4xl font-semibold">{[842, 318, 27][index]}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardShell({
|
export function InnerHero({ eyebrow, title, text }: { eyebrow: string; title: string; text: string }) { return <section className="border-b border-border bg-card"><div className="mx-auto w-full max-w-[1280px] px-5 py-14 sm:px-8"><Badge variant="outline" className="mb-6 rounded-md border-primary/30 text-primary">{eyebrow}</Badge><h1 className="max-w-[960px] text-4xl font-semibold leading-[1.02] tracking-normal sm:text-6xl">{title}</h1><p className="mt-5 max-w-[760px] text-base leading-7 text-muted-foreground">{text}</p></div></section>; }
|
||||||
title,
|
|
||||||
description,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="mx-auto grid w-full max-w-[1440px] gap-0 md:grid-cols-[260px_1fr]">
|
|
||||||
<aside className="hidden min-h-[calc(100vh-4rem)] border-r border-border bg-card p-4 md:block">
|
|
||||||
<div className="mb-5 text-xs font-semibold uppercase text-muted-foreground">Операции</div>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
{site.nav.map((item) => (
|
|
||||||
<Link key={item.href} href={item.href} className="rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground">
|
|
||||||
{item.label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<section className="min-w-0">
|
|
||||||
<div className="border-b border-border bg-card px-4 py-6 sm:px-6">
|
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="font-display text-3xl font-semibold">{title}</h1>
|
|
||||||
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" className="rounded-md"><BellIcon className="size-4" /> Алерты</Button>
|
|
||||||
<Button className="rounded-md">Экспорт</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OpsDashboard() {
|
export function IssueWorkflow() { const flow = [["01", "Сигнал", "topic volume растет быстрее нормы"], ["02", "Кластер", "AI связывает диалоги с общей причиной"], ["03", "Handoff", "оператор получает summary и next action"], ["04", "QA", "система проверяет ответ и тон"]] as const; return <section className="mx-auto w-full max-w-[1280px] px-5 py-14 sm:px-8"><div className="mb-8 max-w-[760px]"><div className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">Workflow</div><h2 className="mt-2 text-4xl font-semibold leading-tight sm:text-6xl">От всплеска тикетов до корректного ответа</h2></div><div className="grid gap-3 md:grid-cols-4">{flow.map(([num, title, text]) => <article key={num} className="rounded-md border border-border bg-card p-5"><div className="mb-8 text-5xl font-semibold text-primary/75">{num}</div><h3 className="text-lg font-semibold">{title}</h3><p className="mt-3 text-sm leading-6 text-muted-foreground">{text}</p></article>)}</div></section>; }
|
||||||
return (
|
|
||||||
<DashboardShell title="Dashboard" description={site.tagline}>
|
|
||||||
<MetricStrip items={[
|
|
||||||
{ title: "Отправлено заказов", value: "1,248", text: "+18.2% к прошлой неделе", icon: CheckIcon },
|
|
||||||
{ title: "Поврежденные возвраты", value: "38", text: "-8.7% к прошлой неделе", icon: XIcon },
|
|
||||||
{ title: "Пропущенные интервалы", value: "27", text: "+4.3% к прошлой неделе", icon: BellIcon },
|
|
||||||
]} />
|
|
||||||
<OpsAnalytics />
|
|
||||||
<ShipmentTable />
|
|
||||||
</DashboardShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OpsAnalytics() {
|
export function MetricStrip(_props: { items?: unknown } = {}) { return <section className="mx-auto grid w-full max-w-[1280px] gap-3 px-5 py-8 sm:px-8 md:grid-cols-3">{highlights.map((item) => { const Icon = item.icon; return <article key={item.title} className="rounded-md border border-border bg-card p-5"><Icon className="mb-6 size-5 text-primary" /><div className="text-4xl font-semibold">{item.value}</div><h3 className="mt-2 font-semibold">{item.title}</h3><p className="mt-2 text-sm leading-6 text-muted-foreground">{item.text}</p></article>; })}</section>; }
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-3 px-4 py-8 sm:px-6 lg:grid-cols-[1fr_360px]">
|
|
||||||
<div className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<h2 className="mb-5 font-display text-2xl font-semibold">Динамика выручки</h2>
|
|
||||||
<div className="h-[300px]">
|
|
||||||
<ClientChart>
|
|
||||||
<ResponsiveContainer width="100%" height="100%" minWidth={1} minHeight={1}>
|
|
||||||
<AreaChart data={revenueData}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
|
||||||
<XAxis dataKey="name" tickLine={false} axisLine={false} />
|
|
||||||
<YAxis hide />
|
|
||||||
<Tooltip />
|
|
||||||
<Area type="monotone" dataKey="value" stroke="var(--primary)" fill="var(--primary)" fillOpacity={0.18} />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ClientChart>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<h2 className="mb-5 font-display text-2xl font-semibold">План выполнен</h2>
|
|
||||||
<div className="h-[220px]">
|
|
||||||
<ClientChart>
|
|
||||||
<ResponsiveContainer width="100%" height="100%" minWidth={1} minHeight={1}>
|
|
||||||
<PieChart>
|
|
||||||
<Pie data={pieData} innerRadius={58} outerRadius={88} dataKey="value">
|
|
||||||
{pieData.map((entry) => <Cell key={entry.name} fill={entry.fill} />)}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</ClientChart>
|
|
||||||
</div>
|
|
||||||
<div className="text-center font-display text-4xl font-semibold">56%</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShipmentTable() {
|
export function FeaturedGrid({ eyebrow, title, text, items }: { eyebrow: string; title: string; text: string; items: readonly TextItem[] }) { return <section className="mx-auto w-full max-w-[1280px] px-5 py-14 sm:px-8"><div className="mb-8 grid gap-4 md:grid-cols-[0.8fr_1fr]"><div><div className="mb-2 text-xs font-semibold uppercase tracking-[0.16em] text-primary">{eyebrow}</div><h2 className="text-4xl font-semibold leading-tight sm:text-6xl">{title}</h2></div><p className="max-w-[620px] text-base leading-7 text-muted-foreground">{text}</p></div><div className="grid gap-3 md:grid-cols-4">{items.map((item) => <article key={item.name} className="rounded-md border border-border bg-card p-5"><Badge variant="secondary" className="mb-8 rounded-md">{item.tag}</Badge><div className="text-sm font-semibold text-primary">{item.price}</div><h3 className="mt-3 text-xl font-semibold">{item.name}</h3><p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p></article>)}</div></section>; }
|
||||||
const rows = [
|
|
||||||
["ORD-2048", "Out for delivery", "A17", "14:20", "В графике"],
|
|
||||||
["ORD-2049", "Packed", "WH-B", "16:10", "Готов"],
|
|
||||||
["RET-8831", "Inspection", "Returns", "11:40", "Риск"],
|
|
||||||
["ORD-2050", "Delayed", "A04", "18:30", "Действие"],
|
|
||||||
] as const;
|
|
||||||
return (
|
|
||||||
<section className="mx-auto w-full max-w-[1180px] px-4 py-8 sm:px-6">
|
|
||||||
<div className="overflow-x-auto rounded-lg border border-border bg-card">
|
|
||||||
<table className="w-full min-w-[720px] text-left text-sm">
|
|
||||||
<thead className="bg-muted text-muted-foreground">
|
|
||||||
<tr>
|
|
||||||
{["Заказ", "Статус", "Ответственный", "ETA", "Состояние"].map((head) => (
|
|
||||||
<th key={head} className="px-4 py-3 font-medium">{head}</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{rows.map((row) => (
|
|
||||||
<tr key={row[0]} className="border-t border-border">
|
|
||||||
{row.map((cell, index) => (
|
|
||||||
<td key={`${row[0]}-${cell}`} className={`px-4 py-3 ${index === 0 ? "font-semibold" : "text-muted-foreground"}`}>
|
|
||||||
{index === 4 ? <Badge variant={cell === "Риск" || cell === "Действие" ? "destructive" : "secondary"} className="rounded-md">{cell}</Badge> : cell}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RouteTimeline() {
|
export function IntegrationStack() { const tools = ["Helpdesk", "CRM", "База знаний", "Биллинг", "Slack", "Email", "Чат", "Склад"] as const; return <section className="mx-auto grid w-full max-w-[1280px] gap-8 px-5 py-14 sm:px-8 lg:grid-cols-[0.75fr_1.25fr]"><div><div className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">Integrations</div><h2 className="mt-2 text-4xl font-semibold leading-tight sm:text-5xl">AI не отвечает вслепую</h2><p className="mt-4 text-sm leading-7 text-muted-foreground">Шаблон показывает продуктовый слой интеграций без внешних вызовов, секретов и API.</p></div><div className="grid grid-cols-2 gap-3 sm:grid-cols-4">{tools.map((tool, index) => <div key={tool} className="rounded-md border border-border bg-card p-4"><div className="mb-7 flex size-9 items-center justify-center rounded-md bg-primary/10 text-xs font-semibold text-primary">{index + 1}</div><div className="font-semibold">{tool}</div></div>)}</div></section>; }
|
||||||
return (
|
|
||||||
<section className="mx-auto w-full max-w-[1180px] px-4 py-8 sm:px-6">
|
|
||||||
<div className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<h2 className="mb-5 font-display text-2xl font-semibold">Таймлайн маршрута</h2>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
{["Забор подтвержден", "Складское сканирование", "Курьер назначен", "Клиент уведомлен", "Доставка window"].map((step, index) => (
|
|
||||||
<div key={step} className="grid grid-cols-[32px_1fr_auto] items-center gap-3">
|
|
||||||
<span className="flex size-8 items-center justify-center rounded-md bg-primary text-xs font-semibold text-primary-foreground">{index + 1}</span>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{step}</div>
|
|
||||||
<div className="text-xs text-muted-foreground">Route N-{204 + index}, checkpoint {index + 1}</div>
|
|
||||||
</div>
|
|
||||||
<Progress value={Math.min(100, 35 + index * 14)} className="w-24" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettingsPanel() {
|
export function IconCards({ items = eventTypes }: { items?: readonly IconItem[] }) { return <section className="mx-auto grid w-full max-w-[1280px] gap-3 px-5 py-12 sm:px-8 md:grid-cols-3">{items.map((item) => { const Icon = item.icon; return <article key={item.title} className="rounded-md border border-border bg-card p-6"><Icon className="mb-8 size-6 text-primary" /><h3 className="text-xl font-semibold">{item.title}</h3><p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p></article>; })}</section>; }
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-3 px-4 py-8 sm:px-6 md:grid-cols-2">
|
|
||||||
<div className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<h2 className="font-display text-2xl font-semibold">Правила уведомлений</h2>
|
|
||||||
<div className="mt-5 grid gap-3">
|
|
||||||
{["ETA drift > 15 min", "Damaged return created", "Carrier webhook delayed"].map((rule) => (
|
|
||||||
<div key={rule} className="flex items-center justify-between rounded-md border border-border p-3">
|
|
||||||
<span className="text-sm font-medium">{rule}</span>
|
|
||||||
<Badge className="rounded-md">Включено</Badge>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<h2 className="font-display text-2xl font-semibold">Рабочая область</h2>
|
|
||||||
<div className="mt-5 grid gap-3">
|
|
||||||
<Input value="FreightOps Moscow" readOnly />
|
|
||||||
<Input value="ops@freight.example" readOnly />
|
|
||||||
<Button className="rounded-md">Сохранить превью</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function PricingTiles({ title, items }: { title: string; items: readonly TileItem[] }) { return <section className="mx-auto w-full max-w-[1280px] px-5 py-14 sm:px-8"><h2 className="mb-7 text-4xl font-semibold sm:text-6xl">{title}</h2><div className="grid gap-4 md:grid-cols-3">{items.map((item, index) => <article key={item.title} className={index === 1 ? "rounded-md bg-foreground p-6 text-background" : "rounded-md border border-border bg-card p-6"}><h3 className="text-2xl font-semibold">{item.title}</h3><div className="mt-3 text-4xl font-semibold">{item.price}</div><Separator className="my-5" />{item.items.map((line) => <div key={line} className={index === 1 ? "mb-2 flex items-center gap-2 text-sm text-background/72" : "mb-2 flex items-center gap-2 text-sm text-muted-foreground"}><CheckIcon className="size-4" />{line}</div>)}</article>)}</div></section>; }
|
||||||
|
|
||||||
export function IssueWorkflow() {
|
export function InfoColumns({ title, items }: { title: string; items: readonly { title: string; text: string }[] }) { return <section className="mx-auto w-full max-w-[1280px] px-5 py-12 sm:px-8"><h2 className="mb-6 text-4xl font-semibold">{title}</h2><div className="grid gap-3 md:grid-cols-3">{items.map((item) => <article key={item.title} className="rounded-md border border-border bg-card p-6"><ShieldCheckIcon className="mb-8 size-5 text-primary" /><h3 className="text-xl font-semibold">{item.title}</h3><p className="mt-3 text-sm leading-6 text-muted-foreground">{item.text}</p></article>)}</div></section>; }
|
||||||
const flow = [
|
|
||||||
["01", "Сигнал", "Рост темы «оплата не проходит» замечен за 30 секунд."],
|
|
||||||
["02", "Гипотеза", "AI группирует затронутые диалоги и показывает общий pattern."],
|
|
||||||
["03", "Handoff", "Оператор получает summary, риск SLA и готовый ответ клиенту."],
|
|
||||||
["04", "QA", "После закрытия система проверяет тон, полноту и compliance."],
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
return (
|
export function SplitStory({ image, eyebrow, title, text, points }: { image: string; eyebrow: string; title: string; text: string; points: readonly string[] }) { return <section className="mx-auto grid w-full max-w-[1280px] gap-10 px-5 py-14 sm:px-8 lg:grid-cols-2"><div className="relative min-h-[430px] overflow-hidden rounded-md border border-border bg-muted"><Image src={image} alt={title} fill className="object-cover" sizes="(min-width: 1024px) 50vw, 100vw" /></div><div className="flex flex-col justify-center"><div className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-primary">{eyebrow}</div><h2 className="text-4xl font-semibold leading-tight sm:text-5xl">{title}</h2><p className="mt-5 text-base leading-7 text-muted-foreground">{text}</p><div className="mt-7 grid gap-3">{points.map((point) => <div key={point} className="flex items-center gap-3 text-sm font-semibold"><CheckIcon className="size-4 text-primary" />{point}</div>)}</div></div></section>; }
|
||||||
<section className="mx-auto w-full max-w-[1180px] px-4 py-12 sm:px-6">
|
|
||||||
<div className="mb-8 max-w-[760px]">
|
|
||||||
<div className="text-xs font-semibold uppercase text-primary">Agent workflow</div>
|
|
||||||
<h2 className="mt-2 font-display text-4xl font-semibold leading-tight sm:text-6xl">От всплеска тикетов до корректного ответа за один рабочий поток</h2>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 md:grid-cols-4">
|
|
||||||
{flow.map(([num, title, text]) => (
|
|
||||||
<article key={num} className="rounded-lg border border-border bg-card p-5">
|
|
||||||
<div className="mb-8 font-display text-5xl font-semibold text-primary/70">{num}</div>
|
|
||||||
<h3 className="text-lg font-semibold">{title}</h3>
|
|
||||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">{text}</p>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IntegrationStack() {
|
export function TestimonialBand(_props: { items?: unknown } = {}) { return <section className="mx-auto grid w-full max-w-[1280px] gap-4 px-5 py-12 sm:px-8 md:grid-cols-2">{testimonials.map((item) => <blockquote key={item.name} className="rounded-md border border-border bg-card p-7"><div className="text-sm font-semibold text-primary">{item.rating}</div><p className="mt-5 text-2xl font-semibold leading-snug">“{item.text}”</p><footer className="mt-5 text-sm text-muted-foreground">{item.name}</footer></blockquote>)}</section>; }
|
||||||
const tools = ["Helpdesk", "CRM", "Docs", "Billing", "Slack", "Email", "Chat", "Warehouse"] as const;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="mx-auto grid w-full max-w-[1180px] gap-8 px-4 py-12 sm:px-6 lg:grid-cols-[0.8fr_1.2fr]">
|
|
||||||
<div>
|
|
||||||
<h2 className="font-display text-4xl font-semibold leading-tight">Интеграции показаны как слой продукта, а не список логотипов</h2>
|
|
||||||
<p className="mt-4 text-sm leading-7 text-muted-foreground">Шаблон оставляет место под реальные коннекторы, но не делает внешних вызовов и не хранит секреты.</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
||||||
{tools.map((tool) => (
|
|
||||||
<div key={tool} className="rounded-lg border border-border bg-card p-4 text-sm font-semibold">
|
|
||||||
<div className="mb-5 size-8 rounded-md bg-primary/12" />
|
|
||||||
{tool}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function CtaPanel({ title, text, href, label }: { title: string; text: string; href: string; label: string }) { return <section className="mx-auto w-full max-w-[1280px] px-5 py-12 sm:px-8"><div className="rounded-md bg-primary p-8 text-primary-foreground"><SparklesIcon className="mb-8 size-6" /><h2 className="text-4xl font-semibold">{title}</h2><p className="mt-4 max-w-[720px] text-sm leading-7 text-primary-foreground/75">{text}</p><Button asChild variant="secondary" className="mt-6 rounded-md"><Link href={href}>{label}<ArrowRightIcon className="size-4" /></Link></Button></div></section>; }
|
||||||
|
|||||||
Reference in New Issue
Block a user