mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
refactor(subscriptions): compact row-based card layout
SubscriptionCard and PackageCard were large centered multi-cell cards designed for a standalone pricing page. In the workbench context (narrow card inside a carousel), they wasted too much vertical space for users to compare plans at a glance. Redesigned both as horizontal rows: - Icon | name+mana | price | action — all in one line - Badges (current/popular) inline next to the plan name - No more 3-column internal grid with 70px min-height cells - Clickable row replaces separate SubscriptionButton SubscriptionPage: - Drop the big centered header (icon + title + subtitle) - Move Usage + Costs into a collapsed <details> section - Section titles as small-caps labels - Billing toggle at top, plans immediately visible Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d83fc370a0
commit
c6c4d630fe
3 changed files with 301 additions and 561 deletions
|
|
@ -1,236 +1,127 @@
|
|||
<script lang="ts">
|
||||
import type { ManaPackage } from './plans';
|
||||
import SubscriptionButton from './SubscriptionButton.svelte';
|
||||
import ManaIcon from './ManaIcon.svelte';
|
||||
|
||||
interface Props {
|
||||
package: ManaPackage;
|
||||
onSelect: (packageId: string) => void;
|
||||
// i18n labels
|
||||
popularLabel?: string;
|
||||
manaLabel?: string;
|
||||
oneTimeLabel?: string;
|
||||
buyLabel?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
package: pkg,
|
||||
onSelect,
|
||||
popularLabel = 'Popular',
|
||||
manaLabel = 'Mana',
|
||||
oneTimeLabel = 'Einmalig',
|
||||
buyLabel = 'Kaufen',
|
||||
}: Props = $props();
|
||||
let { package: pkg, onSelect, popularLabel = 'Beliebt', buyLabel = 'Kaufen' }: Props = $props();
|
||||
|
||||
function formatPrice(pkg: ManaPackage) {
|
||||
return pkg.priceString || `${pkg.price.toFixed(2).replace('.', ',')}€`;
|
||||
function formatPrice(p: ManaPackage) {
|
||||
return p.priceString || `${p.price.toFixed(2).replace('.', ',')}€`;
|
||||
}
|
||||
|
||||
// Package-specific colors and background sizes
|
||||
function getPackageStyles() {
|
||||
function getColor() {
|
||||
const id = pkg.id.toLowerCase();
|
||||
if (id.includes('small')) return { bg: '#E3F2FD', icon: '#2196F3', bgSize: '45%' };
|
||||
if (id.includes('medium')) return { bg: '#BBDEFB', icon: '#1976D2', bgSize: '60%' };
|
||||
if (id.includes('large')) return { bg: '#90CAF9', icon: '#1565C0', bgSize: '75%' };
|
||||
if (id.includes('giant')) return { bg: '#64B5F6', icon: '#0D47A1', bgSize: '90%' };
|
||||
return { bg: '#E1F5FE', icon: '#0288D1', bgSize: '50%' };
|
||||
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 packageStyles = $derived(getPackageStyles());
|
||||
|
||||
// Hover state
|
||||
let isHovered = $state(false);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="package-card"
|
||||
class:package-card--popular={pkg.popular}
|
||||
onmouseenter={() => (isHovered = true)}
|
||||
onmouseleave={() => (isHovered = false)}
|
||||
role="article"
|
||||
>
|
||||
{#if pkg.popular}
|
||||
<div class="package-card__badge">
|
||||
{popularLabel}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Package Name -->
|
||||
<h3 class="package-card__title">
|
||||
{pkg.name}
|
||||
</h3>
|
||||
|
||||
<!-- Three column layout -->
|
||||
<div class="package-card__grid">
|
||||
<!-- Mana Icon with background -->
|
||||
<div class="package-card__cell">
|
||||
<div
|
||||
class="package-card__icon-wrapper"
|
||||
style="width: {packageStyles.bgSize}; height: {packageStyles.bgSize}; background-color: {packageStyles.bg};"
|
||||
>
|
||||
<ManaIcon size={32} color={packageStyles.icon} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mana Amount -->
|
||||
<div class="package-card__cell">
|
||||
<p class="package-card__value">
|
||||
{pkg.manaAmount}
|
||||
</p>
|
||||
<p class="package-card__label">{manaLabel}</p>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="package-card__cell">
|
||||
<p class="package-card__price">
|
||||
{formatPrice(pkg)}
|
||||
</p>
|
||||
<p class="package-card__sublabel">{oneTimeLabel}</p>
|
||||
</div>
|
||||
<button class="row" class:popular={pkg.popular} onclick={() => onSelect(pkg.id)}>
|
||||
<div class="icon">
|
||||
<ManaIcon size={20} color={getColor()} />
|
||||
</div>
|
||||
|
||||
<SubscriptionButton
|
||||
label={buyLabel}
|
||||
onclick={() => onSelect(pkg.id)}
|
||||
iconName="arrow-forward-outline"
|
||||
leftIconName="cart-outline"
|
||||
variant={pkg.popular ? 'accent' : 'primary'}
|
||||
/>
|
||||
</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>
|
||||
.package-card {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
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);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.dark) .package-card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.package-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .package-card:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.package-card--popular {
|
||||
border: 2px solid hsl(var(--color-primary, 221 83% 53%));
|
||||
}
|
||||
|
||||
.package-card__badge {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: hsl(var(--color-primary, 221 83% 53%));
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.package-card__title {
|
||||
margin: 1.5rem 0 1rem 0;
|
||||
text-align: center;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.package-card__grid {
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.package-card__cell {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
min-height: 70px;
|
||||
min-width: 0;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
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;
|
||||
}
|
||||
|
||||
:global(.dark) .package-card__cell {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
.row:hover {
|
||||
background: hsl(var(--color-surface-hover));
|
||||
border-color: hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.package-card__icon-wrapper {
|
||||
.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);
|
||||
}
|
||||
|
||||
.package-card__value {
|
||||
margin: 0 0 0.125rem 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--color-foreground));
|
||||
white-space: nowrap;
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.package-card__price {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--color-foreground));
|
||||
white-space: nowrap;
|
||||
.name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.package-card__label {
|
||||
margin: 0;
|
||||
.mana {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.625rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
text-align: center;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.package-card__sublabel {
|
||||
margin: 0.125rem 0 0 0;
|
||||
font-size: 0.5rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
.price {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.package-card__value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.package-card__price {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.package-card__label {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.package-card__sublabel {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-primary));
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { SubscriptionPlan } from './plans';
|
||||
import SubscriptionButton from './SubscriptionButton.svelte';
|
||||
import ManaIcon from './ManaIcon.svelte';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -8,7 +7,6 @@
|
|||
onSelect: (planId: string) => void;
|
||||
isCurrentPlan?: boolean;
|
||||
isLegacy?: boolean;
|
||||
// i18n labels
|
||||
currentPlanLabel?: string;
|
||||
legacyPlanLabel?: string;
|
||||
popularLabel?: string;
|
||||
|
|
@ -25,254 +23,187 @@
|
|||
onSelect,
|
||||
isCurrentPlan = false,
|
||||
isLegacy = false,
|
||||
currentPlanLabel = 'Current Plan',
|
||||
legacyPlanLabel = 'Legacy Plan',
|
||||
popularLabel = 'Popular',
|
||||
perMonthLabel = 'pro Monat',
|
||||
perYearLabel = 'pro Jahr',
|
||||
monthlyEquivalentLabel = '/Monat',
|
||||
currentPlanLabel = 'Aktuell',
|
||||
legacyPlanLabel = 'Legacy',
|
||||
popularLabel = 'Beliebt',
|
||||
perMonthLabel = '/Mo',
|
||||
perYearLabel = '/Jahr',
|
||||
buyLabel = 'Kaufen',
|
||||
yourPlanLabel = 'Dein Plan',
|
||||
yourLegacyPlanLabel = 'Dein Legacy-Plan',
|
||||
}: Props = $props();
|
||||
|
||||
function formatPrice(plan: SubscriptionPlan) {
|
||||
return plan.priceString || `${plan.price.toFixed(2).replace('.', ',')}€`;
|
||||
function formatPrice(p: SubscriptionPlan) {
|
||||
return p.priceString || `${p.price.toFixed(2).replace('.', ',')}€`;
|
||||
}
|
||||
|
||||
// Tier-specific background colors and sizes for Mana icon
|
||||
function getTierStyles() {
|
||||
function getTierColor() {
|
||||
const id = plan.id.toLowerCase();
|
||||
if (id.includes('free')) return { bg: '#F5F5F5', icon: '#9E9E9E', bgSize: '30%' };
|
||||
if (id.includes('small')) return { bg: '#E3F2FD', icon: '#2196F3', bgSize: '45%' };
|
||||
if (id.includes('medium')) return { bg: '#BBDEFB', icon: '#1976D2', bgSize: '60%' };
|
||||
if (id.includes('large')) return { bg: '#90CAF9', icon: '#1565C0', bgSize: '75%' };
|
||||
if (id.includes('giant')) return { bg: '#64B5F6', icon: '#0D47A1', bgSize: '90%' };
|
||||
return { bg: '#E1F5FE', icon: '#0288D1', bgSize: '50%' };
|
||||
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 tierStyles = $derived(getTierStyles());
|
||||
|
||||
// Hover state
|
||||
let isHovered = $state(false);
|
||||
const isFree = $derived(plan.id.toLowerCase().includes('free'));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="subscription-card"
|
||||
class:subscription-card--current={isCurrentPlan}
|
||||
class:subscription-card--popular={plan.popular && !isCurrentPlan}
|
||||
onmouseenter={() => (isHovered = true)}
|
||||
onmouseleave={() => (isHovered = false)}
|
||||
role="article"
|
||||
<button
|
||||
class="row"
|
||||
class:current={isCurrentPlan}
|
||||
class:popular={plan.popular && !isCurrentPlan}
|
||||
disabled={isCurrentPlan}
|
||||
onclick={() => onSelect(plan.id)}
|
||||
>
|
||||
{#if isCurrentPlan}
|
||||
<div class="subscription-card__badge subscription-card__badge--current">
|
||||
{isLegacy ? legacyPlanLabel : currentPlanLabel}
|
||||
</div>
|
||||
{/if}
|
||||
{#if plan.popular && !isCurrentPlan}
|
||||
<div class="subscription-card__badge subscription-card__badge--popular">
|
||||
{popularLabel}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Tier Name -->
|
||||
<h3 class="subscription-card__title">
|
||||
{plan.name}
|
||||
</h3>
|
||||
|
||||
<!-- Three column layout -->
|
||||
<div class="subscription-card__grid">
|
||||
<!-- Mana Icon with background -->
|
||||
<div class="subscription-card__cell">
|
||||
<div
|
||||
class="subscription-card__icon-wrapper"
|
||||
style="width: {tierStyles.bgSize}; height: {tierStyles.bgSize}; background-color: {tierStyles.bg};"
|
||||
>
|
||||
<ManaIcon size={32} color={tierStyles.icon} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mana Amount -->
|
||||
<div class="subscription-card__cell">
|
||||
<p class="subscription-card__value">
|
||||
{plan.monthlyMana}
|
||||
</p>
|
||||
<p class="subscription-card__label">{perMonthLabel}</p>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="subscription-card__cell">
|
||||
<p class="subscription-card__price">
|
||||
{formatPrice(plan)}
|
||||
</p>
|
||||
<p class="subscription-card__label">
|
||||
{plan.billingCycle === 'yearly' ? perYearLabel : perMonthLabel}
|
||||
</p>
|
||||
{#if plan.billingCycle === 'yearly' && plan.monthlyEquivalent}
|
||||
<p class="subscription-card__sublabel">
|
||||
({plan.monthlyEquivalent.toFixed(2).replace('.', ',')}€{monthlyEquivalentLabel})
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="icon" style="color: {getTierColor()}">
|
||||
<ManaIcon size={20} color={getTierColor()} />
|
||||
</div>
|
||||
|
||||
<!-- Button only show if NOT free plan -->
|
||||
{#if !plan.id.toLowerCase().includes('free')}
|
||||
<SubscriptionButton
|
||||
label={isCurrentPlan ? (isLegacy ? yourLegacyPlanLabel : yourPlanLabel) : buyLabel}
|
||||
onclick={() => onSelect(plan.id)}
|
||||
iconName={isCurrentPlan ? 'checkmark-circle-outline' : 'arrow-forward-outline'}
|
||||
variant={isCurrentPlan ? 'secondary' : plan.popular ? 'accent' : 'primary'}
|
||||
disabled={isCurrentPlan}
|
||||
/>
|
||||
<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}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.subscription-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);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.dark) .subscription-card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.subscription-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .subscription-card:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.subscription-card--current {
|
||||
border: 2px solid hsl(var(--color-primary, 221 83% 53%));
|
||||
}
|
||||
|
||||
.subscription-card--popular {
|
||||
border: 2px solid hsl(var(--color-primary, 221 83% 53%));
|
||||
}
|
||||
|
||||
.subscription-card__badge {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: hsl(var(--color-primary, 221 83% 53%));
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.subscription-card__badge--current {
|
||||
left: 0.75rem;
|
||||
}
|
||||
|
||||
.subscription-card__badge--popular {
|
||||
right: 0.75rem;
|
||||
}
|
||||
|
||||
.subscription-card__title {
|
||||
margin: 1.5rem 0 1rem 0;
|
||||
text-align: center;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.subscription-card__grid {
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.subscription-card__cell {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
min-height: 70px;
|
||||
min-width: 0;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
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;
|
||||
}
|
||||
|
||||
:global(.dark) .subscription-card__cell {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
.row:hover:not(:disabled) {
|
||||
background: hsl(var(--color-surface-hover));
|
||||
border-color: hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.subscription-card__icon-wrapper {
|
||||
.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);
|
||||
}
|
||||
|
||||
.subscription-card__value {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.25rem;
|
||||
.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;
|
||||
color: hsl(var(--color-foreground));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subscription-card__price {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--color-foreground));
|
||||
white-space: nowrap;
|
||||
.price.free {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.subscription-card__label {
|
||||
margin: 0;
|
||||
.period {
|
||||
font-size: 0.625rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subscription-card__sublabel {
|
||||
margin: 0.125rem 0 0 0;
|
||||
font-size: 0.5rem;
|
||||
.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));
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.subscription-card__value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subscription-card__price {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.subscription-card__label {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.subscription-card__sublabel {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,35 +7,22 @@
|
|||
import UsageCard from '../UsageCard.svelte';
|
||||
import CostCard from '../CostCard.svelte';
|
||||
|
||||
// Import default data
|
||||
import defaultSubscriptionData from '../data/subscriptionData.json';
|
||||
import defaultAppCosts from '../data/appCosts.json';
|
||||
import defaultUsageData from '../data/defaultUsageData.json';
|
||||
|
||||
interface Props {
|
||||
/** App name for the page title */
|
||||
appName: string;
|
||||
/** Handler when user selects a subscription plan */
|
||||
onSubscribe: (planId: string) => void;
|
||||
/** Handler when user selects a mana package */
|
||||
onBuyPackage: (packageId: string) => void;
|
||||
/** Current plan ID (e.g., 'free', 'Mana_Stream_Small_v1') */
|
||||
currentPlanId?: string;
|
||||
/** Current user's usage data (optional, uses defaults if not provided) */
|
||||
usageData?: UsageData;
|
||||
/** Custom subscription plans (optional, uses defaults if not provided) */
|
||||
subscriptions?: SubscriptionPlan[];
|
||||
/** Custom mana packages (optional, uses defaults if not provided) */
|
||||
packages?: ManaPackage[];
|
||||
/** Custom cost items (optional, uses defaults if not provided) */
|
||||
costs?: CostItem[];
|
||||
/** Page title */
|
||||
pageTitle?: string;
|
||||
/** Subscriptions section title */
|
||||
subscriptionsTitle?: string;
|
||||
/** One-time purchases section title */
|
||||
packagesTitle?: string;
|
||||
/** Yearly discount label */
|
||||
yearlyDiscount?: string;
|
||||
}
|
||||
|
||||
|
|
@ -54,194 +41,125 @@
|
|||
yearlyDiscount = '33%',
|
||||
}: Props = $props();
|
||||
|
||||
// State
|
||||
let billingCycle = $state<BillingCycle>('monthly');
|
||||
|
||||
// Get current plan name for display
|
||||
const currentPlanName = $derived(() => {
|
||||
const plan = subscriptions.find((p) => p.id === currentPlanId);
|
||||
return plan?.name || 'Free';
|
||||
});
|
||||
|
||||
// Get all subscription plans for current billing cycle
|
||||
function getSubscriptionPlans() {
|
||||
return subscriptions.filter((plan) => plan.id !== 'free' && plan.billingCycle === billingCycle);
|
||||
}
|
||||
|
||||
// Check if a plan is the current plan
|
||||
function isCurrentPlan(planId: string) {
|
||||
if (currentPlanId === 'free' && planId === 'free') return true;
|
||||
return planId === currentPlanId;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Mana - {appName}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="subscription-page">
|
||||
<!-- Content Area -->
|
||||
<div class="subscription-page__content">
|
||||
<div class="subscription-page__container">
|
||||
<!-- Header -->
|
||||
<div class="subscription-page__header">
|
||||
<div class="subscription-page__icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" class="w-10 h-10">
|
||||
<path
|
||||
d="M12.3 1c.03.05 7.3 9.67 7.3 13.7 0 4.03-3.27 7.3-7.3 7.3S5 18.73 5 14.7C5 10.66 12.3 1 12.3 1zm0 6.4c-.02.03-3.65 4.83-3.65 6.84 0 2.02 1.64 3.65 3.65 3.65s3.65-1.64 3.65-3.65c0-2.01-3.62-6.81-3.65-6.84z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="subscription-page__title">{pageTitle}</h1>
|
||||
<p class="subscription-page__subtitle">Wähle das passende Paket für deine Bedürfnisse</p>
|
||||
</div>
|
||||
|
||||
<!-- Active Section (Usage & Costs) -->
|
||||
<section class="subscription-page__section">
|
||||
<div class="subscription-page__usage-grid">
|
||||
<UsageCard {usageData} currentPlan={currentPlanName()} />
|
||||
<CostCard {costs} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Billing Toggle -->
|
||||
<div class="subscription-page__toggle">
|
||||
<BillingToggle
|
||||
{billingCycle}
|
||||
onChange={(cycle: BillingCycle) => (billingCycle = cycle)}
|
||||
{yearlyDiscount}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Subscriptions Section -->
|
||||
<section class="subscription-page__section">
|
||||
<h2 class="subscription-page__section-title">{subscriptionsTitle}</h2>
|
||||
|
||||
<div class="subscription-page__cards-grid">
|
||||
<!-- Free Tier -->
|
||||
<SubscriptionCard
|
||||
plan={subscriptions.find((plan) => plan.id === 'free')!}
|
||||
onSelect={onSubscribe}
|
||||
isCurrentPlan={isCurrentPlan('free')}
|
||||
/>
|
||||
|
||||
<!-- All Paid Subscriptions -->
|
||||
{#each getSubscriptionPlans() as plan}
|
||||
<SubscriptionCard
|
||||
{plan}
|
||||
onSelect={onSubscribe}
|
||||
isCurrentPlan={isCurrentPlan(plan.id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- One-time Purchases Section -->
|
||||
<section class="subscription-page__section">
|
||||
<h2 class="subscription-page__section-title">{packagesTitle}</h2>
|
||||
|
||||
<div class="subscription-page__cards-grid">
|
||||
{#each packages as pkg}
|
||||
<PackageCard package={pkg} onSelect={onBuyPackage} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<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">
|
||||
<SubscriptionCard
|
||||
plan={subscriptions.find((plan) => plan.id === 'free')}
|
||||
onSelect={onSubscribe}
|
||||
isCurrentPlan={isCurrentPlan('free')}
|
||||
/>
|
||||
{#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>
|
||||
.subscription-page {
|
||||
.sub-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.subscription-page__content {
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.subscription-page__container {
|
||||
gap: 1.25rem;
|
||||
max-width: 40rem;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 3rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.subscription-page__header {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.subscription-page__icon {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin: 0 auto 1.5rem;
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 1rem;
|
||||
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 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
color: hsl(var(--color-primary, 221 83% 53%));
|
||||
}
|
||||
|
||||
:global(.dark) .subscription-page__icon {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.subscription-page__title {
|
||||
font-size: 1.875rem;
|
||||
.section-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.subscription-page__subtitle {
|
||||
font-size: 1rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subscription-page__section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.subscription-page__section-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.subscription-page__usage-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.subscription-page__toggle {
|
||||
.card-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.subscription-page__cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue