create-stitchkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/cli.js +230 -0
- package/package.json +50 -0
- package/template/README.md +69 -0
- package/template/_env +11 -0
- package/template/_env.example +12 -0
- package/template/_gitignore +10 -0
- package/template/biome.json +43 -0
- package/template/bun.lock +1372 -0
- package/template/e2e/starter.spec.ts +237 -0
- package/template/ecosystem.config.cjs +28 -0
- package/template/ecosystem.dev.config.cjs +29 -0
- package/template/package.json +55 -0
- package/template/packages/backend/package.json +31 -0
- package/template/packages/backend/src/cli.ts +12 -0
- package/template/packages/backend/src/domain/errors.ts +9 -0
- package/template/packages/backend/src/domain/repository/github-cache.test.ts +72 -0
- package/template/packages/backend/src/domain/repository/github-cache.ts +205 -0
- package/template/packages/backend/src/index.ts +56 -0
- package/template/packages/backend/src/lib/db.ts +7 -0
- package/template/packages/backend/src/surface.ts +13 -0
- package/template/packages/backend/src/tools.ts +21 -0
- package/template/packages/backend/src/transport/errors.ts +28 -0
- package/template/packages/backend/src/transport/repository-service.ts +18 -0
- package/template/packages/backend/tsconfig.json +8 -0
- package/template/packages/config/package.json +22 -0
- package/template/packages/config/src/server.ts +24 -0
- package/template/packages/config/tsconfig.json +5 -0
- package/template/packages/db/migrations/20260808000000_init/migration.sql +16 -0
- package/template/packages/db/migrations/20260808170000_repository_visibility/migration.sql +4 -0
- package/template/packages/db/migrations/migration_lock.toml +1 -0
- package/template/packages/db/package.json +29 -0
- package/template/packages/db/prisma.config.ts +8 -0
- package/template/packages/db/schema.prisma +31 -0
- package/template/packages/db/tsconfig.json +5 -0
- package/template/packages/frontend/messages/en.json +15 -0
- package/template/packages/frontend/messages/ru.json +15 -0
- package/template/packages/frontend/next.config.ts +18 -0
- package/template/packages/frontend/package.json +62 -0
- package/template/packages/frontend/postcss.config.mjs +1 -0
- package/template/packages/frontend/public/ASSETS.md +6 -0
- package/template/packages/frontend/public/favicon/mascot-stitch.png +0 -0
- package/template/packages/frontend/public/icons/biome.svg +1 -0
- package/template/packages/frontend/public/icons/bun.svg +19 -0
- package/template/packages/frontend/public/icons/next.svg +1 -0
- package/template/packages/frontend/public/icons/postgresql.svg +1 -0
- package/template/packages/frontend/public/icons/prisma.svg +6 -0
- package/template/packages/frontend/public/icons/react-query.svg +9 -0
- package/template/packages/frontend/public/icons/react.svg +1 -0
- package/template/packages/frontend/public/icons/tailwind.svg +12 -0
- package/template/packages/frontend/public/icons/tsconfig.svg +1 -0
- package/template/packages/frontend/public/icons/typescript.svg +7 -0
- package/template/packages/frontend/public/icons/zod.svg +1 -0
- package/template/packages/frontend/public/mascot-stitch.png +0 -0
- package/template/packages/frontend/public/theme-dark.svg +8 -0
- package/template/packages/frontend/public/theme-light.svg +8 -0
- package/template/packages/frontend/src/app/[locale]/error.tsx +19 -0
- package/template/packages/frontend/src/app/[locale]/layout.tsx +59 -0
- package/template/packages/frontend/src/app/[locale]/loading.tsx +14 -0
- package/template/packages/frontend/src/app/[locale]/page.tsx +44 -0
- package/template/packages/frontend/src/app/[locale]/starter-page.tsx +151 -0
- package/template/packages/frontend/src/app/[locale]/ui/[story]/page.tsx +40 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/account-showcase.tsx +112 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/admin-showcase.tsx +101 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/catalogue-navigation.tsx +56 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/catalogue-shell.tsx +51 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/code-foundation.tsx +35 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/components-story.tsx +54 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/data-story.tsx +126 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/image-preloader.tsx +8 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/index.ts +3 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-sections.tsx +269 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +159 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/motion-story.tsx +78 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/overlays-story.tsx +134 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/payment-showcase.tsx +82 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/presentation-story.tsx +68 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/primitives-story.tsx +267 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/story-page.tsx +14 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/theme-story-client.tsx +208 -0
- package/template/packages/frontend/src/app/[locale]/ui/_catalogue/theme-story.tsx +13 -0
- package/template/packages/frontend/src/app/[locale]/ui/layout.tsx +6 -0
- package/template/packages/frontend/src/app/[locale]/ui/page.tsx +10 -0
- package/template/packages/frontend/src/app/api/og/[locale]/[page]/route.tsx +53 -0
- package/template/packages/frontend/src/app/globals.css +210 -0
- package/template/packages/frontend/src/app/robots.ts +9 -0
- package/template/packages/frontend/src/app/sitemap.ts +22 -0
- package/template/packages/frontend/src/components/brand-mark.tsx +22 -0
- package/template/packages/frontend/src/components/repository-summary.tsx +107 -0
- package/template/packages/frontend/src/components/system-controls.tsx +92 -0
- package/template/packages/frontend/src/components/ui/adaptive-modal.tsx +382 -0
- package/template/packages/frontend/src/components/ui/animated-counter.tsx +80 -0
- package/template/packages/frontend/src/components/ui/avatar.tsx +46 -0
- package/template/packages/frontend/src/components/ui/badge.tsx +94 -0
- package/template/packages/frontend/src/components/ui/button-variants.ts +24 -0
- package/template/packages/frontend/src/components/ui/button.tsx +81 -0
- package/template/packages/frontend/src/components/ui/card-deck.tsx +73 -0
- package/template/packages/frontend/src/components/ui/card.tsx +81 -0
- package/template/packages/frontend/src/components/ui/charts/area-chart.tsx +167 -0
- package/template/packages/frontend/src/components/ui/charts/index.ts +7 -0
- package/template/packages/frontend/src/components/ui/checkbox.tsx +133 -0
- package/template/packages/frontend/src/components/ui/confirmation-modal.tsx +129 -0
- package/template/packages/frontend/src/components/ui/copyable-text.tsx +85 -0
- package/template/packages/frontend/src/components/ui/drawer.tsx +136 -0
- package/template/packages/frontend/src/components/ui/dropdown-menu.tsx +185 -0
- package/template/packages/frontend/src/components/ui/field.tsx +28 -0
- package/template/packages/frontend/src/components/ui/hand-drawn-icons.tsx +41 -0
- package/template/packages/frontend/src/components/ui/index.ts +36 -0
- package/template/packages/frontend/src/components/ui/input.tsx +24 -0
- package/template/packages/frontend/src/components/ui/label.tsx +18 -0
- package/template/packages/frontend/src/components/ui/otp-input.tsx +140 -0
- package/template/packages/frontend/src/components/ui/select.tsx +240 -0
- package/template/packages/frontend/src/components/ui/sidebar.tsx +343 -0
- package/template/packages/frontend/src/components/ui/skeleton.tsx +8 -0
- package/template/packages/frontend/src/components/ui/slider.tsx +31 -0
- package/template/packages/frontend/src/components/ui/slot-drum.tsx +50 -0
- package/template/packages/frontend/src/components/ui/spinner.tsx +30 -0
- package/template/packages/frontend/src/components/ui/switch.tsx +31 -0
- package/template/packages/frontend/src/components/ui/table.tsx +106 -0
- package/template/packages/frontend/src/components/ui/tabs.tsx +196 -0
- package/template/packages/frontend/src/components/ui/textarea.tsx +21 -0
- package/template/packages/frontend/src/components/ui/toaster.tsx +72 -0
- package/template/packages/frontend/src/components/ui/tooltip.tsx +31 -0
- package/template/packages/frontend/src/env.ts +21 -0
- package/template/packages/frontend/src/features/data-table/components/column-helpers.tsx +136 -0
- package/template/packages/frontend/src/features/data-table/components/data-table.tsx +303 -0
- package/template/packages/frontend/src/features/data-table/components/inline-stat.tsx +113 -0
- package/template/packages/frontend/src/features/data-table/components/sortable-header.tsx +33 -0
- package/template/packages/frontend/src/features/data-table/components/table-error.tsx +24 -0
- package/template/packages/frontend/src/features/data-table/components/table-filters.tsx +82 -0
- package/template/packages/frontend/src/features/data-table/hooks/useTableState.ts +111 -0
- package/template/packages/frontend/src/features/data-table/index.ts +26 -0
- package/template/packages/frontend/src/features/data-table/table-features.ts +26 -0
- package/template/packages/frontend/src/hooks/index.ts +3 -0
- package/template/packages/frontend/src/hooks/useInfiniteScroll.ts +67 -0
- package/template/packages/frontend/src/hooks/useIsTouchDevice.ts +25 -0
- package/template/packages/frontend/src/hooks/useMediaQuery.ts +30 -0
- package/template/packages/frontend/src/i18n/locales.ts +6 -0
- package/template/packages/frontend/src/i18n/navigation.ts +4 -0
- package/template/packages/frontend/src/i18n/request.ts +9 -0
- package/template/packages/frontend/src/i18n/routing.ts +8 -0
- package/template/packages/frontend/src/lib/api/client.ts +20 -0
- package/template/packages/frontend/src/lib/api/queries.ts +11 -0
- package/template/packages/frontend/src/lib/query-client.ts +20 -0
- package/template/packages/frontend/src/lib/realtime/repository.ts +21 -0
- package/template/packages/frontend/src/lib/seo/metadata.ts +50 -0
- package/template/packages/frontend/src/lib/seo/pages.test.ts +25 -0
- package/template/packages/frontend/src/lib/seo/pages.ts +107 -0
- package/template/packages/frontend/src/lib/utils/cn.ts +6 -0
- package/template/packages/frontend/src/lib/utils/format.ts +25 -0
- package/template/packages/frontend/src/lib/utils/index.ts +2 -0
- package/template/packages/frontend/src/providers/index.tsx +18 -0
- package/template/packages/frontend/src/providers/react-query.tsx +9 -0
- package/template/packages/frontend/src/providers/realtime.tsx +16 -0
- package/template/packages/frontend/src/proxy.ts +6 -0
- package/template/packages/frontend/src/theme/config.ts +20 -0
- package/template/packages/frontend/src/theme/transition.ts +54 -0
- package/template/packages/frontend/tsconfig.json +13 -0
- package/template/packages/shared/package.json +23 -0
- package/template/packages/shared/src/contracts/repository.ts +28 -0
- package/template/packages/shared/src/events/repository.ts +9 -0
- package/template/packages/shared/src/index.ts +3 -0
- package/template/packages/shared/src/schemas/repository.test.ts +36 -0
- package/template/packages/shared/src/schemas/repository.ts +28 -0
- package/template/packages/shared/tsconfig.json +5 -0
- package/template/playwright.config.ts +15 -0
- package/template/scripts/check-authored.ts +81 -0
- package/template/scripts/dev.ts +23 -0
- package/template/scripts/local-env.ts +12 -0
- package/template/scripts/runtime-smoke.ts +67 -0
- package/template/scripts/tooling-env.ts +15 -0
- package/template/tsconfig.json +13 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ImageResponse } from 'next/og';
|
|
2
|
+
import { LocaleSchema } from '@/i18n/locales';
|
|
3
|
+
import { getSeoPage, publicPageIds, SeoPageIdSchema, SITE_NAME } from '@/lib/seo/pages';
|
|
4
|
+
|
|
5
|
+
export const runtime = 'nodejs';
|
|
6
|
+
export const size = { width: 1200, height: 630 };
|
|
7
|
+
|
|
8
|
+
export function generateStaticParams() {
|
|
9
|
+
return LocaleSchema.options.flatMap((locale) =>
|
|
10
|
+
publicPageIds.map((page) => ({ locale, page })),
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function GET(
|
|
15
|
+
_request: Request,
|
|
16
|
+
{ params }: { params: Promise<{ locale: string; page: string }> },
|
|
17
|
+
) {
|
|
18
|
+
const { locale, page } = await params;
|
|
19
|
+
const parsedLocale = LocaleSchema.safeParse(locale);
|
|
20
|
+
const parsedPage = SeoPageIdSchema.safeParse(page);
|
|
21
|
+
if (!parsedLocale.success || !parsedPage.success) {
|
|
22
|
+
return new Response('Unknown SEO page', { status: 404 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const metadata = getSeoPage(parsedPage.data, parsedLocale.data);
|
|
26
|
+
return new ImageResponse(
|
|
27
|
+
<div
|
|
28
|
+
style={{
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
background: '#111214',
|
|
31
|
+
color: '#f7f7f8',
|
|
32
|
+
display: 'flex',
|
|
33
|
+
height: '100%',
|
|
34
|
+
justifyContent: 'center',
|
|
35
|
+
width: '100%',
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 26, maxWidth: 940 }}>
|
|
39
|
+
<div style={{ color: '#a3a7b2', fontSize: 28 }}>{metadata.eyebrow}</div>
|
|
40
|
+
<div style={{ fontSize: 74, fontWeight: 500, letterSpacing: -3, lineHeight: 1.05 }}>
|
|
41
|
+
{metadata.title}
|
|
42
|
+
</div>
|
|
43
|
+
<div style={{ color: '#c7cad1', fontSize: 31, lineHeight: 1.35 }}>
|
|
44
|
+
{metadata.description}
|
|
45
|
+
</div>
|
|
46
|
+
<div style={{ color: '#8d929e', display: 'flex', fontSize: 24, marginTop: 10 }}>
|
|
47
|
+
{SITE_NAME}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>,
|
|
51
|
+
size,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
5
|
+
|
|
6
|
+
.technology-icon-invert-dark {
|
|
7
|
+
filter: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.dark .technology-icon-invert-dark {
|
|
11
|
+
filter: invert(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@theme inline {
|
|
15
|
+
--color-background: var(--background);
|
|
16
|
+
--color-foreground: var(--foreground);
|
|
17
|
+
--color-card: var(--card);
|
|
18
|
+
--color-card-foreground: var(--card-foreground);
|
|
19
|
+
--color-primary: var(--primary);
|
|
20
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
21
|
+
--color-muted: var(--muted);
|
|
22
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
23
|
+
--color-secondary: var(--secondary);
|
|
24
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
25
|
+
--color-border: var(--border);
|
|
26
|
+
--color-input: var(--input);
|
|
27
|
+
--color-ring: var(--ring);
|
|
28
|
+
--color-destructive: var(--destructive);
|
|
29
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
30
|
+
--color-success: var(--success);
|
|
31
|
+
--color-success-foreground: var(--success-foreground);
|
|
32
|
+
--color-warning: var(--warning);
|
|
33
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
34
|
+
--color-info: var(--info);
|
|
35
|
+
--color-info-foreground: var(--info-foreground);
|
|
36
|
+
--color-overlay: var(--overlay);
|
|
37
|
+
--font-display: var(--font-montserrat);
|
|
38
|
+
--font-sans: var(--font-montserrat);
|
|
39
|
+
--font-mono:
|
|
40
|
+
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:root,
|
|
44
|
+
.light {
|
|
45
|
+
--background: oklch(1 0 0);
|
|
46
|
+
--foreground: oklch(0.13 0 0);
|
|
47
|
+
--card: oklch(0.99 0 0);
|
|
48
|
+
--card-foreground: oklch(0.13 0 0);
|
|
49
|
+
--primary: oklch(0.13 0 0);
|
|
50
|
+
--primary-foreground: oklch(1 0 0);
|
|
51
|
+
--muted: oklch(0.97 0 0);
|
|
52
|
+
--muted-foreground: oklch(0.45 0 0);
|
|
53
|
+
--secondary: oklch(0.93 0 0);
|
|
54
|
+
--secondary-foreground: oklch(0.18 0 0);
|
|
55
|
+
--border: oklch(0.91 0 0);
|
|
56
|
+
--input: oklch(0.91 0 0);
|
|
57
|
+
--ring: oklch(0.13 0 0);
|
|
58
|
+
--destructive: oklch(0.55 0.2 25);
|
|
59
|
+
--destructive-foreground: oklch(1 0 0);
|
|
60
|
+
--success: oklch(0.6 0.16 145);
|
|
61
|
+
--success-foreground: oklch(1 0 0);
|
|
62
|
+
--warning: oklch(0.75 0.14 70);
|
|
63
|
+
--warning-foreground: oklch(0.2 0.05 70);
|
|
64
|
+
--info: oklch(0.55 0.15 230);
|
|
65
|
+
--info-foreground: oklch(1 0 0);
|
|
66
|
+
--overlay: oklch(0 0 0 / 0.5);
|
|
67
|
+
/* Chart colors */
|
|
68
|
+
--chart-1: oklch(0.25 0 0);
|
|
69
|
+
--chart-2: oklch(0.65 0 0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.dark {
|
|
73
|
+
--background: oklch(0.16 0.005 260);
|
|
74
|
+
--foreground: oklch(0.9 0 0);
|
|
75
|
+
--card: oklch(0.19 0.005 260);
|
|
76
|
+
--card-foreground: oklch(0.9 0 0);
|
|
77
|
+
--primary: oklch(0.9 0 0);
|
|
78
|
+
--primary-foreground: oklch(0.16 0 0);
|
|
79
|
+
--muted: oklch(0.22 0.005 260);
|
|
80
|
+
--muted-foreground: oklch(0.6 0 0);
|
|
81
|
+
--secondary: oklch(0.25 0.005 260);
|
|
82
|
+
--secondary-foreground: oklch(0.9 0 0);
|
|
83
|
+
--border: oklch(0.26 0.005 260);
|
|
84
|
+
--input: oklch(0.22 0.005 260);
|
|
85
|
+
--ring: oklch(0.9 0 0);
|
|
86
|
+
--destructive: oklch(0.55 0.18 25);
|
|
87
|
+
--destructive-foreground: oklch(1 0 0);
|
|
88
|
+
--success: oklch(0.55 0.14 145);
|
|
89
|
+
--success-foreground: oklch(1 0 0);
|
|
90
|
+
--warning: oklch(0.7 0.13 70);
|
|
91
|
+
--warning-foreground: oklch(0.2 0.05 70);
|
|
92
|
+
--info: oklch(0.55 0.15 230);
|
|
93
|
+
--info-foreground: oklch(1 0 0);
|
|
94
|
+
--overlay: oklch(0 0 0 / 0.68);
|
|
95
|
+
/* Chart colors */
|
|
96
|
+
--chart-1: oklch(0.85 0 0);
|
|
97
|
+
--chart-2: oklch(0.5 0 0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@layer base {
|
|
101
|
+
* {
|
|
102
|
+
@apply border-border;
|
|
103
|
+
}
|
|
104
|
+
body {
|
|
105
|
+
@apply bg-background text-foreground;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
button,
|
|
109
|
+
[role="button"],
|
|
110
|
+
a[href],
|
|
111
|
+
summary,
|
|
112
|
+
label:has(input) {
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
button:disabled,
|
|
117
|
+
[role="button"][aria-disabled="true"] {
|
|
118
|
+
cursor: not-allowed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Selection — инвертированные цвета под тему */
|
|
122
|
+
::selection {
|
|
123
|
+
@apply bg-foreground text-background;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
::view-transition-old(root),
|
|
128
|
+
::view-transition-new(root) {
|
|
129
|
+
animation: none;
|
|
130
|
+
mix-blend-mode: normal;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
::view-transition-old(root) {
|
|
134
|
+
z-index: 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
::view-transition-new(root) {
|
|
138
|
+
z-index: 2;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
html[data-theme-transition="crossfade"]::view-transition-new(root) {
|
|
142
|
+
animation: theme-crossfade var(--theme-transition-duration) var(--theme-transition-easing);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
html[data-theme-transition="radial"]::view-transition-new(root) {
|
|
146
|
+
animation: theme-radial-reveal var(--theme-transition-duration) var(--theme-transition-easing);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@keyframes theme-crossfade {
|
|
150
|
+
from {
|
|
151
|
+
opacity: 0;
|
|
152
|
+
}
|
|
153
|
+
to {
|
|
154
|
+
opacity: 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@keyframes theme-radial-reveal {
|
|
159
|
+
from {
|
|
160
|
+
clip-path: circle(0 at var(--theme-transition-x) var(--theme-transition-y));
|
|
161
|
+
}
|
|
162
|
+
to {
|
|
163
|
+
clip-path: circle(150vmax at var(--theme-transition-x) var(--theme-transition-y));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.shiki span {
|
|
168
|
+
color: var(--shiki-light);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.dark .shiki span {
|
|
172
|
+
color: var(--shiki-dark);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Typing animation for code */
|
|
176
|
+
.typing-code .type-char {
|
|
177
|
+
opacity: 0;
|
|
178
|
+
animation: typeReveal 0.05s forwards;
|
|
179
|
+
animation-delay: var(--delay);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@keyframes typeReveal {
|
|
183
|
+
to {
|
|
184
|
+
opacity: 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@media (prefers-reduced-motion: reduce) {
|
|
189
|
+
*,
|
|
190
|
+
*::before,
|
|
191
|
+
*::after {
|
|
192
|
+
scroll-behavior: auto;
|
|
193
|
+
animation-duration: 0.01ms;
|
|
194
|
+
animation-iteration-count: 1;
|
|
195
|
+
transition-duration: 0.01ms;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
::view-transition-old(root),
|
|
199
|
+
::view-transition-new(root) {
|
|
200
|
+
animation: none;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Snapshot capture must override component transitions while the theme class changes. */
|
|
205
|
+
.theme-transitioning *,
|
|
206
|
+
.theme-transitioning *::before,
|
|
207
|
+
.theme-transitioning *::after {
|
|
208
|
+
/* biome-ignore lint/complexity/noImportantStyles: native snapshot integrity requires precedence */
|
|
209
|
+
transition: none !important;
|
|
210
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MetadataRoute } from 'next';
|
|
2
|
+
import { absoluteSiteUrl } from '@/lib/seo/metadata';
|
|
3
|
+
|
|
4
|
+
export default function robots(): MetadataRoute.Robots {
|
|
5
|
+
return {
|
|
6
|
+
rules: { userAgent: '*', allow: '/' },
|
|
7
|
+
sitemap: absoluteSiteUrl('/sitemap.xml'),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MetadataRoute } from 'next';
|
|
2
|
+
import { locales } from '@/i18n/locales';
|
|
3
|
+
import { absoluteSiteUrl } from '@/lib/seo/metadata';
|
|
4
|
+
import { localizedPagePath, publicPageIds } from '@/lib/seo/pages';
|
|
5
|
+
|
|
6
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
7
|
+
return publicPageIds.flatMap((pageId) =>
|
|
8
|
+
locales.map((locale) => ({
|
|
9
|
+
url: absoluteSiteUrl(localizedPagePath(pageId, locale)),
|
|
10
|
+
changeFrequency: pageId === 'home' ? 'weekly' : 'monthly',
|
|
11
|
+
priority: pageId === 'home' ? 1 : 0.7,
|
|
12
|
+
alternates: {
|
|
13
|
+
languages: Object.fromEntries(
|
|
14
|
+
locales.map((availableLocale) => [
|
|
15
|
+
availableLocale,
|
|
16
|
+
absoluteSiteUrl(localizedPagePath(pageId, availableLocale)),
|
|
17
|
+
]),
|
|
18
|
+
),
|
|
19
|
+
},
|
|
20
|
+
})),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Image from 'next/image';
|
|
2
|
+
import { cn } from '@/lib/utils/cn';
|
|
3
|
+
|
|
4
|
+
export function BrandMark({
|
|
5
|
+
className,
|
|
6
|
+
priority = false,
|
|
7
|
+
}: {
|
|
8
|
+
className?: string;
|
|
9
|
+
priority?: boolean;
|
|
10
|
+
}) {
|
|
11
|
+
return (
|
|
12
|
+
<Image
|
|
13
|
+
alt=''
|
|
14
|
+
aria-hidden='true'
|
|
15
|
+
className={cn('size-8 shrink-0 object-contain', className)}
|
|
16
|
+
height={32}
|
|
17
|
+
priority={priority}
|
|
18
|
+
src='/mascot-stitch.png'
|
|
19
|
+
width={32}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
IconArrowUpRight,
|
|
5
|
+
IconBrandGithub,
|
|
6
|
+
IconGitCommit,
|
|
7
|
+
IconRefresh,
|
|
8
|
+
IconStar,
|
|
9
|
+
} from '@tabler/icons-react';
|
|
10
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
11
|
+
import { useLocale } from 'next-intl';
|
|
12
|
+
import { Button, Skeleton } from '@/components/ui';
|
|
13
|
+
import { useRefreshRepository, useRepository } from '@/lib/api/queries';
|
|
14
|
+
|
|
15
|
+
function RepositorySkeleton() {
|
|
16
|
+
return (
|
|
17
|
+
<div className='mx-auto flex w-full max-w-4xl items-center gap-4 rounded-xl border border-border bg-card px-4 py-4'>
|
|
18
|
+
<Skeleton className='size-10 shrink-0 rounded-xl' />
|
|
19
|
+
<div className='flex-1 space-y-2'>
|
|
20
|
+
<Skeleton className='h-4 w-44' />
|
|
21
|
+
<Skeleton className='h-3 w-64 max-w-full' />
|
|
22
|
+
</div>
|
|
23
|
+
<Skeleton className='hidden h-8 w-52 sm:block' />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function RepositorySummary() {
|
|
29
|
+
const locale = useLocale();
|
|
30
|
+
const queryClient = useQueryClient();
|
|
31
|
+
const repository = useRepository();
|
|
32
|
+
const refresh = useRefreshRepository({
|
|
33
|
+
onSuccess: async () => {
|
|
34
|
+
await queryClient.invalidateQueries({ queryKey: useRepository.getKey() });
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (repository.isLoading) return <RepositorySkeleton />;
|
|
39
|
+
if (repository.isError || !repository.data) {
|
|
40
|
+
return (
|
|
41
|
+
<div className='mx-auto w-full max-w-4xl rounded-xl border border-border bg-card px-4 py-4 text-sm text-muted-foreground'>
|
|
42
|
+
Repository data is unavailable. Check the API connection and retry.
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const snapshot = repository.data;
|
|
48
|
+
const commitDate = snapshot.latestCommit
|
|
49
|
+
? new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(
|
|
50
|
+
new Date(snapshot.latestCommit.committedAt),
|
|
51
|
+
)
|
|
52
|
+
: '—';
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className='mx-auto flex w-full max-w-4xl flex-col gap-4 rounded-xl border border-border bg-card/80 px-4 py-4 text-left backdrop-blur sm:flex-row sm:items-center sm:px-5'>
|
|
56
|
+
<a
|
|
57
|
+
className='group flex min-w-0 flex-1 items-center gap-3'
|
|
58
|
+
href={snapshot.htmlUrl}
|
|
59
|
+
rel='noreferrer'
|
|
60
|
+
target='_blank'
|
|
61
|
+
>
|
|
62
|
+
<span className='flex size-10 shrink-0 items-center justify-center rounded-xl border border-border bg-background'>
|
|
63
|
+
<IconBrandGithub size={21} />
|
|
64
|
+
</span>
|
|
65
|
+
<span className='min-w-0'>
|
|
66
|
+
<span className='flex min-w-0 items-center gap-2 font-medium'>
|
|
67
|
+
<span className='truncate'>{snapshot.fullName}</span>
|
|
68
|
+
<IconArrowUpRight
|
|
69
|
+
className='shrink-0 opacity-50 group-hover:opacity-100'
|
|
70
|
+
size={16}
|
|
71
|
+
/>
|
|
72
|
+
</span>
|
|
73
|
+
<span className='mt-1 block truncate text-xs text-muted-foreground'>
|
|
74
|
+
Live backend query · {snapshot.visibility} · {snapshot.language}
|
|
75
|
+
</span>
|
|
76
|
+
</span>
|
|
77
|
+
</a>
|
|
78
|
+
|
|
79
|
+
<div className='flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground sm:justify-end'>
|
|
80
|
+
<span className='flex items-center gap-1.5' title='Commits'>
|
|
81
|
+
<IconGitCommit size={16} />
|
|
82
|
+
<span className='font-medium text-foreground'>
|
|
83
|
+
{snapshot.commitCount.toLocaleString(locale)}
|
|
84
|
+
</span>
|
|
85
|
+
</span>
|
|
86
|
+
<span className='flex items-center gap-1.5' title='Stars'>
|
|
87
|
+
<IconStar size={16} />
|
|
88
|
+
<span className='font-medium text-foreground'>
|
|
89
|
+
{snapshot.stars.toLocaleString(locale)}
|
|
90
|
+
</span>
|
|
91
|
+
</span>
|
|
92
|
+
<span className='whitespace-nowrap text-xs'>{commitDate}</span>
|
|
93
|
+
<Button
|
|
94
|
+
aria-label='Refresh repository data'
|
|
95
|
+
aria-busy={refresh.isPending || undefined}
|
|
96
|
+
className='shrink-0 disabled:opacity-100'
|
|
97
|
+
disabled={refresh.isPending}
|
|
98
|
+
onClick={() => refresh.mutate(undefined)}
|
|
99
|
+
size='icon'
|
|
100
|
+
variant='outline'
|
|
101
|
+
>
|
|
102
|
+
<IconRefresh className={refresh.isPending ? 'animate-spin' : undefined} size={18} />
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
IconCheck,
|
|
5
|
+
IconDeviceDesktop,
|
|
6
|
+
IconLanguage,
|
|
7
|
+
IconMoon,
|
|
8
|
+
IconSun,
|
|
9
|
+
} from '@tabler/icons-react';
|
|
10
|
+
import { useHydrated } from '@wrksz/themes/client/use-hydrated';
|
|
11
|
+
import { useTheme } from '@wrksz/themes/client/use-theme';
|
|
12
|
+
import {
|
|
13
|
+
Button,
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuItem,
|
|
17
|
+
DropdownMenuTrigger,
|
|
18
|
+
} from '@/components/ui';
|
|
19
|
+
import { Link, usePathname } from '@/i18n/navigation';
|
|
20
|
+
import type { AppTheme } from '@/theme/config';
|
|
21
|
+
import { defaultThemeTransition, runThemeTransition } from '@/theme/transition';
|
|
22
|
+
|
|
23
|
+
const themeOptions = [
|
|
24
|
+
{ value: 'light', label: 'Light', icon: IconSun },
|
|
25
|
+
{ value: 'dark', label: 'Dark', icon: IconMoon },
|
|
26
|
+
{ value: 'system', label: 'System', icon: IconDeviceDesktop },
|
|
27
|
+
] satisfies ReadonlyArray<{
|
|
28
|
+
value: AppTheme | 'system';
|
|
29
|
+
label: string;
|
|
30
|
+
icon: typeof IconSun;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
export function ThemeToggle() {
|
|
34
|
+
const hydrated = useHydrated();
|
|
35
|
+
const { theme, resolvedTheme, setTheme } = useTheme<AppTheme>();
|
|
36
|
+
const selectedTheme = hydrated ? theme : undefined;
|
|
37
|
+
const ActiveIcon = resolvedTheme === 'dark' ? IconMoon : IconSun;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<DropdownMenu>
|
|
41
|
+
<DropdownMenuTrigger asChild>
|
|
42
|
+
<Button
|
|
43
|
+
size='icon'
|
|
44
|
+
variant='outline'
|
|
45
|
+
aria-label={`Theme: ${selectedTheme ?? 'loading'}`}
|
|
46
|
+
>
|
|
47
|
+
{hydrated ? <ActiveIcon /> : <IconDeviceDesktop />}
|
|
48
|
+
</Button>
|
|
49
|
+
</DropdownMenuTrigger>
|
|
50
|
+
<DropdownMenuContent align='end'>
|
|
51
|
+
{themeOptions.map((option) => (
|
|
52
|
+
<DropdownMenuItem
|
|
53
|
+
key={option.value}
|
|
54
|
+
onSelect={() =>
|
|
55
|
+
runThemeTransition(() => setTheme(option.value), defaultThemeTransition)
|
|
56
|
+
}
|
|
57
|
+
>
|
|
58
|
+
<option.icon />
|
|
59
|
+
<span className='flex-1'>{option.label}</span>
|
|
60
|
+
{selectedTheme === option.value ? <IconCheck aria-hidden /> : null}
|
|
61
|
+
</DropdownMenuItem>
|
|
62
|
+
))}
|
|
63
|
+
</DropdownMenuContent>
|
|
64
|
+
</DropdownMenu>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function LanguageSwitcher() {
|
|
69
|
+
const pathname = usePathname();
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<DropdownMenu>
|
|
73
|
+
<DropdownMenuTrigger asChild>
|
|
74
|
+
<Button size='icon' variant='outline' aria-label='Change language'>
|
|
75
|
+
<IconLanguage />
|
|
76
|
+
</Button>
|
|
77
|
+
</DropdownMenuTrigger>
|
|
78
|
+
<DropdownMenuContent align='end'>
|
|
79
|
+
<DropdownMenuItem asChild>
|
|
80
|
+
<Link href={pathname} locale='en'>
|
|
81
|
+
English
|
|
82
|
+
</Link>
|
|
83
|
+
</DropdownMenuItem>
|
|
84
|
+
<DropdownMenuItem asChild>
|
|
85
|
+
<Link href={pathname} locale='ru'>
|
|
86
|
+
Русский
|
|
87
|
+
</Link>
|
|
88
|
+
</DropdownMenuItem>
|
|
89
|
+
</DropdownMenuContent>
|
|
90
|
+
</DropdownMenu>
|
|
91
|
+
);
|
|
92
|
+
}
|