mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
feat(manacore): add credits management and improve dashboard
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
eab69c512c
commit
8965328937
5 changed files with 915 additions and 251 deletions
122
apps/manacore/apps/web/src/lib/api/credits.ts
Normal file
122
apps/manacore/apps/web/src/lib/api/credits.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CreditPurchase {
|
||||
id: string;
|
||||
packageId: string;
|
||||
credits: number;
|
||||
priceEuroCents: number;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// Helper function for authenticated requests
|
||||
async function fetchWithAuth<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
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<CreditBalance> {
|
||||
return fetchWithAuth<CreditBalance>('/api/v1/credits/balance');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get transaction history
|
||||
*/
|
||||
async getTransactions(limit = 50, offset = 0): Promise<CreditTransaction[]> {
|
||||
return fetchWithAuth<CreditTransaction[]>(
|
||||
`/api/v1/credits/transactions?limit=${limit}&offset=${offset}`
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get purchase history
|
||||
*/
|
||||
async getPurchases(): Promise<CreditPurchase[]> {
|
||||
return fetchWithAuth<CreditPurchase[]>('/api/v1/credits/purchases');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get available credit packages (public endpoint)
|
||||
*/
|
||||
async getPackages(): Promise<CreditPackage[]> {
|
||||
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 }),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -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<PillDropdownItem[]>([
|
||||
...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"
|
||||
/>
|
||||
|
||||
<!-- Main content with dynamic padding -->
|
||||
|
|
|
|||
327
apps/manacore/apps/web/src/routes/(app)/credits/+page.svelte
Normal file
327
apps/manacore/apps/web/src/routes/(app)/credits/+page.svelte
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Card, PageHeader } from '@manacore/shared-ui';
|
||||
import {
|
||||
creditsService,
|
||||
type CreditBalance,
|
||||
type CreditTransaction,
|
||||
type CreditPackage,
|
||||
} from '$lib/api/credits';
|
||||
|
||||
let balance = $state<CreditBalance | null>(null);
|
||||
let transactions = $state<CreditTransaction[]>([]);
|
||||
let packages = $state<CreditPackage[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let activeTab = $state<'overview' | 'transactions' | 'packages'>('overview');
|
||||
|
||||
onMount(async () => {
|
||||
await loadData();
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const [balanceData, transactionsData, packagesData] = await Promise.all([
|
||||
creditsService.getBalance(),
|
||||
creditsService.getTransactions(20),
|
||||
creditsService.getPackages(),
|
||||
]);
|
||||
balance = balanceData;
|
||||
transactions = transactionsData;
|
||||
packages = packagesData.filter((p) => p.active).sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden der Daten';
|
||||
console.error('Failed to load credits data:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatCredits(amount: number): string {
|
||||
return amount.toLocaleString('de-DE');
|
||||
}
|
||||
|
||||
function formatPrice(cents: number): string {
|
||||
return (cents / 100).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' });
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function getTransactionIcon(type: string): string {
|
||||
switch (type) {
|
||||
case 'purchase':
|
||||
return '💳';
|
||||
case 'usage':
|
||||
return '⚡';
|
||||
case 'refund':
|
||||
return '↩️';
|
||||
case 'bonus':
|
||||
return '🎁';
|
||||
case 'expiry':
|
||||
return '⏰';
|
||||
case 'adjustment':
|
||||
return '🔧';
|
||||
default:
|
||||
return '📝';
|
||||
}
|
||||
}
|
||||
|
||||
function getTransactionColor(type: string): string {
|
||||
switch (type) {
|
||||
case 'purchase':
|
||||
case 'bonus':
|
||||
case 'refund':
|
||||
return 'text-green-600 dark:text-green-400';
|
||||
case 'usage':
|
||||
case 'expiry':
|
||||
return 'text-red-600 dark:text-red-400';
|
||||
default:
|
||||
return 'text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
}
|
||||
|
||||
function handleBuyPackage(pkg: CreditPackage) {
|
||||
// TODO: Integrate with Stripe
|
||||
alert(`Paket "${pkg.name}" kaufen\n\n${formatCredits(pkg.credits)} Credits für ${formatPrice(pkg.priceEuroCents)}\n\nStripe-Integration kommt bald!`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Credits"
|
||||
description="Verwalte deine Mana Credits"
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<Card>
|
||||
<div class="text-center py-8">
|
||||
<p class="text-red-500 mb-4">{error}</p>
|
||||
<button
|
||||
onclick={loadData}
|
||||
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90"
|
||||
>
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
{:else}
|
||||
<!-- Balance Overview Cards -->
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-8">
|
||||
<Card>
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-muted-foreground">Verfügbare Credits</p>
|
||||
<p class="text-3xl font-bold text-primary mt-1">
|
||||
{formatCredits(balance?.balance ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-muted-foreground">Gratis-Credits heute</p>
|
||||
<p class="text-3xl font-bold text-green-600 dark:text-green-400 mt-1">
|
||||
{balance?.freeCreditsRemaining ?? 0} / {balance?.dailyFreeCredits ?? 5}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-muted-foreground">Gesamt erhalten</p>
|
||||
<p class="text-3xl font-bold mt-1">
|
||||
{formatCredits(balance?.totalEarned ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-muted-foreground">Gesamt verbraucht</p>
|
||||
<p class="text-3xl font-bold mt-1">
|
||||
{formatCredits(balance?.totalSpent ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex gap-2 mb-6 border-b border-border">
|
||||
<button
|
||||
onclick={() => (activeTab = 'overview')}
|
||||
class="px-4 py-2 -mb-px transition-colors {activeTab === 'overview'
|
||||
? 'border-b-2 border-primary text-primary font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
Übersicht
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (activeTab = 'transactions')}
|
||||
class="px-4 py-2 -mb-px transition-colors {activeTab === 'transactions'
|
||||
? 'border-b-2 border-primary text-primary font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
Transaktionen
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (activeTab = 'packages')}
|
||||
class="px-4 py-2 -mb-px transition-colors {activeTab === 'packages'
|
||||
? 'border-b-2 border-primary text-primary font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
Credits kaufen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
{#if activeTab === 'overview'}
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<!-- Recent Transactions -->
|
||||
<Card>
|
||||
<h3 class="text-lg font-semibold mb-4">Letzte Transaktionen</h3>
|
||||
{#if transactions.length === 0}
|
||||
<p class="text-muted-foreground text-sm">Noch keine Transaktionen</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each transactions.slice(0, 5) as tx}
|
||||
<div class="flex items-center justify-between py-2 border-b border-border last:border-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl">{getTransactionIcon(tx.type)}</span>
|
||||
<div>
|
||||
<p class="font-medium text-sm">{tx.description || tx.type}</p>
|
||||
<p class="text-xs text-muted-foreground">{formatDate(tx.createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="font-semibold {getTransactionColor(tx.type)}">
|
||||
{tx.amount > 0 ? '+' : ''}{formatCredits(tx.amount)}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
onclick={() => (activeTab = 'transactions')}
|
||||
class="mt-4 text-sm text-primary hover:underline"
|
||||
>
|
||||
Alle anzeigen →
|
||||
</button>
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
<!-- Quick Buy -->
|
||||
<Card>
|
||||
<h3 class="text-lg font-semibold mb-4">Credits kaufen</h3>
|
||||
{#if packages.length === 0}
|
||||
<p class="text-muted-foreground text-sm">Keine Pakete verfügbar</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each packages.slice(0, 3) as pkg}
|
||||
<button
|
||||
onclick={() => handleBuyPackage(pkg)}
|
||||
class="w-full flex items-center justify-between p-3 rounded-lg border border-border hover:bg-surface-hover transition-colors"
|
||||
>
|
||||
<div class="text-left">
|
||||
<p class="font-medium">{pkg.name}</p>
|
||||
<p class="text-sm text-muted-foreground">{formatCredits(pkg.credits)} Credits</p>
|
||||
</div>
|
||||
<span class="font-semibold text-primary">{formatPrice(pkg.priceEuroCents)}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
onclick={() => (activeTab = 'packages')}
|
||||
class="mt-4 text-sm text-primary hover:underline"
|
||||
>
|
||||
Alle Pakete →
|
||||
</button>
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'transactions'}
|
||||
<Card>
|
||||
<h3 class="text-lg font-semibold mb-4">Transaktionsverlauf</h3>
|
||||
{#if transactions.length === 0}
|
||||
<p class="text-muted-foreground">Noch keine Transaktionen vorhanden.</p>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-border text-left text-sm text-muted-foreground">
|
||||
<th class="pb-3 pr-4">Typ</th>
|
||||
<th class="pb-3 pr-4">Beschreibung</th>
|
||||
<th class="pb-3 pr-4">App</th>
|
||||
<th class="pb-3 pr-4 text-right">Betrag</th>
|
||||
<th class="pb-3 pr-4 text-right">Kontostand</th>
|
||||
<th class="pb-3">Datum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each transactions as tx}
|
||||
<tr class="border-b border-border last:border-0">
|
||||
<td class="py-3 pr-4">
|
||||
<span class="text-lg">{getTransactionIcon(tx.type)}</span>
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-sm">{tx.description || '-'}</td>
|
||||
<td class="py-3 pr-4 text-sm text-muted-foreground">{tx.appId || '-'}</td>
|
||||
<td class="py-3 pr-4 text-right font-medium {getTransactionColor(tx.type)}">
|
||||
{tx.amount > 0 ? '+' : ''}{formatCredits(tx.amount)}
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-right text-sm text-muted-foreground">
|
||||
{formatCredits(tx.balanceAfter)}
|
||||
</td>
|
||||
<td class="py-3 text-sm text-muted-foreground">{formatDate(tx.createdAt)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
{:else if activeTab === 'packages'}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each packages as pkg}
|
||||
<Card>
|
||||
<div class="text-center">
|
||||
<h3 class="text-xl font-bold">{pkg.name}</h3>
|
||||
{#if pkg.description}
|
||||
<p class="text-sm text-muted-foreground mt-1">{pkg.description}</p>
|
||||
{/if}
|
||||
<p class="text-4xl font-bold text-primary mt-4">
|
||||
{formatCredits(pkg.credits)}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">Credits</p>
|
||||
<p class="text-2xl font-semibold mt-4">
|
||||
{formatPrice(pkg.priceEuroCents)}
|
||||
</p>
|
||||
<button
|
||||
onclick={() => handleBuyPackage(pkg)}
|
||||
class="mt-4 w-full py-2 px-4 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium"
|
||||
>
|
||||
Kaufen
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
{#if packages.length === 0}
|
||||
<Card>
|
||||
<p class="text-center text-muted-foreground py-8">
|
||||
Aktuell sind keine Credit-Pakete verfügbar.
|
||||
</p>
|
||||
</Card>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -1,84 +1,185 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Card, PageHeader } from '@manacore/shared-ui';
|
||||
import { creditsService, type CreditBalance, type CreditTransaction } from '$lib/api/credits';
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let creditBalance = $state<CreditBalance | null>(null);
|
||||
let recentTransactions = $state<CreditTransaction[]>([]);
|
||||
let loadingCredits = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
if (authStore.isAuthenticated) {
|
||||
try {
|
||||
const [balance, transactions] = await Promise.all([
|
||||
creditsService.getBalance(),
|
||||
creditsService.getTransactions(5),
|
||||
]);
|
||||
creditBalance = balance;
|
||||
recentTransactions = transactions;
|
||||
} catch (e) {
|
||||
console.error('Failed to load credits:', e);
|
||||
} finally {
|
||||
loadingCredits = false;
|
||||
}
|
||||
} else {
|
||||
loadingCredits = false;
|
||||
}
|
||||
});
|
||||
|
||||
const stats = $derived([
|
||||
{
|
||||
name: 'Available Mana',
|
||||
value: data.profile?.credits || 0,
|
||||
name: 'Verfügbare Credits',
|
||||
value: creditBalance?.balance ?? '...',
|
||||
icon: '💰',
|
||||
showProgress: false,
|
||||
href: '/credits',
|
||||
},
|
||||
{
|
||||
name: 'Organizations',
|
||||
value: data.organizationCount || 0,
|
||||
icon: '🏢',
|
||||
showProgress: false,
|
||||
name: 'Gratis-Credits heute',
|
||||
value: creditBalance ? `${creditBalance.freeCreditsRemaining}/${creditBalance.dailyFreeCredits}` : '...',
|
||||
icon: '🎁',
|
||||
href: '/credits',
|
||||
},
|
||||
{
|
||||
name: 'Teams',
|
||||
value: data.teamCount || 0,
|
||||
icon: '👥',
|
||||
showProgress: false,
|
||||
name: 'Gesamt verbraucht',
|
||||
value: creditBalance?.totalSpent ?? '...',
|
||||
icon: '📊',
|
||||
href: '/credits?tab=transactions',
|
||||
},
|
||||
]);
|
||||
|
||||
function formatCredits(amount: number): string {
|
||||
return amount.toLocaleString('de-DE');
|
||||
}
|
||||
|
||||
function getTransactionIcon(type: string): string {
|
||||
switch (type) {
|
||||
case 'purchase': return '💳';
|
||||
case 'usage': return '⚡';
|
||||
case 'bonus': return '🎁';
|
||||
default: return '📝';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
description="Welcome back, {data.profile?.first_name || data.session?.user?.email}"
|
||||
description="Willkommen zurück, {authStore.user?.email || 'User'}"
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each stats as stat}
|
||||
<Card>
|
||||
<div class="flex items-center">
|
||||
<div class="text-4xl">{stat.icon}</div>
|
||||
<div class="ml-4 flex-1">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">{stat.name}</p>
|
||||
<p class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{stat.value}
|
||||
</p>
|
||||
<a href={stat.href} class="block">
|
||||
<Card>
|
||||
<div class="flex items-center">
|
||||
<div class="text-4xl">{stat.icon}</div>
|
||||
<div class="ml-4 flex-1">
|
||||
<p class="text-sm font-medium text-muted-foreground">{stat.name}</p>
|
||||
<p class="mt-1 text-2xl font-semibold">
|
||||
{#if loadingCredits}
|
||||
<span class="inline-block w-16 h-6 bg-muted animate-pulse rounded"></span>
|
||||
{:else}
|
||||
{typeof stat.value === 'number' ? formatCredits(stat.value) : stat.value}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid gap-6 lg:grid-cols-2">
|
||||
<!-- Quick Actions -->
|
||||
<Card>
|
||||
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Quick Actions</h2>
|
||||
<h2 class="mb-4 text-lg font-semibold">Schnellzugriff</h2>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
href="/organizations"
|
||||
class="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
href="/credits"
|
||||
class="block rounded-lg p-3 hover:bg-surface-hover transition-colors"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="text-2xl">🏢</span>
|
||||
<span class="text-2xl">💰</span>
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-900 dark:text-white">Manage Organizations</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">View and create organizations</p>
|
||||
<p class="font-medium">Credits verwalten</p>
|
||||
<p class="text-sm text-muted-foreground">Kontostand und Transaktionen</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/teams" class="block rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<a
|
||||
href="/feedback"
|
||||
class="block rounded-lg p-3 hover:bg-surface-hover transition-colors"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="text-2xl">👥</span>
|
||||
<span class="text-2xl">💬</span>
|
||||
<div class="ml-3">
|
||||
<p class="font-medium text-gray-900 dark:text-white">Manage Teams</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">View and create teams</p>
|
||||
<p class="font-medium">Feedback geben</p>
|
||||
<p class="text-sm text-muted-foreground">Vorschläge und Bug-Reports</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="/profile"
|
||||
class="block rounded-lg p-3 hover:bg-surface-hover transition-colors"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="text-2xl">👤</span>
|
||||
<div class="ml-3">
|
||||
<p class="font-medium">Profil bearbeiten</p>
|
||||
<p class="text-sm text-muted-foreground">Deine Einstellungen</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Recent Transactions -->
|
||||
<Card>
|
||||
<h2 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Recent Activity</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">No recent activity</p>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold">Letzte Transaktionen</h2>
|
||||
<a href="/credits?tab=transactions" class="text-sm text-primary hover:underline">
|
||||
Alle →
|
||||
</a>
|
||||
</div>
|
||||
{#if loadingCredits}
|
||||
<div class="space-y-3">
|
||||
{#each [1, 2, 3] as _}
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 bg-muted animate-pulse rounded"></div>
|
||||
<div class="flex-1">
|
||||
<div class="w-24 h-4 bg-muted animate-pulse rounded"></div>
|
||||
<div class="w-16 h-3 bg-muted animate-pulse rounded mt-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if recentTransactions.length === 0}
|
||||
<p class="text-sm text-muted-foreground">Noch keine Transaktionen vorhanden.</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each recentTransactions as tx}
|
||||
<div class="flex items-center justify-between py-2 border-b border-border last:border-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl">{getTransactionIcon(tx.type)}</span>
|
||||
<div>
|
||||
<p class="font-medium text-sm">{tx.description || tx.type}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{new Date(tx.createdAt).toLocaleDateString('de-DE')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="font-semibold {tx.amount > 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}">
|
||||
{tx.amount > 0 ? '+' : ''}{formatCredits(tx.amount)}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,227 +1,298 @@
|
|||
<script lang="ts">
|
||||
import { Button, Input } from '@manacore/shared-ui';
|
||||
import {
|
||||
SettingsPage,
|
||||
SettingsSection,
|
||||
SettingsCard,
|
||||
SettingsRow,
|
||||
SettingsDangerZone,
|
||||
SettingsDangerButton,
|
||||
} from '@manacore/shared-ui';
|
||||
import { enhance } from '$app/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { Button, Input, Card, PageHeader } from '@manacore/shared-ui';
|
||||
import { authStore } from '$lib/stores/authStore.svelte';
|
||||
import { creditsService, type CreditBalance } from '$lib/api/credits';
|
||||
|
||||
let { data, form } = $props();
|
||||
let loading = $state(false);
|
||||
let loading = $state(true);
|
||||
let savingProfile = $state(false);
|
||||
let profileSuccess = $state(false);
|
||||
let profileError = $state<string | null>(null);
|
||||
|
||||
// Form state
|
||||
let firstName = $state('');
|
||||
let lastName = $state('');
|
||||
|
||||
// Credits data
|
||||
let creditBalance = $state<CreditBalance | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
if (authStore.isAuthenticated) {
|
||||
try {
|
||||
creditBalance = await creditsService.getBalance();
|
||||
} catch (e) {
|
||||
console.error('Failed to load credits:', e);
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
});
|
||||
|
||||
function formatCredits(amount: number): string {
|
||||
return amount.toLocaleString('de-DE');
|
||||
}
|
||||
|
||||
async function handleUpdateProfile() {
|
||||
savingProfile = true;
|
||||
profileSuccess = false;
|
||||
profileError = null;
|
||||
|
||||
try {
|
||||
// TODO: Implement profile update API when available
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
profileSuccess = true;
|
||||
} catch (e) {
|
||||
profileError = e instanceof Error ? e.message : 'Fehler beim Speichern';
|
||||
} finally {
|
||||
savingProfile = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsPage title="Settings" subtitle="Manage your account settings and preferences.">
|
||||
<!-- Profile Section -->
|
||||
<SettingsSection title="Profile Information">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Einstellungen"
|
||||
description="Verwalte deine Kontoeinstellungen und Präferenzen"
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
<SettingsCard>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateProfile"
|
||||
use:enhance={() => {
|
||||
loading = true;
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
loading = false;
|
||||
};
|
||||
}}
|
||||
class="p-5"
|
||||
>
|
||||
{#if form?.success}
|
||||
<div
|
||||
class="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-400"
|
||||
>
|
||||
Profile updated successfully!
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-6">
|
||||
<!-- Profile Section -->
|
||||
<Card>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Profil</h2>
|
||||
<p class="text-sm text-muted-foreground">Deine persönlichen Informationen</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if form?.error}
|
||||
<div
|
||||
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
{#if profileSuccess}
|
||||
<div class="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-400">
|
||||
Profil erfolgreich aktualisiert!
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
for="email"
|
||||
class="mb-2 block text-sm font-medium text-[hsl(var(--foreground))]"
|
||||
{#if profileError}
|
||||
<div class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400">
|
||||
{profileError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="mb-2 block text-sm font-medium">E-Mail</label>
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
value={authStore.user?.email || ''}
|
||||
disabled
|
||||
class="bg-muted"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">E-Mail kann nicht geändert werden</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="firstName" class="mb-2 block text-sm font-medium">Vorname</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="firstName"
|
||||
bind:value={firstName}
|
||||
placeholder="Max"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="lastName" class="mb-2 block text-sm font-medium">Nachname</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="lastName"
|
||||
bind:value={lastName}
|
||||
placeholder="Mustermann"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onclick={handleUpdateProfile}
|
||||
loading={savingProfile}
|
||||
class="w-full sm:w-auto"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
value={data.session?.user?.email || ''}
|
||||
disabled
|
||||
class="bg-[hsl(var(--muted))]"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-[hsl(var(--muted-foreground))]">Email cannot be changed</p>
|
||||
{savingProfile ? 'Speichern...' : 'Änderungen speichern'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="firstName"
|
||||
class="mb-2 block text-sm font-medium text-[hsl(var(--foreground))]"
|
||||
>
|
||||
First Name
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
placeholder="John"
|
||||
value={data.profile?.first_name || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="lastName"
|
||||
class="mb-2 block text-sm font-medium text-[hsl(var(--foreground))]"
|
||||
>
|
||||
Last Name
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
placeholder="Doe"
|
||||
value={data.profile?.last_name || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" {loading} class="w-full">
|
||||
{loading ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
</Card>
|
||||
|
||||
<!-- Account Info Section -->
|
||||
<SettingsSection title="Account Information">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<!-- Credits Section -->
|
||||
<Card>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Credits</h2>
|
||||
<p class="text-sm text-muted-foreground">Dein Guthaben für Mana Apps</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/credits" class="text-sm text-primary hover:underline">Alle Details</a>
|
||||
</div>
|
||||
|
||||
<SettingsCard>
|
||||
<SettingsRow label="Available Credits">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<span class="text-2xl font-bold text-[hsl(var(--primary))]">
|
||||
{data.profile?.credits || 0}
|
||||
</span>
|
||||
</SettingsRow>
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div class="rounded-lg bg-surface-hover p-4 text-center">
|
||||
<p class="text-sm text-muted-foreground">Verfügbar</p>
|
||||
<p class="text-2xl font-bold text-primary">
|
||||
{creditBalance ? formatCredits(creditBalance.balance) : '...'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-surface-hover p-4 text-center">
|
||||
<p class="text-sm text-muted-foreground">Gratis heute</p>
|
||||
<p class="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
{creditBalance ? `${creditBalance.freeCreditsRemaining}/${creditBalance.dailyFreeCredits}` : '...'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-surface-hover p-4 text-center">
|
||||
<p class="text-sm text-muted-foreground">Gesamt verbraucht</p>
|
||||
<p class="text-2xl font-bold">
|
||||
{creditBalance ? formatCredits(creditBalance.totalSpent) : '...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsRow label="Subscription Plan">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<span class="rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900/20 dark:text-blue-400">
|
||||
{data.profile?.subscription_plan_id || 'Free'}
|
||||
</span>
|
||||
</SettingsRow>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<a
|
||||
href="/credits?tab=packages"
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Credits kaufen
|
||||
</a>
|
||||
<a
|
||||
href="/credits?tab=transactions"
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-border px-4 py-2 text-sm font-medium hover:bg-surface-hover transition-colors"
|
||||
>
|
||||
Transaktionen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<SettingsRow label="Subscription Status">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<span
|
||||
class="rounded-full px-3 py-1 text-xs font-medium
|
||||
{data.profile?.subscription_status === 'active'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-400'}"
|
||||
>
|
||||
{data.profile?.subscription_status || 'inactive'}
|
||||
</span>
|
||||
</SettingsRow>
|
||||
<!-- Account Section -->
|
||||
<Card>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Konto</h2>
|
||||
<p class="text-sm text-muted-foreground">Konto- und Sicherheitsinformationen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsRow label="Member Since" border={false}>
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<span class="text-sm text-[hsl(var(--foreground))]">
|
||||
{data.profile?.created_at
|
||||
? new Date(data.profile.created_at).toLocaleDateString()
|
||||
: 'N/A'}
|
||||
</span>
|
||||
</SettingsRow>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between py-3 border-b border-border">
|
||||
<div>
|
||||
<p class="font-medium">Konto-Status</p>
|
||||
<p class="text-sm text-muted-foreground">Dein aktueller Kontostatus</p>
|
||||
</div>
|
||||
<span class="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-800 dark:bg-green-900/20 dark:text-green-400">
|
||||
Aktiv
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<SettingsDangerZone title="Danger Zone">
|
||||
<SettingsDangerButton
|
||||
label="Delete Account"
|
||||
description="Once you delete your account, there is no going back. Please be certain."
|
||||
buttonText="Delete Account"
|
||||
onclick={() => {}}
|
||||
disabled
|
||||
border={false}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
</SettingsDangerButton>
|
||||
</SettingsDangerZone>
|
||||
</SettingsPage>
|
||||
<div class="flex items-center justify-between py-3 border-b border-border">
|
||||
<div>
|
||||
<p class="font-medium">Rolle</p>
|
||||
<p class="text-sm text-muted-foreground">Deine Berechtigungsstufe</p>
|
||||
</div>
|
||||
<span class="rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900/20 dark:text-blue-400">
|
||||
{authStore.user?.role || 'user'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div>
|
||||
<p class="font-medium">Benutzer-ID</p>
|
||||
<p class="text-sm text-muted-foreground">Deine eindeutige Kennung</p>
|
||||
</div>
|
||||
<code class="rounded bg-muted px-2 py-1 text-xs font-mono">
|
||||
{authStore.user?.sub?.slice(0, 8) || '...'}...
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<Card>
|
||||
<div class="p-6 border-red-200 dark:border-red-800">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-red-600 dark:text-red-400">Gefahrenzone</h2>
|
||||
<p class="text-sm text-muted-foreground">Irreversible Aktionen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-red-200 dark:border-red-800 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-medium text-red-600 dark:text-red-400">Konto löschen</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Das Löschen deines Kontos kann nicht rückgängig gemacht werden.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled
|
||||
class="bg-red-600 hover:bg-red-700 text-white"
|
||||
>
|
||||
Konto löschen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue