create-pxlr 1.0.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.
Files changed (153) hide show
  1. package/README.md +160 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +264 -0
  4. package/package.json +51 -0
  5. package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
  6. package/templates/blog/frontend/app/blog/page.tsx +102 -0
  7. package/templates/blog/frontend/app/components/footer.tsx +21 -0
  8. package/templates/blog/frontend/app/components/header.tsx +45 -0
  9. package/templates/blog/frontend/app/globals.css +30 -0
  10. package/templates/blog/frontend/app/layout.tsx +38 -0
  11. package/templates/blog/frontend/app/lib/cms.ts +71 -0
  12. package/templates/blog/frontend/app/page.tsx +155 -0
  13. package/templates/blog/frontend/next.config.ts +16 -0
  14. package/templates/blog/frontend/package.json +24 -0
  15. package/templates/blog/frontend/postcss.config.mjs +7 -0
  16. package/templates/blog/frontend/tsconfig.json +23 -0
  17. package/templates/blog/pxlr-cms/README.md +188 -0
  18. package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
  19. package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
  20. package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
  21. package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
  22. package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
  23. package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
  24. package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
  25. package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
  26. package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
  27. package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
  28. package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
  29. package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
  30. package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
  31. package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
  32. package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
  33. package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
  34. package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
  35. package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
  36. package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
  37. package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
  38. package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
  39. package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
  40. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
  41. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
  42. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
  43. package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
  44. package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
  45. package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
  46. package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
  47. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
  48. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
  49. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
  50. package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
  51. package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
  52. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
  53. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
  54. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
  55. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
  56. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
  57. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
  58. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
  59. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
  60. package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
  61. package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
  62. package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
  63. package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
  64. package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
  65. package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
  66. package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
  67. package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
  68. package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
  69. package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
  70. package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
  71. package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
  72. package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
  73. package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
  74. package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
  75. package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
  76. package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
  77. package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
  78. package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
  79. package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
  80. package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
  81. package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
  82. package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
  83. package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
  84. package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
  85. package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
  86. package/templates/clean/pxlr-cms/README.md +188 -0
  87. package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
  88. package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
  89. package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
  90. package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
  91. package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
  92. package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
  93. package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
  94. package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
  95. package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
  96. package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
  97. package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
  98. package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
  99. package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
  100. package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
  101. package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
  102. package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
  103. package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
  104. package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
  105. package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
  106. package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
  107. package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
  108. package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
  109. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
  110. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
  111. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
  112. package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
  113. package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
  114. package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
  115. package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
  116. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
  117. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
  118. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
  119. package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
  120. package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
  121. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
  122. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
  123. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
  124. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
  125. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
  126. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
  127. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
  128. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
  129. package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
  130. package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
  131. package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
  132. package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
  133. package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
  134. package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
  135. package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
  136. package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
  137. package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
  138. package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
  139. package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
  140. package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
  141. package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
  142. package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
  143. package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
  144. package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
  145. package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
  146. package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
  147. package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
  148. package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
  149. package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
  150. package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
  151. package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
  152. package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
  153. package/templates/clean/pxlr-cms/packages/shared/tsconfig.json +18 -0
@@ -0,0 +1,326 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Check, Copy, Code } from 'lucide-react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { useI18n } from '@/lib/i18n/context';
7
+
8
+ interface SchemaField {
9
+ name: string;
10
+ type: string;
11
+ title?: string;
12
+ required?: boolean;
13
+ }
14
+
15
+ interface SchemaCodeGeneratorProps {
16
+ schemaName: string;
17
+ schemaTitle: string;
18
+ fields: SchemaField[];
19
+ isSingleton?: boolean;
20
+ }
21
+
22
+ export function SchemaCodeGenerator({
23
+ schemaName,
24
+ schemaTitle,
25
+ fields,
26
+ isSingleton
27
+ }: SchemaCodeGeneratorProps) {
28
+ const { locale } = useI18n();
29
+ const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
30
+
31
+ const copyToClipboard = (text: string, index: number) => {
32
+ navigator.clipboard.writeText(text);
33
+ setCopiedIndex(index);
34
+ setTimeout(() => setCopiedIndex(null), 2000);
35
+ };
36
+
37
+ // Generate TypeScript interface
38
+ const generateTypeInterface = () => {
39
+ const typeMap: Record<string, string> = {
40
+ string: 'string',
41
+ text: 'string',
42
+ number: 'number',
43
+ boolean: 'boolean',
44
+ date: 'string',
45
+ datetime: 'string',
46
+ richText: 'any', // or specific rich text type
47
+ image: '{ url: string; alt?: string }',
48
+ file: '{ url: string; filename: string }',
49
+ slug: 'string',
50
+ url: 'string',
51
+ email: 'string',
52
+ reference: '{ _ref: string }',
53
+ array: 'any[]',
54
+ object: 'Record<string, any>',
55
+ };
56
+
57
+ const interfaceFields = fields.map(field => {
58
+ const tsType = typeMap[field.type] || 'any';
59
+ const optional = field.required ? '' : '?';
60
+ return ` ${field.name}${optional}: ${tsType};`;
61
+ }).join('\n');
62
+
63
+ const pascalName = schemaName.charAt(0).toUpperCase() + schemaName.slice(1);
64
+
65
+ return `// app/types/${schemaName}.ts
66
+ export interface ${pascalName} {
67
+ id: string;
68
+ schema_name: string;
69
+ ${interfaceFields}
70
+ created_at: string;
71
+ updated_at: string;
72
+ }`;
73
+ };
74
+
75
+ // Generate API client function
76
+ const generateApiClient = () => {
77
+ const pascalName = schemaName.charAt(0).toUpperCase() + schemaName.slice(1);
78
+
79
+ if (isSingleton) {
80
+ return `// app/lib/cms.ts
81
+ const API_URL = process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:4000';
82
+
83
+ export async function get${pascalName}(): Promise<${pascalName} | null> {
84
+ try {
85
+ const res = await fetch(\`\${API_URL}/content?schema=${schemaName}&limit=1\`, {
86
+ next: { revalidate: 60 } // ISR: revalidate every 60 seconds
87
+ });
88
+ const data = await res.json();
89
+ return data.documents?.[0] || null;
90
+ } catch (error) {
91
+ console.error('Failed to fetch ${schemaName}:', error);
92
+ return null;
93
+ }
94
+ }`;
95
+ }
96
+
97
+ return `// app/lib/cms.ts
98
+ const API_URL = process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:4000';
99
+
100
+ // Получить все документы типа ${schemaTitle}
101
+ export async function getAll${pascalName}s(): Promise<${pascalName}[]> {
102
+ try {
103
+ const res = await fetch(\`\${API_URL}/content?schema=${schemaName}\`, {
104
+ next: { revalidate: 60 }
105
+ });
106
+ const data = await res.json();
107
+ return data.documents || [];
108
+ } catch (error) {
109
+ console.error('Failed to fetch ${schemaName}s:', error);
110
+ return [];
111
+ }
112
+ }
113
+
114
+ // Получить один документ по ID
115
+ export async function get${pascalName}ById(id: string): Promise<${pascalName} | null> {
116
+ try {
117
+ const res = await fetch(\`\${API_URL}/content/\${id}\`, {
118
+ next: { revalidate: 60 }
119
+ });
120
+ const data = await res.json();
121
+ return data.document || null;
122
+ } catch (error) {
123
+ console.error('Failed to fetch ${schemaName}:', error);
124
+ return null;
125
+ }
126
+ }
127
+
128
+ // Получить документ по slug (если есть поле slug)
129
+ export async function get${pascalName}BySlug(slug: string): Promise<${pascalName} | null> {
130
+ try {
131
+ const res = await fetch(\`\${API_URL}/content?schema=${schemaName}&slug=\${slug}\`, {
132
+ next: { revalidate: 60 }
133
+ });
134
+ const data = await res.json();
135
+ return data.documents?.[0] || null;
136
+ } catch (error) {
137
+ console.error('Failed to fetch ${schemaName}:', error);
138
+ return null;
139
+ }
140
+ }`;
141
+ };
142
+
143
+ // Generate page component
144
+ const generatePageComponent = () => {
145
+ const pascalName = schemaName.charAt(0).toUpperCase() + schemaName.slice(1);
146
+
147
+ if (isSingleton) {
148
+ return `// app/${schemaName}/page.tsx
149
+ import { get${pascalName} } from '@/lib/cms';
150
+
151
+ export default async function ${pascalName}Page() {
152
+ const data = await get${pascalName}();
153
+
154
+ if (!data) {
155
+ return <div>Контент не найден</div>;
156
+ }
157
+
158
+ return (
159
+ <div className="container mx-auto py-8">
160
+ <h1 className="text-3xl font-bold">{data.title}</h1>
161
+ {/* Добавьте отображение других полей */}
162
+ </div>
163
+ );
164
+ }`;
165
+ }
166
+
167
+ return `// app/${schemaName}/page.tsx — Список всех записей
168
+ import Link from 'next/link';
169
+ import { getAll${pascalName}s } from '@/lib/cms';
170
+
171
+ export default async function ${pascalName}ListPage() {
172
+ const items = await getAll${pascalName}s();
173
+
174
+ return (
175
+ <div className="container mx-auto py-8">
176
+ <h1 className="text-3xl font-bold mb-6">${schemaTitle}</h1>
177
+ <div className="grid gap-4">
178
+ {items.map((item) => (
179
+ <Link
180
+ key={item.id}
181
+ href={\`/${schemaName}/\${item.data?.slug || item.id}\`}
182
+ className="block p-4 border rounded-lg hover:bg-gray-50"
183
+ >
184
+ <h2 className="text-xl font-semibold">{item.data?.title}</h2>
185
+ </Link>
186
+ ))}
187
+ </div>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ // app/${schemaName}/[slug]/page.tsx — Отдельная страница
193
+ import { get${pascalName}BySlug, getAll${pascalName}s } from '@/lib/cms';
194
+ import { notFound } from 'next/navigation';
195
+
196
+ interface Props {
197
+ params: Promise<{ slug: string }>;
198
+ }
199
+
200
+ // Генерация статических путей (SSG)
201
+ export async function generateStaticParams() {
202
+ const items = await getAll${pascalName}s();
203
+ return items.map((item) => ({
204
+ slug: item.data?.slug || item.id,
205
+ }));
206
+ }
207
+
208
+ export default async function ${pascalName}Page({ params }: Props) {
209
+ const { slug } = await params;
210
+ const data = await get${pascalName}BySlug(slug);
211
+
212
+ if (!data) {
213
+ notFound();
214
+ }
215
+
216
+ return (
217
+ <div className="container mx-auto py-8">
218
+ <h1 className="text-3xl font-bold">{data.data?.title}</h1>
219
+ {/* Отобразите контент */}
220
+ </div>
221
+ );
222
+ }`;
223
+ };
224
+
225
+ // Generate .env example
226
+ const generateEnvExample = () => {
227
+ return `# .env.local
228
+ NEXT_PUBLIC_CMS_API_URL=http://localhost:4000`;
229
+ };
230
+
231
+ const codeBlocks = [
232
+ {
233
+ title: locale === 'ru' ? '1. Переменные окружения' : '1. Environment Variables',
234
+ description: locale === 'ru'
235
+ ? 'Добавьте в .env.local URL вашего CMS API'
236
+ : 'Add CMS API URL to .env.local',
237
+ code: generateEnvExample(),
238
+ language: 'bash',
239
+ },
240
+ {
241
+ title: locale === 'ru' ? '2. TypeScript типы' : '2. TypeScript Types',
242
+ description: locale === 'ru'
243
+ ? 'Создайте файл с типами для типизации'
244
+ : 'Create types file for type safety',
245
+ code: generateTypeInterface(),
246
+ language: 'typescript',
247
+ },
248
+ {
249
+ title: locale === 'ru' ? '3. API клиент' : '3. API Client',
250
+ description: locale === 'ru'
251
+ ? 'Функции для получения данных из CMS'
252
+ : 'Functions to fetch data from CMS',
253
+ code: generateApiClient(),
254
+ language: 'typescript',
255
+ },
256
+ {
257
+ title: locale === 'ru' ? '4. Компонент страницы' : '4. Page Component',
258
+ description: locale === 'ru'
259
+ ? 'Пример использования в Next.js App Router'
260
+ : 'Example usage in Next.js App Router',
261
+ code: generatePageComponent(),
262
+ language: 'typescript',
263
+ },
264
+ ];
265
+
266
+ return (
267
+ <div className="space-y-6">
268
+ <div className="flex items-center gap-2">
269
+ <Code className="h-5 w-5" />
270
+ <h2 className="text-lg font-semibold">
271
+ {locale === 'ru' ? 'Использование в Next.js' : 'Usage in Next.js'}
272
+ </h2>
273
+ </div>
274
+
275
+ <p className="text-sm text-muted-foreground">
276
+ {locale === 'ru'
277
+ ? 'Скопируйте код ниже для использования этой схемы в вашем Next.js приложении (папка app/)'
278
+ : 'Copy the code below to use this schema in your Next.js application (app/ folder)'}
279
+ </p>
280
+
281
+ <div className="space-y-4">
282
+ {codeBlocks.map((block, index) => (
283
+ <div key={index} className="rounded-lg border bg-muted/30">
284
+ <div className="flex items-center justify-between p-4 border-b">
285
+ <div>
286
+ <h3 className="font-medium">{block.title}</h3>
287
+ <p className="text-xs text-muted-foreground">{block.description}</p>
288
+ </div>
289
+ <Button
290
+ variant="outline"
291
+ size="sm"
292
+ onClick={() => copyToClipboard(block.code, index)}
293
+ >
294
+ {copiedIndex === index ? (
295
+ <>
296
+ <Check className="mr-2 h-4 w-4" />
297
+ {locale === 'ru' ? 'Скопировано' : 'Copied'}
298
+ </>
299
+ ) : (
300
+ <>
301
+ <Copy className="mr-2 h-4 w-4" />
302
+ {locale === 'ru' ? 'Копировать' : 'Copy'}
303
+ </>
304
+ )}
305
+ </Button>
306
+ </div>
307
+ <pre className="p-4 overflow-x-auto text-sm">
308
+ <code>{block.code}</code>
309
+ </pre>
310
+ </div>
311
+ ))}
312
+ </div>
313
+
314
+ <div className="rounded-lg bg-blue-50 dark:bg-blue-950 p-4 text-sm">
315
+ <p className="font-medium text-blue-900 dark:text-blue-100">
316
+ {locale === 'ru' ? '💡 Совет' : '💡 Tip'}
317
+ </p>
318
+ <p className="mt-1 text-blue-800 dark:text-blue-200">
319
+ {locale === 'ru'
320
+ ? 'Используйте revalidate для ISR (Incremental Static Regeneration) — страницы будут обновляться автоматически каждые 60 секунд.'
321
+ : 'Use revalidate for ISR (Incremental Static Regeneration) — pages will update automatically every 60 seconds.'}
322
+ </p>
323
+ </div>
324
+ </div>
325
+ );
326
+ }
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as AvatarPrimitive from '@radix-ui/react-avatar';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ const Avatar = React.forwardRef<
8
+ React.ElementRef<typeof AvatarPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <AvatarPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ 'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ ));
20
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
21
+
22
+ const AvatarImage = React.forwardRef<
23
+ React.ElementRef<typeof AvatarPrimitive.Image>,
24
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
25
+ >(({ className, ...props }, ref) => (
26
+ <AvatarPrimitive.Image
27
+ ref={ref}
28
+ className={cn('aspect-square h-full w-full', className)}
29
+ {...props}
30
+ />
31
+ ));
32
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
33
+
34
+ const AvatarFallback = React.forwardRef<
35
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
36
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
37
+ >(({ className, ...props }, ref) => (
38
+ <AvatarPrimitive.Fallback
39
+ ref={ref}
40
+ className={cn(
41
+ 'flex h-full w-full items-center justify-center rounded-full bg-muted',
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ ));
47
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
48
+
49
+ export { Avatar, AvatarImage, AvatarFallback };
@@ -0,0 +1,55 @@
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ const buttonVariants = cva(
7
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
12
+ destructive:
13
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
14
+ outline:
15
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
16
+ secondary:
17
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
18
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
19
+ link: 'text-primary underline-offset-4 hover:underline',
20
+ },
21
+ size: {
22
+ default: 'h-10 px-4 py-2',
23
+ sm: 'h-9 rounded-md px-3',
24
+ lg: 'h-11 rounded-md px-8',
25
+ icon: 'h-10 w-10',
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: 'default',
30
+ size: 'default',
31
+ },
32
+ }
33
+ );
34
+
35
+ export interface ButtonProps
36
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
37
+ VariantProps<typeof buttonVariants> {
38
+ asChild?: boolean;
39
+ }
40
+
41
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
42
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
43
+ const Comp = asChild ? Slot : 'button';
44
+ return (
45
+ <Comp
46
+ className={cn(buttonVariants({ variant, size, className }))}
47
+ ref={ref}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+ );
53
+ Button.displayName = 'Button';
54
+
55
+ export { Button, buttonVariants };
@@ -0,0 +1,194 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
5
+ import { Check, ChevronRight, Circle } from 'lucide-react';
6
+ import { cn } from '@/lib/utils';
7
+
8
+ const DropdownMenu = DropdownMenuPrimitive.Root;
9
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
10
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
11
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
12
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
13
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
14
+
15
+ const DropdownMenuSubTrigger = React.forwardRef<
16
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
17
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
18
+ inset?: boolean;
19
+ }
20
+ >(({ className, inset, children, ...props }, ref) => (
21
+ <DropdownMenuPrimitive.SubTrigger
22
+ ref={ref}
23
+ className={cn(
24
+ 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
25
+ inset && 'pl-8',
26
+ className
27
+ )}
28
+ {...props}
29
+ >
30
+ {children}
31
+ <ChevronRight className="ml-auto h-4 w-4" />
32
+ </DropdownMenuPrimitive.SubTrigger>
33
+ ));
34
+ DropdownMenuSubTrigger.displayName =
35
+ DropdownMenuPrimitive.SubTrigger.displayName;
36
+
37
+ const DropdownMenuSubContent = React.forwardRef<
38
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
39
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
40
+ >(({ className, ...props }, ref) => (
41
+ <DropdownMenuPrimitive.SubContent
42
+ ref={ref}
43
+ className={cn(
44
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
45
+ className
46
+ )}
47
+ {...props}
48
+ />
49
+ ));
50
+ DropdownMenuSubContent.displayName =
51
+ DropdownMenuPrimitive.SubContent.displayName;
52
+
53
+ const DropdownMenuContent = React.forwardRef<
54
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
55
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
56
+ >(({ className, sideOffset = 4, ...props }, ref) => (
57
+ <DropdownMenuPrimitive.Portal>
58
+ <DropdownMenuPrimitive.Content
59
+ ref={ref}
60
+ sideOffset={sideOffset}
61
+ className={cn(
62
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
63
+ className
64
+ )}
65
+ {...props}
66
+ />
67
+ </DropdownMenuPrimitive.Portal>
68
+ ));
69
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
70
+
71
+ const DropdownMenuItem = React.forwardRef<
72
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
73
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
74
+ inset?: boolean;
75
+ }
76
+ >(({ className, inset, ...props }, ref) => (
77
+ <DropdownMenuPrimitive.Item
78
+ ref={ref}
79
+ className={cn(
80
+ 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
81
+ inset && 'pl-8',
82
+ className
83
+ )}
84
+ {...props}
85
+ />
86
+ ));
87
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
88
+
89
+ const DropdownMenuCheckboxItem = React.forwardRef<
90
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
91
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
92
+ >(({ className, children, checked, ...props }, ref) => (
93
+ <DropdownMenuPrimitive.CheckboxItem
94
+ ref={ref}
95
+ className={cn(
96
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
97
+ className
98
+ )}
99
+ checked={checked}
100
+ {...props}
101
+ >
102
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
103
+ <DropdownMenuPrimitive.ItemIndicator>
104
+ <Check className="h-4 w-4" />
105
+ </DropdownMenuPrimitive.ItemIndicator>
106
+ </span>
107
+ {children}
108
+ </DropdownMenuPrimitive.CheckboxItem>
109
+ ));
110
+ DropdownMenuCheckboxItem.displayName =
111
+ DropdownMenuPrimitive.CheckboxItem.displayName;
112
+
113
+ const DropdownMenuRadioItem = React.forwardRef<
114
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
115
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
116
+ >(({ className, children, ...props }, ref) => (
117
+ <DropdownMenuPrimitive.RadioItem
118
+ ref={ref}
119
+ className={cn(
120
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
121
+ className
122
+ )}
123
+ {...props}
124
+ >
125
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
126
+ <DropdownMenuPrimitive.ItemIndicator>
127
+ <Circle className="h-2 w-2 fill-current" />
128
+ </DropdownMenuPrimitive.ItemIndicator>
129
+ </span>
130
+ {children}
131
+ </DropdownMenuPrimitive.RadioItem>
132
+ ));
133
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
134
+
135
+ const DropdownMenuLabel = React.forwardRef<
136
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
137
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
138
+ inset?: boolean;
139
+ }
140
+ >(({ className, inset, ...props }, ref) => (
141
+ <DropdownMenuPrimitive.Label
142
+ ref={ref}
143
+ className={cn(
144
+ 'px-2 py-1.5 text-sm font-semibold',
145
+ inset && 'pl-8',
146
+ className
147
+ )}
148
+ {...props}
149
+ />
150
+ ));
151
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
152
+
153
+ const DropdownMenuSeparator = React.forwardRef<
154
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
155
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
156
+ >(({ className, ...props }, ref) => (
157
+ <DropdownMenuPrimitive.Separator
158
+ ref={ref}
159
+ className={cn('-mx-1 my-1 h-px bg-muted', className)}
160
+ {...props}
161
+ />
162
+ ));
163
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
164
+
165
+ const DropdownMenuShortcut = ({
166
+ className,
167
+ ...props
168
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
169
+ return (
170
+ <span
171
+ className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
172
+ {...props}
173
+ />
174
+ );
175
+ };
176
+ DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
177
+
178
+ export {
179
+ DropdownMenu,
180
+ DropdownMenuTrigger,
181
+ DropdownMenuContent,
182
+ DropdownMenuItem,
183
+ DropdownMenuCheckboxItem,
184
+ DropdownMenuRadioItem,
185
+ DropdownMenuLabel,
186
+ DropdownMenuSeparator,
187
+ DropdownMenuShortcut,
188
+ DropdownMenuGroup,
189
+ DropdownMenuPortal,
190
+ DropdownMenuSub,
191
+ DropdownMenuSubContent,
192
+ DropdownMenuSubTrigger,
193
+ DropdownMenuRadioGroup,
194
+ };
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ export interface InputProps
5
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
6
+
7
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
8
+ ({ className, type, ...props }, ref) => {
9
+ return (
10
+ <input
11
+ type={type}
12
+ className={cn(
13
+ 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
14
+ className
15
+ )}
16
+ ref={ref}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+ );
22
+ Input.displayName = 'Input';
23
+
24
+ export { Input };
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as LabelPrimitive from '@radix-ui/react-label';
5
+ import { cva, type VariantProps } from 'class-variance-authority';
6
+ import { cn } from '@/lib/utils';
7
+
8
+ const labelVariants = cva(
9
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
10
+ );
11
+
12
+ const Label = React.forwardRef<
13
+ React.ElementRef<typeof LabelPrimitive.Root>,
14
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
15
+ VariantProps<typeof labelVariants>
16
+ >(({ className, ...props }, ref) => (
17
+ <LabelPrimitive.Root
18
+ ref={ref}
19
+ className={cn(labelVariants(), className)}
20
+ {...props}
21
+ />
22
+ ));
23
+ Label.displayName = LabelPrimitive.Root.displayName;
24
+
25
+ export { Label };