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.
- package/README.md +160 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +264 -0
- package/package.json +51 -0
- package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
- package/templates/blog/frontend/app/blog/page.tsx +102 -0
- package/templates/blog/frontend/app/components/footer.tsx +21 -0
- package/templates/blog/frontend/app/components/header.tsx +45 -0
- package/templates/blog/frontend/app/globals.css +30 -0
- package/templates/blog/frontend/app/layout.tsx +38 -0
- package/templates/blog/frontend/app/lib/cms.ts +71 -0
- package/templates/blog/frontend/app/page.tsx +155 -0
- package/templates/blog/frontend/next.config.ts +16 -0
- package/templates/blog/frontend/package.json +24 -0
- package/templates/blog/frontend/postcss.config.mjs +7 -0
- package/templates/blog/frontend/tsconfig.json +23 -0
- package/templates/blog/pxlr-cms/README.md +188 -0
- package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
- package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
- package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
- package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
- package/templates/clean/pxlr-cms/README.md +188 -0
- package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
- package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
- package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- package/templates/clean/pxlr-cms/packages/shared/tsconfig.json +18 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
export const translations = {
|
|
2
|
+
en: {
|
|
3
|
+
// Navigation
|
|
4
|
+
'nav.dashboard': 'Dashboard',
|
|
5
|
+
'nav.content': 'Content',
|
|
6
|
+
'nav.schemas': 'Schemas',
|
|
7
|
+
'nav.media': 'Media',
|
|
8
|
+
'nav.settings': 'Settings',
|
|
9
|
+
|
|
10
|
+
// Header
|
|
11
|
+
'header.contentManagement': 'Content Management',
|
|
12
|
+
'header.profile': 'Profile',
|
|
13
|
+
'header.logout': 'Log out',
|
|
14
|
+
|
|
15
|
+
// Dashboard
|
|
16
|
+
'dashboard.title': 'Dashboard',
|
|
17
|
+
'dashboard.welcome': 'Welcome to PXLR CMS. Manage your content from here.',
|
|
18
|
+
'dashboard.contentTypes': 'Content Types',
|
|
19
|
+
'dashboard.definedSchemas': 'Defined schemas',
|
|
20
|
+
'dashboard.documents': 'Documents',
|
|
21
|
+
'dashboard.totalContent': 'Total content items',
|
|
22
|
+
'dashboard.mediaFiles': 'Media Files',
|
|
23
|
+
'dashboard.uploadedFiles': 'Uploaded files',
|
|
24
|
+
'dashboard.systemStatus': 'System Status',
|
|
25
|
+
'dashboard.online': 'Online',
|
|
26
|
+
'dashboard.allServices': 'All services running',
|
|
27
|
+
'dashboard.quickStart': 'Quick Start',
|
|
28
|
+
'dashboard.step1': 'Create a content schema in the Schemas section',
|
|
29
|
+
'dashboard.step2': 'Add content documents based on your schemas',
|
|
30
|
+
'dashboard.step3': 'Upload media files to the Media Library',
|
|
31
|
+
'dashboard.step4': 'Access your content via the REST API',
|
|
32
|
+
'dashboard.apiAccess': 'API Access',
|
|
33
|
+
'dashboard.apiEndpoint': 'API Endpoint',
|
|
34
|
+
'dashboard.documentation': 'Documentation',
|
|
35
|
+
'dashboard.viewDocs': 'View Swagger API Docs',
|
|
36
|
+
|
|
37
|
+
// Schemas
|
|
38
|
+
'schemas.title': 'Content Schemas',
|
|
39
|
+
'schemas.subtitle': 'Define the structure of your content types',
|
|
40
|
+
'schemas.newSchema': 'New Schema',
|
|
41
|
+
'schemas.createNew': 'Create New Schema',
|
|
42
|
+
'schemas.name': 'Name (API identifier)',
|
|
43
|
+
'schemas.namePlaceholder': 'e.g., blogPost, article, my_page',
|
|
44
|
+
'schemas.nameHint': 'Letters, numbers, underscores only. Start with a letter.',
|
|
45
|
+
'schemas.displayName': 'Title (Display name)',
|
|
46
|
+
'schemas.description': 'Description',
|
|
47
|
+
'schemas.optional': 'Optional description',
|
|
48
|
+
'schemas.create': 'Create Schema',
|
|
49
|
+
'schemas.cancel': 'Cancel',
|
|
50
|
+
'schemas.noSchemas': 'No schemas yet',
|
|
51
|
+
'schemas.createFirst': 'Create your first content schema to get started',
|
|
52
|
+
'schemas.fields': 'fields',
|
|
53
|
+
'schemas.singleton': 'Singleton',
|
|
54
|
+
'schemas.editSchema': 'Edit Schema',
|
|
55
|
+
'schemas.basicInfo': 'Basic Information',
|
|
56
|
+
'schemas.singletonHint': 'Singleton (only one document of this type)',
|
|
57
|
+
'schemas.addField': 'Add Field',
|
|
58
|
+
'schemas.noFields': 'No fields defined. Add your first field.',
|
|
59
|
+
'schemas.fieldName': 'Field Name',
|
|
60
|
+
'schemas.fieldTitle': 'Title',
|
|
61
|
+
'schemas.fieldType': 'Type',
|
|
62
|
+
'schemas.required': 'Required',
|
|
63
|
+
'schemas.preview': 'Schema Preview (JSON)',
|
|
64
|
+
'schemas.save': 'Save Changes',
|
|
65
|
+
|
|
66
|
+
// Content
|
|
67
|
+
'content.title': 'Content',
|
|
68
|
+
'content.subtitle': 'Manage your content documents',
|
|
69
|
+
'content.newDocument': 'New Document',
|
|
70
|
+
'content.noSchemas': 'No content schemas defined yet.',
|
|
71
|
+
'content.createSchema': 'Create a schema first to start adding content.',
|
|
72
|
+
'content.goToSchemas': 'Go to Schemas',
|
|
73
|
+
'content.noDocuments': 'No documents yet',
|
|
74
|
+
'content.createFirst': 'Create your first document',
|
|
75
|
+
'content.edit': 'Edit',
|
|
76
|
+
'content.delete': 'Delete',
|
|
77
|
+
'content.lastUpdated': 'Last updated',
|
|
78
|
+
'content.createNew': 'Create New Document',
|
|
79
|
+
'content.selectSchema': 'Select Content Type',
|
|
80
|
+
'content.back': 'Back to Content',
|
|
81
|
+
'content.saving': 'Saving...',
|
|
82
|
+
'content.saveDocument': 'Save Document',
|
|
83
|
+
'content.editDocument': 'Edit Document',
|
|
84
|
+
'content.deleteConfirm': 'Are you sure you want to delete this document?',
|
|
85
|
+
|
|
86
|
+
// Media
|
|
87
|
+
'media.title': 'Media Library',
|
|
88
|
+
'media.subtitle': 'Manage your media files',
|
|
89
|
+
'media.upload': 'Upload File',
|
|
90
|
+
'media.noFiles': 'No media files yet',
|
|
91
|
+
'media.uploadFirst': 'Upload your first media file',
|
|
92
|
+
'media.dragDrop': 'Drag and drop files here or click to browse',
|
|
93
|
+
'media.uploading': 'Uploading...',
|
|
94
|
+
'media.delete': 'Delete',
|
|
95
|
+
'media.copy': 'Copy URL',
|
|
96
|
+
'media.copied': 'URL copied!',
|
|
97
|
+
|
|
98
|
+
// Settings
|
|
99
|
+
'settings.title': 'Settings',
|
|
100
|
+
'settings.subtitle': 'Configure your CMS',
|
|
101
|
+
'settings.general': 'General Settings',
|
|
102
|
+
'settings.localization': 'Localization',
|
|
103
|
+
'settings.defaultLocale': 'Default locale',
|
|
104
|
+
'settings.systemInfo': 'System Information',
|
|
105
|
+
'settings.version': 'Version',
|
|
106
|
+
'settings.environment': 'Environment',
|
|
107
|
+
'settings.language': 'Interface Language',
|
|
108
|
+
|
|
109
|
+
// Profile
|
|
110
|
+
'profile.title': 'Profile',
|
|
111
|
+
'profile.subtitle': 'Manage your account settings',
|
|
112
|
+
'profile.updateProfile': 'Update Profile',
|
|
113
|
+
'profile.displayName': 'Display Name',
|
|
114
|
+
'profile.email': 'Email',
|
|
115
|
+
'profile.emailHint': 'Email cannot be changed',
|
|
116
|
+
'profile.saveChanges': 'Save Changes',
|
|
117
|
+
'profile.changePassword': 'Change Password',
|
|
118
|
+
'profile.currentPassword': 'Current Password',
|
|
119
|
+
'profile.newPassword': 'New Password',
|
|
120
|
+
'profile.confirmPassword': 'Confirm New Password',
|
|
121
|
+
'profile.memberSince': 'Member since',
|
|
122
|
+
|
|
123
|
+
// Login
|
|
124
|
+
'login.title': 'Sign in to PXLR CMS',
|
|
125
|
+
'login.email': 'Email',
|
|
126
|
+
'login.password': 'Password',
|
|
127
|
+
'login.signIn': 'Sign in',
|
|
128
|
+
'login.signingIn': 'Signing in...',
|
|
129
|
+
|
|
130
|
+
// Common
|
|
131
|
+
'common.loading': 'Loading...',
|
|
132
|
+
'common.save': 'Save',
|
|
133
|
+
'common.cancel': 'Cancel',
|
|
134
|
+
'common.delete': 'Delete',
|
|
135
|
+
'common.edit': 'Edit',
|
|
136
|
+
'common.create': 'Create',
|
|
137
|
+
'common.back': 'Back',
|
|
138
|
+
'common.error': 'Error',
|
|
139
|
+
'common.success': 'Success',
|
|
140
|
+
|
|
141
|
+
// Field types
|
|
142
|
+
'fieldType.string': 'Text',
|
|
143
|
+
'fieldType.text': 'Long Text',
|
|
144
|
+
'fieldType.number': 'Number',
|
|
145
|
+
'fieldType.boolean': 'Boolean',
|
|
146
|
+
'fieldType.date': 'Date',
|
|
147
|
+
'fieldType.datetime': 'Date & Time',
|
|
148
|
+
'fieldType.richText': 'Rich Text',
|
|
149
|
+
'fieldType.image': 'Image',
|
|
150
|
+
'fieldType.file': 'File',
|
|
151
|
+
'fieldType.slug': 'Slug',
|
|
152
|
+
'fieldType.url': 'URL',
|
|
153
|
+
'fieldType.email': 'Email',
|
|
154
|
+
'fieldType.reference': 'Reference',
|
|
155
|
+
'fieldType.array': 'Array',
|
|
156
|
+
'fieldType.object': 'Object',
|
|
157
|
+
},
|
|
158
|
+
ru: {
|
|
159
|
+
// Navigation
|
|
160
|
+
'nav.dashboard': 'Главная',
|
|
161
|
+
'nav.content': 'Контент',
|
|
162
|
+
'nav.schemas': 'Схемы',
|
|
163
|
+
'nav.media': 'Медиа',
|
|
164
|
+
'nav.settings': 'Настройки',
|
|
165
|
+
|
|
166
|
+
// Header
|
|
167
|
+
'header.contentManagement': 'Управление контентом',
|
|
168
|
+
'header.profile': 'Профиль',
|
|
169
|
+
'header.logout': 'Выйти',
|
|
170
|
+
|
|
171
|
+
// Dashboard
|
|
172
|
+
'dashboard.title': 'Главная',
|
|
173
|
+
'dashboard.welcome': 'Добро пожаловать в PXLR CMS. Управляйте контентом отсюда.',
|
|
174
|
+
'dashboard.contentTypes': 'Типы контента',
|
|
175
|
+
'dashboard.definedSchemas': 'Определённые схемы',
|
|
176
|
+
'dashboard.documents': 'Документы',
|
|
177
|
+
'dashboard.totalContent': 'Всего записей',
|
|
178
|
+
'dashboard.mediaFiles': 'Медиафайлы',
|
|
179
|
+
'dashboard.uploadedFiles': 'Загруженные файлы',
|
|
180
|
+
'dashboard.systemStatus': 'Статус системы',
|
|
181
|
+
'dashboard.online': 'Онлайн',
|
|
182
|
+
'dashboard.allServices': 'Все сервисы работают',
|
|
183
|
+
'dashboard.quickStart': 'Быстрый старт',
|
|
184
|
+
'dashboard.step1': 'Создайте схему контента в разделе Схемы',
|
|
185
|
+
'dashboard.step2': 'Добавьте документы на основе ваших схем',
|
|
186
|
+
'dashboard.step3': 'Загрузите медиафайлы в Медиатеку',
|
|
187
|
+
'dashboard.step4': 'Получайте контент через REST API',
|
|
188
|
+
'dashboard.apiAccess': 'Доступ к API',
|
|
189
|
+
'dashboard.apiEndpoint': 'API Endpoint',
|
|
190
|
+
'dashboard.documentation': 'Документация',
|
|
191
|
+
'dashboard.viewDocs': 'Открыть Swagger API Docs',
|
|
192
|
+
|
|
193
|
+
// Schemas
|
|
194
|
+
'schemas.title': 'Схемы контента',
|
|
195
|
+
'schemas.subtitle': 'Определите структуру ваших типов контента',
|
|
196
|
+
'schemas.newSchema': 'Новая схема',
|
|
197
|
+
'schemas.createNew': 'Создать новую схему',
|
|
198
|
+
'schemas.name': 'Имя (API идентификатор)',
|
|
199
|
+
'schemas.namePlaceholder': 'например, blogPost, article, my_page',
|
|
200
|
+
'schemas.nameHint': 'Только буквы, цифры, подчёркивания. Начинать с буквы.',
|
|
201
|
+
'schemas.displayName': 'Название (для отображения)',
|
|
202
|
+
'schemas.description': 'Описание',
|
|
203
|
+
'schemas.optional': 'Необязательное описание',
|
|
204
|
+
'schemas.create': 'Создать схему',
|
|
205
|
+
'schemas.cancel': 'Отмена',
|
|
206
|
+
'schemas.noSchemas': 'Схем пока нет',
|
|
207
|
+
'schemas.createFirst': 'Создайте первую схему контента для начала работы',
|
|
208
|
+
'schemas.fields': 'полей',
|
|
209
|
+
'schemas.singleton': 'Единственный',
|
|
210
|
+
'schemas.editSchema': 'Редактировать схему',
|
|
211
|
+
'schemas.basicInfo': 'Основная информация',
|
|
212
|
+
'schemas.singletonHint': 'Единственный экземпляр (только один документ этого типа)',
|
|
213
|
+
'schemas.addField': 'Добавить поле',
|
|
214
|
+
'schemas.noFields': 'Полей не определено. Добавьте первое поле.',
|
|
215
|
+
'schemas.fieldName': 'Имя поля',
|
|
216
|
+
'schemas.fieldTitle': 'Название',
|
|
217
|
+
'schemas.fieldType': 'Тип',
|
|
218
|
+
'schemas.required': 'Обязательное',
|
|
219
|
+
'schemas.preview': 'Предпросмотр схемы (JSON)',
|
|
220
|
+
'schemas.save': 'Сохранить изменения',
|
|
221
|
+
|
|
222
|
+
// Content
|
|
223
|
+
'content.title': 'Контент',
|
|
224
|
+
'content.subtitle': 'Управляйте вашими документами',
|
|
225
|
+
'content.newDocument': 'Новый документ',
|
|
226
|
+
'content.noSchemas': 'Схемы контента ещё не определены.',
|
|
227
|
+
'content.createSchema': 'Сначала создайте схему, чтобы добавлять контент.',
|
|
228
|
+
'content.goToSchemas': 'Перейти к схемам',
|
|
229
|
+
'content.noDocuments': 'Документов пока нет',
|
|
230
|
+
'content.createFirst': 'Создайте первый документ',
|
|
231
|
+
'content.edit': 'Редактировать',
|
|
232
|
+
'content.delete': 'Удалить',
|
|
233
|
+
'content.lastUpdated': 'Обновлено',
|
|
234
|
+
'content.createNew': 'Создать новый документ',
|
|
235
|
+
'content.selectSchema': 'Выберите тип контента',
|
|
236
|
+
'content.back': 'Назад к контенту',
|
|
237
|
+
'content.saving': 'Сохранение...',
|
|
238
|
+
'content.saveDocument': 'Сохранить документ',
|
|
239
|
+
'content.editDocument': 'Редактировать документ',
|
|
240
|
+
'content.deleteConfirm': 'Вы уверены, что хотите удалить этот документ?',
|
|
241
|
+
|
|
242
|
+
// Media
|
|
243
|
+
'media.title': 'Медиатека',
|
|
244
|
+
'media.subtitle': 'Управляйте вашими медиафайлами',
|
|
245
|
+
'media.upload': 'Загрузить файл',
|
|
246
|
+
'media.noFiles': 'Медиафайлов пока нет',
|
|
247
|
+
'media.uploadFirst': 'Загрузите первый медиафайл',
|
|
248
|
+
'media.dragDrop': 'Перетащите файлы сюда или нажмите для выбора',
|
|
249
|
+
'media.uploading': 'Загрузка...',
|
|
250
|
+
'media.delete': 'Удалить',
|
|
251
|
+
'media.copy': 'Копировать URL',
|
|
252
|
+
'media.copied': 'URL скопирован!',
|
|
253
|
+
|
|
254
|
+
// Settings
|
|
255
|
+
'settings.title': 'Настройки',
|
|
256
|
+
'settings.subtitle': 'Конфигурация CMS',
|
|
257
|
+
'settings.general': 'Общие настройки',
|
|
258
|
+
'settings.localization': 'Локализация',
|
|
259
|
+
'settings.defaultLocale': 'Язык по умолчанию',
|
|
260
|
+
'settings.systemInfo': 'Информация о системе',
|
|
261
|
+
'settings.version': 'Версия',
|
|
262
|
+
'settings.environment': 'Окружение',
|
|
263
|
+
'settings.language': 'Язык интерфейса',
|
|
264
|
+
|
|
265
|
+
// Profile
|
|
266
|
+
'profile.title': 'Профиль',
|
|
267
|
+
'profile.subtitle': 'Управление настройками аккаунта',
|
|
268
|
+
'profile.updateProfile': 'Обновить профиль',
|
|
269
|
+
'profile.displayName': 'Отображаемое имя',
|
|
270
|
+
'profile.email': 'Email',
|
|
271
|
+
'profile.emailHint': 'Email нельзя изменить',
|
|
272
|
+
'profile.saveChanges': 'Сохранить изменения',
|
|
273
|
+
'profile.changePassword': 'Изменить пароль',
|
|
274
|
+
'profile.currentPassword': 'Текущий пароль',
|
|
275
|
+
'profile.newPassword': 'Новый пароль',
|
|
276
|
+
'profile.confirmPassword': 'Подтвердите новый пароль',
|
|
277
|
+
'profile.memberSince': 'Участник с',
|
|
278
|
+
|
|
279
|
+
// Login
|
|
280
|
+
'login.title': 'Вход в PXLR CMS',
|
|
281
|
+
'login.email': 'Email',
|
|
282
|
+
'login.password': 'Пароль',
|
|
283
|
+
'login.signIn': 'Войти',
|
|
284
|
+
'login.signingIn': 'Вход...',
|
|
285
|
+
|
|
286
|
+
// Common
|
|
287
|
+
'common.loading': 'Загрузка...',
|
|
288
|
+
'common.save': 'Сохранить',
|
|
289
|
+
'common.cancel': 'Отмена',
|
|
290
|
+
'common.delete': 'Удалить',
|
|
291
|
+
'common.edit': 'Редактировать',
|
|
292
|
+
'common.create': 'Создать',
|
|
293
|
+
'common.back': 'Назад',
|
|
294
|
+
'common.error': 'Ошибка',
|
|
295
|
+
'common.success': 'Успешно',
|
|
296
|
+
|
|
297
|
+
// Field types
|
|
298
|
+
'fieldType.string': 'Текст',
|
|
299
|
+
'fieldType.text': 'Длинный текст',
|
|
300
|
+
'fieldType.number': 'Число',
|
|
301
|
+
'fieldType.boolean': 'Логический',
|
|
302
|
+
'fieldType.date': 'Дата',
|
|
303
|
+
'fieldType.datetime': 'Дата и время',
|
|
304
|
+
'fieldType.richText': 'Форматированный текст',
|
|
305
|
+
'fieldType.image': 'Изображение',
|
|
306
|
+
'fieldType.file': 'Файл',
|
|
307
|
+
'fieldType.slug': 'Слаг',
|
|
308
|
+
'fieldType.url': 'URL',
|
|
309
|
+
'fieldType.email': 'Email',
|
|
310
|
+
'fieldType.reference': 'Ссылка',
|
|
311
|
+
'fieldType.array': 'Массив',
|
|
312
|
+
'fieldType.object': 'Объект',
|
|
313
|
+
},
|
|
314
|
+
} as const;
|
|
315
|
+
|
|
316
|
+
export type Locale = keyof typeof translations;
|
|
317
|
+
export type TranslationKey = keyof typeof translations.en;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
interface User {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
name: string;
|
|
8
|
+
role: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface AuthState {
|
|
12
|
+
token: string | null;
|
|
13
|
+
user: User | null;
|
|
14
|
+
isAuthenticated: boolean;
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
setAuth: (token: string, user: User) => void;
|
|
17
|
+
logout: () => void;
|
|
18
|
+
setLoading: (loading: boolean) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useAuthStore = create<AuthState>()(
|
|
22
|
+
persist(
|
|
23
|
+
(set) => ({
|
|
24
|
+
token: null,
|
|
25
|
+
user: null,
|
|
26
|
+
isAuthenticated: false,
|
|
27
|
+
isLoading: true,
|
|
28
|
+
setAuth: (token, user) =>
|
|
29
|
+
set({
|
|
30
|
+
token,
|
|
31
|
+
user,
|
|
32
|
+
isAuthenticated: true,
|
|
33
|
+
isLoading: false,
|
|
34
|
+
}),
|
|
35
|
+
logout: () =>
|
|
36
|
+
set({
|
|
37
|
+
token: null,
|
|
38
|
+
user: null,
|
|
39
|
+
isAuthenticated: false,
|
|
40
|
+
isLoading: false,
|
|
41
|
+
}),
|
|
42
|
+
setLoading: (loading) => set({ isLoading: loading }),
|
|
43
|
+
}),
|
|
44
|
+
{
|
|
45
|
+
name: 'pxlr-auth',
|
|
46
|
+
onRehydrateStorage: () => (state) => {
|
|
47
|
+
state?.setLoading(false);
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatDate(date: string | Date) {
|
|
9
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
10
|
+
dateStyle: 'medium',
|
|
11
|
+
timeStyle: 'short',
|
|
12
|
+
}).format(new Date(date));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatBytes(bytes: number) {
|
|
16
|
+
if (bytes === 0) return '0 Bytes';
|
|
17
|
+
const k = 1024;
|
|
18
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
19
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
20
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function slugify(text: string) {
|
|
24
|
+
return text
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^\w\s-]/g, '')
|
|
27
|
+
.replace(/[\s_-]+/g, '-')
|
|
28
|
+
.replace(/^-+|-+$/g, '');
|
|
29
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
darkMode: ['class'],
|
|
5
|
+
content: [
|
|
6
|
+
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
7
|
+
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
8
|
+
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
9
|
+
],
|
|
10
|
+
theme: {
|
|
11
|
+
extend: {
|
|
12
|
+
colors: {
|
|
13
|
+
border: 'hsl(var(--border))',
|
|
14
|
+
input: 'hsl(var(--input))',
|
|
15
|
+
ring: 'hsl(var(--ring))',
|
|
16
|
+
background: 'hsl(var(--background))',
|
|
17
|
+
foreground: 'hsl(var(--foreground))',
|
|
18
|
+
primary: {
|
|
19
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
20
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
21
|
+
},
|
|
22
|
+
secondary: {
|
|
23
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
24
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
25
|
+
},
|
|
26
|
+
destructive: {
|
|
27
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
28
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
29
|
+
},
|
|
30
|
+
muted: {
|
|
31
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
32
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
33
|
+
},
|
|
34
|
+
accent: {
|
|
35
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
36
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
37
|
+
},
|
|
38
|
+
popover: {
|
|
39
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
40
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
41
|
+
},
|
|
42
|
+
card: {
|
|
43
|
+
DEFAULT: 'hsl(var(--card))',
|
|
44
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
borderRadius: {
|
|
48
|
+
lg: 'var(--radius)',
|
|
49
|
+
md: 'calc(var(--radius) - 2px)',
|
|
50
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
plugins: [require('tailwindcss-animate')],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default config;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
NODE_ENV=development
|
|
2
|
+
PORT=4000
|
|
3
|
+
|
|
4
|
+
# Database
|
|
5
|
+
DATABASE_URL=postgresql://pxlr:pxlr_secret_2024@localhost:5432/pxlr_cms
|
|
6
|
+
|
|
7
|
+
# Redis
|
|
8
|
+
REDIS_URL=redis://localhost:6379
|
|
9
|
+
|
|
10
|
+
# MinIO S3 Storage
|
|
11
|
+
MINIO_ENDPOINT=localhost
|
|
12
|
+
MINIO_PORT=9000
|
|
13
|
+
MINIO_ACCESS_KEY=pxlr_minio
|
|
14
|
+
MINIO_SECRET_KEY=pxlr_minio_secret_2024
|
|
15
|
+
MINIO_BUCKET=pxlr-media
|
|
16
|
+
MINIO_USE_SSL=false
|
|
17
|
+
|
|
18
|
+
# JWT Authentication
|
|
19
|
+
JWT_SECRET=change_this_to_a_secure_random_string_in_production
|
|
20
|
+
JWT_EXPIRES_IN=7d
|
|
21
|
+
|
|
22
|
+
# CORS
|
|
23
|
+
CORS_ORIGINS=http://localhost:3000
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# PXLR CMS API Dockerfile
|
|
2
|
+
FROM node:20-alpine
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Install dependencies for sharp (image processing)
|
|
7
|
+
RUN apk add --no-cache \
|
|
8
|
+
python3 \
|
|
9
|
+
make \
|
|
10
|
+
g++ \
|
|
11
|
+
vips-dev
|
|
12
|
+
|
|
13
|
+
# Copy package files
|
|
14
|
+
COPY package.json ./
|
|
15
|
+
|
|
16
|
+
# Install dependencies
|
|
17
|
+
RUN npm install
|
|
18
|
+
|
|
19
|
+
# Copy source code
|
|
20
|
+
COPY . .
|
|
21
|
+
|
|
22
|
+
# Expose port
|
|
23
|
+
EXPOSE 4000
|
|
24
|
+
|
|
25
|
+
# Start development server
|
|
26
|
+
CMD ["npm", "run", "dev"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pxlr/api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "PXLR CMS Backend API",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/index.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"lint": "eslint src --ext .ts",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@fastify/cors": "^10.0.0",
|
|
15
|
+
"@fastify/helmet": "^12.0.0",
|
|
16
|
+
"@fastify/jwt": "^9.0.0",
|
|
17
|
+
"@fastify/multipart": "^9.0.0",
|
|
18
|
+
"@fastify/rate-limit": "^10.0.0",
|
|
19
|
+
"@fastify/swagger": "^9.0.0",
|
|
20
|
+
"@fastify/swagger-ui": "^5.0.0",
|
|
21
|
+
"@fastify/websocket": "^11.0.0",
|
|
22
|
+
"bcryptjs": "^2.4.3",
|
|
23
|
+
"dotenv": "^16.4.5",
|
|
24
|
+
"fastify": "^5.0.0",
|
|
25
|
+
"ioredis": "^5.4.1",
|
|
26
|
+
"minio": "^8.0.1",
|
|
27
|
+
"pg": "^8.13.0",
|
|
28
|
+
"sharp": "^0.33.5",
|
|
29
|
+
"uuid": "^10.0.0",
|
|
30
|
+
"zod": "^3.23.8",
|
|
31
|
+
"pino-pretty": "^13.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/bcryptjs": "^2.4.6",
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"@types/pg": "^8.11.10",
|
|
37
|
+
"@types/uuid": "^10.0.0",
|
|
38
|
+
"eslint": "^9.0.0",
|
|
39
|
+
"tsx": "^4.19.0",
|
|
40
|
+
"typescript": "^5.6.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
|
|
3
|
+
export const config = {
|
|
4
|
+
// Environment
|
|
5
|
+
isDev: process.env.NODE_ENV !== 'production',
|
|
6
|
+
port: parseInt(process.env.PORT || '4000', 10),
|
|
7
|
+
|
|
8
|
+
// Database
|
|
9
|
+
databaseUrl: process.env.DATABASE_URL || 'postgresql://pxlr:pxlr_secret_2024@localhost:5432/pxlr_cms',
|
|
10
|
+
|
|
11
|
+
// Redis
|
|
12
|
+
redisUrl: process.env.REDIS_URL || 'redis://localhost:6379',
|
|
13
|
+
|
|
14
|
+
// JWT
|
|
15
|
+
jwtSecret: process.env.JWT_SECRET || 'pxlr_jwt_secret_change_in_production_2024',
|
|
16
|
+
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
|
17
|
+
|
|
18
|
+
// S3 Storage (MinIO, AWS S3, Cloudflare R2, etc.)
|
|
19
|
+
// Supports both MINIO_* and S3_* env variable prefixes
|
|
20
|
+
s3: {
|
|
21
|
+
endpoint: process.env.S3_ENDPOINT || process.env.MINIO_ENDPOINT || 'localhost',
|
|
22
|
+
port: parseInt(process.env.S3_PORT || process.env.MINIO_PORT || '9000', 10),
|
|
23
|
+
accessKey: process.env.S3_ACCESS_KEY || process.env.MINIO_ACCESS_KEY || 'pxlr_minio',
|
|
24
|
+
secretKey: process.env.S3_SECRET_KEY || process.env.MINIO_SECRET_KEY || 'pxlr_minio_secret_2024',
|
|
25
|
+
bucket: process.env.S3_BUCKET || process.env.MINIO_BUCKET || 'pxlr-media',
|
|
26
|
+
useSSL: (process.env.S3_USE_SSL || process.env.MINIO_USE_SSL) === 'true',
|
|
27
|
+
region: process.env.S3_REGION || 'us-east-1',
|
|
28
|
+
// Public URL for browser access
|
|
29
|
+
publicUrl: process.env.S3_PUBLIC_URL || process.env.MINIO_PUBLIC_URL || 'http://localhost:9010',
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Legacy alias for backwards compatibility
|
|
33
|
+
get minio() {
|
|
34
|
+
return this.s3;
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// CORS
|
|
38
|
+
corsOrigins: (process.env.CORS_ORIGINS || process.env.CORS_ORIGIN || 'http://localhost:3000').split(','),
|
|
39
|
+
} as const;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import { config } from '../config.js';
|
|
3
|
+
|
|
4
|
+
const { Pool } = pg;
|
|
5
|
+
|
|
6
|
+
class Database {
|
|
7
|
+
private pool: pg.Pool | null = null;
|
|
8
|
+
|
|
9
|
+
async connect() {
|
|
10
|
+
this.pool = new Pool({
|
|
11
|
+
connectionString: config.databaseUrl,
|
|
12
|
+
max: 20,
|
|
13
|
+
idleTimeoutMillis: 30000,
|
|
14
|
+
connectionTimeoutMillis: 2000,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Test connection
|
|
18
|
+
const client = await this.pool.connect();
|
|
19
|
+
client.release();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async disconnect() {
|
|
23
|
+
if (this.pool) {
|
|
24
|
+
await this.pool.end();
|
|
25
|
+
this.pool = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async query<T = any>(text: string, params?: any[]): Promise<pg.QueryResult<T>> {
|
|
30
|
+
if (!this.pool) {
|
|
31
|
+
throw new Error('Database not connected');
|
|
32
|
+
}
|
|
33
|
+
return this.pool.query(text, params);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getClient(): Promise<pg.PoolClient> {
|
|
37
|
+
if (!this.pool) {
|
|
38
|
+
throw new Error('Database not connected');
|
|
39
|
+
}
|
|
40
|
+
return this.pool.connect();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Transaction helper
|
|
44
|
+
async transaction<T>(fn: (client: pg.PoolClient) => Promise<T>): Promise<T> {
|
|
45
|
+
const client = await this.getClient();
|
|
46
|
+
try {
|
|
47
|
+
await client.query('BEGIN');
|
|
48
|
+
const result = await fn(client);
|
|
49
|
+
await client.query('COMMIT');
|
|
50
|
+
return result;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
await client.query('ROLLBACK');
|
|
53
|
+
throw error;
|
|
54
|
+
} finally {
|
|
55
|
+
client.release();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const db = new Database();
|