first commit

This commit is contained in:
lendry
2026-06-24 14:37:15 +03:00
commit 995adeedd4
188 changed files with 28810 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { apiReference, type ApiEndpoint } from '@/lib/api-endpoints';
import { cn } from '@/lib/utils';
const methodColors: Record<ApiEndpoint['method'], string> = {
GET: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
POST: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300',
PUT: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300',
PATCH: 'bg-violet-100 text-violet-800 dark:bg-violet-950 dark:text-violet-300',
DELETE: 'bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300'
};
export function ApiEndpointCard({ endpoint }: { endpoint: ApiEndpoint }) {
return (
<Card className="overflow-hidden">
<CardContent className="flex flex-col gap-2 p-4 sm:flex-row sm:items-start sm:gap-4">
<Badge className={cn('w-fit shrink-0 font-mono', methodColors[endpoint.method])}>{endpoint.method}</Badge>
<div className="min-w-0 flex-1">
<code className="break-all text-sm font-medium">{endpoint.path}</code>
<p className="mt-1 text-sm font-medium">{endpoint.summary}</p>
{endpoint.description ? <p className="mt-1 text-sm text-zinc-500">{endpoint.description}</p> : null}
{endpoint.auth ? <p className="mt-2 text-xs text-zinc-400">Требуется Authorization: Bearer JWT</p> : null}
</div>
</CardContent>
</Card>
);
}
export function ApiReferenceSection() {
return (
<div className="space-y-8">
{apiReference.map((group) => (
<div key={group.tag}>
<h3 className="mb-4 text-lg font-semibold">{group.tag}</h3>
<div className="space-y-3">
{group.endpoints.map((endpoint) => (
<ApiEndpointCard key={`${endpoint.method}-${endpoint.path}`} endpoint={endpoint} />
))}
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,8 @@
'use client';
import { authLoginExamples } from '@/lib/auth-examples';
import { CodeExampleTabs } from '@/components/code-example-tabs';
export function AuthLoginCodeTabs() {
return <CodeExampleTabs examples={authLoginExamples} />;
}

View File

@@ -0,0 +1,8 @@
'use client';
import { authLdapExamples } from '@/lib/auth-examples';
import { CodeExampleTabs } from '@/components/code-example-tabs';
export function AuthLdapCodeTabs() {
return <CodeExampleTabs examples={authLdapExamples} />;
}

View File

@@ -0,0 +1,43 @@
'use client';
import { useState } from 'react';
import { Check, Copy } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
export function CodeBlock({ code, language, title }: { code: string; language?: string; title?: string }) {
const [copied, setCopied] = useState(false);
async function copy() {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<div className="group relative my-4 overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800">
{(title || language) && (
<div className="flex items-center justify-between border-b border-zinc-200 bg-zinc-50 px-4 py-2 text-xs text-zinc-500 dark:border-zinc-800 dark:bg-zinc-900">
<span>{title ?? language}</span>
<Button variant="ghost" size="sm" className="h-7 px-2 opacity-0 group-hover:opacity-100" onClick={copy}>
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
{copied ? 'Скопировано' : 'Копировать'}
</Button>
</div>
)}
<pre className={cn('overflow-x-auto bg-zinc-950 p-4 text-sm leading-relaxed text-zinc-50', !title && !language && 'rounded-xl')}>
<code>{code}</code>
</pre>
{!title && !language && (
<Button
variant="ghost"
size="sm"
className="absolute right-2 top-2 h-7 bg-zinc-900/80 px-2 text-zinc-300 opacity-0 group-hover:opacity-100"
onClick={copy}
>
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
</Button>
)}
</div>
);
}

View File

@@ -0,0 +1,26 @@
'use client';
import type { CodeExample } from '@/lib/code-example';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { CodeBlock } from '@/components/code-block';
export function CodeExampleTabs({ examples }: { examples: CodeExample[] }) {
if (examples.length === 0) return null;
return (
<Tabs defaultValue={examples[0].id} className="my-6">
<TabsList className="mb-2 flex h-auto max-w-full flex-wrap gap-1">
{examples.map((example) => (
<TabsTrigger key={example.id} value={example.id} className="text-xs sm:text-sm">
{example.label}
</TabsTrigger>
))}
</TabsList>
{examples.map((example) => (
<TabsContent key={example.id} value={example.id}>
<CodeBlock code={example.code} language={example.language} title={example.label} />
</TabsContent>
))}
</Tabs>
);
}

View File

@@ -0,0 +1,78 @@
import type { DocBlock } from '@/lib/docs-pages';
import { OAuthCodeTabs } from '@/components/oauth-code-tabs';
import { AuthLoginCodeTabs } from '@/components/auth-code-tabs';
import { AuthLdapCodeTabs } from '@/components/auth-ldap-code-tabs';
import { ApiReferenceSection } from '@/components/api-endpoint-card';
import { CodeBlock } from '@/components/code-block';
import { cn } from '@/lib/utils';
function Callout({ variant, title, text }: { variant: 'info' | 'warning' | 'tip'; title: string; text: string }) {
const styles = {
info: 'border-blue-200 bg-blue-50 dark:border-blue-900 dark:bg-blue-950/40',
warning: 'border-amber-200 bg-amber-50 dark:border-amber-900 dark:bg-amber-950/40',
tip: 'border-emerald-200 bg-emerald-50 dark:border-emerald-900 dark:bg-emerald-950/40'
};
return (
<div className={cn('my-4 rounded-xl border p-4', styles[variant])}>
<p className="font-semibold">{title}</p>
<p className="mt-1 text-sm leading-relaxed text-zinc-600 dark:text-zinc-300">{text}</p>
</div>
);
}
export function DocBlockRenderer({ block }: { block: DocBlock }) {
switch (block.type) {
case 'paragraph':
return <p className="leading-7 text-zinc-600 dark:text-zinc-300">{block.text}</p>;
case 'list':
return (
<ul className="my-4 list-disc space-y-2 pl-6 leading-7 text-zinc-600 dark:text-zinc-300">
{block.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
case 'code':
return <CodeBlock code={block.code} language={block.language} title={block.title} />;
case 'callout':
return <Callout variant={block.variant} title={block.title} text={block.text} />;
case 'table':
return (
<div className="my-4 overflow-x-auto rounded-xl border border-zinc-200 dark:border-zinc-800">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
{block.headers.map((header) => (
<th key={header} className="px-4 py-3 text-left font-semibold">
{header}
</th>
))}
</tr>
</thead>
<tbody>
{block.rows.map((row, index) => (
<tr key={index} className="border-b border-zinc-100 last:border-0 dark:border-zinc-800">
{row.map((cell, cellIndex) => (
<td key={cellIndex} className="px-4 py-3 align-top text-zinc-600 dark:text-zinc-300">
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
case 'oauth-examples':
return <OAuthCodeTabs />;
case 'auth-login-examples':
return <AuthLoginCodeTabs />;
case 'auth-ldap-examples':
return <AuthLdapCodeTabs />;
case 'api-reference':
return <ApiReferenceSection />;
default:
return null;
}
}

View File

@@ -0,0 +1,103 @@
'use client';
import Link from 'next/link';
import { ArrowRight, BookOpen, Code2, Rocket, Shield } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { docNavigation, groupDocNavigation } from '@/lib/navigation';
import { usePublicSettings } from '@/providers/public-settings-provider';
const highlights = [
{
icon: Rocket,
title: 'Быстрый старт',
description: 'Локальный запуск через Docker Compose за несколько минут.',
href: '/docs/getting-started'
},
{
icon: Code2,
title: 'OAuth 2.0',
description: 'Примеры интеграции на JavaScript, Python, PHP, Go и других языках.',
href: '/docs/oauth'
},
{
icon: Shield,
title: 'Безопасность',
description: 'PIN-код, сессии, LDAP/LDAPS и управление устройствами.',
href: '/docs/sessions'
},
{
icon: BookOpen,
title: 'Справочник API',
description: 'Полный список REST endpoints и ссылка на Swagger.',
href: '/docs/api-reference'
}
];
export function DocsHomeContent() {
const { projectName, projectTagline } = usePublicSettings();
const groups = groupDocNavigation(docNavigation);
return (
<main className="min-w-0 pb-16 xl:col-span-1">
<section className="relative overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-50 px-6 py-12 dark:border-zinc-800 dark:bg-zinc-900/50 sm:px-10 sm:py-16">
<p className="text-sm font-medium text-zinc-500">Документация</p>
<h1 className="mt-3 max-w-2xl text-4xl font-bold tracking-tight text-zinc-950 dark:text-zinc-50 sm:text-5xl">{projectName}</h1>
<p className="mt-4 max-w-2xl text-lg leading-relaxed text-zinc-600 dark:text-zinc-400">{projectTagline}</p>
<p className="mt-4 max-w-2xl text-zinc-600 dark:text-zinc-400">
Enterprise Identity Provider: единый вход, OAuth 2.0, корпоративный LDAP, семейные группы, чат и админ-панель.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<Button asChild size="lg">
<Link href="/docs/getting-started">
Начать
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link href="/docs/deployment">Развёртывание</Link>
</Button>
</div>
</section>
<section className="mt-10 grid gap-4 sm:grid-cols-2">
{highlights.map((item) => (
<Link key={item.href} href={item.href}>
<Card className="h-full transition-colors hover:border-zinc-300 dark:hover:border-zinc-700">
<CardHeader>
<div className="mb-2 flex h-10 w-10 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800">
<item.icon className="h-5 w-5" />
</div>
<CardTitle>{item.title}</CardTitle>
<CardDescription>{item.description}</CardDescription>
</CardHeader>
<CardContent>
<span className="text-sm font-medium text-zinc-500">Читать </span>
</CardContent>
</Card>
</Link>
))}
</section>
<section className="mt-12">
<h2 className="text-xl font-semibold">Все разделы</h2>
<div className="mt-4 grid gap-6 sm:grid-cols-2">
{groups.map(([group, items]) => (
<div key={group}>
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-zinc-500">{group}</p>
<ul className="space-y-2">
{items.map((item) => (
<li key={item.slug}>
<Link href={`/docs/${item.slug}`} className="text-sm text-zinc-600 hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-zinc-50">
{item.title}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</section>
</main>
);
}

View File

@@ -0,0 +1,109 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { BookOpen, ExternalLink, Moon, Sun } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { usePublicSettings } from '@/providers/public-settings-provider';
import { useTheme } from '@/providers/theme-provider';
import { getApiBaseUrl } from '@/lib/api';
import { cn } from '@/lib/utils';
export function DocsHeader() {
const { projectName } = usePublicSettings();
const { resolvedTheme, setTheme } = useTheme();
const apiUrl = getApiBaseUrl();
return (
<header className="sticky top-0 z-50 w-full border-b border-zinc-200 bg-white/80 backdrop-blur dark:border-zinc-800 dark:bg-zinc-950/80">
<div className="mx-auto flex h-14 max-w-screen-2xl items-center justify-between gap-4 px-4 lg:px-8">
<Link href="/docs" className="flex items-center gap-2 font-semibold tracking-tight">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-zinc-900 text-white dark:bg-zinc-50 dark:text-zinc-900">
<BookOpen className="h-4 w-4" />
</div>
<span className="hidden sm:inline">{projectName}</span>
<span className="text-zinc-400 font-normal hidden sm:inline">Docs</span>
</Link>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" asChild className="hidden md:inline-flex">
<a href={`${apiUrl}/api`} target="_blank" rel="noreferrer">
Swagger
<ExternalLink className="h-3.5 w-3.5" />
</a>
</Button>
<Button
variant="outline"
size="icon"
aria-label="Переключить тему"
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
>
{resolvedTheme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
</div>
</div>
</header>
);
}
export function DocsSidebar({ groups }: { groups: Array<[string, Array<{ slug: string; title: string }>]> }) {
const pathname = usePathname();
return (
<nav className="space-y-6">
<Link
href="/docs"
className={cn(
'block rounded-lg px-3 py-2 text-sm font-medium transition-colors',
pathname === '/docs'
? 'bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
: 'text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-50'
)}
>
Главная
</Link>
{groups.map(([group, items]) => (
<div key={group}>
<p className="mb-2 px-2 text-xs font-semibold uppercase tracking-wider text-zinc-500">{group}</p>
<div className="space-y-1">
{items.map((item) => {
const href = `/docs/${item.slug}`;
const active = pathname === href;
return (
<Link
key={item.slug}
href={href}
className={cn(
'block rounded-lg px-3 py-2 text-sm transition-colors',
active
? 'bg-zinc-100 font-medium text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
: 'text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-50'
)}
>
{item.title}
</Link>
);
})}
</div>
</div>
))}
</nav>
);
}
export function DocsToc({ sections }: { sections: Array<{ id: string; title: string }> }) {
return (
<nav className="space-y-2 text-sm">
<p className="mb-3 font-semibold">На этой странице</p>
{sections.map((section) => (
<a
key={section.id}
href={`#${section.id}`}
className="block text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-zinc-50"
>
{section.title}
</a>
))}
</nav>
);
}

View File

@@ -0,0 +1,8 @@
'use client';
import { oauthExamples } from '@/lib/oauth-examples';
import { CodeExampleTabs } from '@/components/code-example-tabs';
export function OAuthCodeTabs() {
return <CodeExampleTabs examples={oauthExamples} />;
}

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
export function Badge({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'inline-flex items-center rounded-md border border-zinc-200 px-2.5 py-0.5 text-xs font-semibold transition-colors dark:border-zinc-800',
className
)}
{...props}
/>
);
}

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default:
'border border-transparent bg-zinc-900 text-zinc-50 shadow hover:bg-zinc-800 dark:border-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:shadow-none dark:hover:bg-zinc-200',
secondary:
'bg-zinc-100 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700',
outline:
'border border-zinc-300 bg-transparent text-zinc-900 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-100 dark:hover:bg-zinc-800',
ghost: 'text-zinc-900 hover:bg-zinc-100 dark:text-zinc-100 dark:hover:bg-zinc-800',
link: 'text-zinc-900 underline-offset-4 hover:underline dark:text-zinc-100'
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-lg px-3 text-xs',
lg: 'h-11 rounded-xl px-8 text-base',
icon: 'h-9 w-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
const Comp = asChild ? Slot : 'button';
return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
}

View File

@@ -0,0 +1,22 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('rounded-xl border border-zinc-200 bg-white text-zinc-950 shadow-sm dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50', className)} {...props} />;
}
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />;
}
export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
return <h3 className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />;
}
export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
return <p className={cn('text-sm text-zinc-500 dark:text-zinc-400', className)} {...props} />;
}
export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('p-6 pt-0', className)} {...props} />;
}

View File

@@ -0,0 +1,31 @@
'use client';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '@/lib/utils';
export function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root className={cn('relative overflow-hidden', className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({ className, orientation = 'vertical', ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-zinc-200 dark:bg-zinc-800" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}

View File

@@ -0,0 +1,31 @@
'use client';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '@/lib/utils';
export const Tabs = TabsPrimitive.Root;
export function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
className={cn('inline-flex h-10 items-center justify-center rounded-lg bg-zinc-100 p-1 text-zinc-500 dark:bg-zinc-900', className)}
{...props}
/>
);
}
export function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow dark:ring-offset-zinc-950 dark:data-[state=active]:bg-zinc-800 dark:data-[state=active]:text-zinc-50',
className
)}
{...props}
/>
);
}
export function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
return <TabsPrimitive.Content className={cn('mt-4 focus-visible:outline-none', className)} {...props} />;
}