@axium/client 0.3.4 → 0.4.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/lib/Preference.svelte +161 -0
- package/lib/Preferences.svelte +27 -0
- package/lib/index.ts +2 -0
- package/package.json +2 -2
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fetchAPI } from '@axium/client/requests';
|
|
3
|
+
import { preferenceDefaults, zIs, type Preferences, type ZodPref } from '@axium/core';
|
|
4
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
5
|
+
import { getByString, pick, setByString } from 'utilium';
|
|
6
|
+
import Icon from './Icon.svelte';
|
|
7
|
+
import Preference from './Preference.svelte';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
userId: string;
|
|
11
|
+
preferences: Preferences;
|
|
12
|
+
path: string;
|
|
13
|
+
schema: ZodPref;
|
|
14
|
+
optional?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { preferences = $bindable(), userId, path, schema, optional = false }: Props = $props();
|
|
18
|
+
const id = $props.id();
|
|
19
|
+
|
|
20
|
+
let input = $state<HTMLInputElement | HTMLSelectElement>()!;
|
|
21
|
+
let checked = $state(schema.def.type == 'boolean' && getByString<boolean>(preferences, path));
|
|
22
|
+
|
|
23
|
+
function dateAttr(date: Date | null, format: 'date' | 'time' | 'datetime' | 'time+sec') {
|
|
24
|
+
if (!date) return null;
|
|
25
|
+
|
|
26
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
27
|
+
|
|
28
|
+
const yyyy = date.getFullYear();
|
|
29
|
+
const mm = pad(date.getMonth() + 1);
|
|
30
|
+
const dd = pad(date.getDate());
|
|
31
|
+
const dateStr = `${yyyy}-${mm}-${dd}`;
|
|
32
|
+
|
|
33
|
+
const HH = pad(date.getHours());
|
|
34
|
+
const MM = pad(date.getMinutes());
|
|
35
|
+
const timeStr = `${HH}:${MM}`;
|
|
36
|
+
|
|
37
|
+
switch (format) {
|
|
38
|
+
case 'date':
|
|
39
|
+
return dateStr;
|
|
40
|
+
case 'time':
|
|
41
|
+
return timeStr;
|
|
42
|
+
case 'time+sec':
|
|
43
|
+
return `${timeStr}:${pad(date.getSeconds())}`;
|
|
44
|
+
case 'datetime':
|
|
45
|
+
default:
|
|
46
|
+
return `${dateStr}T${timeStr}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onchange(e: Event) {
|
|
51
|
+
const value = schema.parse(input instanceof HTMLInputElement && input.type === 'checkbox' ? input.checked : input.value);
|
|
52
|
+
if (value == getByString(preferences, path)) return;
|
|
53
|
+
|
|
54
|
+
if (getByString(preferenceDefaults, path) == value) {
|
|
55
|
+
const parts = path.split('.');
|
|
56
|
+
const prop = parts.pop()!;
|
|
57
|
+
delete getByString<Record<string, any>>(preferences, parts.join('.'))[prop];
|
|
58
|
+
} else setByString(preferences, path, value);
|
|
59
|
+
|
|
60
|
+
fetchAPI('PATCH', 'users/:id', { preferences }, userId);
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
{#snippet _in(rest: HTMLInputAttributes)}
|
|
65
|
+
<input
|
|
66
|
+
bind:this={input}
|
|
67
|
+
{id}
|
|
68
|
+
{...rest}
|
|
69
|
+
value={getByString(preferences, path) ?? getByString(preferenceDefaults, path)}
|
|
70
|
+
{onchange}
|
|
71
|
+
required={!optional}
|
|
72
|
+
/>
|
|
73
|
+
{/snippet}
|
|
74
|
+
|
|
75
|
+
{#if zIs(schema, 'string')}
|
|
76
|
+
{@render _in({ type: schema.format == 'email' ? 'email' : 'text', ...pick(schema, 'minLength', 'maxLength') })}
|
|
77
|
+
{:else if zIs(schema, 'number')}
|
|
78
|
+
{@render _in({ type: 'number', min: schema.minValue, max: schema.maxValue, step: schema.format?.includes('int') ? 1 : 0.1 })}
|
|
79
|
+
{:else if zIs(schema, 'bigint')}
|
|
80
|
+
{@render _in({ type: 'number', min: Number(schema.minValue), max: Number(schema.maxValue), step: 1 })}
|
|
81
|
+
{:else if zIs(schema, 'boolean')}
|
|
82
|
+
<input bind:checked bind:this={input} {id} type="checkbox" {onchange} required={!optional} />
|
|
83
|
+
<label for={id} class="checkbox">
|
|
84
|
+
{#if checked}<Icon i="check" --size="1.3em" />{/if}
|
|
85
|
+
</label>
|
|
86
|
+
{:else if zIs(schema, 'date')}
|
|
87
|
+
{@render _in({
|
|
88
|
+
type: 'date',
|
|
89
|
+
min: dateAttr(schema.minDate, 'date'),
|
|
90
|
+
max: dateAttr(schema.maxDate, 'date'),
|
|
91
|
+
})}
|
|
92
|
+
{:else if zIs(schema, 'file')}
|
|
93
|
+
<!-- todo -->
|
|
94
|
+
{:else if zIs(schema, 'literal')}
|
|
95
|
+
<select bind:this={input} {id} {onchange} required={!optional}>
|
|
96
|
+
{#each schema.values as value}
|
|
97
|
+
<option {value}>{value}</option>
|
|
98
|
+
{/each}
|
|
99
|
+
</select>
|
|
100
|
+
{:else if zIs(schema, 'template_literal')}
|
|
101
|
+
<!-- todo -->
|
|
102
|
+
{:else if zIs(schema, 'nullable') || zIs(schema, 'optional')}
|
|
103
|
+
<!-- defaults are handled differently -->
|
|
104
|
+
<Preference {userId} bind:preferences {path} schema={schema.def.innerType} optional={true} />
|
|
105
|
+
{:else if zIs(schema, 'array')}
|
|
106
|
+
<div class="pref-sub">
|
|
107
|
+
{#each getByString<unknown[]>(preferences, path), i}
|
|
108
|
+
<div class="pref-record-entry">
|
|
109
|
+
<Preference {userId} bind:preferences path="{path}.{i}" schema={schema.element} />
|
|
110
|
+
</div>
|
|
111
|
+
{/each}
|
|
112
|
+
</div>
|
|
113
|
+
{:else if zIs(schema, 'record')}
|
|
114
|
+
<div class="pref-sub">
|
|
115
|
+
{#each Object.keys(getByString<object>(preferences, path)) as key}
|
|
116
|
+
<div class="pref-record-entry">
|
|
117
|
+
<label for={id}>{key}</label>
|
|
118
|
+
<Preference {userId} bind:preferences path="{path}.{key}" schema={schema.valueType} />
|
|
119
|
+
</div>
|
|
120
|
+
{/each}
|
|
121
|
+
</div>
|
|
122
|
+
{:else if zIs(schema, 'object')}
|
|
123
|
+
{#each Object.entries(schema.shape) as [key, value]}
|
|
124
|
+
<div class="pref-sub">
|
|
125
|
+
<label for={id}>{key}</label>
|
|
126
|
+
<Preference {userId} bind:preferences path="{path}.{key}" schema={value} />
|
|
127
|
+
</div>
|
|
128
|
+
{/each}
|
|
129
|
+
{:else if zIs(schema, 'tuple')}
|
|
130
|
+
<div class="pref-sub" data-rest={schema.def.rest}>
|
|
131
|
+
{#each schema.def.items as item, i}
|
|
132
|
+
<Preference {userId} bind:preferences path="{path}.{i}" schema={item} />
|
|
133
|
+
{/each}
|
|
134
|
+
</div>
|
|
135
|
+
{:else if zIs(schema, 'enum')}
|
|
136
|
+
<select bind:this={input} {id} {onchange} required={!optional}>
|
|
137
|
+
{#each Object.entries(schema.enum) as [key, val]}
|
|
138
|
+
<option value={val}>{key}</option>
|
|
139
|
+
{/each}
|
|
140
|
+
</select>
|
|
141
|
+
{:else}
|
|
142
|
+
<!-- No idea how to render this -->
|
|
143
|
+
<i class="error-text">Invalid preference type: {JSON.stringify((schema as ZodPref)?.def?.type)}</i>
|
|
144
|
+
{/if}
|
|
145
|
+
|
|
146
|
+
<style>
|
|
147
|
+
input[type='checkbox'] {
|
|
148
|
+
display: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
label.checkbox {
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
width: 1.5em;
|
|
154
|
+
height: 1.5em;
|
|
155
|
+
border: 1px solid var(--border-accent);
|
|
156
|
+
border-radius: 0.5em;
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
justify-content: center;
|
|
159
|
+
align-items: center;
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { preferenceLabels, preferenceSchemas, type PreferenceName, type Preferences } from '@axium/core';
|
|
3
|
+
import Preference from './Preference.svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
userId: string;
|
|
7
|
+
preferences: Preferences;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { preferences = $bindable(), userId }: Props = $props();
|
|
11
|
+
const id = $props.id();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
{#each Object.keys(preferenceSchemas) as PreferenceName[] as path}
|
|
15
|
+
<div class="pref">
|
|
16
|
+
<label for={id}>{preferenceLabels[path]}</label>
|
|
17
|
+
<Preference {userId} bind:preferences {path} schema={preferenceSchemas[path]} />
|
|
18
|
+
</div>
|
|
19
|
+
{/each}
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
.pref {
|
|
23
|
+
display: grid;
|
|
24
|
+
grid-template-columns: 1fr 1fr;
|
|
25
|
+
gap: 1em;
|
|
26
|
+
}
|
|
27
|
+
</style>
|
package/lib/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ export { default as Login } from './Login.svelte';
|
|
|
9
9
|
export { default as Logout } from './Logout.svelte';
|
|
10
10
|
export { default as NumberBar } from './NumberBar.svelte';
|
|
11
11
|
export { default as Popover } from './Popover.svelte';
|
|
12
|
+
export { default as Preference } from './Preference.svelte';
|
|
13
|
+
export { default as Preferences } from './Preferences.svelte';
|
|
12
14
|
export { default as Register } from './Register.svelte';
|
|
13
15
|
export { default as Toast } from './Toast.svelte';
|
|
14
16
|
export { default as Upload } from './Upload.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"author": "James Prevett <jp@jamespre.dev>",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"build": "tsc"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@axium/core": ">=0.
|
|
35
|
+
"@axium/core": ">=0.6.0",
|
|
36
36
|
"utilium": "^2.3.8",
|
|
37
37
|
"zod": "^4.0.5",
|
|
38
38
|
"svelte": "^5.36.0"
|