From 99a9f908f15c30caa52875d4a4b5bd12762b0763 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Fri, 7 Aug 2026 15:49:43 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20move=20tabs=20into=20own=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/BalanceSummary.vue | 27 +++- app/composables/useConfirm.ts | 8 + app/composables/useIdentity.ts | 17 +-- app/pages/p/[slug].vue | 163 ++++++++++++++++++++ app/pages/p/[slug]/expenses.vue | 94 ++++++++++++ app/pages/p/[slug]/index.vue | 242 ++---------------------------- app/pages/p/[slug]/settings.vue | 3 +- app/utils/identity.test.ts | 47 ------ app/utils/identity.ts | 25 --- server/database/client.ts | 2 +- server/plugins/db-migrate.ts | 2 +- server/utils/pots.ts | 6 +- shared/utils/money.ts | 10 +- 13 files changed, 321 insertions(+), 325 deletions(-) create mode 100644 app/composables/useConfirm.ts create mode 100644 app/pages/p/[slug].vue create mode 100644 app/pages/p/[slug]/expenses.vue delete mode 100644 app/utils/identity.test.ts delete mode 100644 app/utils/identity.ts diff --git a/app/components/BalanceSummary.vue b/app/components/BalanceSummary.vue index 8a9caf9..8984856 100644 --- a/app/components/BalanceSummary.vue +++ b/app/components/BalanceSummary.vue @@ -14,6 +14,7 @@ const emit = defineEmits<{ settled: []; updated: [] }>(); const { lastLink, rememberLink } = usePaymentLinkCache(); +const showSettlements = ref(false); const settlingKey = ref(null); const editingId = ref(null); const editValue = ref(''); @@ -200,17 +201,33 @@ async function markSettled(settlement: SettlementView) {
-

- Suggested payments -

+

Everyone is settled up.

-
    +
    • ('identity-name', () => null); - const loaded = useState('identity-loaded', () => false); - - if (import.meta.client && !loaded.value) { - const identity = readIdentity(window.localStorage); - name.value = identity?.name ?? null; - loaded.value = true; - } + const name = useCookie('stb-identity-name', { + default: () => null, + maxAge: 60 * 60 * 24 * 365, + sameSite: 'lax', + }); function setName(newName: string) { const trimmed = newName.trim(); if (!trimmed) return; name.value = trimmed; - if (import.meta.client) - writeIdentity(window.localStorage, { name: trimmed }); } return { name, setName }; diff --git a/app/pages/p/[slug].vue b/app/pages/p/[slug].vue new file mode 100644 index 0000000..f4cf69d --- /dev/null +++ b/app/pages/p/[slug].vue @@ -0,0 +1,163 @@ + + + diff --git a/app/pages/p/[slug]/expenses.vue b/app/pages/p/[slug]/expenses.vue new file mode 100644 index 0000000..36018b5 --- /dev/null +++ b/app/pages/p/[slug]/expenses.vue @@ -0,0 +1,94 @@ + + + diff --git a/app/pages/p/[slug]/index.vue b/app/pages/p/[slug]/index.vue index e4053c2..bbeac9f 100644 --- a/app/pages/p/[slug]/index.vue +++ b/app/pages/p/[slug]/index.vue @@ -1,238 +1,22 @@ diff --git a/app/pages/p/[slug]/settings.vue b/app/pages/p/[slug]/settings.vue index 4871525..ef5415c 100644 --- a/app/pages/p/[slug]/settings.vue +++ b/app/pages/p/[slug]/settings.vue @@ -5,6 +5,7 @@ const route = useRoute(); const slug = route.params.slug as string; const { data, refresh, error } = await useFetch(`/api/pots/${slug}`); +const { confirm } = useConfirm(); const adding = ref(false); const removingId = ref(null); @@ -24,7 +25,7 @@ async function addParticipant(participantName: string) { } async function removeParticipant(participant: { id: number; name: string }) { - if (!window.confirm(`Remove ${participant.name} from this Pot?`)) return; + if (!confirm(`Remove ${participant.name} from this Pot?`)) return; removeError.value = ''; removingId.value = participant.id; diff --git a/app/utils/identity.test.ts b/app/utils/identity.test.ts deleted file mode 100644 index d85afe3..0000000 --- a/app/utils/identity.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { readIdentity, writeIdentity } from './identity'; - -function createMockStorage(): Storage { - const store = new Map(); - return { - getItem: (key: string) => store.get(key) ?? null, - setItem: (key: string, value: string) => void store.set(key, value), - removeItem: (key: string) => void store.delete(key), - clear: () => store.clear(), - key: () => null, - get length() { - return store.size; - }, - }; -} - -describe('identity storage', () => { - it('returns null when nothing is stored', () => { - expect(readIdentity(createMockStorage())).toBeNull(); - }); - - it('round-trips a written identity', () => { - const storage = createMockStorage(); - writeIdentity(storage, { name: 'Alice' }); - expect(readIdentity(storage)).toEqual({ name: 'Alice' }); - }); - - it('ignores malformed JSON', () => { - const storage = createMockStorage(); - storage.setItem('stb:identity', '{not json'); - expect(readIdentity(storage)).toBeNull(); - }); - - it('ignores an empty stored name', () => { - const storage = createMockStorage(); - storage.setItem('stb:identity', JSON.stringify({ name: ' ' })); - expect(readIdentity(storage)).toBeNull(); - }); - - it('overwrites a previously stored identity', () => { - const storage = createMockStorage(); - writeIdentity(storage, { name: 'Alice' }); - writeIdentity(storage, { name: 'Bob' }); - expect(readIdentity(storage)).toEqual({ name: 'Bob' }); - }); -}); diff --git a/app/utils/identity.ts b/app/utils/identity.ts deleted file mode 100644 index 606d48b..0000000 --- a/app/utils/identity.ts +++ /dev/null @@ -1,25 +0,0 @@ -const STORAGE_KEY = 'stb:identity'; - -export interface Identity { - name: string; -} - -type StorageLike = Pick; - -export function readIdentity(storage: StorageLike): Identity | null { - const raw = storage.getItem(STORAGE_KEY); - if (!raw) return null; - - try { - const parsed = JSON.parse(raw); - if (typeof parsed?.name !== 'string' || !parsed.name.trim()) return null; - - return { name: parsed.name }; - } catch { - return null; - } -} - -export function writeIdentity(storage: StorageLike, identity: Identity): void { - storage.setItem(STORAGE_KEY, JSON.stringify(identity)); -} diff --git a/server/database/client.ts b/server/database/client.ts index 7284f42..200f45b 100644 --- a/server/database/client.ts +++ b/server/database/client.ts @@ -5,7 +5,7 @@ import type { H3Event } from 'h3'; let instance: ReturnType> | undefined; -export function useDb(event: H3Event) { +export function useDb(event?: H3Event) { if (!instance) { const config = useRuntimeConfig(event); const client = createClient({ diff --git a/server/plugins/db-migrate.ts b/server/plugins/db-migrate.ts index f2865de..9ae3365 100644 --- a/server/plugins/db-migrate.ts +++ b/server/plugins/db-migrate.ts @@ -2,7 +2,7 @@ import { migrate } from 'drizzle-orm/libsql/migrator'; import { useDb } from '../database/client'; export default defineNitroPlugin(async () => { - await migrate(useDb(event), { + await migrate(useDb(), { migrationsFolder: 'server/database/migrations', }); }); diff --git a/server/utils/pots.ts b/server/utils/pots.ts index 7c7a4c5..1eb74e8 100644 --- a/server/utils/pots.ts +++ b/server/utils/pots.ts @@ -18,7 +18,7 @@ export interface ExpenseWithDetails { } export async function getPotOrThrow(slug: string) { - const db = useDb(event); + const db = useDb(); const [pot] = await db.select().from(pots).where(eq(pots.slug, slug)); if (!pot) { throw createError({ statusCode: 404, statusMessage: 'Pot not found' }); @@ -27,14 +27,14 @@ export async function getPotOrThrow(slug: string) { } export async function getParticipants(slug: string) { - const db = useDb(event); + const db = useDb(); return db.select().from(participants).where(eq(participants.potSlug, slug)); } export async function getExpensesWithParticipants( slug: string, ): Promise { - const db = useDb(event); + const db = useDb(); const expenseRows = await db .select() .from(expenses) diff --git a/shared/utils/money.ts b/shared/utils/money.ts index a2c72dd..05b85b0 100644 --- a/shared/utils/money.ts +++ b/shared/utils/money.ts @@ -6,8 +6,16 @@ export function fromCents(cents: number): number { return cents / 100; } +const LOCALE_BY_CURRENCY: Record = { + EUR: 'de-DE', + USD: 'en-US', + GBP: 'en-GB', + CHF: 'fr-CH', +}; + export function formatCurrency(cents: number, currency: string): string { - return new Intl.NumberFormat(undefined, { + const locale = LOCALE_BY_CURRENCY[currency] ?? 'de-DE'; + return new Intl.NumberFormat(locale, { style: 'currency', currency, }).format(fromCents(cents));