@xleddyl/nuxt-cms 0.1.9

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/dist/module.d.mts +36 -0
  4. package/dist/module.json +9 -0
  5. package/dist/module.mjs +765 -0
  6. package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +15 -0
  7. package/dist/runtime/app/components/cms/BlocksField.vue +110 -0
  8. package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +15 -0
  9. package/dist/runtime/app/components/cms/EmptyState.d.vue.ts +18 -0
  10. package/dist/runtime/app/components/cms/EmptyState.vue +20 -0
  11. package/dist/runtime/app/components/cms/EmptyState.vue.d.ts +18 -0
  12. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +23 -0
  13. package/dist/runtime/app/components/cms/EntryForm.vue +78 -0
  14. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +23 -0
  15. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +16 -0
  16. package/dist/runtime/app/components/cms/FieldInput.vue +85 -0
  17. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +16 -0
  18. package/dist/runtime/app/components/cms/JsonField.d.vue.ts +10 -0
  19. package/dist/runtime/app/components/cms/JsonField.vue +51 -0
  20. package/dist/runtime/app/components/cms/JsonField.vue.d.ts +10 -0
  21. package/dist/runtime/app/components/cms/MediaField.d.vue.ts +16 -0
  22. package/dist/runtime/app/components/cms/MediaField.vue +81 -0
  23. package/dist/runtime/app/components/cms/MediaField.vue.d.ts +16 -0
  24. package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +19 -0
  25. package/dist/runtime/app/components/cms/MediaGallery.vue +309 -0
  26. package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +19 -0
  27. package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +14 -0
  28. package/dist/runtime/app/components/cms/MediaUpload.vue +132 -0
  29. package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +14 -0
  30. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +19 -0
  31. package/dist/runtime/app/components/cms/PageHeader.vue +23 -0
  32. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +19 -0
  33. package/dist/runtime/app/components/cms/RelationField.d.vue.ts +16 -0
  34. package/dist/runtime/app/components/cms/RelationField.vue +87 -0
  35. package/dist/runtime/app/components/cms/RelationField.vue.d.ts +16 -0
  36. package/dist/runtime/app/components/cms/RichTextField.d.vue.ts +10 -0
  37. package/dist/runtime/app/components/cms/RichTextField.vue +123 -0
  38. package/dist/runtime/app/components/cms/RichTextField.vue.d.ts +10 -0
  39. package/dist/runtime/app/components/cms/SlugField.d.vue.ts +14 -0
  40. package/dist/runtime/app/components/cms/SlugField.vue +44 -0
  41. package/dist/runtime/app/components/cms/SlugField.vue.d.ts +14 -0
  42. package/dist/runtime/app/components/cms/TranslatableField.d.vue.ts +15 -0
  43. package/dist/runtime/app/components/cms/TranslatableField.vue +64 -0
  44. package/dist/runtime/app/components/cms/TranslatableField.vue.d.ts +15 -0
  45. package/dist/runtime/app/composables/cms-query.d.ts +4 -0
  46. package/dist/runtime/app/composables/cms-query.js +18 -0
  47. package/dist/runtime/app/composables/cms-runtime.d.ts +5 -0
  48. package/dist/runtime/app/composables/cms-runtime.js +4 -0
  49. package/dist/runtime/app/i18n/en.json +93 -0
  50. package/dist/runtime/app/i18n/it.json +93 -0
  51. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +13 -0
  52. package/dist/runtime/app/layouts/cms-admin.vue +97 -0
  53. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +13 -0
  54. package/dist/runtime/app/middleware/cms-auth.d.ts +2 -0
  55. package/dist/runtime/app/middleware/cms-auth.js +7 -0
  56. package/dist/runtime/app/pages/admin-collection.d.vue.ts +3 -0
  57. package/dist/runtime/app/pages/admin-collection.vue +273 -0
  58. package/dist/runtime/app/pages/admin-collection.vue.d.ts +3 -0
  59. package/dist/runtime/app/pages/admin-entry.d.vue.ts +3 -0
  60. package/dist/runtime/app/pages/admin-entry.vue +156 -0
  61. package/dist/runtime/app/pages/admin-entry.vue.d.ts +3 -0
  62. package/dist/runtime/app/pages/admin-index.d.vue.ts +3 -0
  63. package/dist/runtime/app/pages/admin-index.vue +11 -0
  64. package/dist/runtime/app/pages/admin-index.vue.d.ts +3 -0
  65. package/dist/runtime/app/pages/admin-login.d.vue.ts +3 -0
  66. package/dist/runtime/app/pages/admin-login.vue +87 -0
  67. package/dist/runtime/app/pages/admin-login.vue.d.ts +3 -0
  68. package/dist/runtime/app/pages/admin-media.d.vue.ts +3 -0
  69. package/dist/runtime/app/pages/admin-media.vue +13 -0
  70. package/dist/runtime/app/pages/admin-media.vue.d.ts +3 -0
  71. package/dist/runtime/app/utils/api.d.ts +7 -0
  72. package/dist/runtime/app/utils/api.js +3 -0
  73. package/dist/runtime/app/utils/ui.d.ts +8 -0
  74. package/dist/runtime/app/utils/ui.js +9 -0
  75. package/dist/runtime/assets/main.css +1 -0
  76. package/dist/runtime/server/api/collection.get.d.ts +6 -0
  77. package/dist/runtime/server/api/collection.get.js +19 -0
  78. package/dist/runtime/server/api/collection.post.d.ts +2 -0
  79. package/dist/runtime/server/api/collection.post.js +30 -0
  80. package/dist/runtime/server/api/collection.put.d.ts +2 -0
  81. package/dist/runtime/server/api/collection.put.js +28 -0
  82. package/dist/runtime/server/api/item.delete.d.ts +4 -0
  83. package/dist/runtime/server/api/item.delete.js +19 -0
  84. package/dist/runtime/server/api/item.get.d.ts +2 -0
  85. package/dist/runtime/server/api/item.get.js +18 -0
  86. package/dist/runtime/server/api/item.put.d.ts +2 -0
  87. package/dist/runtime/server/api/item.put.js +40 -0
  88. package/dist/runtime/server/api/media-presign.post.d.ts +11 -0
  89. package/dist/runtime/server/api/media-presign.post.js +51 -0
  90. package/dist/runtime/server/api/media.delete.d.ts +4 -0
  91. package/dist/runtime/server/api/media.delete.js +25 -0
  92. package/dist/runtime/server/api/media.get.d.ts +5 -0
  93. package/dist/runtime/server/api/media.get.js +13 -0
  94. package/dist/runtime/server/api/media.post.d.ts +3 -0
  95. package/dist/runtime/server/api/media.post.js +32 -0
  96. package/dist/runtime/server/api/media.put.d.ts +3 -0
  97. package/dist/runtime/server/api/media.put.js +28 -0
  98. package/dist/runtime/server/plugins/migrate-postgres.d.ts +2 -0
  99. package/dist/runtime/server/plugins/migrate-postgres.js +9 -0
  100. package/dist/runtime/server/plugins/migrate-sqlite.d.ts +2 -0
  101. package/dist/runtime/server/plugins/migrate-sqlite.js +9 -0
  102. package/dist/runtime/server/plugins/session-check.d.ts +2 -0
  103. package/dist/runtime/server/plugins/session-check.js +9 -0
  104. package/dist/runtime/server/routes/auth/login.post.d.ts +4 -0
  105. package/dist/runtime/server/routes/auth/login.post.js +62 -0
  106. package/dist/runtime/server/routes/graphql.d.ts +2 -0
  107. package/dist/runtime/server/routes/graphql.js +34 -0
  108. package/dist/runtime/server/tsconfig.json +3 -0
  109. package/dist/runtime/server/utils/custom-id.d.ts +2 -0
  110. package/dist/runtime/server/utils/custom-id.js +10 -0
  111. package/dist/runtime/server/utils/db-errors.d.ts +1 -0
  112. package/dist/runtime/server/utils/db-errors.js +31 -0
  113. package/dist/runtime/server/utils/db-postgres.d.ts +3 -0
  114. package/dist/runtime/server/utils/db-postgres.js +16 -0
  115. package/dist/runtime/server/utils/db-sqlite.d.ts +4 -0
  116. package/dist/runtime/server/utils/db-sqlite.js +17 -0
  117. package/dist/runtime/server/utils/graphql-depth.d.ts +4 -0
  118. package/dist/runtime/server/utils/graphql-depth.js +72 -0
  119. package/dist/runtime/server/utils/graphql.d.ts +14 -0
  120. package/dist/runtime/server/utils/graphql.js +317 -0
  121. package/dist/runtime/server/utils/media.d.ts +24 -0
  122. package/dist/runtime/server/utils/media.js +38 -0
  123. package/dist/runtime/server/utils/registry.d.ts +20 -0
  124. package/dist/runtime/server/utils/registry.js +47 -0
  125. package/dist/runtime/server/utils/relations.d.ts +12 -0
  126. package/dist/runtime/server/utils/relations.js +80 -0
  127. package/dist/runtime/server/utils/require-admin.d.ts +2 -0
  128. package/dist/runtime/server/utils/require-admin.js +13 -0
  129. package/dist/runtime/shared/empty-config.d.ts +2 -0
  130. package/dist/runtime/shared/empty-config.js +1 -0
  131. package/dist/runtime/shared/graphql-sdl.d.ts +5 -0
  132. package/dist/runtime/shared/graphql-sdl.js +147 -0
  133. package/dist/runtime/shared/index.d.ts +62 -0
  134. package/dist/runtime/shared/index.js +55 -0
  135. package/dist/runtime/shared/validation.d.ts +16 -0
  136. package/dist/runtime/shared/validation.js +82 -0
  137. package/dist/types.d.mts +3 -0
  138. package/package.json +93 -0
@@ -0,0 +1,2 @@
1
+ declare const _default: import("#app").RouteMiddleware;
2
+ export default _default;
@@ -0,0 +1,7 @@
1
+ import { defineNuxtRouteMiddleware, navigateTo, useUserSession } from "#imports";
2
+ export default defineNuxtRouteMiddleware(() => {
3
+ const { loggedIn } = useUserSession();
4
+ if (!loggedIn.value) {
5
+ return navigateTo("/cms/login");
6
+ }
7
+ });
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,273 @@
1
+ <template>
2
+ <div class="cms-rise flex flex-col gap-7">
3
+ <CmsPageHeader :kicker="kicker" :title="config.label">
4
+ <UButton
5
+ v-if="config.kind === 'single'"
6
+ type="submit"
7
+ :form="FORM_ID"
8
+ :label="t('cms.form.save')"
9
+ :loading="saving"
10
+ class="rounded-full px-6"
11
+ />
12
+ <div v-if="config.kind === 'collection'" class="flex items-center gap-2">
13
+ <UDropdownMenu
14
+ v-if="rows.length"
15
+ :items="columnItems"
16
+ :content="{ align: 'end' }"
17
+ :ui="{ content: 'w-48' }"
18
+ >
19
+ <UButton
20
+ :label="t('cms.collection.columns')"
21
+ icon="i-lucide-columns-3"
22
+ variant="subtle"
23
+ class="rounded-full px-4"
24
+ />
25
+ </UDropdownMenu>
26
+ <UButton
27
+ :label="t('cms.collection.new')"
28
+ icon="i-lucide-plus"
29
+ class="rounded-full px-4"
30
+ @click="openCreate"
31
+ />
32
+ </div>
33
+ </CmsPageHeader>
34
+
35
+ <div v-if="config.kind === 'single'" class="cms-card p-7">
36
+ <CmsEntryForm
37
+ v-model="formState"
38
+ :fields="config.fields"
39
+ :form-id="FORM_ID"
40
+ :loading="saving"
41
+ @submit="saveSingle"
42
+ />
43
+ </div>
44
+
45
+ <template v-else>
46
+ <div v-if="rows.length" class="cms-card overflow-hidden">
47
+ <UTable
48
+ v-model:column-visibility="columnVisibility"
49
+ :data="rows"
50
+ :columns="columns"
51
+ :ui="tableUi"
52
+ @select="onSelect"
53
+ >
54
+ <template v-if="drafts" #status-cell="{ row }">
55
+ <button
56
+ type="button"
57
+ class="cms-badge"
58
+ :class="row.original.status === 'published' ? 'is-published' : 'is-draft'"
59
+ :title="
60
+ row.original.status === 'published' ? t('cms.status.unpublish') : t('cms.status.publish')
61
+ "
62
+ @click.stop="toggleStatus(row.original)"
63
+ >
64
+ {{
65
+ row.original.status === "published" ? t("cms.status.published") : t("cms.status.draft")
66
+ }}
67
+ </button>
68
+ </template>
69
+ <template #actions-cell="{ row }">
70
+ <div class="flex justify-end">
71
+ <UButton
72
+ icon="i-lucide-trash-2"
73
+ variant="ghost"
74
+ color="error"
75
+ size="xs"
76
+ class="rounded-full"
77
+ @click.stop="deleteRow(row.original)"
78
+ />
79
+ </div>
80
+ </template>
81
+ </UTable>
82
+ </div>
83
+
84
+ <CmsEmptyState
85
+ v-else
86
+ icon="i-lucide-sprout"
87
+ :title="t('cms.collection.emptyTitle')"
88
+ :body="t('cms.collection.emptyBody')"
89
+ >
90
+ <UButton
91
+ :label="t('cms.collection.new')"
92
+ icon="i-lucide-plus"
93
+ variant="subtle"
94
+ class="mt-3 rounded-full px-4"
95
+ @click="openCreate"
96
+ />
97
+ </CmsEmptyState>
98
+ </template>
99
+ </div>
100
+ </template>
101
+
102
+ <script setup>
103
+ import { isTranslatableField } from "#nuxt-cms";
104
+ import {
105
+ computed,
106
+ createError,
107
+ definePageMeta,
108
+ navigateTo,
109
+ onMounted,
110
+ ref,
111
+ useFetch,
112
+ useI18n,
113
+ useRoute,
114
+ useToast,
115
+ watch
116
+ } from "#imports";
117
+ import cmsConfig from "#cms-config";
118
+ import { useCmsRuntime } from "../composables/cms-runtime";
119
+ import { cmsApi } from "../utils/api";
120
+ import { errorMessage } from "../utils/ui";
121
+ definePageMeta({
122
+ layout: "cms-admin",
123
+ middleware: "cms-auth",
124
+ validate: (route2) => Object.hasOwn(cmsConfig, route2.params.collection),
125
+ key: (route2) => route2.fullPath
126
+ });
127
+ const FORM_ID = "cms-single-form";
128
+ const route = useRoute();
129
+ const toast = useToast();
130
+ const { t } = useI18n();
131
+ const name = route.params.collection;
132
+ const config = cmsConfig[name];
133
+ if (!config) {
134
+ throw createError({ statusCode: 404, statusMessage: "Unknown collection", fatal: true });
135
+ }
136
+ const fieldKeys = Object.keys(config.fields);
137
+ const drafts = config.kind === "collection" && !!config.drafts;
138
+ const formKeys = drafts ? [...fieldKeys, "status"] : fieldKeys;
139
+ const endpoint = `/api/cms/admin/${name}`;
140
+ const { data, refresh } = await useFetch(endpoint);
141
+ const rows = computed(() => Array.isArray(data.value) ? data.value : []);
142
+ const tableUi = {
143
+ thead: "border-b border-(--cms-line)",
144
+ th: "cms-kicker px-5 py-3 font-normal",
145
+ td: "px-5 py-3.5 text-sm text-(--ui-text)",
146
+ tr: "__clickable hover:bg-(--cms-paper)/50"
147
+ };
148
+ const saving = ref(false);
149
+ const formState = ref({});
150
+ function emptyState() {
151
+ return Object.fromEntries(formKeys.map((k) => [k, null]));
152
+ }
153
+ function pickFields(row) {
154
+ return Object.fromEntries(formKeys.map((k) => [k, row[k] ?? null]));
155
+ }
156
+ async function submit(action) {
157
+ saving.value = true;
158
+ try {
159
+ await action();
160
+ await refresh();
161
+ return true;
162
+ } catch (error) {
163
+ toast.add({
164
+ title: t("cms.toast.saveFailed"),
165
+ description: errorMessage(error),
166
+ color: "error"
167
+ });
168
+ return false;
169
+ } finally {
170
+ saving.value = false;
171
+ }
172
+ }
173
+ if (config.kind === "single") {
174
+ formState.value = data.value && !Array.isArray(data.value) ? pickFields(data.value) : emptyState();
175
+ }
176
+ async function saveSingle() {
177
+ const ok = await submit(() => cmsApi(endpoint, { method: "PUT", body: formState.value }));
178
+ if (ok) toast.add({ title: t("cms.toast.saved"), color: "success" });
179
+ }
180
+ const { i18n: contentI18n } = useCmsRuntime();
181
+ function stripHtml(html) {
182
+ return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
183
+ }
184
+ function truncate(value) {
185
+ return value.length > 60 ? `${value.slice(0, 57)}\u2026` : value;
186
+ }
187
+ function displayValue(field, value) {
188
+ if (value == null || value === "") return "";
189
+ if (isTranslatableField(field)) {
190
+ const record = value;
191
+ const raw = record[contentI18n.defaultLocale] ?? Object.values(record)[0] ?? "";
192
+ return truncate(field.type === "richtext" ? stripHtml(raw) : raw);
193
+ }
194
+ switch (field.type) {
195
+ case "boolean":
196
+ return value ? "\u2713" : "\u2014";
197
+ case "richtext":
198
+ return truncate(stripHtml(String(value)));
199
+ case "json":
200
+ return truncate(JSON.stringify(value));
201
+ case "blocks":
202
+ return `${value.length} \u25A4`;
203
+ default:
204
+ return truncate(String(value));
205
+ }
206
+ }
207
+ const columns = computed(() => [
208
+ ...fieldKeys.map((key) => ({
209
+ id: key,
210
+ accessorFn: (row) => displayValue(config.fields[key], row[key]),
211
+ header: config.fields[key].label
212
+ })),
213
+ ...drafts ? [{ accessorKey: "status", header: t("cms.collection.status") }] : [],
214
+ { id: "actions", header: "" }
215
+ ]);
216
+ const DEFAULT_VISIBLE_COLUMNS = 4;
217
+ const columnStorageKey = `cms:columns:${name}`;
218
+ const columnVisibility = ref(
219
+ Object.fromEntries(fieldKeys.map((key, index) => [key, index < DEFAULT_VISIBLE_COLUMNS]))
220
+ );
221
+ onMounted(() => {
222
+ const stored = localStorage.getItem(columnStorageKey);
223
+ if (stored) {
224
+ try {
225
+ const parsed = JSON.parse(stored);
226
+ columnVisibility.value = Object.fromEntries(
227
+ fieldKeys.map((key) => [key, parsed[key] ?? columnVisibility.value[key] ?? false])
228
+ );
229
+ } catch {
230
+ }
231
+ }
232
+ watch(
233
+ columnVisibility,
234
+ (value) => localStorage.setItem(columnStorageKey, JSON.stringify(value)),
235
+ { deep: true }
236
+ );
237
+ });
238
+ const columnItems = computed(
239
+ () => fieldKeys.map((key) => ({
240
+ label: config.fields[key].label,
241
+ type: "checkbox",
242
+ checked: columnVisibility.value[key],
243
+ onUpdateChecked(checked) {
244
+ columnVisibility.value = { ...columnVisibility.value, [key]: checked };
245
+ },
246
+ onSelect(event) {
247
+ event.preventDefault();
248
+ }
249
+ }))
250
+ );
251
+ const kicker = computed(
252
+ () => config.kind === "single" ? t("cms.collection.kickerSingle") : t("cms.collection.kicker", rows.value.length)
253
+ );
254
+ function onSelect(_event, row) {
255
+ navigateTo(`/cms/${name}/${row.original.id}`);
256
+ }
257
+ async function toggleStatus(row) {
258
+ const next = row.status === "published" ? "draft" : "published";
259
+ await submit(
260
+ () => cmsApi(`${endpoint}/${row.id}`, {
261
+ method: "PUT",
262
+ body: { ...pickFields(row), status: next }
263
+ })
264
+ );
265
+ }
266
+ function openCreate() {
267
+ navigateTo(`/cms/${name}/new`);
268
+ }
269
+ async function deleteRow(row) {
270
+ if (!confirm(t("cms.collection.deleteConfirm"))) return;
271
+ await submit(() => cmsApi(`${endpoint}/${row.id}`, { method: "DELETE" }));
272
+ }
273
+ </script>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,156 @@
1
+ <template>
2
+ <div class="cms-rise flex flex-col gap-7">
3
+ <CmsPageHeader
4
+ :kicker="config.label"
5
+ :title="isNew ? t('cms.collection.new') : t('cms.collection.edit')"
6
+ >
7
+ <template v-if="drafts" #badge>
8
+ <span
9
+ class="cms-badge pointer-events-none"
10
+ :class="published ? 'is-published' : 'is-draft'"
11
+ >
12
+ {{ published ? t("cms.status.published") : t("cms.status.draft") }}
13
+ </span>
14
+ </template>
15
+ <div class="flex items-center gap-3">
16
+ <UButton
17
+ :label="t('cms.collection.back')"
18
+ icon="i-lucide-arrow-left"
19
+ variant="subtle"
20
+ class="rounded-full px-4"
21
+ @click="goBack"
22
+ />
23
+ <UButton
24
+ type="submit"
25
+ :form="FORM_ID"
26
+ :label="t('cms.form.save')"
27
+ :loading="saving"
28
+ class="rounded-full px-6"
29
+ />
30
+ <UButton
31
+ v-if="drafts"
32
+ type="submit"
33
+ :form="FORM_ID"
34
+ :label="published ? t('cms.status.unpublish') : t('cms.status.publish')"
35
+ :loading="saving"
36
+ class="rounded-full px-6"
37
+ @click="togglePublished"
38
+ />
39
+ </div>
40
+ </CmsPageHeader>
41
+
42
+ <div class="cms-card p-7">
43
+ <CmsEntryForm
44
+ v-model="formState"
45
+ :fields="config.fields"
46
+ :drafts="drafts"
47
+ :form-id="FORM_ID"
48
+ :loading="saving"
49
+ @submit="save"
50
+ @error="revertStatus"
51
+ />
52
+ </div>
53
+ </div>
54
+ </template>
55
+
56
+ <script setup>
57
+ import {
58
+ computed,
59
+ createError,
60
+ definePageMeta,
61
+ navigateTo,
62
+ onBeforeRouteLeave,
63
+ onBeforeUnmount,
64
+ ref,
65
+ useFetch,
66
+ useI18n,
67
+ useRoute,
68
+ useToast,
69
+ watch
70
+ } from "#imports";
71
+ import cmsConfig from "#cms-config";
72
+ import { cmsApi } from "../utils/api";
73
+ import { errorMessage } from "../utils/ui";
74
+ const FORM_ID = "cms-entry-form";
75
+ definePageMeta({
76
+ layout: "cms-admin",
77
+ middleware: "cms-auth",
78
+ validate: (route2) => Object.hasOwn(cmsConfig, route2.params.collection),
79
+ key: (route2) => route2.fullPath
80
+ });
81
+ const route = useRoute();
82
+ const toast = useToast();
83
+ const { t } = useI18n();
84
+ const name = route.params.collection;
85
+ const config = cmsConfig[name];
86
+ if (!config || config.kind !== "collection") {
87
+ throw createError({ statusCode: 404, statusMessage: "Unknown collection", fatal: true });
88
+ }
89
+ const id = route.params.id;
90
+ const isNew = id === void 0;
91
+ const drafts = !!config.drafts;
92
+ const fieldKeys = Object.keys(config.fields);
93
+ const formKeys = drafts ? [...fieldKeys, "status"] : fieldKeys;
94
+ const endpoint = `/api/cms/admin/${name}`;
95
+ function emptyState() {
96
+ return Object.fromEntries(formKeys.map((k) => [k, null]));
97
+ }
98
+ function pickFields(row) {
99
+ return Object.fromEntries(formKeys.map((k) => [k, row[k] ?? null]));
100
+ }
101
+ const formState = ref(emptyState());
102
+ if (!isNew) {
103
+ const { data } = await useFetch(`${endpoint}/${id}`);
104
+ if (!data.value) {
105
+ throw createError({ statusCode: 404, statusMessage: "Entry not found", fatal: true });
106
+ }
107
+ formState.value = pickFields(data.value);
108
+ }
109
+ const saving = ref(false);
110
+ const snapshot = ref(JSON.stringify(formState.value));
111
+ const dirty = computed(() => JSON.stringify(formState.value) !== snapshot.value);
112
+ function onBeforeUnload(event) {
113
+ event.preventDefault();
114
+ }
115
+ watch(dirty, (value) => {
116
+ if (value) window.addEventListener("beforeunload", onBeforeUnload);
117
+ else window.removeEventListener("beforeunload", onBeforeUnload);
118
+ });
119
+ onBeforeUnmount(() => window.removeEventListener("beforeunload", onBeforeUnload));
120
+ onBeforeRouteLeave(() => {
121
+ if (dirty.value && !confirm(t("cms.entry.unsavedChanges"))) return false;
122
+ });
123
+ const published = computed({
124
+ get: () => formState.value.status === "published",
125
+ set: (value) => {
126
+ formState.value.status = value ? "published" : "draft";
127
+ }
128
+ });
129
+ function togglePublished() {
130
+ published.value = !published.value;
131
+ }
132
+ function revertStatus() {
133
+ if (!drafts) return;
134
+ formState.value.status = JSON.parse(snapshot.value).status ?? null;
135
+ }
136
+ function goBack() {
137
+ navigateTo(`/cms/${name}`);
138
+ }
139
+ async function save() {
140
+ saving.value = true;
141
+ try {
142
+ await (isNew ? cmsApi(endpoint, { method: "POST", body: formState.value }) : cmsApi(`${endpoint}/${id}`, { method: "PUT", body: formState.value }));
143
+ snapshot.value = JSON.stringify(formState.value);
144
+ toast.add({ title: t("cms.toast.saved"), color: "success" });
145
+ goBack();
146
+ } catch (error) {
147
+ toast.add({
148
+ title: t("cms.toast.saveFailed"),
149
+ description: errorMessage(error),
150
+ color: "error"
151
+ });
152
+ } finally {
153
+ saving.value = false;
154
+ }
155
+ }
156
+ </script>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div />
3
+ </template>
4
+
5
+ <script setup>
6
+ import { definePageMeta, navigateTo } from "#imports";
7
+ import cmsConfig from "#cms-config";
8
+ definePageMeta({ layout: "cms-admin", middleware: "cms-auth" });
9
+ const first = Object.keys(cmsConfig)[0];
10
+ await navigateTo(first ? `/cms/${first}` : "/cms/media", { replace: true });
11
+ </script>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <div class="cms-scope cms-canvas cms-grain flex min-h-screen items-center justify-center p-6">
3
+ <div class="flex w-full max-w-sm flex-col gap-8">
4
+ <header class="cms-rise flex flex-col items-center gap-3 text-center">
5
+ <h1 class="cms-display text-(--ui-text-highlighted) text-4xl font-medium">
6
+ {{ t("cms.login.title") }}<span class="text-(--cms-fern)">.</span>
7
+ </h1>
8
+ <p class="text-(--ui-text-muted) text-sm">
9
+ {{ t("cms.login.subtitle") }}
10
+ </p>
11
+ </header>
12
+
13
+ <div class="cms-card cms-rise p-6 sm:p-7" style="animation-delay: 90ms">
14
+ <UForm :state="state" class="flex flex-col gap-5" @submit="login">
15
+ <UAlert
16
+ v-if="error"
17
+ color="error"
18
+ variant="subtle"
19
+ :title="error"
20
+ class="rounded-xl"
21
+ />
22
+ <UFormField :label="t('cms.login.email')" name="email" required :ui="CMS_FIELD_UI">
23
+ <UInput
24
+ v-model="state.email"
25
+ type="email"
26
+ size="lg"
27
+ autocomplete="username"
28
+ placeholder="you@example.com"
29
+ class="w-full"
30
+ />
31
+ </UFormField>
32
+ <UFormField
33
+ :label="t('cms.login.password')"
34
+ name="password"
35
+ required
36
+ :ui="CMS_FIELD_UI"
37
+ >
38
+ <UInput
39
+ v-model="state.password"
40
+ type="password"
41
+ size="lg"
42
+ autocomplete="current-password"
43
+ placeholder="••••••••"
44
+ class="w-full"
45
+ />
46
+ </UFormField>
47
+ <UButton
48
+ type="submit"
49
+ :label="t('cms.login.submit')"
50
+ size="lg"
51
+ block
52
+ :loading="loading"
53
+ class="mt-1 rounded-full"
54
+ />
55
+ </UForm>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </template>
60
+
61
+ <script setup>
62
+ import { definePageMeta, navigateTo, ref, useI18n, useUserSession } from "#imports";
63
+ import { CMS_FIELD_UI, errorMessage } from "../utils/ui";
64
+ definePageMeta({ layout: false });
65
+ const { loggedIn, fetch: refreshSession } = useUserSession();
66
+ if (loggedIn.value) {
67
+ await navigateTo("/cms", { replace: true });
68
+ }
69
+ const { t } = useI18n();
70
+ const state = ref({ email: "", password: "" });
71
+ const error = ref(null);
72
+ const loading = ref(false);
73
+ async function login() {
74
+ loading.value = true;
75
+ error.value = null;
76
+ try {
77
+ await $fetch("/api/cms/auth/login", { method: "POST", body: state.value });
78
+ await refreshSession();
79
+ await navigateTo("/cms", { replace: true });
80
+ } catch (e) {
81
+ const status = e.statusCode;
82
+ error.value = status === 401 ? t("cms.login.invalid") : errorMessage(e) ?? t("cms.login.failed");
83
+ } finally {
84
+ loading.value = false;
85
+ }
86
+ }
87
+ </script>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <div class="cms-rise flex flex-col gap-7">
3
+ <CmsPageHeader :kicker="t('cms.media.kicker')" :title="t('cms.media.title')" />
4
+
5
+ <CmsMediaGallery />
6
+ </div>
7
+ </template>
8
+
9
+ <script setup>
10
+ import { definePageMeta, useI18n } from "#imports";
11
+ definePageMeta({ layout: "cms-admin", middleware: "cms-auth" });
12
+ const { t } = useI18n();
13
+ </script>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,7 @@
1
+ interface CmsApiOptions {
2
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
3
+ body?: unknown;
4
+ query?: Record<string, unknown>;
5
+ }
6
+ export declare function cmsApi<T = unknown>(url: string, options?: CmsApiOptions): Promise<T>;
7
+ export {};
@@ -0,0 +1,3 @@
1
+ export function cmsApi(url, options) {
2
+ return $fetch(url, options);
3
+ }
@@ -0,0 +1,8 @@
1
+ export declare const CMS_FIELD_UI: {
2
+ label: string;
3
+ };
4
+ export declare const CMS_MODAL_UI: {
5
+ content: string;
6
+ title: string;
7
+ };
8
+ export declare function errorMessage(error: unknown): string | undefined;
@@ -0,0 +1,9 @@
1
+ export const CMS_FIELD_UI = { label: "text-[13px] font-medium text-(--ui-text-toned)" };
2
+ export const CMS_MODAL_UI = {
3
+ content: "rounded-2xl shadow-xl sm:max-w-3xl",
4
+ title: "cms-display text-xl font-medium text-(--ui-text-highlighted)"
5
+ };
6
+ export function errorMessage(error) {
7
+ const err = error;
8
+ return err.data?.message ?? err.message;
9
+ }
@@ -0,0 +1 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&family=Hanken+Grotesk:ital,wght@0,300..800;1,300..800&family=Fragment+Mono:ital@0;1&display=swap");@import "tailwindcss";@import "@nuxt/ui";@source "../app";.cms-scope,body:has(.cms-scope){--cms-paper:#f6f4ef;--cms-surface:#fdfcfa;--cms-field:#f1ede4;--cms-ink:#262119;--cms-line:#e8e3d8;--cms-line-strong:#d8d1c1;--cms-fern:#3e6b4f;--cms-sage:#e3ebe1;--cms-sage-ink:#2e5138;--cms-shadow-soft:0 1px 2px rgba(38,33,25,.05),0 16px 40px -24px rgba(38,33,25,.22);color-scheme:light;--ui-text-dimmed:#a69d8b;--ui-text-muted:#877e6c;--ui-text-toned:#60594a;--ui-text:#403a2f;--ui-text-highlighted:var(--cms-ink);--ui-text-inverted:#fffdf8;--ui-bg:var(--cms-surface);--ui-bg-muted:var(--cms-paper);--ui-bg-elevated:var(--cms-field);--ui-bg-accented:#e7e1d3;--ui-bg-inverted:var(--cms-ink);--ui-border:var(--cms-line);--ui-border-muted:var(--cms-line);--ui-border-accented:var(--cms-line-strong);--ui-border-inverted:var(--cms-ink);--ui-primary:var(--cms-fern);--ui-radius:0.4rem;color:var(--ui-text);font-family:Hanken Grotesk,ui-sans-serif,system-ui,sans-serif;-webkit-font-smoothing:antialiased}body:has(.cms-scope) ::-moz-selection{background:var(--cms-sage);color:var(--cms-sage-ink)}body:has(.cms-scope) ::selection{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-display{font-family:Poppins,ui-sans-serif,system-ui,sans-serif;letter-spacing:-.02em}.cms-kicker{color:var(--ui-text-dimmed);font-family:Fragment Mono,ui-monospace,monospace;font-size:.6875rem;letter-spacing:.16em;line-height:1.4;text-transform:uppercase}body:has(.cms-scope) :is(button,a,label,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]){@apply cursor-pointer transition-all duration-200 select-none}body:has(.cms-scope) :is(button,a,label,[role=button],[role=menuitem],[role=option],.__clickable):not(:disabled,[aria-disabled=true]):active{@apply transition-none;scale:.99}.cms-canvas{background-attachment:fixed;background-color:var(--cms-paper);background-image:radial-gradient(50rem 34rem at 10% -10%,rgba(62,107,79,.07),transparent 62%),radial-gradient(44rem 30rem at 108% 112%,rgba(193,149,87,.09),transparent 62%)}.cms-grain:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='a'%3E%3CfeTurbulence baseFrequency='.8' numOctaves='2' stitchTiles='stitch' type='fractalNoise'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");content:"";inset:0;mix-blend-mode:multiply;opacity:.035;pointer-events:none;position:fixed;z-index:50}.cms-card{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:1rem;box-shadow:var(--cms-shadow-soft)}.cms-empty{background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem}.cms-navlink{align-items:center;border-radius:.75rem;color:var(--ui-text-toned);display:flex;font-size:.875rem;font-weight:500;gap:.625rem;padding:.5rem .75rem;transition:background-color .15s ease,color .15s ease}.cms-navlink:hover{background:rgba(38,33,25,.05);color:var(--ui-text-highlighted)}.cms-navlink.is-active{background:var(--cms-sage);color:var(--cms-sage-ink);font-weight:600}.cms-navlink.is-active .cms-navlink-icon{color:var(--cms-fern)}.cms-badge{border:1px solid transparent;border-radius:9999px;cursor:pointer;font-family:Fragment Mono,ui-monospace,monospace;font-size:.625rem;letter-spacing:.12em;padding:.25rem .625rem;text-transform:uppercase;transition:background-color .15s ease,border-color .15s ease}.cms-badge.is-published{background:var(--cms-sage);color:var(--cms-sage-ink)}.cms-badge.is-published:hover{background:#d6e2d3}.cms-badge.is-draft{background:transparent;border:1px dashed var(--cms-line-strong);color:var(--ui-text-muted)}.cms-badge.is-draft:hover{background:rgba(38,33,25,.04)}.cms-dropzone{align-items:center;background:rgba(253,252,250,.5);border:1.5px dashed var(--cms-line-strong);border-radius:1rem;color:var(--ui-text-muted);cursor:pointer;display:flex;flex-direction:column;gap:.5rem;justify-content:center;padding:2rem 1.5rem;transition:border-color .15s ease,background-color .15s ease,color .15s ease;width:100%}.cms-dropzone.is-over,.cms-dropzone:hover{background:var(--cms-sage);border-color:var(--cms-fern);color:var(--cms-sage-ink)}.cms-dropzone.is-dense{border-radius:.75rem;flex-direction:row;gap:.625rem;padding:.875rem 1.25rem}.cms-dropzone.is-busy{opacity:.7;pointer-events:none}.cms-pill{background:var(--cms-surface);border:1px solid var(--cms-line);border-radius:9999px;color:var(--ui-text-dimmed);cursor:pointer;font-family:Fragment Mono,ui-monospace,monospace;font-size:.625rem;letter-spacing:.12em;padding:.3rem .8rem;transition:background-color .15s ease,color .15s ease}.cms-pill:hover{background:rgba(38,33,25,.05);color:var(--ui-text)}.cms-pill.is-active{background:var(--cms-sage);color:var(--cms-sage-ink)}@keyframes cms-rise{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}.cms-rise{animation:cms-rise .55s cubic-bezier(.22,1,.36,1) both}@media (prefers-reduced-motion:reduce){.cms-rise{animation:none}}.cms-richtext{border:1px solid var(--cms-line);border-radius:.75rem;overflow:hidden}.cms-richtext-toolbar{align-items:center;border-bottom:1px solid var(--cms-line);display:flex;flex-wrap:wrap;gap:2px;padding:4px}.cms-richtext-body{font-size:.875rem;min-height:9rem;outline:none;padding:.75rem 1rem}.cms-richtext-body h2{font-size:1.25rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body h3{font-size:1.1rem;font-weight:600;margin:.75em 0 .35em}.cms-richtext-body p{margin:.35em 0}.cms-richtext-body ul{list-style:disc;padding-left:1.25rem}.cms-richtext-body ol{list-style:decimal;padding-left:1.25rem}.cms-richtext-body blockquote{border-left:3px solid var(--cms-line);color:var(--ui-text-muted);padding-left:.75rem}
@@ -0,0 +1,6 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ [x: string]: any;
3
+ } | {
4
+ [x: string]: any;
5
+ }[] | null>>;
6
+ export default _default;