feat(manacore): add costs overview tab to credits page

Adds a "Kosten" tab showing all 40+ credit operations across all apps,
grouped by app with category filters (AI, Productivity, Premium) and
color-coded cost badges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-24 10:10:44 +01:00
parent d7cef38379
commit 44a9e02525
17 changed files with 200 additions and 192 deletions

View file

@ -23,7 +23,7 @@ async function seed() {
address: 'Münsterplatz 1, 78462 Konstanz',
latitude: 47.6603,
longitude: 9.1757,
imageUrl: '/images/muenster.svg',
imageUrl: '/images/muenster.jpg',
timeline: [
{ year: '615', event: 'Grundsteinlegung' },
{ year: '1089', event: 'Romanischer Neubau' },
@ -38,7 +38,7 @@ async function seed() {
address: 'Hafenstraße, 78462 Konstanz',
latitude: 47.6596,
longitude: 9.1784,
imageUrl: '/images/imperia.svg',
imageUrl: '/images/imperia.jpg',
timeline: [{ year: '1993', event: 'Aufstellung im Hafen' }],
},
{
@ -49,7 +49,7 @@ async function seed() {
address: 'Seestraße 25, 78464 Konstanz',
latitude: 47.6589,
longitude: 9.1795,
imageUrl: '/images/ophelia.svg',
imageUrl: '/images/ophelia.jpg',
},
{
name: 'LAGO Shopping Center',
@ -58,7 +58,7 @@ async function seed() {
address: 'Bodanstraße 1, 78462 Konstanz',
latitude: 47.6615,
longitude: 9.1742,
imageUrl: '/images/lago.svg',
imageUrl: '/images/lago.jpg',
},
{
name: 'Rosgartenmuseum',

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View file

@ -1,23 +0,0 @@
<svg width="800" height="500" viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg">
<rect width="800" height="500" fill="#87CEEB"/>
<rect x="0" y="370" width="800" height="130" fill="#8B9467"/>
<!-- Modern museum building -->
<rect x="150" y="160" width="500" height="220" fill="#D4C5A0" rx="2"/>
<!-- Large entrance area -->
<rect x="300" y="200" width="200" height="180" fill="#B8A888"/>
<!-- Glass entrance -->
<rect x="340" y="250" width="120" height="130" fill="#A8D8EA" opacity="0.7" rx="2"/>
<!-- Windows row -->
<rect x="170" y="200" width="100" height="30" fill="#A8D8EA" opacity="0.5"/>
<rect x="530" y="200" width="100" height="30" fill="#A8D8EA" opacity="0.5"/>
<rect x="170" y="260" width="100" height="30" fill="#A8D8EA" opacity="0.5"/>
<rect x="530" y="260" width="100" height="30" fill="#A8D8EA" opacity="0.5"/>
<!-- Archaeological artifacts (decorative) -->
<circle cx="250" cy="340" r="25" fill="#CD853F" opacity="0.6"/>
<rect x="540" y="320" width="40" height="50" fill="#CD853F" opacity="0.6" rx="2"/>
<!-- Banner -->
<rect x="320" y="170" width="160" height="25" fill="#9333ea" rx="3"/>
<text x="400" y="189" text-anchor="middle" font-family="system-ui" font-size="12" fill="#fff" font-weight="600">ARCHÄOLOGIE</text>
<!-- Label -->
<text x="400" y="460" text-anchor="middle" font-family="system-ui" font-size="20" fill="#fff" font-weight="600">Archäologisches Landesmuseum</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View file

@ -1,35 +0,0 @@
<svg width="800" height="500" viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="water" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#5B9BD5"/>
<stop offset="100%" stop-color="#2E75B6"/>
</linearGradient>
</defs>
<!-- Sky -->
<rect width="800" height="280" fill="#FDE8D0"/>
<!-- Water -->
<rect x="0" y="280" width="800" height="220" fill="url(#water)"/>
<!-- Pier -->
<rect x="300" y="260" width="200" height="40" fill="#A08060" rx="2"/>
<rect x="340" y="300" width="20" height="60" fill="#8B7355"/>
<rect x="440" y="300" width="20" height="60" fill="#8B7355"/>
<!-- Statue base -->
<rect x="370" y="200" width="60" height="80" fill="#808080"/>
<!-- Statue figure -->
<ellipse cx="400" cy="160" rx="35" ry="50" fill="#C0C0C0"/>
<!-- Head -->
<circle cx="400" cy="105" r="20" fill="#D4C5A0"/>
<!-- Crown -->
<polygon points="385,90 390,75 395,88 400,72 405,88 410,75 415,90" fill="#DAA520"/>
<!-- Arms holding figures -->
<line x1="365" y1="145" x2="340" y2="130" stroke="#C0C0C0" stroke-width="8" stroke-linecap="round"/>
<line x1="435" y1="145" x2="460" y2="130" stroke="#C0C0C0" stroke-width="8" stroke-linecap="round"/>
<circle cx="335" cy="120" r="12" fill="#DAA520"/>
<circle cx="465" cy="120" r="12" fill="#8B0000"/>
<!-- Sun -->
<circle cx="680" cy="80" r="50" fill="#FFD700" opacity="0.6"/>
<!-- Waves -->
<path d="M0,320 Q50,310 100,320 Q150,330 200,320 Q250,310 300,320 Q350,330 400,320 Q450,310 500,320 Q550,330 600,320 Q650,310 700,320 Q750,330 800,320" fill="none" stroke="#fff" stroke-width="2" opacity="0.3"/>
<!-- Label -->
<text x="400" y="470" text-anchor="middle" font-family="system-ui" font-size="22" fill="#fff" font-weight="600">Imperia</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

View file

@ -1,33 +0,0 @@
<svg width="800" height="500" viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="glass" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#E8F4FD"/>
<stop offset="100%" stop-color="#B8D4E8"/>
</linearGradient>
</defs>
<rect width="800" height="500" fill="#87CEEB"/>
<!-- Ground -->
<rect x="0" y="400" width="800" height="100" fill="#888"/>
<!-- Building -->
<rect x="150" y="120" width="500" height="290" fill="#F5F5F5" rx="6"/>
<!-- Glass facade -->
<rect x="160" y="130" width="480" height="270" fill="url(#glass)" rx="4"/>
<!-- Grid lines (glass panels) -->
<line x1="280" y1="130" x2="280" y2="400" stroke="#ccc" stroke-width="1"/>
<line x1="400" y1="130" x2="400" y2="400" stroke="#ccc" stroke-width="1"/>
<line x1="520" y1="130" x2="520" y2="400" stroke="#ccc" stroke-width="1"/>
<line x1="160" y1="200" x2="640" y2="200" stroke="#ccc" stroke-width="1"/>
<line x1="160" y1="270" x2="640" y2="270" stroke="#ccc" stroke-width="1"/>
<line x1="160" y1="340" x2="640" y2="340" stroke="#ccc" stroke-width="1"/>
<!-- Entrance -->
<rect x="350" y="350" width="100" height="50" fill="#4A90D9" rx="2"/>
<!-- LAGO text on building -->
<text x="400" y="180" text-anchor="middle" font-family="system-ui" font-size="48" fill="#2563eb" font-weight="700" letter-spacing="8">LAGO</text>
<!-- People (simple) -->
<circle cx="320" cy="420" r="6" fill="#555"/>
<line x1="320" y1="426" x2="320" y2="445" stroke="#555" stroke-width="3"/>
<circle cx="480" cy="418" r="6" fill="#555"/>
<line x1="480" y1="424" x2="480" y2="443" stroke="#555" stroke-width="3"/>
<!-- Label -->
<text x="400" y="480" text-anchor="middle" font-family="system-ui" font-size="22" fill="#fff" font-weight="600">LAGO Shopping-Center</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

View file

@ -1,31 +0,0 @@
<svg width="800" height="500" viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#87CEEB"/>
<stop offset="100%" stop-color="#E0F0FF"/>
</linearGradient>
</defs>
<rect width="800" height="500" fill="url(#sky)"/>
<!-- Ground -->
<rect x="0" y="380" width="800" height="120" fill="#8B9467"/>
<rect x="0" y="370" width="800" height="20" fill="#A0A878"/>
<!-- Cathedral body -->
<rect x="250" y="150" width="300" height="230" fill="#D4C5A0" rx="4"/>
<!-- Tower -->
<rect x="370" y="50" width="60" height="330" fill="#C8B890"/>
<!-- Spire -->
<polygon points="400,10 370,80 430,80" fill="#6B7B5E"/>
<!-- Windows -->
<circle cx="400" cy="200" r="30" fill="#4A6FA5" opacity="0.6"/>
<rect x="310" y="280" width="30" height="50" fill="#4A6FA5" opacity="0.5" rx="15"/>
<rect x="360" y="280" width="30" height="50" fill="#4A6FA5" opacity="0.5" rx="15"/>
<rect x="410" y="280" width="30" height="50" fill="#4A6FA5" opacity="0.5" rx="15"/>
<rect x="460" y="280" width="30" height="50" fill="#4A6FA5" opacity="0.5" rx="15"/>
<!-- Door -->
<rect x="365" y="320" width="70" height="60" fill="#5C4A3A" rx="35"/>
<!-- Cross -->
<rect x="396" y="15" width="8" height="25" fill="#8B7355"/>
<rect x="389" y="22" width="22" height="8" fill="#8B7355"/>
<!-- Label -->
<text x="400" y="470" text-anchor="middle" font-family="system-ui" font-size="22" fill="#fff" font-weight="600">Konstanzer Münster</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

View file

@ -1,33 +0,0 @@
<svg width="800" height="500" viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="evening" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a1a2e"/>
<stop offset="100%" stop-color="#16213e"/>
</linearGradient>
</defs>
<rect width="800" height="500" fill="url(#evening)"/>
<!-- Lake reflection -->
<rect x="0" y="350" width="800" height="150" fill="#0f3460" opacity="0.5"/>
<!-- Building -->
<rect x="200" y="180" width="400" height="190" fill="#2a2a4a" rx="4"/>
<!-- Windows with warm glow -->
<rect x="240" y="210" width="50" height="40" fill="#FFD700" opacity="0.7" rx="2"/>
<rect x="310" y="210" width="50" height="40" fill="#FFD700" opacity="0.5" rx="2"/>
<rect x="380" y="210" width="50" height="40" fill="#FFD700" opacity="0.8" rx="2"/>
<rect x="450" y="210" width="50" height="40" fill="#FFD700" opacity="0.6" rx="2"/>
<rect x="520" y="210" width="50" height="40" fill="#FFD700" opacity="0.4" rx="2"/>
<!-- Restaurant windows (ground floor, brighter) -->
<rect x="240" y="290" width="110" height="60" fill="#FFE4B5" opacity="0.9" rx="2"/>
<rect x="370" y="290" width="110" height="60" fill="#FFE4B5" opacity="0.9" rx="2"/>
<rect x="500" y="290" width="70" height="60" fill="#FFE4B5" opacity="0.7" rx="2"/>
<!-- Stars -->
<circle cx="100" cy="60" r="2" fill="#fff" opacity="0.8"/>
<circle cx="250" cy="40" r="1.5" fill="#fff" opacity="0.6"/>
<circle cx="500" cy="80" r="2" fill="#fff" opacity="0.7"/>
<circle cx="650" cy="50" r="1.5" fill="#fff" opacity="0.5"/>
<circle cx="720" cy="100" r="2" fill="#fff" opacity="0.8"/>
<!-- Michelin stars indicator -->
<text x="400" y="160" text-anchor="middle" font-size="28" fill="#FFD700">★★</text>
<!-- Label -->
<text x="400" y="460" text-anchor="middle" font-family="system-ui" font-size="22" fill="#fff" font-weight="600">Restaurant Ophelia</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View file

@ -1,25 +0,0 @@
<svg width="800" height="500" viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg">
<rect width="800" height="500" fill="#87CEEB"/>
<rect x="0" y="380" width="800" height="120" fill="#8B9467"/>
<!-- Historic building -->
<rect x="200" y="150" width="400" height="240" fill="#E8D5B0" rx="4"/>
<!-- Roof -->
<polygon points="180,150 400,50 620,150" fill="#8B4513"/>
<!-- Dormers -->
<polygon points="300,120 330,80 360,120" fill="#8B4513"/>
<rect x="315" y="95" width="30" height="25" fill="#4A6FA5" opacity="0.6"/>
<polygon points="440,120 470,80 500,120" fill="#8B4513"/>
<rect x="455" y="95" width="30" height="25" fill="#4A6FA5" opacity="0.6"/>
<!-- Windows (historic style) -->
<rect x="240" y="190" width="40" height="55" fill="#4A6FA5" opacity="0.5" rx="20"/>
<rect x="310" y="190" width="40" height="55" fill="#4A6FA5" opacity="0.5" rx="20"/>
<rect x="450" y="190" width="40" height="55" fill="#4A6FA5" opacity="0.5" rx="20"/>
<rect x="520" y="190" width="40" height="55" fill="#4A6FA5" opacity="0.5" rx="20"/>
<!-- Door -->
<rect x="370" y="300" width="60" height="90" fill="#5C4A3A" rx="30"/>
<!-- Museum sign -->
<rect x="300" y="270" width="200" height="25" fill="#8B4513" rx="3"/>
<text x="400" y="289" text-anchor="middle" font-family="serif" font-size="14" fill="#FFE4B5" font-weight="600">ROSGARTENMUSEUM</text>
<!-- Label -->
<text x="400" y="460" text-anchor="middle" font-family="system-ui" font-size="22" fill="#fff" font-weight="600">Rosgartenmuseum</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -4,7 +4,7 @@
"name": "Konstanzer Münster",
"category": "Sehenswürdigkeit",
"description": "Das Konstanzer Münster ist eine imposante Basilika, die über Jahrhunderte das Zentrum des Bistums Konstanz war. Besucher können den Turm besteigen und einen atemberaubenden Blick über die Stadt und den Bodensee genießen.",
"image": "/images/muenster.svg",
"image": "/images/muenster.jpg",
"address": "Münsterplatz 1, 78462 Konstanz",
"coordinates": {
"lat": 47.663,
@ -28,7 +28,7 @@
"name": "Imperia",
"category": "Sehenswürdigkeit",
"description": "Die Imperia ist eine satirische Statue im Hafen von Konstanz, die an das Konzil von Konstanz erinnert. Sie dreht sich langsam um ihre Achse und ist ein beliebtes Fotomotiv.",
"image": "/images/imperia.svg",
"image": "/images/imperia.jpg",
"address": "Hafenstraße, 78462 Konstanz",
"coordinates": {
"lat": 47.66,
@ -40,7 +40,7 @@
"name": "Restaurant Ophelia",
"category": "Restaurant",
"description": "Das mit zwei Michelin-Sternen ausgezeichnete Restaurant Ophelia bietet eine exquisite Küche in einem eleganten Ambiente. Es befindet sich im Hotel Riva am Ufer des Bodensees.",
"image": "/images/ophelia.svg",
"image": "/images/ophelia.jpg",
"address": "Seestraße 25, 78464 Konstanz",
"coordinates": {
"lat": 47.67,
@ -52,7 +52,7 @@
"name": "LAGO Shopping-Center",
"category": "Laden",
"description": "Das LAGO ist das größte Einkaufszentrum am Bodensee und bietet eine Vielzahl von Geschäften, Restaurants und Cafés. Es ist ein beliebter Treffpunkt für Einheimische und Touristen.",
"image": "/images/lago.svg",
"image": "/images/lago.jpg",
"address": "Bodanstraße 1, 78462 Konstanz",
"coordinates": {
"lat": 47.658,
@ -64,7 +64,7 @@
"name": "Rosgartenmuseum",
"category": "Museum",
"description": "Das Rosgartenmuseum ist das städtische Museum für Kunst, Kultur und Geschichte von Konstanz und der Bodenseeregion. Es wurde 1870 gegründet und befindet sich in einem ehemaligen Zunfthaus.",
"image": "/images/rosgartenmuseum.svg",
"image": "/images/rosgartenmuseum.jpg",
"address": "Rosgartenstraße 3-5, 78462 Konstanz",
"coordinates": {
"lat": 47.661,
@ -83,7 +83,7 @@
"name": "Archäologisches Landesmuseum Baden-Württemberg",
"category": "Museum",
"description": "Das Archäologische Landesmuseum (ALM) in Konstanz ist ein Zweigmuseum des ALM in Stuttgart und zeigt Funde aus der Archäologie, Geschichte und Kultur der Region.",
"image": "/images/alm.svg",
"image": "/images/alm.jpg",
"address": "Benediktinerplatz 5, 78467 Konstanz",
"coordinates": {
"lat": 47.665,

View file

@ -42,8 +42,8 @@
"vitest": "^4.0.14"
},
"dependencies": {
"@manacore/credit-operations": "workspace:^",
"@manacore/qr-export": "workspace:*",
"@manacore/wallpaper-generator": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
@ -63,6 +63,7 @@
"@manacore/shared-types": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"@manacore/shared-utils": "workspace:*",
"@manacore/wallpaper-generator": "workspace:*",
"svelte-dnd-action": "^0.9.68",
"svelte-i18n": "^4.0.0"
},

View file

@ -8,13 +8,91 @@
type CreditTransaction,
type CreditPackage,
} from '$lib/api/credits';
import {
OPERATION_METADATA,
CREDIT_COSTS,
CreditCategory,
formatCreditCost,
type CreditOperationType,
} from '@manacore/credit-operations';
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');
let activeTab = $state<'overview' | 'transactions' | 'packages' | 'costs'>('overview');
let costFilter = $state<'all' | 'ai' | 'productivity' | 'premium'>('all');
// Build pricing data grouped by app
const allOperations = $derived(
Object.entries(OPERATION_METADATA).map(([op, meta]) => ({
operation: op as CreditOperationType,
name: meta.name,
description: meta.description,
category: meta.category,
app: meta.app,
cost: CREDIT_COSTS[op as CreditOperationType],
formattedCost: formatCreditCost(CREDIT_COSTS[op as CreditOperationType]),
}))
);
const filteredOperations = $derived(
costFilter === 'all'
? allOperations
: allOperations.filter((op) => {
if (costFilter === 'ai') return op.category === CreditCategory.AI;
if (costFilter === 'productivity') return op.category === CreditCategory.PRODUCTIVITY;
if (costFilter === 'premium') return op.category === CreditCategory.PREMIUM;
return true;
})
);
const operationsByApp = $derived(() => {
const groups: Record<string, typeof filteredOperations> = {};
for (const op of filteredOperations) {
if (!groups[op.app]) groups[op.app] = [];
groups[op.app].push(op);
}
// Sort apps alphabetically
return Object.fromEntries(Object.entries(groups).sort(([a], [b]) => a.localeCompare(b)));
});
const APP_LABELS: Record<string, string> = {
calendar: 'Kalender',
chat: 'Chat',
contacts: 'Kontakte',
context: 'Context',
general: 'Allgemein',
manadeck: 'ManaDeck',
matrix: 'Matrix Bots',
nutriphi: 'NutriPhi',
picture: 'Picture',
planta: 'Planta',
presi: 'Presi',
questions: 'Questions',
skilltree: 'SkillTree',
todo: 'Todo',
traces: 'Traces',
zitare: 'Zitare',
};
function getAppLabel(app: string): string {
return APP_LABELS[app] ?? app.charAt(0).toUpperCase() + app.slice(1);
}
function getCategoryLabel(category: CreditCategory): string {
switch (category) {
case CreditCategory.AI:
return 'KI-Features';
case CreditCategory.PRODUCTIVITY:
return 'Erstellen';
case CreditCategory.PREMIUM:
return 'Premium';
default:
return category;
}
}
let processingPackageId = $state<string | null>(null);
// Toast notification
@ -26,6 +104,7 @@
const tab = $page.url.searchParams.get('tab');
if (tab === 'packages') activeTab = 'packages';
else if (tab === 'transactions') activeTab = 'transactions';
else if (tab === 'costs') activeTab = 'costs';
// Handle success/canceled from Stripe redirect
const success = $page.url.searchParams.get('success');
@ -212,6 +291,14 @@
>
Credits kaufen
</button>
<button
onclick={() => (activeTab = 'costs')}
class="px-4 py-2 -mb-px transition-colors {activeTab === 'costs'
? 'border-b-2 border-primary text-primary font-medium'
: 'text-muted-foreground hover:text-foreground'}"
>
Kosten
</button>
</div>
<!-- Tab Content -->
@ -394,6 +481,75 @@
</p>
</Card>
{/if}
{:else if activeTab === 'costs'}
<!-- Category Filter -->
<div class="flex flex-wrap gap-2 mb-6">
{#each [{ key: 'all', label: 'Alle' }, { key: 'ai', label: 'KI-Features' }, { key: 'productivity', label: 'Erstellen' }, { key: 'premium', label: 'Premium' }] as filter}
<button
onclick={() => (costFilter = filter.key as typeof costFilter)}
class="px-3 py-1.5 rounded-full text-sm font-medium transition-colors {costFilter ===
filter.key
? 'bg-primary text-primary-foreground'
: 'bg-surface-hover text-muted-foreground hover:text-foreground'}"
>
{filter.label}
</button>
{/each}
</div>
<!-- Info -->
<div
class="mb-6 p-4 rounded-lg bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800"
>
<p class="text-sm text-blue-800 dark:text-blue-200">
Lesen, Bearbeiten, Löschen und Organisieren von Einträgen ist immer <strong
>kostenlos</strong
>. Credits werden nur für die unten aufgeführten Aktionen verbraucht.
</p>
</div>
<!-- Operations grouped by app -->
{@const groups = operationsByApp()}
<div class="space-y-4">
{#each Object.entries(groups) as [app, operations]}
<Card>
<h3 class="text-lg font-semibold mb-4">{getAppLabel(app)}</h3>
<div class="divide-y divide-border">
{#each operations as op}
<div class="flex items-center justify-between py-3">
<div class="flex-1 min-w-0 pr-4">
<p class="font-medium text-sm">{op.name}</p>
<p class="text-xs text-muted-foreground mt-0.5">{op.description}</p>
</div>
<div class="flex items-center gap-3 flex-shrink-0">
<span class="text-xs text-muted-foreground"
>{getCategoryLabel(op.category)}</span
>
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold tabular-nums {op.cost ===
0
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: op.cost < 1
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'}"
>
{op.cost === 0 ? 'Kostenlos' : op.formattedCost}
</span>
</div>
</div>
{/each}
</div>
</Card>
{/each}
{#if Object.keys(groups).length === 0}
<Card>
<p class="text-center text-muted-foreground py-8">
Keine Operationen in dieser Kategorie.
</p>
</Card>
{/if}
</div>
{/if}
{/if}
</div>

31
pnpm-lock.yaml generated
View file

@ -2174,6 +2174,9 @@ importers:
apps/manacore/apps/web:
dependencies:
'@manacore/credit-operations':
specifier: workspace:^
version: link:../../../../packages/credit-operations
'@manacore/qr-export':
specifier: workspace:*
version: link:../../../../packages/qr-export
@ -7289,6 +7292,34 @@ importers:
specifier: ^3.0.5
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
services/it-landing:
dependencies:
'@astrojs/check':
specifier: ^0.9.0
version: 0.9.5(prettier-plugin-astro@0.14.1)(prettier@3.6.2)(typescript@5.9.3)
'@astrojs/sitemap':
specifier: ^3.2.1
version: 3.6.0
'@manacore/shared-landing-ui':
specifier: workspace:*
version: link:../../packages/shared-landing-ui
astro:
specifier: ^5.16.0
version: 5.18.1(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1)
typescript:
specifier: ^5.0.0
version: 5.9.3
devDependencies:
'@astrojs/tailwind':
specifier: ^6.0.0
version: 6.0.2(astro@5.18.1(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.1))
tailwindcss:
specifier: ^3.4.17
version: 3.4.18(tsx@4.21.0)(yaml@2.8.1)
services/mana-api-gateway:
dependencies:
'@manacore/shared-nestjs-auth':