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,284 @@
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import { z } from 'zod';
3
+ import { db } from '../../database/index.js';
4
+ import { redis } from '../../database/redis.js';
5
+
6
+ // Schema field types (like Sanity)
7
+ export type FieldType =
8
+ | 'string'
9
+ | 'text'
10
+ | 'number'
11
+ | 'boolean'
12
+ | 'date'
13
+ | 'datetime'
14
+ | 'richText'
15
+ | 'image'
16
+ | 'file'
17
+ | 'reference'
18
+ | 'array'
19
+ | 'object'
20
+ | 'slug'
21
+ | 'url'
22
+ | 'email'
23
+ | 'color';
24
+
25
+ export interface SchemaField {
26
+ name: string;
27
+ type: FieldType;
28
+ title?: string;
29
+ description?: string;
30
+ required?: boolean;
31
+ localized?: boolean;
32
+ hidden?: boolean;
33
+ readOnly?: boolean;
34
+ options?: Record<string, any>;
35
+ validation?: any[];
36
+ of?: SchemaField[]; // For array type
37
+ fields?: SchemaField[]; // For object type
38
+ to?: string | string[]; // For reference type
39
+ }
40
+
41
+ export interface SchemaDefinition {
42
+ name: string;
43
+ title: string;
44
+ description?: string;
45
+ icon?: string;
46
+ fields: SchemaField[];
47
+ preview?: {
48
+ select: Record<string, string>;
49
+ prepare?: string;
50
+ };
51
+ }
52
+
53
+ const schemaFieldSchema = z.object({
54
+ name: z.string().min(1),
55
+ type: z.string(),
56
+ title: z.string().optional(),
57
+ description: z.string().optional(),
58
+ required: z.boolean().optional(),
59
+ localized: z.boolean().optional(),
60
+ hidden: z.boolean().optional(),
61
+ readOnly: z.boolean().optional(),
62
+ options: z.record(z.any()).optional(),
63
+ validation: z.array(z.any()).optional(),
64
+ of: z.array(z.any()).optional(),
65
+ fields: z.array(z.any()).optional(),
66
+ to: z.union([z.string(), z.array(z.string())]).optional(),
67
+ });
68
+
69
+ const createSchemaSchema = z.object({
70
+ name: z.string().min(1).regex(/^[a-zA-Z][a-zA-Z0-9_]*$/, {
71
+ message: 'Name must start with a letter and contain only letters, numbers, and underscores (e.g. blogPost, my_page)',
72
+ }),
73
+ title: z.string().min(1),
74
+ description: z.string().optional(),
75
+ icon: z.string().optional(),
76
+ fields: z.array(schemaFieldSchema),
77
+ isSingleton: z.boolean().optional(),
78
+ });
79
+
80
+ export const schemaRoutes: FastifyPluginAsync = async (fastify) => {
81
+ // List all schemas
82
+ fastify.get('/', {
83
+ schema: {
84
+ tags: ['Schemas'],
85
+ summary: 'List all content schemas',
86
+ },
87
+ }, async () => {
88
+ // Try cache first
89
+ const cached = await redis.get<any[]>('schemas:all');
90
+ if (cached) {
91
+ return { schemas: cached };
92
+ }
93
+
94
+ const result = await db.query(
95
+ `SELECT id, name, title, description, icon, is_singleton, sort_order,
96
+ definition, version, created_at, updated_at
97
+ FROM schemas
98
+ ORDER BY sort_order, title`
99
+ );
100
+
101
+ const schemas = result.rows.map(row => ({
102
+ ...row,
103
+ definition: row.definition,
104
+ }));
105
+
106
+ // Cache for 5 minutes
107
+ await redis.set('schemas:all', schemas, 300);
108
+
109
+ return { schemas };
110
+ });
111
+
112
+ // Get single schema
113
+ fastify.get('/:name', {
114
+ schema: {
115
+ tags: ['Schemas'],
116
+ summary: 'Get schema by name',
117
+ params: {
118
+ type: 'object',
119
+ properties: {
120
+ name: { type: 'string' },
121
+ },
122
+ },
123
+ },
124
+ }, async (request, reply) => {
125
+ const { name } = request.params as { name: string };
126
+
127
+ const result = await db.query(
128
+ `SELECT id, name, title, description, icon, is_singleton,
129
+ definition, version, created_at, updated_at
130
+ FROM schemas WHERE name = $1`,
131
+ [name]
132
+ );
133
+
134
+ if (result.rows.length === 0) {
135
+ return reply.status(404).send({ error: true, message: 'Schema not found' });
136
+ }
137
+
138
+ return { schema: result.rows[0] };
139
+ });
140
+
141
+ // Create schema
142
+ fastify.post('/', {
143
+ schema: {
144
+ tags: ['Schemas'],
145
+ summary: 'Create a new content schema',
146
+ security: [{ bearerAuth: [] }],
147
+ },
148
+ preHandler: [fastify.authenticate],
149
+ }, async (request, reply) => {
150
+ const body = createSchemaSchema.parse(request.body);
151
+
152
+ // Check if schema exists
153
+ const existing = await db.query(
154
+ 'SELECT id FROM schemas WHERE name = $1',
155
+ [body.name]
156
+ );
157
+
158
+ if (existing.rows.length > 0) {
159
+ return reply.status(400).send({ error: true, message: 'Schema already exists' });
160
+ }
161
+
162
+ const definition: SchemaDefinition = {
163
+ name: body.name,
164
+ title: body.title,
165
+ description: body.description,
166
+ icon: body.icon,
167
+ fields: body.fields as SchemaField[],
168
+ };
169
+
170
+ const result = await db.query(
171
+ `INSERT INTO schemas (name, title, description, icon, is_singleton, definition)
172
+ VALUES ($1, $2, $3, $4, $5, $6)
173
+ RETURNING id, name, title, description, icon, is_singleton, definition, created_at`,
174
+ [body.name, body.title, body.description, body.icon, body.isSingleton || false, definition]
175
+ );
176
+
177
+ // Invalidate cache
178
+ await redis.del('schemas:all');
179
+
180
+ return { schema: result.rows[0] };
181
+ });
182
+
183
+ // Update schema
184
+ fastify.put('/:name', {
185
+ schema: {
186
+ tags: ['Schemas'],
187
+ summary: 'Update a content schema',
188
+ security: [{ bearerAuth: [] }],
189
+ },
190
+ preHandler: [fastify.authenticate],
191
+ }, async (request, reply) => {
192
+ const { name } = request.params as { name: string };
193
+ const body = createSchemaSchema.partial().parse(request.body);
194
+
195
+ const existing = await db.query(
196
+ 'SELECT id, version FROM schemas WHERE name = $1',
197
+ [name]
198
+ );
199
+
200
+ if (existing.rows.length === 0) {
201
+ return reply.status(404).send({ error: true, message: 'Schema not found' });
202
+ }
203
+
204
+ const updates: string[] = [];
205
+ const values: any[] = [];
206
+ let paramIndex = 1;
207
+
208
+ if (body.title) {
209
+ updates.push(`title = $${paramIndex++}`);
210
+ values.push(body.title);
211
+ }
212
+ if (body.description !== undefined) {
213
+ updates.push(`description = $${paramIndex++}`);
214
+ values.push(body.description);
215
+ }
216
+ if (body.icon !== undefined) {
217
+ updates.push(`icon = $${paramIndex++}`);
218
+ values.push(body.icon);
219
+ }
220
+ if (body.isSingleton !== undefined) {
221
+ updates.push(`is_singleton = $${paramIndex++}`);
222
+ values.push(body.isSingleton);
223
+ }
224
+ if (body.fields) {
225
+ const definition: SchemaDefinition = {
226
+ name: body.name || name,
227
+ title: body.title || '',
228
+ description: body.description,
229
+ fields: body.fields as SchemaField[],
230
+ };
231
+ updates.push(`definition = $${paramIndex++}`);
232
+ values.push(definition);
233
+ }
234
+
235
+ // Increment version
236
+ updates.push(`version = version + 1`);
237
+
238
+ values.push(name);
239
+
240
+ const result = await db.query(
241
+ `UPDATE schemas SET ${updates.join(', ')}
242
+ WHERE name = $${paramIndex}
243
+ RETURNING *`,
244
+ values
245
+ );
246
+
247
+ // Invalidate cache
248
+ await redis.del('schemas:all');
249
+
250
+ return { schema: result.rows[0] };
251
+ });
252
+
253
+ // Delete schema
254
+ fastify.delete('/:name', {
255
+ schema: {
256
+ tags: ['Schemas'],
257
+ summary: 'Delete a content schema',
258
+ security: [{ bearerAuth: [] }],
259
+ },
260
+ preHandler: [fastify.authenticate],
261
+ }, async (request, reply) => {
262
+ const { name } = request.params as { name: string };
263
+
264
+ // Check if documents exist
265
+ const docsCount = await db.query(
266
+ 'SELECT COUNT(*) FROM documents WHERE schema_name = $1',
267
+ [name]
268
+ );
269
+
270
+ if (parseInt(docsCount.rows[0].count) > 0) {
271
+ return reply.status(400).send({
272
+ error: true,
273
+ message: 'Cannot delete schema with existing documents'
274
+ });
275
+ }
276
+
277
+ await db.query('DELETE FROM schemas WHERE name = $1', [name]);
278
+
279
+ // Invalidate cache
280
+ await redis.del('schemas:all');
281
+
282
+ return { success: true, message: 'Schema deleted' };
283
+ });
284
+ };
@@ -0,0 +1,70 @@
1
+ // Version management is integrated into content routes
2
+ // This file exports types and utilities for versioning
3
+
4
+ export interface DocumentVersion {
5
+ id: string;
6
+ documentId: string;
7
+ version: number;
8
+ data: Record<string, any>;
9
+ locale: string;
10
+ changeSummary?: string;
11
+ createdBy: string;
12
+ createdAt: Date;
13
+ }
14
+
15
+ export interface VersionDiff {
16
+ field: string;
17
+ oldValue: any;
18
+ newValue: any;
19
+ type: 'added' | 'removed' | 'changed';
20
+ }
21
+
22
+ /**
23
+ * Compare two versions and return the differences
24
+ */
25
+ export function compareVersions(oldData: Record<string, any>, newData: Record<string, any>): VersionDiff[] {
26
+ const diffs: VersionDiff[] = [];
27
+ const allKeys = new Set([...Object.keys(oldData), ...Object.keys(newData)]);
28
+
29
+ for (const key of allKeys) {
30
+ const oldValue = oldData[key];
31
+ const newValue = newData[key];
32
+
33
+ if (oldValue === undefined && newValue !== undefined) {
34
+ diffs.push({ field: key, oldValue, newValue, type: 'added' });
35
+ } else if (oldValue !== undefined && newValue === undefined) {
36
+ diffs.push({ field: key, oldValue, newValue, type: 'removed' });
37
+ } else if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
38
+ diffs.push({ field: key, oldValue, newValue, type: 'changed' });
39
+ }
40
+ }
41
+
42
+ return diffs;
43
+ }
44
+
45
+ /**
46
+ * Generate a human-readable summary of changes
47
+ */
48
+ export function generateChangeSummary(diffs: VersionDiff[]): string {
49
+ if (diffs.length === 0) {
50
+ return 'No changes';
51
+ }
52
+
53
+ const parts: string[] = [];
54
+
55
+ const added = diffs.filter(d => d.type === 'added');
56
+ const removed = diffs.filter(d => d.type === 'removed');
57
+ const changed = diffs.filter(d => d.type === 'changed');
58
+
59
+ if (added.length > 0) {
60
+ parts.push(`Added: ${added.map(d => d.field).join(', ')}`);
61
+ }
62
+ if (removed.length > 0) {
63
+ parts.push(`Removed: ${removed.map(d => d.field).join(', ')}`);
64
+ }
65
+ if (changed.length > 0) {
66
+ parts.push(`Changed: ${changed.map(d => d.field).join(', ')}`);
67
+ }
68
+
69
+ return parts.join('; ');
70
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true,
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "@/*": ["src/*"]
20
+ }
21
+ },
22
+ "include": ["src/**/*"],
23
+ "exclude": ["node_modules", "dist"]
24
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@pxlr/shared",
3
+ "version": "1.0.0",
4
+ "description": "Shared types and utilities for PXLR CMS",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch"
10
+ },
11
+ "devDependencies": {
12
+ "typescript": "^5.6.0"
13
+ }
14
+ }
@@ -0,0 +1,139 @@
1
+ // Schema field types
2
+ export type FieldType =
3
+ | 'string'
4
+ | 'text'
5
+ | 'number'
6
+ | 'boolean'
7
+ | 'date'
8
+ | 'datetime'
9
+ | 'richText'
10
+ | 'image'
11
+ | 'file'
12
+ | 'reference'
13
+ | 'array'
14
+ | 'object'
15
+ | 'slug'
16
+ | 'url'
17
+ | 'email'
18
+ | 'color';
19
+
20
+ // Schema field definition
21
+ export interface SchemaField {
22
+ name: string;
23
+ type: FieldType;
24
+ title?: string;
25
+ description?: string;
26
+ required?: boolean;
27
+ localized?: boolean;
28
+ hidden?: boolean;
29
+ readOnly?: boolean;
30
+ options?: Record<string, any>;
31
+ validation?: ValidationRule[];
32
+ of?: SchemaField[];
33
+ fields?: SchemaField[];
34
+ to?: string | string[];
35
+ }
36
+
37
+ // Validation rules
38
+ export interface ValidationRule {
39
+ type: 'required' | 'min' | 'max' | 'regex' | 'custom';
40
+ value?: any;
41
+ message?: string;
42
+ }
43
+
44
+ // Schema definition
45
+ export interface SchemaDefinition {
46
+ name: string;
47
+ title: string;
48
+ description?: string;
49
+ icon?: string;
50
+ fields: SchemaField[];
51
+ isSingleton?: boolean;
52
+ preview?: {
53
+ select: Record<string, string>;
54
+ prepare?: string;
55
+ };
56
+ }
57
+
58
+ // Document
59
+ export interface Document {
60
+ id: string;
61
+ schemaName: string;
62
+ data: Record<string, any>;
63
+ locale: string;
64
+ status: 'draft' | 'published' | 'archived';
65
+ publishedAt?: string;
66
+ createdBy: string;
67
+ updatedBy: string;
68
+ createdAt: string;
69
+ updatedAt: string;
70
+ }
71
+
72
+ // Document version
73
+ export interface DocumentVersion {
74
+ id: string;
75
+ documentId: string;
76
+ version: number;
77
+ data: Record<string, any>;
78
+ locale: string;
79
+ changeSummary?: string;
80
+ createdBy: string;
81
+ createdAt: string;
82
+ }
83
+
84
+ // Media file
85
+ export interface MediaFile {
86
+ id: string;
87
+ filename: string;
88
+ originalFilename: string;
89
+ mimeType: string;
90
+ sizeBytes: number;
91
+ width?: number;
92
+ height?: number;
93
+ url: string;
94
+ thumbnailUrl?: string;
95
+ altText?: string;
96
+ caption?: string;
97
+ metadata?: Record<string, any>;
98
+ folder: string;
99
+ uploadedBy: string;
100
+ createdAt: string;
101
+ }
102
+
103
+ // User
104
+ export interface User {
105
+ id: string;
106
+ email: string;
107
+ name: string;
108
+ role: 'admin' | 'editor' | 'viewer';
109
+ avatarUrl?: string;
110
+ isActive: boolean;
111
+ lastLoginAt?: string;
112
+ createdAt: string;
113
+ }
114
+
115
+ // Locale
116
+ export interface Locale {
117
+ code: string;
118
+ name: string;
119
+ nativeName?: string;
120
+ isDefault: boolean;
121
+ isActive: boolean;
122
+ }
123
+
124
+ // API Response types
125
+ export interface ApiResponse<T> {
126
+ data?: T;
127
+ error?: boolean;
128
+ message?: string;
129
+ }
130
+
131
+ export interface PaginatedResponse<T> {
132
+ items: T[];
133
+ pagination: {
134
+ page: number;
135
+ limit: number;
136
+ total: number;
137
+ totalPages: number;
138
+ };
139
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }