Compare commits
5 Commits
67700c0e75
...
c99442b54b
| Author | SHA1 | Date | |
|---|---|---|---|
|
c99442b54b
|
|||
|
5b35c9e63b
|
|||
|
b66c458a4d
|
|||
|
2f2fcc2f51
|
|||
|
693db06128
|
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.41.3",
|
||||
"version": "1.43.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -178,6 +178,9 @@ const translations: Translations = {
|
||||
search_icon: { en: 'Search icon...', de: 'Icon suchen...' },
|
||||
save: { en: 'Save', de: 'Speichern' },
|
||||
saving: { en: 'Saving...', de: 'Speichern...' },
|
||||
edit_name: { en: 'Name', de: 'Name' },
|
||||
edit_qty: { en: 'Amount', de: 'Menge' },
|
||||
edit_qty_ph: { en: 'e.g. 3x, 500g, 1L', de: 'z.B. 3x, 500g, 1L' },
|
||||
|
||||
// EnhancedBalance
|
||||
your_balance: { en: 'Your Balance', de: 'Dein Saldo' },
|
||||
|
||||
@@ -219,6 +219,11 @@ export function createShoppingSync() {
|
||||
debouncedPush();
|
||||
}
|
||||
|
||||
function updateItem(id: string, patch: Partial<Omit<ShoppingItem, 'id'>>) {
|
||||
items = items.map(item => item.id === id ? { ...item, ...patch } : item);
|
||||
debouncedPush();
|
||||
}
|
||||
|
||||
return {
|
||||
get items() { return items; },
|
||||
get status() { return status; },
|
||||
@@ -232,6 +237,7 @@ export function createShoppingSync() {
|
||||
removeItem,
|
||||
clearChecked,
|
||||
updateItemCategory,
|
||||
updateItem,
|
||||
disconnect
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ParamMatcher } from '@sveltejs/kit';
|
||||
|
||||
// Accepts the new Latin slugs (`vetus` / `novus`) plus the legacy year slugs
|
||||
// so old bookmarks land on a load handler that 307s them to the new shape.
|
||||
export const match: ParamMatcher = (param) => {
|
||||
return param === '1962' || param === '1969';
|
||||
return param === 'vetus' || param === 'novus' || param === '1962' || param === '1969';
|
||||
};
|
||||
|
||||
@@ -61,14 +61,12 @@
|
||||
};
|
||||
const STORE_NAMES = Object.keys(STORE_PRESETS);
|
||||
|
||||
let selectedStore = $state(
|
||||
(typeof localStorage !== 'undefined' && localStorage.getItem('shopping-store')) || STORE_NAMES[0]
|
||||
);
|
||||
let selectedStore = $state(STORE_NAMES[0]);
|
||||
let categoryOrder = $derived(STORE_PRESETS[selectedStore] || STORE_PRESETS[STORE_NAMES[0]]);
|
||||
|
||||
function setStore(name) {
|
||||
selectedStore = name;
|
||||
localStorage.setItem('shopping-store', name);
|
||||
try { localStorage.setItem('shopping-store', name); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
let newItemName = $state('');
|
||||
@@ -149,6 +147,11 @@
|
||||
let totalCount = $derived(sync.items.length);
|
||||
|
||||
onMount(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('shopping-store');
|
||||
if (saved && STORE_PRESETS[saved]) selectedStore = saved;
|
||||
} catch { /* ignore */ }
|
||||
|
||||
if (data.initialList) {
|
||||
sync.connect(shareToken);
|
||||
} else {
|
||||
@@ -205,6 +208,8 @@
|
||||
let longPressTimer = $state(null);
|
||||
/** @type {import('$lib/js/shoppingSync.svelte').ShoppingItem | null} */
|
||||
let editingItem = $state(null);
|
||||
let editName = $state('');
|
||||
let editQty = $state('');
|
||||
let editCategory = $state('');
|
||||
let editIcon = $state('');
|
||||
let iconSearch = $state('');
|
||||
@@ -238,6 +243,9 @@
|
||||
function startLongPress(item) {
|
||||
longPressTimer = window.setTimeout(() => {
|
||||
editingItem = item;
|
||||
const parsed = parseQuantity(item.name);
|
||||
editName = parsed.name;
|
||||
editQty = parsed.qty || '';
|
||||
editCategory = item.category;
|
||||
editIcon = item.icon || '';
|
||||
iconSearch = '';
|
||||
@@ -355,14 +363,21 @@
|
||||
async function saveEdit() {
|
||||
if (!editingItem) return;
|
||||
editSaving = true;
|
||||
const cleanName = parseQuantity(editingItem.name).name;
|
||||
const trimmedName = editName.trim();
|
||||
const trimmedQty = editQty.trim();
|
||||
if (!trimmedName) { editSaving = false; return; }
|
||||
const newRawName = trimmedQty ? `${trimmedQty} ${trimmedName}` : trimmedName;
|
||||
try {
|
||||
await fetch(sync.apiUrl('/api/cospend/list/categorize/override'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: cleanName, category: editCategory, icon: editIcon || null })
|
||||
body: JSON.stringify({ name: trimmedName, category: editCategory, icon: editIcon || null })
|
||||
});
|
||||
sync.updateItem(editingItem.id, {
|
||||
name: newRawName,
|
||||
category: editCategory,
|
||||
icon: editIcon || null
|
||||
});
|
||||
sync.updateItemCategory(editingItem.id, editCategory, editIcon || null);
|
||||
closeEdit();
|
||||
} catch (err) {
|
||||
console.error('[shopping] Save override error:', err);
|
||||
@@ -482,6 +497,19 @@
|
||||
<div class="edit-modal" onclick={(e) => e.stopPropagation()}>
|
||||
<h3>{parseQuantity(editingItem.name).name}</h3>
|
||||
|
||||
<div class="name-qty-row">
|
||||
<div class="field name-field">
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="edit-label">{t('edit_name', lang)}</label>
|
||||
<input class="edit-input" type="text" bind:value={editName} />
|
||||
</div>
|
||||
<div class="field qty-field">
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="edit-label">{t('edit_qty', lang)}</label>
|
||||
<input class="edit-input" type="text" bind:value={editQty} placeholder={t('edit_qty_ph', lang)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="edit-label">{t('kategorie', lang)}</label>
|
||||
<div class="category-picker">
|
||||
@@ -934,6 +962,29 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.name-qty-row {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.field { display: flex; flex-direction: column; }
|
||||
.name-field { flex: 2; }
|
||||
.qty-field { flex: 1; }
|
||||
.edit-input {
|
||||
padding: 0.5rem 0.65rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.9rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.edit-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--nord10);
|
||||
}
|
||||
|
||||
.category-picker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -17,7 +17,7 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, locals, fetch }) => {
|
||||
default: async ({ request, locals, fetch, params }) => {
|
||||
const session = await locals.auth();
|
||||
|
||||
if (!session || !session.user?.nickname) {
|
||||
@@ -217,7 +217,7 @@ export const actions: Actions = {
|
||||
}
|
||||
|
||||
// Success - redirect to dashboard
|
||||
throw redirect(303, '/cospend');
|
||||
throw redirect(303, `/${params.cospendRoot}/dash`);
|
||||
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === 'object' && 'status' in error && (error as { status: number }).status === 303) throw error; // Re-throw redirect
|
||||
|
||||
@@ -10,5 +10,5 @@ export const load: PageServerLoad = async ({ params, url, fetch }) => {
|
||||
throw redirect(307, `/${params.faithLang}/${slug}`);
|
||||
}
|
||||
const search = url.search ?? '';
|
||||
throw redirect(307, `/${params.faithLang}/${params.calendar}/1962${search}`);
|
||||
throw redirect(307, `/${params.faithLang}/${params.calendar}/vetus${search}`);
|
||||
};
|
||||
|
||||
+18
-7
@@ -40,7 +40,18 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
const lang: CalendarLang =
|
||||
params.faithLang === 'faith' ? 'en' : params.faithLang === 'fides' ? 'la' : 'de';
|
||||
|
||||
const rite: Rite = params.rite === '1969' ? '1969' : '1962';
|
||||
// Legacy year-slug links (/.../1962/... or /.../1969/...) forward to the new
|
||||
// Latin slugs so old bookmarks stay alive.
|
||||
if (params.rite === '1962' || params.rite === '1969') {
|
||||
const tail = url.pathname.split('/').slice(4).join('/');
|
||||
const suffix = tail ? `/${tail}` : '';
|
||||
throw redirect(
|
||||
307,
|
||||
`/${params.faithLang}/${params.calendar}/${params.rite === '1962' ? 'vetus' : 'novus'}${suffix}${url.search}`
|
||||
);
|
||||
}
|
||||
|
||||
const rite: Rite = params.rite === 'novus' ? 'novus' : 'vetus';
|
||||
|
||||
const dioceseParam = url.searchParams.get('diocese');
|
||||
const diocese1962: Diocese1962 = isDiocese1962(dioceseParam)
|
||||
@@ -57,7 +68,7 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const minYear = rite === '1962' ? 1900 : 1969;
|
||||
const minYear = rite === 'vetus' ? 1900 : 1969;
|
||||
const yParam = params.yyyy ? Number(params.yyyy) : NaN;
|
||||
const mParam = params.mm ? Number(params.mm) - 1 : NaN;
|
||||
|
||||
@@ -65,7 +76,7 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
const month = Number.isFinite(mParam) && mParam >= 0 && mParam <= 11 ? mParam : today.getMonth();
|
||||
|
||||
const fetchLy = async (y: number) =>
|
||||
rite === '1962'
|
||||
rite === 'vetus'
|
||||
? await getYear1962(lang, diocese1962, y)
|
||||
: await getYear(lang, diocese1969, y);
|
||||
|
||||
@@ -135,7 +146,7 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
|
||||
const todayIso = today.toISOString().slice(0, 10);
|
||||
const todayYearMap =
|
||||
rite === '1962'
|
||||
rite === 'vetus'
|
||||
? await getYear1962(lang, diocese1962, today.getFullYear())
|
||||
: await getYear(lang, diocese1969, today.getFullYear());
|
||||
const todayEntry = todayYearMap.get(todayIso) ?? null;
|
||||
@@ -154,7 +165,7 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
? yearMap
|
||||
: selectedYear === today.getFullYear()
|
||||
? todayYearMap
|
||||
: rite === '1962'
|
||||
: rite === 'vetus'
|
||||
? await getYear1962(lang, diocese1962, selectedYear)
|
||||
: await getYear(lang, diocese1969, selectedYear);
|
||||
const selectedEntry = selectedYearMap.get(selectedIso) ?? monthDays[0];
|
||||
@@ -241,7 +252,7 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
// are unresolved i18n paths like 'lent.season', so always go through
|
||||
// `season1962Label`. 1969: prefer the engine-resolved localized name.
|
||||
const name =
|
||||
rite === '1962'
|
||||
rite === 'vetus'
|
||||
? (key ? season1962Label(key, lang) : '')
|
||||
: key && key !== d.seasonKey
|
||||
? key
|
||||
@@ -264,7 +275,7 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
|
||||
return {
|
||||
rite,
|
||||
diocese: rite === '1962' ? diocese1962 : diocese1969,
|
||||
diocese: rite === 'vetus' ? diocese1962 : diocese1969,
|
||||
wip: false,
|
||||
year,
|
||||
liturgicalYear,
|
||||
|
||||
+16
-16
@@ -65,7 +65,7 @@
|
||||
|
||||
const rite = $derived(data.rite);
|
||||
const wip = $derived(data.wip);
|
||||
const riteSubtitle = $derived(t(rite === '1962' ? 'rite1962Long' : 'rite1969Long', lang));
|
||||
const riteSubtitle = $derived(t(rite === 'vetus' ? 'rite1962Long' : 'rite1969Long', lang));
|
||||
|
||||
function pad(n: number) {
|
||||
return String(n).padStart(2, '0');
|
||||
@@ -94,11 +94,11 @@
|
||||
|
||||
// Only append ?diocese= when non-default, keeps default URLs clean.
|
||||
const dioceseQuery = $derived.by(() => {
|
||||
const def = rite === '1962' ? DEFAULT_DIOCESE_1962 : DEFAULT_DIOCESE_1969;
|
||||
const def = rite === 'vetus' ? DEFAULT_DIOCESE_1962 : DEFAULT_DIOCESE_1969;
|
||||
return diocese && diocese !== def ? `?diocese=${diocese}` : '';
|
||||
});
|
||||
|
||||
const dioceseOptions = $derived(rite === '1962' ? DIOCESES_1962 : DIOCESES_1969);
|
||||
const dioceseOptions = $derived(rite === 'vetus' ? DIOCESES_1962 : DIOCESES_1969);
|
||||
|
||||
// URL: /{faithLang}/{calendar}/{rite}/{yyyy}/{mm}/{dd} — rite is a required
|
||||
// path segment so day/month nav stays inside the active rite.
|
||||
@@ -133,14 +133,14 @@
|
||||
// When switching rites we drop ?diocese because the ID spaces differ (1962 has
|
||||
// diocesan calendars, 1969 only "general" or "switzerland"). The server
|
||||
// re-applies each rite's default if none is given.
|
||||
function riteHref(r: '1969' | '1962') {
|
||||
function riteHref(r: 'novus' | 'vetus') {
|
||||
const dd = selectedIso.slice(8, 10);
|
||||
return `${calendarBase}/${r}/${year}/${pad(month + 1)}/${dd}`;
|
||||
}
|
||||
|
||||
function onDioceseChange(e: Event) {
|
||||
const next = (e.currentTarget as HTMLSelectElement).value;
|
||||
const def = rite === '1962' ? DEFAULT_DIOCESE_1962 : DEFAULT_DIOCESE_1969;
|
||||
const def = rite === 'vetus' ? DEFAULT_DIOCESE_1962 : DEFAULT_DIOCESE_1969;
|
||||
const dd = selectedIso.slice(8, 10);
|
||||
const path = `${riteBase}/${year}/${pad(month + 1)}/${dd}`;
|
||||
goto(next === def ? path : `${path}?diocese=${next}`, { noScroll: true });
|
||||
@@ -159,21 +159,21 @@
|
||||
<div class="rite-toggle" role="tablist" aria-label="Rite">
|
||||
<a
|
||||
role="tab"
|
||||
aria-selected={rite === '1969'}
|
||||
aria-selected={rite === 'novus'}
|
||||
class="rite-pill"
|
||||
class:active={rite === '1969'}
|
||||
href={riteHref('1969')}
|
||||
class:active={rite === 'novus'}
|
||||
href={riteHref('novus')}
|
||||
>
|
||||
1969
|
||||
Novus
|
||||
</a>
|
||||
<a
|
||||
role="tab"
|
||||
aria-selected={rite === '1962'}
|
||||
aria-selected={rite === 'vetus'}
|
||||
class="rite-pill"
|
||||
class:active={rite === '1962'}
|
||||
href={riteHref('1962')}
|
||||
class:active={rite === 'vetus'}
|
||||
href={riteHref('vetus')}
|
||||
>
|
||||
1962
|
||||
Vetus
|
||||
</a>
|
||||
</div>
|
||||
<label class="diocese-picker">
|
||||
@@ -184,7 +184,7 @@
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{#if rite === '1969'}
|
||||
{#if rite === 'novus'}
|
||||
<p class="diocese-note">{t('rite1969SwissNote', lang)}</p>
|
||||
{/if}
|
||||
</header>
|
||||
@@ -196,7 +196,7 @@
|
||||
<p>{t('wipBody', lang)}</p>
|
||||
</section>
|
||||
{:else}
|
||||
{#if rite === '1962'}
|
||||
{#if rite === 'vetus'}
|
||||
<aside class="disclaimer" role="note">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
|
||||
<div>
|
||||
@@ -210,7 +210,7 @@
|
||||
day={hero}
|
||||
{lang}
|
||||
{todayIso}
|
||||
href={rite === '1962' ? detailHref(hero.iso) : undefined}
|
||||
href={rite === 'vetus' ? detailHref(hero.iso) : undefined}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
+34
-7
@@ -24,8 +24,18 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
const lang: CalendarLang =
|
||||
params.faithLang === 'faith' ? 'en' : params.faithLang === 'fides' ? 'la' : 'de';
|
||||
|
||||
const rite: Rite = params.rite === '1969' ? '1969' : '1962';
|
||||
if (rite !== '1962') await errorWithVerse(fetch, url.pathname, 404, 'Not found');
|
||||
// Legacy year-slug links forward to the Latin slugs.
|
||||
if (params.rite === '1962' || params.rite === '1969') {
|
||||
const tail = url.pathname.split('/').slice(4).join('/');
|
||||
const suffix = tail ? `/${tail}` : '';
|
||||
throw redirect(
|
||||
307,
|
||||
`/${params.faithLang}/${params.calendar}/${params.rite === '1962' ? 'vetus' : 'novus'}${suffix}${url.search}`
|
||||
);
|
||||
}
|
||||
|
||||
const rite: Rite = params.rite === 'novus' ? 'novus' : 'vetus';
|
||||
if (rite !== 'vetus') await errorWithVerse(fetch, url.pathname, 404, 'Not found');
|
||||
|
||||
const dioceseParam = url.searchParams.get('diocese');
|
||||
const diocese1962: Diocese1962 = isDiocese1962(dioceseParam)
|
||||
@@ -35,7 +45,7 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
? dioceseParam
|
||||
: DEFAULT_DIOCESE_1969;
|
||||
|
||||
const minYear = rite === '1962' ? 1900 : 1969;
|
||||
const minYear = rite === 'vetus' ? 1900 : 1969;
|
||||
const year = Number(params.yyyy);
|
||||
const month = Number(params.mm) - 1;
|
||||
const day = Number(params.dd);
|
||||
@@ -46,10 +56,27 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
if (!Number.isFinite(day) || day < 1 || day > daysInMonth) await errorWithVerse(fetch, url.pathname, 404, 'Not found');
|
||||
|
||||
const iso = isoFor(year, month, day);
|
||||
|
||||
// Romcal (scope: liturgical) emits LY N past Saturday-before-Advent-I of
|
||||
// civil year N with a stale tail (still on the previous post-Pentecost
|
||||
// cycle) before going missing. Dates from Advent I of civil year N onward
|
||||
// belong to LY N+1, so fetch both and pick the correct one — mirrors the
|
||||
// rollover logic in the month page.
|
||||
const fetchLy = async (y: number) =>
|
||||
rite === 'vetus'
|
||||
? await getYear1962(lang, diocese1962, y)
|
||||
: await getYear(lang, diocese1969, y);
|
||||
const yearMapN = await fetchLy(year);
|
||||
const yearMapNext = await fetchLy(year + 1);
|
||||
let adventIOfUrlYear: string | null = null;
|
||||
for (const [i, d] of yearMapNext) {
|
||||
if (d.id === 'advent_1_sunday' || d.id === 'first_sunday_of_advent') {
|
||||
adventIOfUrlYear = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const yearMap =
|
||||
rite === '1962'
|
||||
? await getYear1962(lang, diocese1962, year)
|
||||
: await getYear(lang, diocese1969, year);
|
||||
adventIOfUrlYear != null && iso >= adventIOfUrlYear ? yearMapNext : yearMapN;
|
||||
const entry = yearMap.get(iso);
|
||||
if (!entry) await errorWithVerse(fetch, url.pathname, 404, 'Not found');
|
||||
|
||||
@@ -59,7 +86,7 @@ export const load: PageServerLoad = async ({ params, url, locals, fetch }) => {
|
||||
return {
|
||||
lang,
|
||||
rite,
|
||||
diocese: rite === '1962' ? diocese1962 : diocese1969,
|
||||
diocese: rite === 'vetus' ? diocese1962 : diocese1969,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
|
||||
@@ -145,9 +145,9 @@ export function t(key: keyof typeof ui, lang: CalendarLang): string {
|
||||
return ui[key][lang] ?? ui[key].en;
|
||||
}
|
||||
|
||||
export type Rite = '1969' | '1962';
|
||||
export type Rite = 'novus' | 'vetus';
|
||||
export function isValidRite(v: string | null): v is Rite {
|
||||
return v === '1969' || v === '1962';
|
||||
return v === 'novus' || v === 'vetus';
|
||||
}
|
||||
|
||||
// --- Diocese selection ---
|
||||
|
||||
Reference in New Issue
Block a user