From 8965328937e505cb880a6ec204a14e707e52ef51 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 30 Nov 2025 00:29:25 +0100 Subject: [PATCH] feat(manacore): add credits management and improve dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add credits API service with balance, transactions, packages support - Create Credits page with overview, transaction history, and package store - Improve Dashboard with real-time credit data and recent transactions - Update Settings page with credits overview and German localization - Add Credits and Feedback to navigation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/manacore/apps/web/src/lib/api/credits.ts | 122 +++++ .../apps/web/src/routes/(app)/+layout.svelte | 55 +- .../web/src/routes/(app)/credits/+page.svelte | 327 ++++++++++++ .../src/routes/(app)/dashboard/+page.svelte | 171 ++++-- .../src/routes/(app)/settings/+page.svelte | 491 ++++++++++-------- 5 files changed, 915 insertions(+), 251 deletions(-) create mode 100644 apps/manacore/apps/web/src/lib/api/credits.ts create mode 100644 apps/manacore/apps/web/src/routes/(app)/credits/+page.svelte diff --git a/apps/manacore/apps/web/src/lib/api/credits.ts b/apps/manacore/apps/web/src/lib/api/credits.ts new file mode 100644 index 000000000..70bf4f761 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/api/credits.ts @@ -0,0 +1,122 @@ +/** + * Credits Service for ManaCore Web App + * Handles credit balance, transactions, and packages + */ + +import { authStore } from '$lib/stores/authStore.svelte'; + +const MANA_AUTH_URL = 'http://localhost:3001'; // TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env + +// Types +export interface CreditBalance { + balance: number; + freeCreditsRemaining: number; + totalEarned: number; + totalSpent: number; + dailyFreeCredits: number; +} + +export interface CreditTransaction { + id: string; + type: 'purchase' | 'usage' | 'refund' | 'bonus' | 'expiry' | 'adjustment'; + status: 'pending' | 'completed' | 'failed' | 'cancelled'; + amount: number; + balanceBefore: number; + balanceAfter: number; + appId?: string; + description?: string; + createdAt: string; +} + +export interface CreditPackage { + id: string; + name: string; + description?: string; + credits: number; + priceEuroCents: number; + stripePriceId?: string; + active: boolean; + sortOrder: number; + metadata?: Record; +} + +export interface CreditPurchase { + id: string; + packageId: string; + credits: number; + priceEuroCents: number; + status: string; + createdAt: string; +} + +// Helper function for authenticated requests +async function fetchWithAuth(endpoint: string, options: RequestInit = {}): Promise { + const token = await authStore.getAccessToken(); + + const response = await fetch(`${MANA_AUTH_URL}${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...options.headers, + }, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Request failed' })); + throw new Error(error.message || `HTTP ${response.status}`); + } + + return response.json(); +} + +// Credits Service +export const creditsService = { + /** + * Get current credit balance + */ + async getBalance(): Promise { + return fetchWithAuth('/api/v1/credits/balance'); + }, + + /** + * Get transaction history + */ + async getTransactions(limit = 50, offset = 0): Promise { + return fetchWithAuth( + `/api/v1/credits/transactions?limit=${limit}&offset=${offset}` + ); + }, + + /** + * Get purchase history + */ + async getPurchases(): Promise { + return fetchWithAuth('/api/v1/credits/purchases'); + }, + + /** + * Get available credit packages (public endpoint) + */ + async getPackages(): Promise { + const response = await fetch(`${MANA_AUTH_URL}/api/v1/credits/packages`); + if (!response.ok) { + throw new Error('Failed to fetch packages'); + } + return response.json(); + }, + + /** + * Use credits (for apps that need to deduct credits) + */ + async useCredits( + amount: number, + appId: string, + description: string + ): Promise<{ success: boolean; newBalance: CreditBalance }> { + return fetchWithAuth('/api/v1/credits/use', { + method: 'POST', + body: JSON.stringify({ amount, appId, description }), + }); + }, +}; diff --git a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte index ae9945e64..e9aa778cd 100644 --- a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte @@ -4,30 +4,58 @@ import type { Snippet } from 'svelte'; import { onMount } from 'svelte'; import { PillNavigation } from '@manacore/shared-ui'; - import type { PillNavItem } from '@manacore/shared-ui'; + import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui'; + import { THEME_DEFINITIONS } from '@manacore/shared-theme'; import { theme } from '$lib/stores/theme'; import { authStore } from '$lib/stores/authStore.svelte'; import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore, } from '$lib/stores/navigation'; + import { getPillAppItems } from '@manacore/shared-branding'; let { children }: { children: Snippet } = $props(); + // App switcher items + const appItems = getPillAppItems('manacore'); + let loading = $state(true); let isSidebarMode = $state(false); let isCollapsed = $state(false); // Get theme state - let effectiveMode = $derived(theme.effectiveMode); + let isDark = $derived(theme.isDark); + + // Theme variant dropdown items + let themeVariantItems = $derived([ + ...theme.variants.map((variant) => ({ + id: variant, + label: THEME_DEFINITIONS[variant].label, + icon: THEME_DEFINITIONS[variant].icon, + onClick: () => theme.setVariant(variant), + active: theme.variant === variant, + })), + { + id: 'all-themes', + label: 'Alle Themes', + icon: 'palette', + onClick: () => goto('/themes'), + active: false, + }, + ]); + + // Current theme variant label + let currentThemeVariantLabel = $derived(THEME_DEFINITIONS[theme.variant].label); + + // User email for user dropdown + let userEmail = $derived(authStore.user?.email); // Navigation items for ManaCore const navItems: PillNavItem[] = [ { href: '/dashboard', label: 'Dashboard', icon: 'home' }, - { href: '/organizations', label: 'Organizations', icon: 'building' }, - { href: '/teams', label: 'Teams', icon: 'users' }, + { href: '/credits', label: 'Credits', icon: 'sparkles' }, + { href: '/feedback', label: 'Feedback', icon: 'message-square' }, { href: '/profile', label: 'Profil', icon: 'user' }, - { href: '/mana', label: 'Mana', icon: 'mana' }, { href: '/settings', label: 'Settings', icon: 'settings' }, ]; @@ -72,6 +100,10 @@ theme.toggleMode(); } + function handleThemeModeChange(mode: 'light' | 'dark' | 'system') { + theme.setMode(mode); + } + async function handleSignOut() { await authStore.signOut(); goto('/login'); @@ -124,14 +156,25 @@ homeRoute="/dashboard" onLogout={handleSignOut} onToggleTheme={handleToggleTheme} - isDark={effectiveMode === 'dark'} + {isDark} {isSidebarMode} onModeChange={handleModeChange} {isCollapsed} onCollapsedChange={handleCollapsedChange} showThemeToggle={true} + showThemeVariants={true} + {themeVariantItems} + {currentThemeVariantLabel} + themeMode={theme.mode} + onThemeModeChange={handleThemeModeChange} showLanguageSwitcher={false} + showLogout={true} primaryColor="#6366f1" + showAppSwitcher={true} + {appItems} + {userEmail} + settingsHref="/settings" + profileHref="/profile" /> diff --git a/apps/manacore/apps/web/src/routes/(app)/credits/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/credits/+page.svelte new file mode 100644 index 000000000..fe1510489 --- /dev/null +++ b/apps/manacore/apps/web/src/routes/(app)/credits/+page.svelte @@ -0,0 +1,327 @@ + + +
+ + + {#if loading} +
+
+
+ {:else if error} + +
+

{error}

+ +
+
+ {:else} + +
+ +
+

Verfügbare Credits

+

+ {formatCredits(balance?.balance ?? 0)} +

+
+
+ +
+

Gratis-Credits heute

+

+ {balance?.freeCreditsRemaining ?? 0} / {balance?.dailyFreeCredits ?? 5} +

+
+
+ +
+

Gesamt erhalten

+

+ {formatCredits(balance?.totalEarned ?? 0)} +

+
+
+ +
+

Gesamt verbraucht

+

+ {formatCredits(balance?.totalSpent ?? 0)} +

+
+
+
+ + +
+ + + +
+ + + {#if activeTab === 'overview'} +
+ + +

Letzte Transaktionen

+ {#if transactions.length === 0} +

Noch keine Transaktionen

+ {:else} +
+ {#each transactions.slice(0, 5) as tx} +
+
+ {getTransactionIcon(tx.type)} +
+

{tx.description || tx.type}

+

{formatDate(tx.createdAt)}

+
+
+ + {tx.amount > 0 ? '+' : ''}{formatCredits(tx.amount)} + +
+ {/each} +
+ + {/if} +
+ + + +

Credits kaufen

+ {#if packages.length === 0} +

Keine Pakete verfügbar

+ {:else} +
+ {#each packages.slice(0, 3) as pkg} + + {/each} +
+ + {/if} +
+
+ + {:else if activeTab === 'transactions'} + +

Transaktionsverlauf

+ {#if transactions.length === 0} +

Noch keine Transaktionen vorhanden.

+ {:else} +
+ + + + + + + + + + + + + {#each transactions as tx} + + + + + + + + + {/each} + +
TypBeschreibungAppBetragKontostandDatum
+ {getTransactionIcon(tx.type)} + {tx.description || '-'}{tx.appId || '-'} + {tx.amount > 0 ? '+' : ''}{formatCredits(tx.amount)} + + {formatCredits(tx.balanceAfter)} + {formatDate(tx.createdAt)}
+
+ {/if} +
+ + {:else if activeTab === 'packages'} +
+ {#each packages as pkg} + +
+

{pkg.name}

+ {#if pkg.description} +

{pkg.description}

+ {/if} +

+ {formatCredits(pkg.credits)} +

+

Credits

+

+ {formatPrice(pkg.priceEuroCents)} +

+ +
+
+ {/each} +
+ {#if packages.length === 0} + +

+ Aktuell sind keine Credit-Pakete verfügbar. +

+
+ {/if} + {/if} + {/if} +
diff --git a/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte index 8cd047807..5c55bbd8d 100644 --- a/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/dashboard/+page.svelte @@ -1,84 +1,185 @@
-
+ +
{#each stats as stat} - -
+ -

Quick Actions

+

Schnellzugriff

+ -

Recent Activity

-

No recent activity

+
+

Letzte Transaktionen

+ + Alle → + +
+ {#if loadingCredits} +
+ {#each [1, 2, 3] as _} +
+
+
+
+
+
+
+ {/each} +
+ {:else if recentTransactions.length === 0} +

Noch keine Transaktionen vorhanden.

+ {:else} +
+ {#each recentTransactions as tx} +
+
+ {getTransactionIcon(tx.type)} +
+

{tx.description || tx.type}

+

+ {new Date(tx.createdAt).toLocaleDateString('de-DE')} +

+
+
+ + {tx.amount > 0 ? '+' : ''}{formatCredits(tx.amount)} + +
+ {/each} +
+ {/if}
diff --git a/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte index 1d1a07429..643ff85d7 100644 --- a/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte @@ -1,227 +1,298 @@ - - - - {#snippet icon()} - - - - {/snippet} +
+ - -
{ - loading = true; - return async ({ update }) => { - await update(); - loading = false; - }; - }} - class="p-5" - > - {#if form?.success} -
- Profile updated successfully! + {#if loading} +
+
+
+ {:else} +
+ + +
+
+
+ + + +
+
+

Profil

+

Deine persönlichen Informationen

+
- {/if} - {#if form?.error} -
- {form.error} -
- {/if} + {#if profileSuccess} +
+ Profil erfolgreich aktualisiert! +
+ {/if} -
-
-
+ {/if} + +
+
+ + +

E-Mail kann nicht geändert werden

+
+ +
+
+ + +
+ +
+ + +
+
+ +
- -
- - -
- -
- - -
- -
- - - + - - - {#snippet icon()} - - - - {/snippet} + + +
+
+
+
+ + + +
+
+

Credits

+

Dein Guthaben für Mana Apps

+
+
+ Alle Details +
- - - {#snippet icon()} - - - - {/snippet} - - {data.profile?.credits || 0} - - +
+
+

Verfügbar

+

+ {creditBalance ? formatCredits(creditBalance.balance) : '...'} +

+
+
+

Gratis heute

+

+ {creditBalance ? `${creditBalance.freeCreditsRemaining}/${creditBalance.dailyFreeCredits}` : '...'} +

+
+
+

Gesamt verbraucht

+

+ {creditBalance ? formatCredits(creditBalance.totalSpent) : '...'} +

+
+
- - {#snippet icon()} - - - - {/snippet} - - {data.profile?.subscription_plan_id || 'Free'} - - + +
+
- - {#snippet icon()} - - - - {/snippet} - - {data.profile?.subscription_status || 'inactive'} - - + + +
+
+
+ + + +
+
+

Konto

+

Konto- und Sicherheitsinformationen

+
+
- - {#snippet icon()} - - - - {/snippet} - - {data.profile?.created_at - ? new Date(data.profile.created_at).toLocaleDateString() - : 'N/A'} - - - - +
+
+
+

Konto-Status

+

Dein aktueller Kontostatus

+
+ + Aktiv + +
- - - {}} - disabled - border={false} - > - {#snippet icon()} - - - - {/snippet} - - - +
+
+

Rolle

+

Deine Berechtigungsstufe

+
+ + {authStore.user?.role || 'user'} + +
+ +
+
+

Benutzer-ID

+

Deine eindeutige Kennung

+
+ + {authStore.user?.sub?.slice(0, 8) || '...'}... + +
+
+
+
+ + + +
+
+
+ + + +
+
+

Gefahrenzone

+

Irreversible Aktionen

+
+
+ +
+
+
+

Konto löschen

+

+ Das Löschen deines Kontos kann nicht rückgängig gemacht werden. +

+
+ +
+
+
+
+
+ {/if} +