@quoin-cms/admin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/biome.json +62 -0
- package/dist/assets/index-C9Y5-AKj.js +33 -0
- package/dist/assets/index-uVdiUjty.css +1 -0
- package/dist/index.html +20 -0
- package/index.html +19 -0
- package/package.json +43 -0
- package/src/AdminRoot.svelte +98 -0
- package/src/app.css +211 -0
- package/src/lib/Slot.svelte +65 -0
- package/src/lib/api/auth.ts +26 -0
- package/src/lib/api/client.ts +73 -0
- package/src/lib/api/files.ts +56 -0
- package/src/lib/api/globals.ts +13 -0
- package/src/lib/api/records.ts +102 -0
- package/src/lib/api/schema.ts +7 -0
- package/src/lib/api/versions.ts +40 -0
- package/src/lib/components/AdminHeader.svelte +107 -0
- package/src/lib/components/AdminSidebar.svelte +262 -0
- package/src/lib/components/DeleteDialog.svelte +58 -0
- package/src/lib/components/DocumentEditLayout.svelte +263 -0
- package/src/lib/components/DynamicForm.svelte +74 -0
- package/src/lib/components/KpiCard.svelte +75 -0
- package/src/lib/components/MediaLibrary.svelte +311 -0
- package/src/lib/components/Pagination.svelte +78 -0
- package/src/lib/components/RangeFilter.svelte +41 -0
- package/src/lib/components/RecordGrid.svelte +123 -0
- package/src/lib/components/RecordTable.svelte +156 -0
- package/src/lib/components/cells/CheckboxCell.svelte +10 -0
- package/src/lib/components/cells/ColorCell.svelte +15 -0
- package/src/lib/components/cells/DateCell.svelte +8 -0
- package/src/lib/components/cells/RelationshipCell.svelte +20 -0
- package/src/lib/components/cells/RichTextCell.svelte +21 -0
- package/src/lib/components/cells/SelectCell.svelte +26 -0
- package/src/lib/components/cells/TextCell.svelte +8 -0
- package/src/lib/components/cells/UploadCell.svelte +34 -0
- package/src/lib/components/cells/index.ts +28 -0
- package/src/lib/components/charts/TimeSeriesChart.svelte +184 -0
- package/src/lib/components/doc/ApiView.svelte +181 -0
- package/src/lib/components/doc/Autosave.svelte +102 -0
- package/src/lib/components/doc/DocHeader.svelte +86 -0
- package/src/lib/components/doc/DocMetaStrip.svelte +103 -0
- package/src/lib/components/doc/DocTabBar.svelte +26 -0
- package/src/lib/components/doc/HeaderModeSwitch.svelte +32 -0
- package/src/lib/components/doc/PublishButton.svelte +114 -0
- package/src/lib/components/doc/ScheduleModal.svelte +110 -0
- package/src/lib/components/doc/VersionHistory.svelte +20 -0
- package/src/lib/components/fields/ArrayFieldEditor.svelte +62 -0
- package/src/lib/components/fields/BlockCard.svelte +63 -0
- package/src/lib/components/fields/BlocksFieldEditor.svelte +83 -0
- package/src/lib/components/fields/CheckboxField.svelte +27 -0
- package/src/lib/components/fields/ColorField.svelte +46 -0
- package/src/lib/components/fields/DateField.svelte +52 -0
- package/src/lib/components/fields/EmailField.svelte +30 -0
- package/src/lib/components/fields/FileField.svelte +280 -0
- package/src/lib/components/fields/JsonField.svelte +145 -0
- package/src/lib/components/fields/NumberField.svelte +44 -0
- package/src/lib/components/fields/PasswordField.svelte +38 -0
- package/src/lib/components/fields/RelationshipField.svelte +271 -0
- package/src/lib/components/fields/RichTextField.svelte +139 -0
- package/src/lib/components/fields/SelectField.svelte +33 -0
- package/src/lib/components/fields/SlugField.svelte +70 -0
- package/src/lib/components/fields/TabsField.svelte +56 -0
- package/src/lib/components/fields/TagsField.svelte +85 -0
- package/src/lib/components/fields/TextField.svelte +36 -0
- package/src/lib/components/fields/TextareaField.svelte +32 -0
- package/src/lib/components/fields/UploadField.svelte +166 -0
- package/src/lib/components/fields/UploadFieldDispatch.svelte +21 -0
- package/src/lib/components/fields/UploadGalleryField.svelte +166 -0
- package/src/lib/components/fields/index.ts +22 -0
- package/src/lib/components/fields/registry.ts +58 -0
- package/src/lib/components/lexical/CustomHTMLComponent.svelte +52 -0
- package/src/lib/components/lexical/CustomHTMLNode.ts +94 -0
- package/src/lib/components/lexical/PullQuoteComponent.svelte +73 -0
- package/src/lib/components/lexical/PullQuoteNode.ts +112 -0
- package/src/lib/components/lexical/lexical-helpers.ts +24 -0
- package/src/lib/components/lexical/nodes.ts +8 -0
- package/src/lib/components/lexical/toolbar/EditorToolbar.svelte +159 -0
- package/src/lib/components/lexical/toolbar/InsertBlockDropdown.svelte +278 -0
- package/src/lib/components/versions/CompareSelector.svelte +31 -0
- package/src/lib/components/versions/FieldDiff.svelte +141 -0
- package/src/lib/components/versions/RestoreModal.svelte +67 -0
- package/src/lib/components/versions/StatusPill.svelte +21 -0
- package/src/lib/context.svelte.ts +156 -0
- package/src/lib/router/index.svelte.ts +282 -0
- package/src/lib/router/matcher.ts +52 -0
- package/src/lib/stores/branding.svelte.ts +74 -0
- package/src/lib/stores/schema.svelte.ts +17 -0
- package/src/lib/types/schema.ts +126 -0
- package/src/lib/utils/cn.ts +6 -0
- package/src/lib/utils/diff.ts +112 -0
- package/src/lib/utils/dirty.svelte.ts +50 -0
- package/src/lib/utils/format.ts +28 -0
- package/src/lib/utils/json-highlight.ts +34 -0
- package/src/lib/utils/slug.ts +8 -0
- package/src/main.ts +32 -0
- package/src/views/AdminLayout.svelte +73 -0
- package/src/views/AdsAnalyticsView.svelte +152 -0
- package/src/views/CollectionEditView.svelte +117 -0
- package/src/views/CollectionListView.svelte +347 -0
- package/src/views/CollectionNewView.svelte +68 -0
- package/src/views/CustomPageView.svelte +59 -0
- package/src/views/DashboardView.svelte +370 -0
- package/src/views/GlobalEditView.svelte +100 -0
- package/src/views/LoginView.svelte +231 -0
- package/src/views/NotFoundView.svelte +9 -0
- package/src/views/VersionDetailView.svelte +307 -0
- package/src/views/VersionsListView.svelte +201 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +80 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$lib/router/index.svelte.js'
|
|
3
|
+
import { resolve } from '$lib/router/index.svelte.js'
|
|
4
|
+
import { goto } from '$lib/router/index.svelte.js'
|
|
5
|
+
import { schema, getCollectionByKey } from '$lib/stores/schema.svelte.js'
|
|
6
|
+
import { listVersions, restoreVersion } from '$lib/api/versions.js'
|
|
7
|
+
import type { Version } from '$lib/api/versions.js'
|
|
8
|
+
import StatusPill from '$lib/components/versions/StatusPill.svelte'
|
|
9
|
+
import RestoreModal from '$lib/components/versions/RestoreModal.svelte'
|
|
10
|
+
import Pagination from '$lib/components/Pagination.svelte'
|
|
11
|
+
import { toast } from 'svelte-sonner'
|
|
12
|
+
import { RotateCcw } from 'lucide-svelte'
|
|
13
|
+
import { onMount } from 'svelte'
|
|
14
|
+
|
|
15
|
+
let collectionKey = $derived(page.params.collection ?? '')
|
|
16
|
+
let recordId = $derived(page.params.id ?? '')
|
|
17
|
+
let collection = $derived(getCollectionByKey(schema.collections, collectionKey))
|
|
18
|
+
|
|
19
|
+
let versions = $state<Version[]>([])
|
|
20
|
+
let isLoading = $state(true)
|
|
21
|
+
let currentPage = $state(1)
|
|
22
|
+
let perPage = 20
|
|
23
|
+
let totalRecords = $state(0)
|
|
24
|
+
|
|
25
|
+
let restoreModalOpen = $state(false)
|
|
26
|
+
let restoreTargetId = $state('')
|
|
27
|
+
let isRestoring = $state(false)
|
|
28
|
+
|
|
29
|
+
let totalPages = $derived(Math.max(1, Math.ceil(totalRecords / perPage)))
|
|
30
|
+
|
|
31
|
+
let latestPublishedId = $derived.by(() => {
|
|
32
|
+
const v = versions.find((v) => v._status === 'published' && v.latest)
|
|
33
|
+
return v?.id ?? versions.find((v) => v._status === 'published')?.id ?? ''
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
let latestDraftId = $derived.by(() => {
|
|
37
|
+
const v = versions.find((v) => v._status === 'draft' && v.latest)
|
|
38
|
+
return v?.id ?? ''
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
let hasDrafts = $derived(collection?.versions?.drafts != null)
|
|
42
|
+
|
|
43
|
+
async function loadVersions(page: number) {
|
|
44
|
+
isLoading = true
|
|
45
|
+
const result = await listVersions(collectionKey, recordId, { page, limit: perPage })
|
|
46
|
+
isLoading = false
|
|
47
|
+
if (result.ok) {
|
|
48
|
+
versions = result.data.versions ?? []
|
|
49
|
+
totalRecords = result.data.totalRecords ?? 0
|
|
50
|
+
currentPage = page
|
|
51
|
+
} else {
|
|
52
|
+
toast.error(result.error)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onMount(() => {
|
|
57
|
+
loadVersions(1)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
function formatDate(iso: string): string {
|
|
61
|
+
if (!iso) return ''
|
|
62
|
+
return new Date(iso).toLocaleString()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function timeAgo(iso: string): string {
|
|
66
|
+
if (!iso) return ''
|
|
67
|
+
const diff = Date.now() - new Date(iso).getTime()
|
|
68
|
+
const seconds = Math.floor(diff / 1000)
|
|
69
|
+
if (seconds < 60) return `${seconds}s ago`
|
|
70
|
+
const minutes = Math.floor(seconds / 60)
|
|
71
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
72
|
+
const hours = Math.floor(minutes / 60)
|
|
73
|
+
if (hours < 24) return `${hours}h ago`
|
|
74
|
+
const days = Math.floor(hours / 24)
|
|
75
|
+
return `${days}d ago`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function openRestore(id: string) {
|
|
79
|
+
restoreTargetId = id
|
|
80
|
+
restoreModalOpen = true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleRestore(asDraft: boolean) {
|
|
84
|
+
isRestoring = true
|
|
85
|
+
const result = await restoreVersion(collectionKey, recordId, restoreTargetId, asDraft)
|
|
86
|
+
isRestoring = false
|
|
87
|
+
restoreModalOpen = false
|
|
88
|
+
if (result.ok) {
|
|
89
|
+
toast.success('Version restored')
|
|
90
|
+
await goto(resolve(`/${collectionKey}/${recordId}`))
|
|
91
|
+
} else {
|
|
92
|
+
toast.error(result.error)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handlePageChange(p: number) {
|
|
97
|
+
loadVersions(p)
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
{#if !collection}
|
|
102
|
+
<div class="flex h-full items-center justify-center">
|
|
103
|
+
<p class="text-muted-foreground">Collection not found</p>
|
|
104
|
+
</div>
|
|
105
|
+
{:else if isLoading}
|
|
106
|
+
<div class="flex h-full items-center justify-center">
|
|
107
|
+
<div class="flex flex-col items-center gap-3">
|
|
108
|
+
<div class="h-6 w-6 rounded-full border-2 border-primary/20 border-t-primary animate-spin-slow"></div>
|
|
109
|
+
<p class="text-sm text-muted-foreground">Loading...</p>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
{:else}
|
|
113
|
+
<div class="space-y-5">
|
|
114
|
+
<!-- Breadcrumb -->
|
|
115
|
+
<nav class="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
116
|
+
<a href={resolve(`/${collectionKey}`)} class="hover:text-foreground">{collection.labelPlural || collection.label}</a>
|
|
117
|
+
<span>/</span>
|
|
118
|
+
<a href={resolve(`/${collectionKey}/${recordId}`)} class="hover:text-foreground">{recordId}</a>
|
|
119
|
+
<span>/</span>
|
|
120
|
+
<span class="text-foreground font-medium">Versions</span>
|
|
121
|
+
</nav>
|
|
122
|
+
|
|
123
|
+
<h1 class="text-xl font-semibold text-foreground">Version History</h1>
|
|
124
|
+
|
|
125
|
+
{#if versions.length === 0}
|
|
126
|
+
<div class="flex flex-col items-center justify-center py-16">
|
|
127
|
+
<p class="text-sm text-muted-foreground">No versions found</p>
|
|
128
|
+
</div>
|
|
129
|
+
{:else}
|
|
130
|
+
<Pagination
|
|
131
|
+
page={currentPage}
|
|
132
|
+
{totalPages}
|
|
133
|
+
{totalRecords}
|
|
134
|
+
{perPage}
|
|
135
|
+
onPageChange={handlePageChange}
|
|
136
|
+
class="border-b border-border/60"
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
<div class="overflow-x-auto rounded-lg border border-border/80 bg-card shadow-sm">
|
|
140
|
+
<table class="w-full text-sm">
|
|
141
|
+
<thead>
|
|
142
|
+
<tr class="border-b border-border/60 bg-secondary/40">
|
|
143
|
+
<th class="px-4 py-2.5 text-left font-medium text-muted-foreground">Date</th>
|
|
144
|
+
<th class="px-4 py-2.5 text-left font-medium text-muted-foreground">Status</th>
|
|
145
|
+
<th class="px-4 py-2.5 text-left font-medium text-muted-foreground">Age</th>
|
|
146
|
+
<th class="px-4 py-2.5 text-right font-medium text-muted-foreground">Actions</th>
|
|
147
|
+
</tr>
|
|
148
|
+
</thead>
|
|
149
|
+
<tbody>
|
|
150
|
+
{#each versions as v (v.id)}
|
|
151
|
+
<tr class="border-b border-border/40 transition-colors hover:bg-secondary/30">
|
|
152
|
+
<td class="px-4 py-2.5">
|
|
153
|
+
<a
|
|
154
|
+
href={resolve(`/${collectionKey}/${recordId}/versions/${v.id}`)}
|
|
155
|
+
class="text-primary hover:underline"
|
|
156
|
+
>
|
|
157
|
+
{formatDate(v.createdAt)}
|
|
158
|
+
</a>
|
|
159
|
+
</td>
|
|
160
|
+
<td class="px-4 py-2.5">
|
|
161
|
+
<StatusPill
|
|
162
|
+
status={v._status ?? ''}
|
|
163
|
+
isLatest={v.id === latestPublishedId || v.id === latestDraftId}
|
|
164
|
+
/>
|
|
165
|
+
</td>
|
|
166
|
+
<td class="px-4 py-2.5 text-muted-foreground">
|
|
167
|
+
{timeAgo(v.createdAt)}
|
|
168
|
+
</td>
|
|
169
|
+
<td class="px-4 py-2.5 text-right">
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
|
173
|
+
title="Restore this version"
|
|
174
|
+
onclick={() => openRestore(v.id)}
|
|
175
|
+
>
|
|
176
|
+
<RotateCcw class="h-4 w-4" />
|
|
177
|
+
</button>
|
|
178
|
+
</td>
|
|
179
|
+
</tr>
|
|
180
|
+
{/each}
|
|
181
|
+
</tbody>
|
|
182
|
+
</table>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<Pagination
|
|
186
|
+
page={currentPage}
|
|
187
|
+
{totalPages}
|
|
188
|
+
{totalRecords}
|
|
189
|
+
{perPage}
|
|
190
|
+
onPageChange={handlePageChange}
|
|
191
|
+
/>
|
|
192
|
+
{/if}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<RestoreModal
|
|
196
|
+
bind:open={restoreModalOpen}
|
|
197
|
+
{hasDrafts}
|
|
198
|
+
{isRestoring}
|
|
199
|
+
onRestore={handleRestore}
|
|
200
|
+
/>
|
|
201
|
+
{/if}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@tsconfig/svelte/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"allowJs": true,
|
|
14
|
+
"checkJs": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"types": ["svelte", "vite/client"],
|
|
18
|
+
"baseUrl": ".",
|
|
19
|
+
"paths": {
|
|
20
|
+
"$lib": ["src/lib"],
|
|
21
|
+
"$lib/*": ["src/lib/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.svelte", "vite.config.ts"]
|
|
25
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
|
2
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
3
|
+
import { defineConfig, type Plugin } from 'vite'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Svelte 5 SPA — NOT SvelteKit.
|
|
8
|
+
*
|
|
9
|
+
* This config is used for @quoin-cms/admin's own standalone `pnpm dev` / `pnpm
|
|
10
|
+
* build` during LIBRARY development (contributors iterating on admin source
|
|
11
|
+
* without a real consumer). Consumer projects use @quoin-cms/cli, which wraps
|
|
12
|
+
* Vite with its own quoinPlugin that injects consumer-owned config +
|
|
13
|
+
* override components.
|
|
14
|
+
*
|
|
15
|
+
* Here we provide stub versions of the two virtual modules so `main.ts`
|
|
16
|
+
* imports resolve to a minimal working config.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
function stubVirtualModulesPlugin(): Plugin {
|
|
20
|
+
const CONFIG_ID = 'virtual:quoin-config'
|
|
21
|
+
const IMPORT_MAP_ID = 'virtual:quoin-import-map'
|
|
22
|
+
const resolvedConfig = '\0' + CONFIG_ID
|
|
23
|
+
const resolvedImportMap = '\0' + IMPORT_MAP_ID
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
name: 'quoin:stub-virtual-modules',
|
|
27
|
+
resolveId(id) {
|
|
28
|
+
if (id === CONFIG_ID) return resolvedConfig
|
|
29
|
+
if (id === IMPORT_MAP_ID) return resolvedImportMap
|
|
30
|
+
return null
|
|
31
|
+
},
|
|
32
|
+
load(id) {
|
|
33
|
+
if (id === resolvedConfig) {
|
|
34
|
+
return `export default ${JSON.stringify(
|
|
35
|
+
{
|
|
36
|
+
apiBase: '/api',
|
|
37
|
+
basePath: '/admin',
|
|
38
|
+
buildOutDir: './dist',
|
|
39
|
+
devPort: 5173,
|
|
40
|
+
brand: { name: 'Quoin', logo: null },
|
|
41
|
+
fields: {},
|
|
42
|
+
slots: {},
|
|
43
|
+
views: {},
|
|
44
|
+
pages: {},
|
|
45
|
+
},
|
|
46
|
+
null,
|
|
47
|
+
2,
|
|
48
|
+
)};`
|
|
49
|
+
}
|
|
50
|
+
if (id === resolvedImportMap) {
|
|
51
|
+
return 'export default {};'
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default defineConfig({
|
|
59
|
+
plugins: [tailwindcss(), svelte(), stubVirtualModulesPlugin()],
|
|
60
|
+
resolve: {
|
|
61
|
+
alias: {
|
|
62
|
+
$lib: resolve(__dirname, 'src/lib'),
|
|
63
|
+
},
|
|
64
|
+
dedupe: ['svelte', 'lexical', 'svelte-lexical', '@tanstack/svelte-query'],
|
|
65
|
+
},
|
|
66
|
+
optimizeDeps: {
|
|
67
|
+
include: ['lexical', 'svelte-lexical', '@tanstack/svelte-query'],
|
|
68
|
+
},
|
|
69
|
+
base: '/admin/',
|
|
70
|
+
build: {
|
|
71
|
+
outDir: 'dist',
|
|
72
|
+
emptyOutDir: true,
|
|
73
|
+
},
|
|
74
|
+
server: {
|
|
75
|
+
port: 5173,
|
|
76
|
+
proxy: {
|
|
77
|
+
'/api': 'http://localhost:8090',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
})
|