mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
refactor(settings): rewrite GeneralSection inline + delete @mana/subscriptions
GeneralSection: replace the GlobalSettingsSection wrapper (which rendered its own SettingsSection pill + SettingsCard, requiring title="" to suppress the inner header) with inline settings rows. Each setting is a label+control row with scoped CSS — no double-card, no wrapper hack. Delete packages/subscriptions/ — the package is dead after merging its SubscriptionPage into the Credits & Abo workbench app. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f203e100c1
commit
e2d540a958
18 changed files with 247 additions and 1755 deletions
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"name": "@mana/subscriptions",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Unified subscription package — types and UI components",
|
||||
"type": "module",
|
||||
"svelte": "./src/index.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"svelte": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { BillingCycle } from './plans';
|
||||
|
||||
interface Props {
|
||||
billingCycle: BillingCycle;
|
||||
onChange: (cycle: BillingCycle) => void;
|
||||
yearlyDiscount?: string;
|
||||
monthlyLabel?: string;
|
||||
yearlyLabel?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
billingCycle,
|
||||
onChange,
|
||||
yearlyDiscount = '33%',
|
||||
monthlyLabel = 'Monatlich',
|
||||
yearlyLabel = 'Jährlich',
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="billing-toggle">
|
||||
<button
|
||||
onclick={() => onChange('monthly')}
|
||||
class="billing-toggle__button"
|
||||
class:billing-toggle__button--active={billingCycle === 'monthly'}
|
||||
>
|
||||
<span class="billing-toggle__label">
|
||||
{monthlyLabel}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => onChange('yearly')}
|
||||
class="billing-toggle__button billing-toggle__button--yearly"
|
||||
class:billing-toggle__button--active={billingCycle === 'yearly'}
|
||||
>
|
||||
<span class="billing-toggle__label">
|
||||
{yearlyLabel}
|
||||
</span>
|
||||
{#if yearlyDiscount}
|
||||
<span class="billing-toggle__discount">
|
||||
-{yearlyDiscount}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.billing-toggle {
|
||||
display: flex;
|
||||
max-width: 32rem;
|
||||
margin: 0 auto 0.5rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:global(.dark) .billing-toggle {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.billing-toggle__button {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.billing-toggle__button--yearly {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.billing-toggle__button--active {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: hsl(var(--color-primary, 221 83% 53%));
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .billing-toggle__button--active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.billing-toggle__button:hover:not(.billing-toggle__button--active) {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
:global(.dark) .billing-toggle__button:hover:not(.billing-toggle__button--active) {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.billing-toggle__label {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.billing-toggle__discount {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: hsl(var(--color-primary, 221 83% 53%));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { CostItem } from './usage';
|
||||
|
||||
interface Props {
|
||||
costs: CostItem[];
|
||||
title?: string;
|
||||
manaLabel?: string;
|
||||
}
|
||||
|
||||
let { costs, title = 'Mana-Kosten', manaLabel = 'Mana' }: Props = $props();
|
||||
|
||||
// Icon mapping
|
||||
const iconPaths: Record<string, string> = {
|
||||
'mic-outline':
|
||||
'M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z',
|
||||
'chatbubble-outline':
|
||||
'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',
|
||||
'add-circle-outline': 'M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'copy-outline':
|
||||
'M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z',
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="cost-card">
|
||||
<h3 class="cost-card__title">{title}</h3>
|
||||
|
||||
<div class="cost-card__list">
|
||||
{#each costs as item}
|
||||
<div class="cost-card__item">
|
||||
<div class="cost-card__item-left">
|
||||
<svg
|
||||
class="cost-card__icon"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d={iconPaths[item.icon] || iconPaths['mic-outline']} />
|
||||
</svg>
|
||||
<p class="cost-card__action">
|
||||
{item.action}
|
||||
</p>
|
||||
</div>
|
||||
<p class="cost-card__cost">
|
||||
{item.cost}
|
||||
{manaLabel}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cost-card {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:global(.dark) .cost-card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.cost-card__title {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.cost-card__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.cost-card__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cost-card__item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cost-card__icon {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
margin-right: 0.5rem;
|
||||
color: hsl(var(--color-primary, 221 83% 53%));
|
||||
}
|
||||
|
||||
.cost-card__action {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.cost-card__cost {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
color?: string;
|
||||
size?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { color = '#0099FF', size = 24, class: className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" class={className}>
|
||||
<path
|
||||
d="M12.3047 1C12.3392 1.04573 19.608 10.6706 19.6084 14.6953C19.6084 18.7293 16.3386 21.9998 12.3047 22C8.27061 22 5 18.7294 5 14.6953C5.00041 10.661 12.3047 1 12.3047 1ZM12.3047 7.3916C12.2811 7.42276 8.65234 12.2288 8.65234 14.2393C8.65241 16.2562 10.2877 17.8916 12.3047 17.8916C14.3217 17.8916 15.957 16.2562 15.957 14.2393C15.957 12.2301 12.3331 7.42917 12.3047 7.3916Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { ManaPackage } from './plans';
|
||||
import ManaIcon from './ManaIcon.svelte';
|
||||
|
||||
interface Props {
|
||||
package: ManaPackage;
|
||||
onSelect: (packageId: string) => void;
|
||||
popularLabel?: string;
|
||||
buyLabel?: string;
|
||||
}
|
||||
|
||||
let { package: pkg, onSelect, popularLabel = 'Beliebt', buyLabel = 'Kaufen' }: Props = $props();
|
||||
|
||||
function formatPrice(p: ManaPackage) {
|
||||
return p.priceString || `${p.price.toFixed(2).replace('.', ',')}€`;
|
||||
}
|
||||
|
||||
function getColor() {
|
||||
const id = pkg.id.toLowerCase();
|
||||
if (id.includes('small')) return '#2196F3';
|
||||
if (id.includes('medium')) return '#1976D2';
|
||||
if (id.includes('large')) return '#1565C0';
|
||||
if (id.includes('giant')) return '#0D47A1';
|
||||
return '#0288D1';
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="row" class:popular={pkg.popular} onclick={() => onSelect(pkg.id)}>
|
||||
<div class="icon">
|
||||
<ManaIcon size={20} color={getColor()} />
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<span class="name">
|
||||
{pkg.name}
|
||||
{#if pkg.popular}
|
||||
<span class="badge">{popularLabel}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="mana">{pkg.manaAmount.toLocaleString('de-DE')} Mana</span>
|
||||
</div>
|
||||
|
||||
<span class="price">{formatPrice(pkg)}</span>
|
||||
<span class="action">{buyLabel}</span>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: hsl(var(--color-card));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-align: left;
|
||||
color: hsl(var(--color-foreground));
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.row:hover {
|
||||
background: hsl(var(--color-surface-hover));
|
||||
border-color: hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.row.popular {
|
||||
border-color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.mana {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.625rem;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.price {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-primary));
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
label: string;
|
||||
onclick: () => void;
|
||||
iconName?: string;
|
||||
leftIconName?: string;
|
||||
variant?: 'primary' | 'secondary' | 'accent';
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
label,
|
||||
onclick,
|
||||
iconName = 'arrow-forward-outline',
|
||||
leftIconName = 'cart-outline',
|
||||
variant = 'primary',
|
||||
disabled = false,
|
||||
}: Props = $props();
|
||||
|
||||
// Icon mapping (simple SVG paths for common icons)
|
||||
const iconPaths: Record<string, string> = {
|
||||
'arrow-forward-outline': 'M5 12h14M12 5l7 7-7 7',
|
||||
'checkmark-circle-outline': 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'cart-outline':
|
||||
'M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z',
|
||||
};
|
||||
</script>
|
||||
|
||||
<button
|
||||
{disabled}
|
||||
onclick={disabled ? undefined : onclick}
|
||||
class="subscription-button"
|
||||
class:subscription-button--primary={variant === 'primary'}
|
||||
class:subscription-button--secondary={variant === 'secondary'}
|
||||
class:subscription-button--accent={variant === 'accent'}
|
||||
class:subscription-button--disabled={disabled}
|
||||
>
|
||||
<div class="subscription-button__content">
|
||||
<svg
|
||||
class="subscription-button__icon subscription-button__icon--left"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d={iconPaths[leftIconName] || iconPaths['cart-outline']} />
|
||||
</svg>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
|
||||
<svg
|
||||
class="subscription-button__icon subscription-button__icon--right"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d={iconPaths[iconName] || iconPaths['arrow-forward-outline']} />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.subscription-button {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.subscription-button--primary {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
:global(.dark) .subscription-button--primary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.subscription-button--primary:hover:not(.subscription-button--disabled) {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:global(.dark) .subscription-button--primary:hover:not(.subscription-button--disabled) {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.subscription-button--secondary {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
:global(.dark) .subscription-button--secondary {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.subscription-button--secondary:hover:not(.subscription-button--disabled) {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
:global(.dark) .subscription-button--secondary:hover:not(.subscription-button--disabled) {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.subscription-button--accent {
|
||||
background: hsl(var(--color-primary, 221 83% 53%));
|
||||
border: 1px solid hsl(var(--color-primary, 221 83% 53%));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subscription-button--accent:hover:not(.subscription-button--disabled) {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.subscription-button--disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.subscription-button__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.subscription-button__icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.subscription-button__icon--left {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.subscription-button__icon--right {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { SubscriptionPlan } from './plans';
|
||||
import ManaIcon from './ManaIcon.svelte';
|
||||
|
||||
interface Props {
|
||||
plan: SubscriptionPlan;
|
||||
onSelect: (planId: string) => void;
|
||||
isCurrentPlan?: boolean;
|
||||
isLegacy?: boolean;
|
||||
currentPlanLabel?: string;
|
||||
legacyPlanLabel?: string;
|
||||
popularLabel?: string;
|
||||
perMonthLabel?: string;
|
||||
perYearLabel?: string;
|
||||
monthlyEquivalentLabel?: string;
|
||||
buyLabel?: string;
|
||||
yourPlanLabel?: string;
|
||||
yourLegacyPlanLabel?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
plan,
|
||||
onSelect,
|
||||
isCurrentPlan = false,
|
||||
isLegacy = false,
|
||||
currentPlanLabel = 'Aktuell',
|
||||
legacyPlanLabel = 'Legacy',
|
||||
popularLabel = 'Beliebt',
|
||||
perMonthLabel = '/Mo',
|
||||
perYearLabel = '/Jahr',
|
||||
buyLabel = 'Kaufen',
|
||||
yourPlanLabel = 'Dein Plan',
|
||||
}: Props = $props();
|
||||
|
||||
function formatPrice(p: SubscriptionPlan) {
|
||||
return p.priceString || `${p.price.toFixed(2).replace('.', ',')}€`;
|
||||
}
|
||||
|
||||
function getTierColor() {
|
||||
const id = plan.id.toLowerCase();
|
||||
if (id.includes('free')) return '#9E9E9E';
|
||||
if (id.includes('small')) return '#2196F3';
|
||||
if (id.includes('medium')) return '#1976D2';
|
||||
if (id.includes('large')) return '#1565C0';
|
||||
if (id.includes('giant')) return '#0D47A1';
|
||||
return '#0288D1';
|
||||
}
|
||||
|
||||
const isFree = $derived(plan.id.toLowerCase().includes('free'));
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="row"
|
||||
class:current={isCurrentPlan}
|
||||
class:popular={plan.popular && !isCurrentPlan}
|
||||
disabled={isCurrentPlan}
|
||||
onclick={() => onSelect(plan.id)}
|
||||
>
|
||||
<div class="icon" style="color: {getTierColor()}">
|
||||
<ManaIcon size={20} color={getTierColor()} />
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<span class="name">
|
||||
{plan.name}
|
||||
{#if isCurrentPlan}
|
||||
<span class="badge current-badge">{isLegacy ? legacyPlanLabel : currentPlanLabel}</span>
|
||||
{/if}
|
||||
{#if plan.popular && !isCurrentPlan}
|
||||
<span class="badge popular-badge">{popularLabel}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="mana">{plan.monthlyMana} Mana{perMonthLabel}</span>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
{#if isFree}
|
||||
<span class="price free">Kostenlos</span>
|
||||
{:else}
|
||||
<span class="price">{formatPrice(plan)}</span>
|
||||
<span class="period">{plan.billingCycle === 'yearly' ? perYearLabel : perMonthLabel}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !isFree && !isCurrentPlan}
|
||||
<span class="action">{buyLabel}</span>
|
||||
{:else if isCurrentPlan}
|
||||
<span class="action muted">{yourPlanLabel}</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: hsl(var(--color-card));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-align: left;
|
||||
color: hsl(var(--color-foreground));
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.row:hover:not(:disabled) {
|
||||
background: hsl(var(--color-surface-hover));
|
||||
border-color: hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.row:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.row.current {
|
||||
border-color: hsl(var(--color-primary));
|
||||
background: hsl(var(--color-primary) / 0.05);
|
||||
}
|
||||
|
||||
.row.popular {
|
||||
border-color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.mana {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.625rem;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.current-badge {
|
||||
background: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.popular-badge {
|
||||
background: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.right {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.price.free {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.period {
|
||||
font-size: 0.625rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-primary));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action.muted {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UsageData } from './usage';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
usageData: UsageData;
|
||||
currentPlan?: string;
|
||||
// i18n labels
|
||||
yourManaLabel?: string;
|
||||
availableLabel?: string;
|
||||
consumedLabel?: string;
|
||||
currentPlanLabel?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
usageData,
|
||||
currentPlan,
|
||||
yourManaLabel = 'Dein Mana',
|
||||
availableLabel = 'verfügbar',
|
||||
consumedLabel = 'verbraucht',
|
||||
currentPlanLabel = 'Aktueller Plan',
|
||||
}: Props = $props();
|
||||
|
||||
// Use real credits (this would normally come from a store/API)
|
||||
// svelte-ignore state_referenced_locally
|
||||
const currentMana = usageData.currentMana;
|
||||
|
||||
// Calculate used vs available Mana
|
||||
// svelte-ignore state_referenced_locally
|
||||
const usedMana = usageData.maxMana - currentMana;
|
||||
const formattedCurrentMana = currentMana.toString();
|
||||
const formattedUsedMana = usedMana.toString();
|
||||
// svelte-ignore state_referenced_locally
|
||||
const calculatedPercentage = Math.round((currentMana / usageData.maxMana) * 100);
|
||||
// Minimum 1% for numbers up to 5, so that a small blue bar is always visible
|
||||
const availablePercentage =
|
||||
currentMana <= 5 && currentMana > 0 ? Math.max(1, calculatedPercentage) : calculatedPercentage;
|
||||
</script>
|
||||
|
||||
<div class="usage-card">
|
||||
<!-- Mana Progress Bar -->
|
||||
<div>
|
||||
<div class="usage-card__header">
|
||||
<div class="usage-card__title-wrapper">
|
||||
<h2 class="usage-card__title">{title || yourManaLabel}</h2>
|
||||
</div>
|
||||
<div class="usage-card__value-wrapper">
|
||||
<div class="usage-card__value-badge">
|
||||
<p class="usage-card__value">
|
||||
{formattedCurrentMana}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="usage-card__progress-track">
|
||||
<div class="usage-card__progress-fill" style="width: {availablePercentage}%;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Percentage -->
|
||||
<div class="usage-card__stats">
|
||||
<p class="usage-card__stat">
|
||||
{availablePercentage}% {availableLabel}
|
||||
</p>
|
||||
<p class="usage-card__stat">
|
||||
{formattedUsedMana}
|
||||
{consumedLabel}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Current Plan -->
|
||||
{#if currentPlan}
|
||||
<div class="usage-card__plan">
|
||||
<p class="usage-card__plan-text">
|
||||
{currentPlanLabel}: {currentPlan}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.usage-card {
|
||||
position: relative;
|
||||
padding: 1.25rem;
|
||||
border-radius: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:global(.dark) .usage-card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.usage-card__header {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.usage-card__title-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.usage-card__title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.usage-card__value-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.usage-card__value-badge {
|
||||
align-self: flex-start;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.375rem 1rem;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .usage-card__value-badge {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.usage-card__value {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.usage-card__progress-track {
|
||||
position: relative;
|
||||
margin-bottom: 0.5rem;
|
||||
height: 1rem;
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .usage-card__progress-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.usage-card__progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 0.5rem;
|
||||
background: linear-gradient(90deg, #4287f5 0%, #66b2ff 100%);
|
||||
box-shadow: 0 0 4px rgba(66, 135, 245, 0.5);
|
||||
}
|
||||
|
||||
.usage-card__stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.usage-card__stat {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.usage-card__plan {
|
||||
margin-top: 0.75rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
:global(.dark) .usage-card__plan {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.usage-card__plan-text {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"costs": [
|
||||
{
|
||||
"action": "Pro Minute Aufnahme",
|
||||
"actionKey": "subscription.cost_per_minute_recording",
|
||||
"cost": 2,
|
||||
"icon": "mic-outline"
|
||||
},
|
||||
{
|
||||
"action": "Frage an Memo stellen",
|
||||
"actionKey": "subscription.cost_ask_memo_question",
|
||||
"cost": 5,
|
||||
"icon": "chatbubble-outline"
|
||||
},
|
||||
{
|
||||
"action": "Neue Memory erstellen",
|
||||
"actionKey": "subscription.cost_create_memory",
|
||||
"cost": 5,
|
||||
"icon": "add-circle-outline"
|
||||
},
|
||||
{
|
||||
"action": "Memos kombinieren (pro Memo)",
|
||||
"actionKey": "subscription.cost_combine_memos",
|
||||
"cost": 5,
|
||||
"icon": "copy-outline"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"usage": {
|
||||
"total": 0,
|
||||
"lastWeek": 0,
|
||||
"lastMonth": 0,
|
||||
"currentMana": 150,
|
||||
"maxMana": 150,
|
||||
"history": []
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
{
|
||||
"subscriptions": [
|
||||
{
|
||||
"id": "free",
|
||||
"name": "Mana Quelle Free",
|
||||
"nameEn": "Mana Source Free",
|
||||
"nameIt": "Mana Fonte Free",
|
||||
"price": 0,
|
||||
"priceUnit": "",
|
||||
"monthlyMana": 50,
|
||||
"canGiftMana": false,
|
||||
"popular": false,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Quelle_S_v1",
|
||||
"name": "Mana Quelle S",
|
||||
"nameEn": "Mana Source S",
|
||||
"nameIt": "Mana Fonte S",
|
||||
"price": 4.99,
|
||||
"priceUnit": "/ Monat",
|
||||
"monthlyMana": 500,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Quelle_S_Yearly_v1",
|
||||
"name": "Mana Quelle S",
|
||||
"nameEn": "Mana Source S",
|
||||
"nameIt": "Mana Fonte S",
|
||||
"price": 47.9,
|
||||
"priceUnit": "/ Jahr",
|
||||
"priceBreakdown": "(entspricht 3,99€ / Monat, 20% Rabatt)",
|
||||
"monthlyMana": 500,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "yearly",
|
||||
"monthlyEquivalent": 3.99,
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Quelle_M_v1",
|
||||
"name": "Mana Quelle M",
|
||||
"nameEn": "Mana Source M",
|
||||
"nameIt": "Mana Fonte M",
|
||||
"price": 9.99,
|
||||
"priceUnit": "/ Monat",
|
||||
"monthlyMana": 1000,
|
||||
"canGiftMana": true,
|
||||
"popular": true,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Quelle_M_Yearly_v1",
|
||||
"name": "Mana Quelle M",
|
||||
"nameEn": "Mana Source M",
|
||||
"nameIt": "Mana Fonte M",
|
||||
"price": 95.9,
|
||||
"priceUnit": "/ Jahr",
|
||||
"priceBreakdown": "(entspricht 7,99€ / Monat, 20% Rabatt)",
|
||||
"monthlyMana": 1000,
|
||||
"canGiftMana": true,
|
||||
"popular": true,
|
||||
"billingCycle": "yearly",
|
||||
"monthlyEquivalent": 7.99,
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Quelle_L_v1",
|
||||
"name": "Mana Quelle L",
|
||||
"nameEn": "Mana Source L",
|
||||
"nameIt": "Mana Fonte L",
|
||||
"price": 19.99,
|
||||
"priceUnit": "/ Monat",
|
||||
"monthlyMana": 2000,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Quelle_L_Yearly_v1",
|
||||
"name": "Mana Quelle L",
|
||||
"nameEn": "Mana Source L",
|
||||
"nameIt": "Mana Fonte L",
|
||||
"price": 191.9,
|
||||
"priceUnit": "/ Jahr",
|
||||
"priceBreakdown": "(entspricht 15,99€ / Monat, 20% Rabatt)",
|
||||
"monthlyMana": 2000,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "yearly",
|
||||
"monthlyEquivalent": 15.99,
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Quelle_XL_v1",
|
||||
"name": "Mana Quelle XL",
|
||||
"nameEn": "Mana Source XL",
|
||||
"nameIt": "Mana Fonte XL",
|
||||
"price": 39.99,
|
||||
"priceUnit": "/ Monat",
|
||||
"monthlyMana": 4000,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Quelle_XL_Yearly_v1",
|
||||
"name": "Mana Quelle XL",
|
||||
"nameEn": "Mana Source XL",
|
||||
"nameIt": "Mana Fonte XL",
|
||||
"price": 383.9,
|
||||
"priceUnit": "/ Jahr",
|
||||
"priceBreakdown": "(entspricht 31,99€ / Monat, 20% Rabatt)",
|
||||
"monthlyMana": 4000,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "yearly",
|
||||
"monthlyEquivalent": 31.99,
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Quelle_XXL_v1",
|
||||
"name": "Mana Quelle XXL",
|
||||
"nameEn": "Mana Source XXL",
|
||||
"nameIt": "Mana Fonte XXL",
|
||||
"price": 99.99,
|
||||
"priceUnit": "/ Monat",
|
||||
"monthlyMana": 10000,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Quelle_XXL_Yearly_v1",
|
||||
"name": "Mana Quelle XXL",
|
||||
"nameEn": "Mana Source XXL",
|
||||
"nameIt": "Mana Fonte XXL",
|
||||
"price": 959.9,
|
||||
"priceUnit": "/ Jahr",
|
||||
"priceBreakdown": "(entspricht 79,99€ / Monat, 20% Rabatt)",
|
||||
"monthlyMana": 10000,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "yearly",
|
||||
"monthlyEquivalent": 79.99,
|
||||
"available": true
|
||||
}
|
||||
],
|
||||
"packages": [
|
||||
{
|
||||
"id": "Mana_Potion_Small_v1",
|
||||
"name": "Kleiner Mana Trank",
|
||||
"nameEn": "Small Mana Potion",
|
||||
"nameIt": "Piccola Pozione di Mana",
|
||||
"manaAmount": 350,
|
||||
"price": 4.9,
|
||||
"popular": false
|
||||
},
|
||||
{
|
||||
"id": "Mana_Potion_Medium_v1",
|
||||
"name": "Mittlerer Mana Trank",
|
||||
"nameEn": "Medium Mana Potion",
|
||||
"nameIt": "Media Pozione di Mana",
|
||||
"manaAmount": 700,
|
||||
"price": 9.8,
|
||||
"popular": false
|
||||
},
|
||||
{
|
||||
"id": "Mana_Potion_Large_v1",
|
||||
"name": "Großer Mana Trank",
|
||||
"nameEn": "Large Mana Potion",
|
||||
"nameIt": "Grande Pozione di Mana",
|
||||
"manaAmount": 1400,
|
||||
"price": 19.6,
|
||||
"popular": false
|
||||
},
|
||||
{
|
||||
"id": "Mana_Potion_Giant_v2",
|
||||
"name": "Riesiger Mana Trank",
|
||||
"nameEn": "Giant Mana Potion",
|
||||
"nameIt": "Pozione di Mana Gigante",
|
||||
"manaAmount": 2800,
|
||||
"price": 39.2,
|
||||
"popular": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/**
|
||||
* @mana/subscriptions — Unified subscription package
|
||||
*
|
||||
* Consolidates shared-subscription-types + shared-subscription-ui.
|
||||
*/
|
||||
|
||||
// === Types ===
|
||||
export {
|
||||
type BillingCycle,
|
||||
type PlanCategory,
|
||||
type SubscriptionPlan,
|
||||
type ManaPackage,
|
||||
type ProductMapping,
|
||||
type PackageMapping,
|
||||
type FreeTierConfig,
|
||||
DEFAULT_FREE_TIER,
|
||||
} from './plans';
|
||||
|
||||
export {
|
||||
type UsageData,
|
||||
type UsageHistoryEntry,
|
||||
type CostItem,
|
||||
type ManaBalance,
|
||||
type CreditTransaction,
|
||||
type OperationPricing,
|
||||
} from './usage';
|
||||
|
||||
export {
|
||||
type RevenueCatSubscriptionPlan,
|
||||
type RevenueCatManaPackage,
|
||||
type SubscriptionServiceData,
|
||||
type PurchaseResult,
|
||||
type CustomerSubscriptionStatus,
|
||||
type RestorePurchasesResult,
|
||||
type RevenueCatOffering,
|
||||
} from './revenueCat';
|
||||
|
||||
// === UI Components ===
|
||||
export { default as SubscriptionPage } from './pages/SubscriptionPage.svelte';
|
||||
export { default as SubscriptionCard } from './SubscriptionCard.svelte';
|
||||
export { default as PackageCard } from './PackageCard.svelte';
|
||||
export { default as BillingToggle } from './BillingToggle.svelte';
|
||||
export { default as UsageCard } from './UsageCard.svelte';
|
||||
export { default as CostCard } from './CostCard.svelte';
|
||||
export { default as SubscriptionButton } from './SubscriptionButton.svelte';
|
||||
export { default as ManaIcon } from './ManaIcon.svelte';
|
||||
|
||||
// === Default data ===
|
||||
export { default as defaultSubscriptionData } from './data/subscriptionData.json';
|
||||
export { default as defaultAppCosts } from './data/appCosts.json';
|
||||
export { default as defaultUsageData } from './data/defaultUsageData.json';
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { SubscriptionPlan, ManaPackage, BillingCycle } from '../plans';
|
||||
import type { UsageData, CostItem } from '../usage';
|
||||
import BillingToggle from '../BillingToggle.svelte';
|
||||
import SubscriptionCard from '../SubscriptionCard.svelte';
|
||||
import PackageCard from '../PackageCard.svelte';
|
||||
import UsageCard from '../UsageCard.svelte';
|
||||
import CostCard from '../CostCard.svelte';
|
||||
|
||||
import defaultSubscriptionData from '../data/subscriptionData.json';
|
||||
import defaultAppCosts from '../data/appCosts.json';
|
||||
import defaultUsageData from '../data/defaultUsageData.json';
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
onSubscribe: (planId: string) => void;
|
||||
onBuyPackage: (packageId: string) => void;
|
||||
currentPlanId?: string;
|
||||
usageData?: UsageData;
|
||||
subscriptions?: SubscriptionPlan[];
|
||||
packages?: ManaPackage[];
|
||||
costs?: CostItem[];
|
||||
pageTitle?: string;
|
||||
subscriptionsTitle?: string;
|
||||
packagesTitle?: string;
|
||||
yearlyDiscount?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
appName,
|
||||
onSubscribe,
|
||||
onBuyPackage,
|
||||
currentPlanId = 'free',
|
||||
usageData = defaultUsageData.usage as UsageData,
|
||||
subscriptions = defaultSubscriptionData.subscriptions as SubscriptionPlan[],
|
||||
packages = defaultSubscriptionData.packages as ManaPackage[],
|
||||
costs = defaultAppCosts.costs as CostItem[],
|
||||
pageTitle = 'Mana kaufen',
|
||||
subscriptionsTitle = 'Abonnements',
|
||||
packagesTitle = 'Einmalkäufe',
|
||||
yearlyDiscount = '33%',
|
||||
}: Props = $props();
|
||||
|
||||
let billingCycle = $state<BillingCycle>('monthly');
|
||||
|
||||
const currentPlanName = $derived(() => {
|
||||
const plan = subscriptions.find((p) => p.id === currentPlanId);
|
||||
return plan?.name || 'Free';
|
||||
});
|
||||
|
||||
function getSubscriptionPlans() {
|
||||
return subscriptions.filter((plan) => plan.id !== 'free' && plan.billingCycle === billingCycle);
|
||||
}
|
||||
|
||||
function isCurrentPlan(planId: string) {
|
||||
if (currentPlanId === 'free' && planId === 'free') return true;
|
||||
return planId === currentPlanId;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sub-page">
|
||||
<!-- Billing toggle -->
|
||||
<div class="toggle-row">
|
||||
<BillingToggle {billingCycle} onChange={(cycle) => (billingCycle = cycle)} {yearlyDiscount} />
|
||||
</div>
|
||||
|
||||
<!-- Subscriptions -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">{subscriptionsTitle}</h2>
|
||||
<div class="card-list">
|
||||
{#if subscriptions.find((p) => p.id === 'free')}
|
||||
<SubscriptionCard
|
||||
plan={subscriptions.find((p) => p.id === 'free')!}
|
||||
onSelect={onSubscribe}
|
||||
isCurrentPlan={isCurrentPlan('free')}
|
||||
/>
|
||||
{/if}
|
||||
{#each getSubscriptionPlans() as plan}
|
||||
<SubscriptionCard {plan} onSelect={onSubscribe} isCurrentPlan={isCurrentPlan(plan.id)} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- One-time packages -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">{packagesTitle}</h2>
|
||||
<div class="card-list">
|
||||
{#each packages as pkg}
|
||||
<PackageCard package={pkg} onSelect={onBuyPackage} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage & Costs (collapsed, less prominent) -->
|
||||
<section class="section">
|
||||
<details class="details">
|
||||
<summary class="summary">Verbrauch & Kosten</summary>
|
||||
<div class="details-content">
|
||||
<UsageCard {usageData} currentPlan={currentPlanName()} />
|
||||
<CostCard {costs} />
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sub-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
max-width: 40rem;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.details {
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.summary {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.summary:hover {
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.details-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
/**
|
||||
* Subscription plan and package types
|
||||
*/
|
||||
|
||||
/**
|
||||
* Billing cycle options
|
||||
*/
|
||||
export type BillingCycle = 'monthly' | 'yearly';
|
||||
|
||||
/**
|
||||
* Subscription plan category
|
||||
*/
|
||||
export type PlanCategory = 'individual' | 'team' | 'enterprise';
|
||||
|
||||
/**
|
||||
* Base subscription plan interface
|
||||
*/
|
||||
export interface SubscriptionPlan {
|
||||
/** Unique identifier */
|
||||
id: string;
|
||||
/** Display name (localized) */
|
||||
name: string;
|
||||
/** English name */
|
||||
nameEn?: string;
|
||||
/** German name */
|
||||
nameDe?: string;
|
||||
/** Italian name */
|
||||
nameIt?: string;
|
||||
/** Price in local currency */
|
||||
price: number;
|
||||
/** Formatted price string (e.g., "5,99€") */
|
||||
priceString?: string;
|
||||
/** Currency code (e.g., "EUR") */
|
||||
currencyCode?: string;
|
||||
/** Price breakdown text */
|
||||
priceBreakdown?: string;
|
||||
/** Monthly equivalent for yearly plans */
|
||||
monthlyEquivalent?: number;
|
||||
/** Mana amount per month */
|
||||
monthlyMana: number;
|
||||
/** Initial mana grant on signup */
|
||||
initialMana?: number;
|
||||
/** Daily mana regeneration */
|
||||
dailyMana?: number;
|
||||
/** Maximum mana capacity */
|
||||
maxMana?: number;
|
||||
/** Whether user can gift mana */
|
||||
canGiftMana: boolean;
|
||||
/** Mark as popular/recommended */
|
||||
popular?: boolean;
|
||||
/** Billing frequency */
|
||||
billingCycle: BillingCycle;
|
||||
/** Team subscription flag */
|
||||
isTeamSubscription?: boolean;
|
||||
/** Enterprise subscription flag */
|
||||
isEnterpriseSubscription?: boolean;
|
||||
/** Plan features list */
|
||||
features?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* One-time mana package interface
|
||||
*/
|
||||
export interface ManaPackage {
|
||||
/** Unique identifier */
|
||||
id: string;
|
||||
/** Display name (localized) */
|
||||
name: string;
|
||||
/** English name */
|
||||
nameEn?: string;
|
||||
/** German name */
|
||||
nameDe?: string;
|
||||
/** Italian name */
|
||||
nameIt?: string;
|
||||
/** Mana amount */
|
||||
manaAmount: number;
|
||||
/** Price in local currency */
|
||||
price: number;
|
||||
/** Formatted price string */
|
||||
priceString?: string;
|
||||
/** Currency code */
|
||||
currencyCode?: string;
|
||||
/** Team package flag */
|
||||
isTeamPackage?: boolean;
|
||||
/** Enterprise package flag */
|
||||
isEnterprisePackage?: boolean;
|
||||
/** Mark as popular */
|
||||
popular?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Product mapping for RevenueCat
|
||||
*/
|
||||
export interface ProductMapping {
|
||||
/** Internal subscription ID */
|
||||
subscriptionId: string;
|
||||
/** App Store/Play Store product ID */
|
||||
productId: string;
|
||||
/** Billing cycle */
|
||||
billingCycle: BillingCycle;
|
||||
/** Category */
|
||||
category: PlanCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Package mapping for RevenueCat
|
||||
*/
|
||||
export interface PackageMapping {
|
||||
/** Internal package ID */
|
||||
packageId: string;
|
||||
/** App Store/Play Store product ID */
|
||||
productId: string;
|
||||
/** Category */
|
||||
category: PlanCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free tier configuration
|
||||
*/
|
||||
export interface FreeTierConfig {
|
||||
/** Initial mana for free users */
|
||||
initialMana: number;
|
||||
/** Daily mana regeneration */
|
||||
dailyMana: number;
|
||||
/** Maximum mana capacity */
|
||||
maxMana: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default free tier configuration
|
||||
*/
|
||||
export const DEFAULT_FREE_TIER: FreeTierConfig = {
|
||||
initialMana: 150,
|
||||
dailyMana: 5,
|
||||
maxMana: 150,
|
||||
};
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
/**
|
||||
* RevenueCat integration types
|
||||
*/
|
||||
|
||||
import type { SubscriptionPlan, ManaPackage } from './plans';
|
||||
|
||||
/**
|
||||
* RevenueCat-enhanced subscription plan
|
||||
*/
|
||||
export interface RevenueCatSubscriptionPlan extends SubscriptionPlan {
|
||||
/** RevenueCat package object */
|
||||
revenueCatPackage?: unknown;
|
||||
/** RevenueCat product object */
|
||||
revenueCatProduct?: unknown;
|
||||
/** App Store/Play Store product ID */
|
||||
productId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* RevenueCat-enhanced mana package
|
||||
*/
|
||||
export interface RevenueCatManaPackage extends ManaPackage {
|
||||
/** RevenueCat package object */
|
||||
revenueCatPackage?: unknown;
|
||||
/** RevenueCat product object */
|
||||
revenueCatProduct?: unknown;
|
||||
/** App Store/Play Store product ID */
|
||||
productId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription service data response
|
||||
*/
|
||||
export interface SubscriptionServiceData {
|
||||
/** All available subscription plans */
|
||||
subscriptions: RevenueCatSubscriptionPlan[];
|
||||
/** All available one-time packages */
|
||||
packages: RevenueCatManaPackage[];
|
||||
/** Whether data is from RevenueCat or fallback */
|
||||
isFromRevenueCat: boolean;
|
||||
/** Last update timestamp */
|
||||
lastUpdated: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Purchase result
|
||||
*/
|
||||
export interface PurchaseResult {
|
||||
/** Whether purchase was successful */
|
||||
success: boolean;
|
||||
/** Customer info from RevenueCat */
|
||||
customerInfo?: unknown;
|
||||
/** Error message if failed */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer subscription status
|
||||
*/
|
||||
export interface CustomerSubscriptionStatus {
|
||||
/** Whether user has active subscription */
|
||||
hasActiveSubscription: boolean;
|
||||
/** Current plan ID */
|
||||
currentPlanId?: string;
|
||||
/** Subscription expiration date */
|
||||
expirationDate?: Date;
|
||||
/** Whether in grace period */
|
||||
isInGracePeriod?: boolean;
|
||||
/** Whether subscription will renew */
|
||||
willRenew?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore purchases result
|
||||
*/
|
||||
export interface RestorePurchasesResult {
|
||||
/** Whether restore was successful */
|
||||
success: boolean;
|
||||
/** Restored subscription plan ID */
|
||||
restoredPlanId?: string;
|
||||
/** Error message if failed */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offering from RevenueCat
|
||||
*/
|
||||
export interface RevenueCatOffering {
|
||||
/** Offering identifier */
|
||||
identifier: string;
|
||||
/** Available packages in this offering */
|
||||
availablePackages: RevenueCatSubscriptionPlan[];
|
||||
/** Lifetime package (if available) */
|
||||
lifetime?: RevenueCatManaPackage;
|
||||
/** Annual package */
|
||||
annual?: RevenueCatSubscriptionPlan;
|
||||
/** Monthly package */
|
||||
monthly?: RevenueCatSubscriptionPlan;
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/**
|
||||
* Usage and cost tracking types
|
||||
*/
|
||||
|
||||
/**
|
||||
* Usage data for displaying user's mana consumption
|
||||
*/
|
||||
export interface UsageData {
|
||||
/** Total mana consumed all time */
|
||||
total: number;
|
||||
/** Mana consumed last week */
|
||||
lastWeek: number;
|
||||
/** Mana consumed last month */
|
||||
lastMonth: number;
|
||||
/** Current mana balance */
|
||||
currentMana: number;
|
||||
/** Maximum mana capacity */
|
||||
maxMana: number;
|
||||
/** Usage history */
|
||||
history?: UsageHistoryEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Single usage history entry
|
||||
*/
|
||||
export interface UsageHistoryEntry {
|
||||
/** Date of usage (ISO string) */
|
||||
date: string;
|
||||
/** Amount consumed */
|
||||
amount: number;
|
||||
/** Action type (optional) */
|
||||
action?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cost item for displaying operation costs
|
||||
*/
|
||||
export interface CostItem {
|
||||
/** Action description */
|
||||
action: string;
|
||||
/** Translation key for action */
|
||||
actionKey?: string;
|
||||
/** Mana cost */
|
||||
cost: number;
|
||||
/** Icon name */
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* User's credit/mana balance
|
||||
*/
|
||||
export interface ManaBalance {
|
||||
/** Current mana amount */
|
||||
current: number;
|
||||
/** Maximum capacity */
|
||||
max: number;
|
||||
/** Last updated timestamp */
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit transaction record
|
||||
*/
|
||||
export interface CreditTransaction {
|
||||
/** Transaction ID */
|
||||
id: string;
|
||||
/** User ID */
|
||||
userId: string;
|
||||
/** Amount (positive = credit, negative = debit) */
|
||||
amount: number;
|
||||
/** Transaction type */
|
||||
type: 'purchase' | 'subscription' | 'usage' | 'gift' | 'refund' | 'bonus';
|
||||
/** Description */
|
||||
description: string;
|
||||
/** Timestamp */
|
||||
createdAt: string;
|
||||
/** Related operation ID (if applicable) */
|
||||
operationId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pricing information for operations
|
||||
*/
|
||||
export interface OperationPricing {
|
||||
/** Operation key */
|
||||
operation: string;
|
||||
/** Base cost in mana */
|
||||
baseCost: number;
|
||||
/** Per-unit cost (e.g., per minute, per token) */
|
||||
perUnitCost?: number;
|
||||
/** Unit type */
|
||||
unitType?: 'minute' | 'token' | 'request';
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "dist",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.svelte"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue