@quoin-cms/admin 0.2.0 → 0.3.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/package.json +4 -1
- package/src/AdminRoot.svelte +5 -1
- package/src/lib/api/auth.ts +3 -6
- package/src/lib/api/files.ts +3 -2
- package/src/lib/components/AdminSidebar.svelte +23 -2
- package/src/lib/components/DocumentEditLayout.svelte +17 -0
- package/src/lib/components/DynamicForm.svelte +3 -3
- package/src/lib/components/MediaLibrary.svelte +55 -7
- package/src/lib/components/UploadCreateView.svelte +173 -0
- package/src/lib/components/doc/ApiView.svelte +95 -103
- package/src/lib/components/doc/RenderJson.svelte +93 -0
- package/src/lib/components/fields/RichTextField.svelte +5 -0
- package/src/lib/components/fields/UploadField.svelte +26 -34
- package/src/lib/components/fields/UploadGalleryField.svelte +28 -37
- package/src/lib/components/lexical/BlockField.svelte +41 -0
- package/src/lib/components/lexical/BlockHost.svelte +85 -0
- package/src/lib/components/lexical/BlockNode.ts +102 -0
- package/src/lib/components/lexical/block-defaults.ts +40 -0
- package/src/lib/components/lexical/lexical-helpers.ts +3 -0
- package/src/lib/components/lexical/nodes.ts +2 -0
- package/src/lib/components/lexical/toolbar/InsertBlockDropdown.svelte +27 -2
- package/src/lib/context.svelte.ts +1 -0
- package/src/lib/types/schema.ts +5 -0
- package/src/views/CollectionNewView.svelte +3 -0
- package/src/views/LoginView.svelte +47 -23
- package/biome.json +0 -62
- package/dist/assets/index-BaOy5Of3.js +0 -32
- package/dist/assets/index-DINUk481.css +0 -1
- package/dist/index.html +0 -20
- package/index.html +0 -19
- package/tsconfig.json +0 -25
- package/vite.config.ts +0 -80
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component } from 'svelte'
|
|
3
|
+
import { getContext } from 'svelte'
|
|
4
|
+
import { getEditor } from 'svelte-lexical'
|
|
5
|
+
import { getQuoinContext, loadComponent } from '$lib/context.svelte.js'
|
|
6
|
+
import { getNodeByKey, isBlockNode } from './lexical-helpers.js'
|
|
7
|
+
import BlockField from './BlockField.svelte'
|
|
8
|
+
import type { BlockDef } from './block-defaults.js'
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
blockType = '',
|
|
12
|
+
data = {},
|
|
13
|
+
nodeKey = '',
|
|
14
|
+
}: { blockType?: string; data?: Record<string, unknown>; nodeKey?: string } = $props()
|
|
15
|
+
|
|
16
|
+
const editor = getEditor()
|
|
17
|
+
const ctx = getQuoinContext()
|
|
18
|
+
const blocks = getContext<BlockDef[]>('quoin.lexical.blocks') ?? []
|
|
19
|
+
|
|
20
|
+
const def = $derived(blocks.find((b) => b.slug === blockType))
|
|
21
|
+
const customPath = $derived(def ? ctx.config.editorBlocks?.[blockType] : undefined)
|
|
22
|
+
|
|
23
|
+
// svelte-ignore state_referenced_locally
|
|
24
|
+
let model = $state<Record<string, unknown>>(structuredClone($state.snapshot(data)) ?? {})
|
|
25
|
+
|
|
26
|
+
function commit(snapshot: Record<string, unknown>): void {
|
|
27
|
+
editor.update(() => {
|
|
28
|
+
const node = getNodeByKey(nodeKey)
|
|
29
|
+
if (isBlockNode(node)) node.setData(snapshot)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let primed = false
|
|
34
|
+
$effect(() => {
|
|
35
|
+
const snap = $state.snapshot(model)
|
|
36
|
+
if (!primed) {
|
|
37
|
+
primed = true
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
const id = setTimeout(() => commit(snap), 250)
|
|
41
|
+
return () => clearTimeout(id)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
let CustomComp = $state<Component<any> | null>(null)
|
|
45
|
+
let customError = $state(false)
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (customPath) {
|
|
48
|
+
customError = false
|
|
49
|
+
loadComponent(customPath, ctx.importMap)
|
|
50
|
+
.then((c) => (CustomComp = c))
|
|
51
|
+
.catch(() => {
|
|
52
|
+
CustomComp = null
|
|
53
|
+
customError = true
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
function setData(next: Record<string, unknown>): void {
|
|
59
|
+
model = next
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<div class="my-3 rounded-md border bg-card p-3">
|
|
64
|
+
{#if !def}
|
|
65
|
+
<p class="text-sm text-destructive">Unknown block: {blockType}</p>
|
|
66
|
+
{:else}
|
|
67
|
+
<div class="mb-2 flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
|
68
|
+
{def.label}
|
|
69
|
+
</div>
|
|
70
|
+
{#if customPath}
|
|
71
|
+
{#if CustomComp}
|
|
72
|
+
{@const C = CustomComp}
|
|
73
|
+
<C schema={def.fields} data={model} {setData} {BlockField} />
|
|
74
|
+
{:else if customError}
|
|
75
|
+
<p class="text-sm text-destructive">Failed to load editor component for block "{blockType}".</p>
|
|
76
|
+
{/if}
|
|
77
|
+
{:else}
|
|
78
|
+
<div class="space-y-2">
|
|
79
|
+
{#each def.fields as f (f.name)}
|
|
80
|
+
<BlockField field={f} bind:data={model} />
|
|
81
|
+
{/each}
|
|
82
|
+
</div>
|
|
83
|
+
{/if}
|
|
84
|
+
{/if}
|
|
85
|
+
</div>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { DecoratorNode } from 'lexical';
|
|
2
|
+
import type {
|
|
3
|
+
EditorConfig,
|
|
4
|
+
LexicalEditor,
|
|
5
|
+
LexicalNode,
|
|
6
|
+
NodeKey,
|
|
7
|
+
SerializedLexicalNode,
|
|
8
|
+
Spread,
|
|
9
|
+
} from 'lexical';
|
|
10
|
+
import BlockHost from './BlockHost.svelte';
|
|
11
|
+
|
|
12
|
+
export type SerializedBlockNode = Spread<
|
|
13
|
+
{
|
|
14
|
+
type: 'block';
|
|
15
|
+
version: 1;
|
|
16
|
+
blockType: string;
|
|
17
|
+
data: Record<string, unknown>;
|
|
18
|
+
},
|
|
19
|
+
SerializedLexicalNode
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* BlockNode is the single generic decorator node for all custom blocks.
|
|
24
|
+
* __blockType holds the block's slug; the block schema + optional editor
|
|
25
|
+
* component are resolved at render time by BlockHost.
|
|
26
|
+
*/
|
|
27
|
+
export class BlockNode extends DecoratorNode<unknown> {
|
|
28
|
+
__blockType: string;
|
|
29
|
+
__data: Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
static getType(): string {
|
|
32
|
+
return 'block';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static clone(node: BlockNode): BlockNode {
|
|
36
|
+
return new BlockNode(node.__blockType, node.__data, node.__key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
constructor(blockType: string, data: Record<string, unknown>, key?: NodeKey) {
|
|
40
|
+
super(key);
|
|
41
|
+
this.__blockType = blockType;
|
|
42
|
+
this.__data = data ?? {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
createDOM(_config: EditorConfig): HTMLElement {
|
|
46
|
+
const div = document.createElement('div');
|
|
47
|
+
div.className = 'lexical-block';
|
|
48
|
+
div.setAttribute('contenteditable', 'false');
|
|
49
|
+
return div;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
updateDOM(): boolean {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static importJSON(json: SerializedBlockNode): BlockNode {
|
|
57
|
+
return new BlockNode(json.blockType, json.data ?? {});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
exportJSON(): SerializedBlockNode {
|
|
61
|
+
return {
|
|
62
|
+
...super.exportJSON(),
|
|
63
|
+
type: 'block',
|
|
64
|
+
version: 1,
|
|
65
|
+
blockType: this.__blockType,
|
|
66
|
+
data: this.__data,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
decorate(_editor: LexicalEditor, _config: EditorConfig) {
|
|
71
|
+
return {
|
|
72
|
+
componentClass: BlockHost,
|
|
73
|
+
updateProps: (props: Record<string, unknown>) => {
|
|
74
|
+
props.blockType = this.__blockType;
|
|
75
|
+
props.data = this.__data;
|
|
76
|
+
props.nodeKey = this.__key;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
isInline(): boolean {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getData(): Record<string, unknown> {
|
|
86
|
+
return this.__data;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setData(data: Record<string, unknown>): this {
|
|
90
|
+
const writable = this.getWritable();
|
|
91
|
+
writable.__data = data;
|
|
92
|
+
return writable;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function $createBlockNode(blockType: string, data: Record<string, unknown> = {}): BlockNode {
|
|
97
|
+
return new BlockNode(blockType, data);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function $isBlockNode(node: LexicalNode | null | undefined): node is BlockNode {
|
|
101
|
+
return node instanceof BlockNode;
|
|
102
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FieldSchema } from '$lib/types/schema.js'
|
|
2
|
+
|
|
3
|
+
export interface BlockDef {
|
|
4
|
+
slug: string
|
|
5
|
+
label: string
|
|
6
|
+
icon?: string
|
|
7
|
+
fields: FieldSchema[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build an empty data object for a block from its field schema. Each field
|
|
12
|
+
* gets a type-appropriate zero value (or its declared defaultValue). Used when
|
|
13
|
+
* inserting a fresh block node.
|
|
14
|
+
*/
|
|
15
|
+
export function defaultBlockData(fields: FieldSchema[]): Record<string, unknown> {
|
|
16
|
+
const out: Record<string, unknown> = {}
|
|
17
|
+
for (const f of fields) {
|
|
18
|
+
const dv = (f as { defaultValue?: unknown }).defaultValue
|
|
19
|
+
if (dv !== undefined) {
|
|
20
|
+
out[f.name] = dv
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
switch (f.type as string) {
|
|
24
|
+
case 'checkbox':
|
|
25
|
+
out[f.name] = false
|
|
26
|
+
break
|
|
27
|
+
case 'number':
|
|
28
|
+
out[f.name] = null
|
|
29
|
+
break
|
|
30
|
+
case 'array':
|
|
31
|
+
case 'blocks':
|
|
32
|
+
case 'tags':
|
|
33
|
+
out[f.name] = []
|
|
34
|
+
break
|
|
35
|
+
default:
|
|
36
|
+
out[f.name] = ''
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
@@ -15,6 +15,9 @@ export { $isCustomHTMLNode as isCustomHTMLNode } from './CustomHTMLNode.js';
|
|
|
15
15
|
export { $createPullQuoteNode as createPullQuoteNode } from './PullQuoteNode.js';
|
|
16
16
|
export { $createCustomHTMLNode as createCustomHTMLNode } from './CustomHTMLNode.js';
|
|
17
17
|
|
|
18
|
+
export { $isBlockNode as isBlockNode } from './BlockNode.js';
|
|
19
|
+
export { $createBlockNode as createBlockNode } from './BlockNode.js';
|
|
20
|
+
|
|
18
21
|
export { $createYouTubeNode as createYouTubeNode } from 'svelte-lexical';
|
|
19
22
|
export { $createImageNode as createImageNode } from 'svelte-lexical';
|
|
20
23
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Klass, LexicalNode } from 'lexical';
|
|
2
2
|
import { PullQuoteNode } from './PullQuoteNode.js';
|
|
3
3
|
import { CustomHTMLNode } from './CustomHTMLNode.js';
|
|
4
|
+
import { BlockNode } from './BlockNode.js';
|
|
4
5
|
|
|
5
6
|
export const customNodes: Array<Klass<LexicalNode>> = [
|
|
6
7
|
PullQuoteNode,
|
|
7
8
|
CustomHTMLNode,
|
|
9
|
+
BlockNode,
|
|
8
10
|
];
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { getContext } from 'svelte';
|
|
2
3
|
import { getEditor } from 'svelte-lexical';
|
|
3
|
-
import { insertNodes, createParagraphNode, createPullQuoteNode, createCustomHTMLNode, createYouTubeNode, createImageNode } from '../lexical-helpers.js';
|
|
4
|
-
import {
|
|
4
|
+
import { insertNodes, createParagraphNode, createPullQuoteNode, createCustomHTMLNode, createYouTubeNode, createImageNode, createBlockNode } from '../lexical-helpers.js';
|
|
5
|
+
import { defaultBlockData } from '../block-defaults.js';
|
|
6
|
+
import type { BlockDef } from '../block-defaults.js';
|
|
7
|
+
import { Plus, Youtube, Quote, Code2, Image as ImageIcon, Blocks as BlocksIcon } from 'lucide-svelte';
|
|
5
8
|
import MediaLibrary from '../../MediaLibrary.svelte';
|
|
6
9
|
|
|
7
10
|
const editor = getEditor();
|
|
8
11
|
|
|
12
|
+
const customBlocks = getContext<BlockDef[]>('quoin.lexical.blocks') ?? [];
|
|
13
|
+
|
|
14
|
+
function insertBlock(b: BlockDef) {
|
|
15
|
+
editor.update(() => {
|
|
16
|
+
const node = createBlockNode(b.slug, defaultBlockData(b.fields));
|
|
17
|
+
insertNodes([node]);
|
|
18
|
+
const paragraph = createParagraphNode();
|
|
19
|
+
node.insertAfter(paragraph);
|
|
20
|
+
});
|
|
21
|
+
closeDropdown();
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
let isOpen = $state(false);
|
|
10
25
|
let showYoutubeDialog = $state(false);
|
|
11
26
|
let showImageDialog = $state(false);
|
|
@@ -188,6 +203,16 @@ function handleImageKeydown(e: KeyboardEvent) {
|
|
|
188
203
|
<Code2 class="h-4 w-4" />
|
|
189
204
|
Custom HTML
|
|
190
205
|
</button>
|
|
206
|
+
{#each customBlocks as b (b.slug)}
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
|
|
210
|
+
onclick={() => insertBlock(b)}
|
|
211
|
+
>
|
|
212
|
+
<BlocksIcon class="h-4 w-4" />
|
|
213
|
+
{b.label}
|
|
214
|
+
</button>
|
|
215
|
+
{/each}
|
|
191
216
|
</div>
|
|
192
217
|
{/if}
|
|
193
218
|
</div>
|
package/src/lib/types/schema.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface FieldSchema {
|
|
|
25
25
|
label: string
|
|
26
26
|
required: boolean
|
|
27
27
|
hidden?: boolean
|
|
28
|
+
/** Hides the field from admin edit/list views (API still returns it). */
|
|
29
|
+
adminHidden?: boolean
|
|
28
30
|
/** Sub-variant for text fields (e.g. "password"). Phase 21 D-06.
|
|
29
31
|
* Emitted by field.Text.ToJSON() when Text.Type != "". */
|
|
30
32
|
variant?: string
|
|
@@ -93,6 +95,9 @@ export interface CollectionSchema {
|
|
|
93
95
|
fields: FieldSchema[]
|
|
94
96
|
admin?: AdminConfig
|
|
95
97
|
versions?: VersionsSchema
|
|
98
|
+
/** Present when the collection is an upload collection (col.Upload != nil).
|
|
99
|
+
* Signals the admin to render a file dropzone on create/edit. */
|
|
100
|
+
upload?: { accept?: string[]; imageVariants?: boolean }
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
export interface GlobalSchema {
|
|
@@ -5,6 +5,7 @@ import { resolve } from '$lib/router/index.svelte.js'
|
|
|
5
5
|
import { schema, getCollectionByKey } from '$lib/stores/schema.svelte.js'
|
|
6
6
|
import { createRecord } from '$lib/api/records.js'
|
|
7
7
|
import DocumentEditLayout from '$lib/components/DocumentEditLayout.svelte'
|
|
8
|
+
import UploadCreateView from '$lib/components/UploadCreateView.svelte'
|
|
8
9
|
import { toast } from 'svelte-sonner'
|
|
9
10
|
import { onMount } from 'svelte'
|
|
10
11
|
|
|
@@ -57,6 +58,8 @@ async function handleSave(data: Record<string, any>) {
|
|
|
57
58
|
<p class="text-sm text-muted-foreground">Creating draft...</p>
|
|
58
59
|
</div>
|
|
59
60
|
</div>
|
|
61
|
+
{:else if collection.upload}
|
|
62
|
+
<UploadCreateView {collection} />
|
|
60
63
|
{:else}
|
|
61
64
|
<DocumentEditLayout
|
|
62
65
|
{collection}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { goto } from '$lib/router/index.svelte.js'
|
|
3
3
|
import { resolve } from '$lib/router/index.svelte.js'
|
|
4
|
-
import { login, register, getMe
|
|
4
|
+
import { login, register, getMe } from '$lib/api/auth.js'
|
|
5
5
|
import { toast } from 'svelte-sonner'
|
|
6
6
|
import { onMount } from 'svelte'
|
|
7
7
|
import { BookOpen } from 'lucide-svelte'
|
|
@@ -9,9 +9,10 @@ import { branding, loadBranding } from '$lib/stores/branding.svelte.js'
|
|
|
9
9
|
import Slot from '$lib/Slot.svelte'
|
|
10
10
|
|
|
11
11
|
let email = $state('')
|
|
12
|
+
let name = $state('')
|
|
12
13
|
let password = $state('')
|
|
13
14
|
let confirmPassword = $state('')
|
|
14
|
-
let
|
|
15
|
+
let isCreatingAccount = $state(false)
|
|
15
16
|
let isLoading = $state(false)
|
|
16
17
|
let isCheckingAuth = $state(true)
|
|
17
18
|
|
|
@@ -22,21 +23,19 @@ onMount(async () => {
|
|
|
22
23
|
await goto(resolve('/'))
|
|
23
24
|
return
|
|
24
25
|
}
|
|
25
|
-
const status = await getSetupStatus()
|
|
26
|
-
if (status.ok && status.data.needsSetup) {
|
|
27
|
-
isSetup = true
|
|
28
|
-
}
|
|
29
26
|
isCheckingAuth = false
|
|
30
27
|
})
|
|
31
28
|
|
|
32
29
|
async function handleLogin() {
|
|
33
|
-
|
|
30
|
+
const trimmedEmail = email.trim()
|
|
31
|
+
const trimmedPassword = password.trim()
|
|
32
|
+
if (!trimmedEmail || !trimmedPassword) {
|
|
34
33
|
toast.error('Please enter email and password')
|
|
35
34
|
return
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
isLoading = true
|
|
39
|
-
const result = await login(
|
|
38
|
+
const result = await login(trimmedEmail, trimmedPassword)
|
|
40
39
|
isLoading = false
|
|
41
40
|
|
|
42
41
|
if (result.ok) {
|
|
@@ -50,32 +49,41 @@ async function handleLogin() {
|
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
async function
|
|
54
|
-
|
|
52
|
+
async function handleCreateAccount() {
|
|
53
|
+
const trimmedName = name.trim()
|
|
54
|
+
const trimmedEmail = email.trim()
|
|
55
|
+
const trimmedPassword = password.trim()
|
|
56
|
+
const trimmedConfirmPassword = confirmPassword.trim()
|
|
57
|
+
|
|
58
|
+
if (!trimmedName) {
|
|
59
|
+
toast.error('Please enter your name')
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if (!trimmedEmail || !trimmedPassword) {
|
|
55
63
|
toast.error('Please enter email and password')
|
|
56
64
|
return
|
|
57
65
|
}
|
|
58
|
-
if (
|
|
66
|
+
if (trimmedPassword !== trimmedConfirmPassword) {
|
|
59
67
|
toast.error('Passwords do not match')
|
|
60
68
|
return
|
|
61
69
|
}
|
|
62
|
-
if (
|
|
70
|
+
if (trimmedPassword.length < 6) {
|
|
63
71
|
toast.error('Password must be at least 6 characters')
|
|
64
72
|
return
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
isLoading = true
|
|
68
|
-
const result = await register(
|
|
76
|
+
const result = await register(trimmedEmail, trimmedPassword, trimmedName)
|
|
69
77
|
isLoading = false
|
|
70
78
|
|
|
71
79
|
if (result.ok) {
|
|
72
80
|
toast.success('Admin account created! Logging in...')
|
|
73
|
-
const loginResult = await login(
|
|
81
|
+
const loginResult = await login(trimmedEmail, trimmedPassword)
|
|
74
82
|
if (loginResult.ok) {
|
|
75
83
|
await goto(resolve('/'))
|
|
76
84
|
} else {
|
|
77
85
|
toast.error('Account created but login failed. Please try logging in.')
|
|
78
|
-
|
|
86
|
+
isCreatingAccount = false
|
|
79
87
|
}
|
|
80
88
|
} else {
|
|
81
89
|
toast.error(result.error)
|
|
@@ -83,7 +91,7 @@ async function handleSetup() {
|
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
function toggleMode() {
|
|
86
|
-
|
|
94
|
+
isCreatingAccount = !isCreatingAccount
|
|
87
95
|
confirmPassword = ''
|
|
88
96
|
}
|
|
89
97
|
</script>
|
|
@@ -149,20 +157,36 @@ function toggleMode() {
|
|
|
149
157
|
|
|
150
158
|
<div class="mb-8">
|
|
151
159
|
<h2 class="text-xl font-semibold tracking-tight" style="font-family: var(--font-display);">
|
|
152
|
-
{
|
|
160
|
+
{isCreatingAccount ? 'Create your account' : 'Sign in'}
|
|
153
161
|
</h2>
|
|
154
162
|
<p class="mt-1.5 text-sm text-muted-foreground">
|
|
155
|
-
{
|
|
163
|
+
{isCreatingAccount ? 'Create the first admin account to get started.' : 'Enter your credentials to continue.'}
|
|
156
164
|
</p>
|
|
157
165
|
</div>
|
|
158
166
|
|
|
159
167
|
<form
|
|
160
168
|
onsubmit={(e) => {
|
|
161
169
|
e.preventDefault();
|
|
162
|
-
|
|
170
|
+
isCreatingAccount ? handleCreateAccount() : handleLogin();
|
|
163
171
|
}}
|
|
164
172
|
class="space-y-5"
|
|
165
173
|
>
|
|
174
|
+
{#if isCreatingAccount}
|
|
175
|
+
<div>
|
|
176
|
+
<label for="name" class="mb-2 block text-[13px] font-medium text-foreground">
|
|
177
|
+
Name
|
|
178
|
+
</label>
|
|
179
|
+
<input
|
|
180
|
+
id="name"
|
|
181
|
+
type="text"
|
|
182
|
+
bind:value={name}
|
|
183
|
+
class="h-11 w-full rounded-lg border border-border bg-card px-3.5 text-sm shadow-sm transition-all placeholder:text-muted-foreground/50 focus:border-primary/40 focus:outline-none focus:ring-2 focus:ring-primary/10"
|
|
184
|
+
placeholder="Your name"
|
|
185
|
+
autocomplete="name"
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
{/if}
|
|
189
|
+
|
|
166
190
|
<div>
|
|
167
191
|
<label for="email" class="mb-2 block text-[13px] font-medium text-foreground">
|
|
168
192
|
Email
|
|
@@ -187,11 +211,11 @@ function toggleMode() {
|
|
|
187
211
|
bind:value={password}
|
|
188
212
|
class="h-11 w-full rounded-lg border border-border bg-card px-3.5 text-sm shadow-sm transition-all placeholder:text-muted-foreground/50 focus:border-primary/40 focus:outline-none focus:ring-2 focus:ring-primary/10"
|
|
189
213
|
placeholder="Enter password"
|
|
190
|
-
autocomplete={
|
|
214
|
+
autocomplete={isCreatingAccount ? 'new-password' : 'current-password'}
|
|
191
215
|
/>
|
|
192
216
|
</div>
|
|
193
217
|
|
|
194
|
-
{#if
|
|
218
|
+
{#if isCreatingAccount}
|
|
195
219
|
<div>
|
|
196
220
|
<label for="confirmPassword" class="mb-2 block text-[13px] font-medium text-foreground">
|
|
197
221
|
Confirm Password
|
|
@@ -212,7 +236,7 @@ function toggleMode() {
|
|
|
212
236
|
disabled={isLoading}
|
|
213
237
|
class="h-11 w-full rounded-lg bg-primary text-sm font-medium text-primary-foreground shadow-sm transition-all hover:bg-primary/90 hover:shadow-md active:scale-[0.99] disabled:opacity-50 disabled:shadow-none"
|
|
214
238
|
>
|
|
215
|
-
{isLoading ? 'Please wait...' :
|
|
239
|
+
{isLoading ? 'Please wait...' : isCreatingAccount ? 'Create Account' : 'Sign In'}
|
|
216
240
|
</button>
|
|
217
241
|
</form>
|
|
218
242
|
|
|
@@ -222,7 +246,7 @@ function toggleMode() {
|
|
|
222
246
|
onclick={toggleMode}
|
|
223
247
|
class="text-[13px] text-muted-foreground transition-colors hover:text-primary"
|
|
224
248
|
>
|
|
225
|
-
{
|
|
249
|
+
{isCreatingAccount ? 'Already have an account? Sign in' : 'First time? Create admin account'}
|
|
226
250
|
</button>
|
|
227
251
|
</div>
|
|
228
252
|
</div>
|
package/biome.json
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
|
|
3
|
-
"vcs": {
|
|
4
|
-
"enabled": true,
|
|
5
|
-
"clientKind": "git",
|
|
6
|
-
"useIgnoreFile": false,
|
|
7
|
-
"defaultBranch": "main"
|
|
8
|
-
},
|
|
9
|
-
"files": {
|
|
10
|
-
"ignoreUnknown": false
|
|
11
|
-
},
|
|
12
|
-
"formatter": {
|
|
13
|
-
"enabled": true,
|
|
14
|
-
"indentStyle": "tab",
|
|
15
|
-
"lineWidth": 100
|
|
16
|
-
},
|
|
17
|
-
"linter": {
|
|
18
|
-
"enabled": true,
|
|
19
|
-
"rules": {
|
|
20
|
-
"recommended": true,
|
|
21
|
-
"suspicious": {
|
|
22
|
-
"noExplicitAny": "off"
|
|
23
|
-
},
|
|
24
|
-
"style": {
|
|
25
|
-
"noNonNullAssertion": "off"
|
|
26
|
-
},
|
|
27
|
-
"correctness": {
|
|
28
|
-
"noUnusedVariables": "warn"
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"javascript": {
|
|
33
|
-
"formatter": {
|
|
34
|
-
"quoteStyle": "single",
|
|
35
|
-
"semicolons": "asNeeded",
|
|
36
|
-
"trailingCommas": "es5",
|
|
37
|
-
"arrowParentheses": "always"
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
"css": {
|
|
41
|
-
"parser": {
|
|
42
|
-
"cssModules": false,
|
|
43
|
-
"tailwindDirectives": true
|
|
44
|
-
},
|
|
45
|
-
"linter": {
|
|
46
|
-
"enabled": true
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
"overrides": [
|
|
50
|
-
{
|
|
51
|
-
"includes": ["**/*.svelte"],
|
|
52
|
-
"linter": {
|
|
53
|
-
"rules": {
|
|
54
|
-
"correctness": {
|
|
55
|
-
"noUnusedVariables": "off",
|
|
56
|
-
"noUnusedImports": "off"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
]
|
|
62
|
-
}
|