includio-cms 0.26.0 → 0.28.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/API.md +58 -2
- package/CHANGELOG.md +105 -0
- package/DOCS.md +1 -1
- package/ROADMAP.md +8 -0
- package/dist/admin/auth-client.d.ts +42 -42
- package/dist/admin/client/admin/admin-layout.svelte +12 -2
- package/dist/admin/client/admin/admin-layout.svelte.d.ts +2 -1
- package/dist/admin/client/collection/data-table.svelte +0 -39
- package/dist/admin/client/collection/data-table.svelte.d.ts +0 -2
- package/dist/admin/client/shop/coupon-schema.d.ts +1 -1
- package/dist/admin/client/shop/refund-dialog.svelte +37 -1
- package/dist/admin/client/shop/refund-dialog.svelte.d.ts +3 -0
- package/dist/admin/client/shop/shop-order-detail-page.svelte +192 -0
- package/dist/admin/components/fields/field-renderer.svelte +6 -1
- package/dist/admin/components/fields/icon-field.svelte +86 -0
- package/dist/admin/components/fields/icon-field.svelte.d.ts +8 -0
- package/dist/admin/components/fields/icon-picker-dialog.svelte +174 -0
- package/dist/admin/components/fields/icon-picker-dialog.svelte.d.ts +11 -0
- package/dist/admin/components/fields/object-field.svelte +27 -7
- package/dist/admin/components/fields/shop-field.svelte +210 -20
- package/dist/admin/components/layout/layout-tabs.svelte +1 -0
- package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte +109 -0
- package/dist/admin/components/variant-form/VariantAttributeRenderer.svelte.d.ts +9 -0
- package/dist/admin/helpers/build-icon-set-map.d.ts +8 -0
- package/dist/admin/helpers/build-icon-set-map.js +16 -0
- package/dist/admin/helpers/index.d.ts +2 -0
- package/dist/admin/helpers/index.js +2 -0
- package/dist/admin/remote/shop.remote.d.ts +116 -24
- package/dist/admin/remote/shop.remote.js +79 -6
- package/dist/admin/state/icon-sets.svelte.d.ts +9 -0
- package/dist/admin/state/icon-sets.svelte.js +20 -0
- package/dist/cli/scaffold/admin.js +2 -2
- package/dist/components/ui/checkbox/checkbox.svelte +3 -3
- package/dist/core/cms.d.ts +11 -2
- package/dist/core/cms.js +29 -0
- package/dist/core/fields/fieldSchemaToTs.js +7 -0
- package/dist/core/server/generator/fields.d.ts +2 -0
- package/dist/core/server/generator/fields.js +34 -1
- package/dist/core/server/generator/generator.js +2 -1
- package/dist/db-postgres/schema/shop/index.d.ts +1 -0
- package/dist/db-postgres/schema/shop/index.js +1 -0
- package/dist/db-postgres/schema/shop/invoice.d.ts +254 -0
- package/dist/db-postgres/schema/shop/invoice.js +27 -0
- package/dist/db-postgres/schema/shop/order.d.ts +107 -1
- package/dist/db-postgres/schema/shop/order.js +7 -1
- package/dist/db-postgres/schema/shop/payment.d.ts +20 -0
- package/dist/db-postgres/schema/shop/payment.js +4 -1
- package/dist/db-postgres/schema/shop/product.d.ts +20 -0
- package/dist/db-postgres/schema/shop/product.js +3 -1
- package/dist/db-postgres/schema/shop/productVariant.d.ts +12 -2
- package/dist/db-postgres/schema/shop/productVariant.js +22 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/shop/adapters/fakturownia/client.d.ts +28 -0
- package/dist/shop/adapters/fakturownia/client.js +67 -0
- package/dist/shop/adapters/fakturownia/index.d.ts +27 -0
- package/dist/shop/adapters/fakturownia/index.js +36 -0
- package/dist/shop/adapters/fakturownia/payload.d.ts +35 -0
- package/dist/shop/adapters/fakturownia/payload.js +45 -0
- package/dist/shop/cart/types.d.ts +1 -0
- package/dist/shop/client/index.d.ts +61 -0
- package/dist/shop/client/index.js +5 -1
- package/dist/shop/expiry.d.ts +35 -0
- package/dist/shop/expiry.js +68 -0
- package/dist/shop/http/balance-handler.d.ts +20 -0
- package/dist/shop/http/balance-handler.js +91 -0
- package/dist/shop/http/cart-handler.js +19 -0
- package/dist/shop/http/checkout-handler.js +30 -1
- package/dist/shop/http/index.d.ts +2 -0
- package/dist/shop/http/index.js +2 -0
- package/dist/shop/http/upcoming-handler.d.ts +16 -0
- package/dist/shop/http/upcoming-handler.js +65 -0
- package/dist/shop/http/webhook-handler.js +46 -9
- package/dist/shop/index.d.ts +7 -1
- package/dist/shop/index.js +10 -1
- package/dist/shop/nip.d.ts +12 -0
- package/dist/shop/nip.js +23 -0
- package/dist/shop/server/balance-payment.d.ts +40 -0
- package/dist/shop/server/balance-payment.js +140 -0
- package/dist/shop/server/cart-hydrate.js +2 -0
- package/dist/shop/server/init.d.ts +14 -0
- package/dist/shop/server/init.js +35 -0
- package/dist/shop/server/invoices.d.ts +64 -0
- package/dist/shop/server/invoices.js +237 -0
- package/dist/shop/server/orders.d.ts +38 -0
- package/dist/shop/server/orders.js +152 -2
- package/dist/shop/server/payment-policy.d.ts +35 -0
- package/dist/shop/server/payment-policy.js +55 -0
- package/dist/shop/server/payments.d.ts +29 -0
- package/dist/shop/server/payments.js +64 -0
- package/dist/shop/server/populate.d.ts +1 -1
- package/dist/shop/server/refund.d.ts +17 -12
- package/dist/shop/server/refund.js +96 -13
- package/dist/shop/server/shop-data.d.ts +4 -1
- package/dist/shop/server/shop-data.js +24 -2
- package/dist/shop/template.d.ts +13 -0
- package/dist/shop/template.js +98 -0
- package/dist/shop/types.d.ts +208 -1
- package/dist/shop/variant-attributes.d.ts +28 -0
- package/dist/shop/variant-attributes.js +69 -0
- package/dist/sveltekit/server/index.d.ts +1 -0
- package/dist/sveltekit/server/index.js +2 -0
- package/dist/types/cms.d.ts +4 -3
- package/dist/types/cms.schema.d.ts +1 -1
- package/dist/types/cms.schema.js +9 -0
- package/dist/types/fields.d.ts +21 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/plugins.d.ts +40 -0
- package/dist/types/plugins.js +4 -1
- package/dist/updates/0.26.1/index.d.ts +2 -0
- package/dist/updates/0.26.1/index.js +19 -0
- package/dist/updates/0.27.0/index.d.ts +2 -0
- package/dist/updates/0.27.0/index.js +50 -0
- package/dist/updates/0.28.0/index.d.ts +2 -0
- package/dist/updates/0.28.0/index.js +38 -0
- package/dist/updates/index.js +7 -1
- package/package.json +1 -1
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { getContext } from 'svelte';
|
|
2
|
+
import { getContext, untrack } from 'svelte';
|
|
3
3
|
import { Button } from '../../../components/ui/button/index.js';
|
|
4
4
|
import { Switch } from '../../../components/ui/switch/index.js';
|
|
5
5
|
import TrashIcon from '@tabler/icons-svelte/icons/trash';
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
import { getRemotes } from '../../../sveltekit/index.js';
|
|
9
9
|
import { errorMessages } from '../../i18n/errors.js';
|
|
10
10
|
import type { ShopField } from '../../../types/fields.js';
|
|
11
|
+
import VariantAttributeRenderer from '../variant-form/VariantAttributeRenderer.svelte';
|
|
12
|
+
import { interpolateTemplate } from '../../../shop/template.js';
|
|
13
|
+
import { isVariantExpired } from '../../../shop/expiry.js';
|
|
11
14
|
|
|
12
15
|
type Props = { field: ShopField };
|
|
13
16
|
const { field }: Props = $props();
|
|
@@ -22,14 +25,21 @@
|
|
|
22
25
|
let inputPrice = $state('0.00');
|
|
23
26
|
let vatRate = $state<number | string>(23);
|
|
24
27
|
let isActive = $state(true);
|
|
28
|
+
type PolicyType = 'full' | 'deposit';
|
|
29
|
+
type DepositKind = 'percent' | 'amount';
|
|
30
|
+
let policyType = $state<PolicyType>('full');
|
|
31
|
+
let depositKind = $state<DepositKind>('percent');
|
|
32
|
+
let depositValue = $state<string>('30');
|
|
25
33
|
let variants = $state<
|
|
26
34
|
Array<{
|
|
27
35
|
id?: string;
|
|
28
36
|
sku: string;
|
|
29
37
|
name: string;
|
|
38
|
+
nameDirty: boolean;
|
|
30
39
|
priceDelta: string;
|
|
31
40
|
priceDeltaMode: InputMode;
|
|
32
41
|
stock: string;
|
|
42
|
+
attributes: Record<string, unknown>;
|
|
33
43
|
}>
|
|
34
44
|
>([]);
|
|
35
45
|
|
|
@@ -37,6 +47,7 @@
|
|
|
37
47
|
let saving = $state(false);
|
|
38
48
|
let errorMessage = $state<string | null>(null);
|
|
39
49
|
let successMessage = $state<string | null>(null);
|
|
50
|
+
let showExpired = $state(false);
|
|
40
51
|
|
|
41
52
|
// PLN-precise math — storage w numeric(20,6). Round do groszy tylko przy prezentacji.
|
|
42
53
|
const inputPln = $derived(parseFloat(inputPrice || '0') || 0);
|
|
@@ -68,20 +79,35 @@
|
|
|
68
79
|
inputPrice = formatPln(grossInitial);
|
|
69
80
|
vatRate = shop.vatRate;
|
|
70
81
|
isActive = shop.isActive;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
const policy = shop.paymentPolicy ?? null;
|
|
83
|
+
if (policy && policy.type === 'deposit') {
|
|
84
|
+
policyType = 'deposit';
|
|
85
|
+
depositKind = policy.depositAmount.type;
|
|
86
|
+
depositValue = String(policy.depositAmount.value);
|
|
87
|
+
} else {
|
|
88
|
+
policyType = 'full';
|
|
89
|
+
}
|
|
90
|
+
variants = shop.variants.map((v) => {
|
|
91
|
+
const name =
|
|
75
92
|
typeof v.name === 'object' && v.name !== null
|
|
76
93
|
? String(
|
|
77
94
|
Object.values(v.name as Record<string, string>)[0] ?? ''
|
|
78
95
|
)
|
|
79
|
-
: ''
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
: '';
|
|
97
|
+
return {
|
|
98
|
+
id: v.id,
|
|
99
|
+
sku: v.sku ?? '',
|
|
100
|
+
name,
|
|
101
|
+
// Saved non-empty name = editor's choice; lock pre-fill for this session.
|
|
102
|
+
nameDirty: name !== '',
|
|
103
|
+
// Delta hydratowana jako netto (kanoniczna), mode default 'net' dla istniejących.
|
|
104
|
+
priceDelta: formatPln(Number(v.priceDelta)),
|
|
105
|
+
priceDeltaMode: 'net' as InputMode,
|
|
106
|
+
stock: v.stock == null ? '' : String(v.stock),
|
|
107
|
+
attributes:
|
|
108
|
+
(v.attributes as Record<string, unknown> | null | undefined) ?? {}
|
|
109
|
+
};
|
|
110
|
+
});
|
|
85
111
|
} else {
|
|
86
112
|
const defaultVat = configQuery.current?.vatRates[0] ?? 23;
|
|
87
113
|
vatRate = defaultVat;
|
|
@@ -101,12 +127,31 @@
|
|
|
101
127
|
id: undefined,
|
|
102
128
|
sku: '',
|
|
103
129
|
name: '',
|
|
130
|
+
nameDirty: false,
|
|
104
131
|
priceDelta: '0.00',
|
|
105
132
|
priceDeltaMode: 'gross',
|
|
106
|
-
stock: ''
|
|
133
|
+
stock: '',
|
|
134
|
+
attributes: {}
|
|
107
135
|
});
|
|
108
136
|
}
|
|
109
137
|
|
|
138
|
+
// Auto-prefill `name` from variantLabel.template whenever attributes change,
|
|
139
|
+
// unless the user has edited the name manually (dirty flag, per session).
|
|
140
|
+
$effect(() => {
|
|
141
|
+
const template = configQuery.current?.variantLabel?.template;
|
|
142
|
+
if (!template) return;
|
|
143
|
+
for (let i = 0; i < variants.length; i++) {
|
|
144
|
+
// Tracked: re-run when this variant's attributes change.
|
|
145
|
+
const attrs = variants[i].attributes;
|
|
146
|
+
const next = interpolateTemplate(template, attrs, 'pl');
|
|
147
|
+
untrack(() => {
|
|
148
|
+
const v = variants[i];
|
|
149
|
+
if (v.nameDirty) return;
|
|
150
|
+
if (v.name !== next) v.name = next;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
110
155
|
function switchVariantMode(i: number, newMode: InputMode) {
|
|
111
156
|
const v = variants[i];
|
|
112
157
|
if (v.priceDeltaMode === newMode) return;
|
|
@@ -119,6 +164,38 @@
|
|
|
119
164
|
variants.splice(i, 1);
|
|
120
165
|
}
|
|
121
166
|
|
|
167
|
+
const expiryConfig = $derived(configQuery.current?.variantExpiry ?? null);
|
|
168
|
+
|
|
169
|
+
const displayVariants = $derived.by(() => {
|
|
170
|
+
const enriched = variants.map((v, originalIndex) => ({
|
|
171
|
+
v,
|
|
172
|
+
originalIndex,
|
|
173
|
+
expired: expiryConfig
|
|
174
|
+
? isVariantExpired({ attributes: v.attributes }, expiryConfig)
|
|
175
|
+
: false
|
|
176
|
+
}));
|
|
177
|
+
const visible = showExpired ? enriched : enriched.filter((x) => !x.expired);
|
|
178
|
+
const source = expiryConfig?.source;
|
|
179
|
+
if (!source) return visible;
|
|
180
|
+
return [...visible].sort((a, b) => {
|
|
181
|
+
const aVal = a.v.attributes?.[source];
|
|
182
|
+
const bVal = b.v.attributes?.[source];
|
|
183
|
+
const aTs = typeof aVal === 'string' ? Date.parse(aVal) : NaN;
|
|
184
|
+
const bTs = typeof bVal === 'string' ? Date.parse(bVal) : NaN;
|
|
185
|
+
if (Number.isNaN(aTs) && Number.isNaN(bTs)) return 0;
|
|
186
|
+
if (Number.isNaN(aTs)) return 1;
|
|
187
|
+
if (Number.isNaN(bTs)) return -1;
|
|
188
|
+
return aTs - bTs;
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const expiredCount = $derived(
|
|
193
|
+
expiryConfig
|
|
194
|
+
? variants.filter((v) => isVariantExpired({ attributes: v.attributes }, expiryConfig))
|
|
195
|
+
.length
|
|
196
|
+
: 0
|
|
197
|
+
);
|
|
198
|
+
|
|
122
199
|
async function save() {
|
|
123
200
|
if (!entryId) {
|
|
124
201
|
errorMessage = 'Brak ID wpisu — zapisz najpierw wpis, potem dane sklepu.';
|
|
@@ -130,12 +207,26 @@
|
|
|
130
207
|
try {
|
|
131
208
|
const stockEnabled = configQuery.current?.features.stock ?? false;
|
|
132
209
|
const variantsEnabled = configQuery.current?.features.variants ?? false;
|
|
210
|
+
const policyValue = Number(depositValue);
|
|
211
|
+
const paymentPolicy =
|
|
212
|
+
policyType === 'full'
|
|
213
|
+
? ({ type: 'full' as const })
|
|
214
|
+
: Number.isFinite(policyValue) && policyValue > 0
|
|
215
|
+
? ({
|
|
216
|
+
type: 'deposit' as const,
|
|
217
|
+
depositAmount:
|
|
218
|
+
depositKind === 'percent'
|
|
219
|
+
? { type: 'percent' as const, value: policyValue }
|
|
220
|
+
: { type: 'amount' as const, value: Math.round(policyValue) }
|
|
221
|
+
})
|
|
222
|
+
: ({ type: 'full' as const });
|
|
133
223
|
await remotes.upsertShopDataForEntry({
|
|
134
224
|
entryId,
|
|
135
225
|
data: {
|
|
136
226
|
basePrice: netPln,
|
|
137
227
|
vatRate: Number(vatRate),
|
|
138
|
-
isActive
|
|
228
|
+
isActive,
|
|
229
|
+
paymentPolicy
|
|
139
230
|
},
|
|
140
231
|
variants: variantsEnabled
|
|
141
232
|
? variants.map((v) => ({
|
|
@@ -144,7 +235,7 @@
|
|
|
144
235
|
name: v.name ? { pl: v.name } : null,
|
|
145
236
|
priceDelta: variantDeltaNetPln(v),
|
|
146
237
|
stock: stockEnabled && v.stock !== '' ? parseInt(v.stock, 10) : null,
|
|
147
|
-
attributes: null
|
|
238
|
+
attributes: Object.keys(v.attributes).length > 0 ? v.attributes : null
|
|
148
239
|
}))
|
|
149
240
|
: []
|
|
150
241
|
});
|
|
@@ -243,24 +334,122 @@
|
|
|
243
334
|
</div>
|
|
244
335
|
</div>
|
|
245
336
|
|
|
337
|
+
<div class="border-border space-y-3 rounded-lg border-t pt-4">
|
|
338
|
+
<h4 class="text-sm font-bold">Zasady płatności</h4>
|
|
339
|
+
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
|
|
340
|
+
<label class="flex items-center gap-2 text-sm">
|
|
341
|
+
<input
|
|
342
|
+
type="radio"
|
|
343
|
+
name="policy-type"
|
|
344
|
+
value="full"
|
|
345
|
+
checked={policyType === 'full'}
|
|
346
|
+
onchange={() => (policyType = 'full')}
|
|
347
|
+
/>
|
|
348
|
+
<span>Płatność z góry (pełna kwota)</span>
|
|
349
|
+
</label>
|
|
350
|
+
<label class="flex items-center gap-2 text-sm">
|
|
351
|
+
<input
|
|
352
|
+
type="radio"
|
|
353
|
+
name="policy-type"
|
|
354
|
+
value="deposit"
|
|
355
|
+
checked={policyType === 'deposit'}
|
|
356
|
+
onchange={() => (policyType = 'deposit')}
|
|
357
|
+
/>
|
|
358
|
+
<span>Zaliczka + dopłata</span>
|
|
359
|
+
</label>
|
|
360
|
+
</div>
|
|
361
|
+
{#if policyType === 'deposit'}
|
|
362
|
+
<div class="bg-muted/30 grid grid-cols-1 gap-3 rounded-md p-3 sm:grid-cols-[auto_1fr]">
|
|
363
|
+
<label class="flex flex-col gap-1 text-xs">
|
|
364
|
+
<span class="text-muted-foreground font-semibold">Typ zaliczki</span>
|
|
365
|
+
<select
|
|
366
|
+
class="border-border rounded-md border px-2 py-1.5 text-sm"
|
|
367
|
+
bind:value={depositKind}
|
|
368
|
+
>
|
|
369
|
+
<option value="percent">Procent ceny</option>
|
|
370
|
+
<option value="amount">Kwota stała (grosze)</option>
|
|
371
|
+
</select>
|
|
372
|
+
</label>
|
|
373
|
+
<label class="flex flex-col gap-1 text-xs">
|
|
374
|
+
<span class="text-muted-foreground font-semibold">
|
|
375
|
+
{depositKind === 'percent' ? 'Procent (1–100)' : 'Kwota w groszach'}
|
|
376
|
+
</span>
|
|
377
|
+
<input
|
|
378
|
+
type="number"
|
|
379
|
+
inputmode="numeric"
|
|
380
|
+
min="1"
|
|
381
|
+
max={depositKind === 'percent' ? '100' : undefined}
|
|
382
|
+
step={depositKind === 'percent' ? '1' : '1'}
|
|
383
|
+
class="border-border rounded-md border px-2 py-1.5 text-sm tabular-nums"
|
|
384
|
+
bind:value={depositValue}
|
|
385
|
+
/>
|
|
386
|
+
</label>
|
|
387
|
+
<p class="text-muted-foreground col-span-full text-xs">
|
|
388
|
+
{#if depositKind === 'percent'}
|
|
389
|
+
Klient płaci {depositValue || '0'}% przy zamówieniu, resztę po wygenerowaniu linka do dopłaty w panelu admina.
|
|
390
|
+
{:else}
|
|
391
|
+
Klient płaci {(Number(depositValue) || 0) / 100} zł przy zamówieniu, resztę po wygenerowaniu linka do dopłaty.
|
|
392
|
+
{/if}
|
|
393
|
+
</p>
|
|
394
|
+
</div>
|
|
395
|
+
{/if}
|
|
396
|
+
</div>
|
|
397
|
+
|
|
246
398
|
{#if config.features.variants}
|
|
247
399
|
<div class="border-border space-y-3 rounded-lg border-t pt-4">
|
|
248
|
-
<div class="flex items-center justify-between">
|
|
400
|
+
<div class="flex items-center justify-between gap-2">
|
|
249
401
|
<h4 class="text-sm font-bold">Warianty</h4>
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
402
|
+
<div class="flex items-center gap-3">
|
|
403
|
+
{#if expiryConfig && expiredCount > 0}
|
|
404
|
+
<label class="text-muted-foreground flex items-center gap-1.5 text-xs">
|
|
405
|
+
<input type="checkbox" bind:checked={showExpired} class="size-3.5" />
|
|
406
|
+
Pokaż zakończone ({expiredCount})
|
|
407
|
+
</label>
|
|
408
|
+
{/if}
|
|
409
|
+
<Button type="button" variant="outline" size="sm" onclick={addVariant}>
|
|
410
|
+
<PlusIcon class="mr-1 size-4" /> Dodaj wariant
|
|
411
|
+
</Button>
|
|
412
|
+
</div>
|
|
253
413
|
</div>
|
|
254
414
|
{#if variants.length === 0}
|
|
255
415
|
<p class="text-muted-foreground text-xs">Brak wariantów — zostanie użyta cena bazowa.</p>
|
|
416
|
+
{:else if displayVariants.length === 0}
|
|
417
|
+
<p class="text-muted-foreground text-xs">
|
|
418
|
+
Wszystkie warianty są zakończone. Zaznacz „Pokaż zakończone", aby je zobaczyć.
|
|
419
|
+
</p>
|
|
256
420
|
{/if}
|
|
257
|
-
{#each
|
|
258
|
-
|
|
421
|
+
{#each displayVariants as { v, originalIndex, expired } (originalIndex)}
|
|
422
|
+
{@const i = originalIndex}
|
|
423
|
+
<div
|
|
424
|
+
class="border-border space-y-3 rounded-lg border p-3 {expired ? 'opacity-60' : ''}"
|
|
425
|
+
>
|
|
426
|
+
{#if expired}
|
|
427
|
+
<span
|
|
428
|
+
class="bg-muted text-muted-foreground inline-flex items-center rounded-md border border-border px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide"
|
|
429
|
+
aria-label="Wariant zakończony"
|
|
430
|
+
>
|
|
431
|
+
Zakończony
|
|
432
|
+
</span>
|
|
433
|
+
{/if}
|
|
434
|
+
{#if config.variantAttributes && Object.keys(config.variantAttributes).length > 0}
|
|
435
|
+
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
436
|
+
{#each Object.entries(config.variantAttributes) as [attrKey, attrConfig] (attrKey)}
|
|
437
|
+
<VariantAttributeRenderer
|
|
438
|
+
{attrKey}
|
|
439
|
+
{attrConfig}
|
|
440
|
+
bind:value={v.attributes[attrKey]}
|
|
441
|
+
/>
|
|
442
|
+
{/each}
|
|
443
|
+
</div>
|
|
444
|
+
<div class="border-border border-t pt-3" aria-hidden="true"></div>
|
|
445
|
+
{/if}
|
|
446
|
+
<div class="grid grid-cols-2 gap-2">
|
|
259
447
|
<label class="block">
|
|
260
448
|
<span class="text-muted-foreground mb-0.5 block text-xs">Nazwa</span>
|
|
261
449
|
<input
|
|
262
450
|
type="text"
|
|
263
451
|
bind:value={v.name}
|
|
452
|
+
oninput={() => (v.nameDirty = true)}
|
|
264
453
|
class="border-border w-full rounded border px-2 py-1 text-sm"
|
|
265
454
|
/>
|
|
266
455
|
</label>
|
|
@@ -323,6 +512,7 @@
|
|
|
323
512
|
<TrashIcon class="size-4" />
|
|
324
513
|
</Button>
|
|
325
514
|
</div>
|
|
515
|
+
</div>
|
|
326
516
|
</div>
|
|
327
517
|
{/each}
|
|
328
518
|
</div>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { VariantAttribute } from '../../../shop/types.js';
|
|
3
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
4
|
+
import { getLocalizedLabel } from '../../utils/collectionLabel.js';
|
|
5
|
+
import TextField from '../fields/text-field.svelte';
|
|
6
|
+
import NumberField from '../fields/number-field.svelte';
|
|
7
|
+
import DatetimeField from '../fields/datetime-field.svelte';
|
|
8
|
+
import SelectField from '../fields/select-field.svelte';
|
|
9
|
+
import BooleanField from '../fields/boolean-field.svelte';
|
|
10
|
+
import Input from '../../../components/ui/input/input.svelte';
|
|
11
|
+
import type {
|
|
12
|
+
TextField as TextFieldType,
|
|
13
|
+
NumberField as NumberFieldType,
|
|
14
|
+
DateTimeField as DateTimeFieldType,
|
|
15
|
+
SelectField as SelectFieldType,
|
|
16
|
+
BooleanField as BooleanFieldType
|
|
17
|
+
} from '../../../types/fields.js';
|
|
18
|
+
|
|
19
|
+
type Props = {
|
|
20
|
+
attrKey: string;
|
|
21
|
+
attrConfig: VariantAttribute;
|
|
22
|
+
value: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let { attrKey, attrConfig, value = $bindable() }: Props = $props();
|
|
26
|
+
|
|
27
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
28
|
+
const label = $derived(
|
|
29
|
+
getLocalizedLabel(attrConfig.label, interfaceLanguage.current) || attrKey
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Adapter — VariantAttribute → CMS Field shape that each field component expects.
|
|
33
|
+
// Booleans render their own label inline; everything else uses our outer <label>.
|
|
34
|
+
const textField = $derived<TextFieldType>({
|
|
35
|
+
type: 'text',
|
|
36
|
+
slug: attrKey,
|
|
37
|
+
label,
|
|
38
|
+
required: attrConfig.required,
|
|
39
|
+
multiline: false
|
|
40
|
+
});
|
|
41
|
+
const numberField = $derived<NumberFieldType>({
|
|
42
|
+
type: 'number',
|
|
43
|
+
slug: attrKey,
|
|
44
|
+
label,
|
|
45
|
+
required: attrConfig.required
|
|
46
|
+
});
|
|
47
|
+
const datetimeField = $derived<DateTimeFieldType>({
|
|
48
|
+
type: 'datetime',
|
|
49
|
+
slug: attrKey,
|
|
50
|
+
label,
|
|
51
|
+
required: attrConfig.required
|
|
52
|
+
});
|
|
53
|
+
const selectField = $derived<SelectFieldType>(
|
|
54
|
+
attrConfig.type === 'select'
|
|
55
|
+
? {
|
|
56
|
+
type: 'select',
|
|
57
|
+
slug: attrKey,
|
|
58
|
+
label,
|
|
59
|
+
required: attrConfig.required,
|
|
60
|
+
options: attrConfig.options
|
|
61
|
+
}
|
|
62
|
+
: ({} as SelectFieldType)
|
|
63
|
+
);
|
|
64
|
+
const booleanField = $derived<BooleanFieldType>({
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
slug: attrKey,
|
|
67
|
+
label,
|
|
68
|
+
required: attrConfig.required,
|
|
69
|
+
defaultValue: false
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
{#if attrConfig.type === 'boolean'}
|
|
74
|
+
<!-- boolean-field renders its own label, asterisk, and switch row -->
|
|
75
|
+
<BooleanField field={booleanField} bind:value={value as boolean | undefined} />
|
|
76
|
+
{:else}
|
|
77
|
+
<label class="block">
|
|
78
|
+
<span class="text-muted-foreground mb-1 block text-xs font-semibold">
|
|
79
|
+
{label}
|
|
80
|
+
{#if attrConfig.required}<span class="text-destructive ml-0.5" aria-hidden="true"
|
|
81
|
+
>*</span
|
|
82
|
+
>{/if}
|
|
83
|
+
</span>
|
|
84
|
+
|
|
85
|
+
{#if attrConfig.type === 'text'}
|
|
86
|
+
<TextField field={textField} bind:value={value as string | undefined} />
|
|
87
|
+
{:else if attrConfig.type === 'number'}
|
|
88
|
+
<NumberField field={numberField} bind:value={value as number | undefined} />
|
|
89
|
+
{:else if attrConfig.type === 'datetime'}
|
|
90
|
+
<DatetimeField field={datetimeField} bind:value={value as string | undefined} />
|
|
91
|
+
{:else if attrConfig.type === 'select'}
|
|
92
|
+
<SelectField field={selectField} bind:value={value as string | undefined} />
|
|
93
|
+
{:else if attrConfig.type === 'slug'}
|
|
94
|
+
<Input
|
|
95
|
+
bind:value={value as string | undefined}
|
|
96
|
+
type="text"
|
|
97
|
+
pattern="[a-z0-9-]+"
|
|
98
|
+
placeholder="moj-slug"
|
|
99
|
+
/>
|
|
100
|
+
{:else if attrConfig.type === 'image' || attrConfig.type === 'entry'}
|
|
101
|
+
<!-- Pełny picker UI defer post-1.0; tymczasowo: wklej UUID -->
|
|
102
|
+
<Input
|
|
103
|
+
bind:value={value as string | undefined}
|
|
104
|
+
type="text"
|
|
105
|
+
placeholder="UUID ({attrConfig.type === 'image' ? 'media' : 'wpis'})"
|
|
106
|
+
/>
|
|
107
|
+
{/if}
|
|
108
|
+
</label>
|
|
109
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { VariantAttribute } from '../../../shop/types.js';
|
|
2
|
+
type Props = {
|
|
3
|
+
attrKey: string;
|
|
4
|
+
attrConfig: VariantAttribute;
|
|
5
|
+
value: unknown;
|
|
6
|
+
};
|
|
7
|
+
declare const VariantAttributeRenderer: import("svelte").Component<Props, {}, "value">;
|
|
8
|
+
type VariantAttributeRenderer = ReturnType<typeof VariantAttributeRenderer>;
|
|
9
|
+
export default VariantAttributeRenderer;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IconSetPlugin } from '../../types/plugins.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a Map<slug, IconSetPlugin> from icon-set plugin instances.
|
|
4
|
+
* Use in consumer's admin `+layout.svelte` to pass `iconSets` to AdminLayout —
|
|
5
|
+
* Svelte icon components cannot serialize through SvelteKit data, so the same
|
|
6
|
+
* plugin instances must be imported on the client and forwarded explicitly.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildIconSetMap(...plugins: IconSetPlugin[]): Map<string, IconSetPlugin>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a Map<slug, IconSetPlugin> from icon-set plugin instances.
|
|
3
|
+
* Use in consumer's admin `+layout.svelte` to pass `iconSets` to AdminLayout —
|
|
4
|
+
* Svelte icon components cannot serialize through SvelteKit data, so the same
|
|
5
|
+
* plugin instances must be imported on the client and forwarded explicitly.
|
|
6
|
+
*/
|
|
7
|
+
export function buildIconSetMap(...plugins) {
|
|
8
|
+
const map = new Map();
|
|
9
|
+
for (const p of plugins) {
|
|
10
|
+
if (map.has(p.slug)) {
|
|
11
|
+
throw new Error(`Duplicate icon-set plugin slug: "${p.slug}"`);
|
|
12
|
+
}
|
|
13
|
+
map.set(p.slug, p);
|
|
14
|
+
}
|
|
15
|
+
return map;
|
|
16
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { useField } from './use-field.js';
|
|
2
2
|
export { buildCustomFieldsMap } from './build-custom-fields-map.js';
|
|
3
|
+
export { buildIconSetMap } from './build-icon-set-map.js';
|
|
3
4
|
export { useInterfaceLanguage } from '../state/interface-language.svelte.js';
|
|
4
5
|
export { getContentLanguage } from '../state/content-language.svelte.js';
|
|
5
6
|
export { getRemotes } from '../context/remotes.js';
|
|
6
7
|
export { getCustomFields } from '../state/custom-fields.svelte.js';
|
|
8
|
+
export { getIconSets, resolveIconSet } from '../state/icon-sets.svelte.js';
|
|
7
9
|
export { getLocalizedLabel } from '../utils/collectionLabel.js';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { useField } from './use-field.js';
|
|
2
2
|
export { buildCustomFieldsMap } from './build-custom-fields-map.js';
|
|
3
|
+
export { buildIconSetMap } from './build-icon-set-map.js';
|
|
3
4
|
export { useInterfaceLanguage } from '../state/interface-language.svelte.js';
|
|
4
5
|
export { getContentLanguage } from '../state/content-language.svelte.js';
|
|
5
6
|
export { getRemotes } from '../context/remotes.js';
|
|
6
7
|
export { getCustomFields } from '../state/custom-fields.svelte.js';
|
|
8
|
+
export { getIconSets, resolveIconSet } from '../state/icon-sets.svelte.js';
|
|
7
9
|
export { getLocalizedLabel } from '../utils/collectionLabel.js';
|