mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
feat: unify UI components, AppSlider, and login screens across apps
SUMMARY:
Consolidate shared UI components into @manacore/shared-ui and add
AppSlider to all login screens for a consistent Mana ecosystem experience.
CHANGES:
1. UI Components Migration:
- Added Card.svelte to @manacore/shared-ui with variants (elevated, outlined, ghost)
- Migrated Manacore (7 files) and Manadeck (7 files) to use shared-ui
- Removed local ui/ directories from both apps (8 components total)
2. AppSlider Unification:
- Created shared AppSlider in @manacore/shared-ui with configurable props
- Props: apps[], title, isDark, statusLabels, comingSoonLabel, openAppLabel, onAppClick
- Supports both i18n and static text configurations
- Updated Memoro AppSlider to use shared component with svelte-i18n
- Updated Manacore AppSlider to use shared component
- Created new AppSlider for ManaDeck and Märchenzauber
3. Login Page Enhancements:
- Extended LoginPage in @manacore/shared-auth-ui with new snippets:
- appSlider: Renders AppSlider at bottom (initial mode only)
- headerControls: Renders controls (theme toggle, etc.) top-right
- Updated all app login pages to include AppSlider:
- ManaCore: indigo theme (#6366f1)
- ManaDeck: violet theme (#8b5cf6)
- Märchenzauber: pink theme (#FF6B9D)
4. Subscription Page Consolidation:
- Created SubscriptionPage component in @manacore/shared-subscription-ui
- Moved subscription data (plans, packages, costs) to shared package
- Reduced subscription page code from ~100 to ~18 lines per app
FILES CHANGED:
- packages/shared-ui: Added Card, AppSlider, updated exports
- packages/shared-auth-ui: Extended LoginPage with snippets
- packages/shared-subscription-ui: Added SubscriptionPage, data files
- manacore/web: Migrated 7 files to shared-ui, updated login
- manadeck/web: Migrated 7 files to shared-ui, added AppSlider, updated login
- maerchenzauber/web: Added AppSlider, updated login
- memoro/web: Updated AppSlider to use shared component
DELETED (moved to shared packages):
- manacore/web/src/lib/components/ui/* (3 files)
- manadeck/web/src/lib/components/ui/* (5 files)
- memoro/web/src/lib/data/*.json (3 files)
- Various pnpm-lock.yaml and pnpm-workspace.yaml files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
96e0aceb93
commit
22cb7d2c5f
67 changed files with 894 additions and 22131 deletions
|
|
@ -16,6 +16,7 @@
|
|||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.43.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"svelte": "^5.39.5",
|
||||
|
|
@ -30,10 +31,12 @@
|
|||
"@manacore/shared-config": "workspace:*",
|
||||
"@manacore/shared-i18n": "workspace:*",
|
||||
"@manacore/shared-icons": "workspace:*",
|
||||
"@manacore/shared-supabase": "workspace:*",
|
||||
"@manacore/shared-subscription-types": "workspace:*",
|
||||
"@manacore/shared-subscription-ui": "workspace:*",
|
||||
"@manacore/shared-supabase": "workspace:*",
|
||||
"@manacore/shared-tailwind": "workspace:*",
|
||||
"@manacore/shared-theme": "workspace:*",
|
||||
"@manacore/shared-theme-ui": "workspace:*",
|
||||
"@manacore/shared-types": "workspace:*",
|
||||
"@manacore/shared-ui": "workspace:*",
|
||||
"@manacore/shared-utils": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
'@tailwindcss/postcss': {}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@import '@manacore/shared-tailwind/theme.css';
|
||||
@import '@manacore/shared-tailwind/themes.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
|
|
|||
|
|
@ -1,26 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { AppSlider, type AppItem } from '@manacore/shared-ui';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import MemoroLogo from './MemoroLogo.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface App {
|
||||
name: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
icon?: string;
|
||||
color: string;
|
||||
comingSoon?: boolean;
|
||||
status: 'published' | 'beta' | 'development' | 'planning';
|
||||
}
|
||||
|
||||
let currentTheme = $derived($theme);
|
||||
let isDark = $derived(currentTheme.effectiveMode === 'dark');
|
||||
let selectedApp = $state<number | null>(null);
|
||||
let hoveredApp = $state<number | null>(null);
|
||||
let cardRotations = $state<{ [key: number]: { rotateX: number; rotateY: number } }>({});
|
||||
let modalScrollContainer = $state<HTMLDivElement | null>(null);
|
||||
|
||||
let apps = $derived<App[]>([
|
||||
let apps = $derived<AppItem[]>([
|
||||
{
|
||||
name: 'Memoro',
|
||||
description: $t('app_slider.memoro_desc'),
|
||||
|
|
@ -59,262 +45,24 @@
|
|||
}
|
||||
]);
|
||||
|
||||
function getPrimaryColor() {
|
||||
const variant = currentTheme.variant;
|
||||
if (isDark) {
|
||||
const colors = {
|
||||
lume: '#f8d62b',
|
||||
nature: '#4CAF50',
|
||||
stone: '#78909C',
|
||||
ocean: '#039BE5'
|
||||
};
|
||||
return colors[variant];
|
||||
} else {
|
||||
const colors = {
|
||||
lume: '#f8d62b',
|
||||
nature: '#4CAF50',
|
||||
stone: '#607D8B',
|
||||
ocean: '#039BE5'
|
||||
};
|
||||
return colors[variant];
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusColor(status: App['status']) {
|
||||
const colors = {
|
||||
published: '#4CAF50', // Green
|
||||
beta: '#FFD700', // Yellow/Gold
|
||||
development: '#FF9800', // Orange
|
||||
planning: '#F44336' // Red (not used)
|
||||
};
|
||||
return colors[status];
|
||||
}
|
||||
|
||||
function openModal(index: number) {
|
||||
selectedApp = index;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
selectedApp = null;
|
||||
}
|
||||
|
||||
function handleCardMouseMove(e: MouseEvent, index: number, cardElement: HTMLElement) {
|
||||
const rect = cardElement.getBoundingClientRect();
|
||||
const cardCenterX = rect.left + rect.width / 2;
|
||||
const cardCenterY = rect.top + rect.height / 2;
|
||||
|
||||
// Calculate mouse position relative to card center
|
||||
const mouseXRelative = e.clientX - cardCenterX;
|
||||
const mouseYRelative = e.clientY - cardCenterY;
|
||||
|
||||
// Calculate rotation (max 3 degrees)
|
||||
const maxRotation = 3;
|
||||
const rotateY = (mouseXRelative / (rect.width / 2)) * maxRotation;
|
||||
const rotateX = -(mouseYRelative / (rect.height / 2)) * maxRotation;
|
||||
|
||||
cardRotations[index] = { rotateX, rotateY };
|
||||
}
|
||||
|
||||
function handleCardMouseLeave(index: number) {
|
||||
cardRotations[index] = { rotateX: 0, rotateY: 0 };
|
||||
}
|
||||
|
||||
// Scroll to selected app when modal opens
|
||||
$effect(() => {
|
||||
if (selectedApp !== null && modalScrollContainer) {
|
||||
setTimeout(() => {
|
||||
if (selectedApp === null) return;
|
||||
const cardWidth = 360 + 24; // card width + gap
|
||||
const scrollPosition = selectedApp * cardWidth;
|
||||
modalScrollContainer?.scrollTo({
|
||||
left: scrollPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
let statusLabels = $derived({
|
||||
published: $t('app_slider.status_published'),
|
||||
beta: $t('app_slider.status_beta'),
|
||||
development: $t('app_slider.status_development'),
|
||||
planning: $t('app_slider.status_planning')
|
||||
});
|
||||
|
||||
function handleAppClick(app: AppItem, index: number) {
|
||||
console.log('Opening app:', app.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<!-- Title -->
|
||||
<h3
|
||||
class="mb-4 text-center text-sm font-medium"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};"
|
||||
>
|
||||
{$t('app_slider.title')}
|
||||
</h3>
|
||||
|
||||
<!-- Slider Container with horizontal scroll -->
|
||||
<div class="relative">
|
||||
<div class="flex gap-4 justify-center overflow-x-auto pb-6 scrollbar-hide snap-x snap-mandatory scroll-smooth px-4 py-4" style="perspective: 1000px;">
|
||||
{#each apps as app, index}
|
||||
<button
|
||||
class="group relative flex-shrink-0 rounded-2xl p-5 cursor-pointer snap-center"
|
||||
style="width: 160px; background-color: {hoveredApp === index
|
||||
? isDark
|
||||
? 'rgba(255, 255, 255, 0.08)'
|
||||
: 'rgba(0, 0, 0, 0.08)'
|
||||
: isDark
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.05)'}; border: 1px solid {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}; box-shadow: 0 4px 20px rgba(0, 0, 0, {isDark ? '0.3' : '0.15'}); transform: perspective(1000px) rotateX({cardRotations[index]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY || 0}deg); transform-style: preserve-3d; transition: transform 0.1s ease-out, background-color 0.2s ease-out;"
|
||||
onmouseenter={() => hoveredApp = index}
|
||||
onmousemove={(e) => handleCardMouseMove(e, index, e.currentTarget)}
|
||||
onmouseleave={() => { handleCardMouseLeave(index); hoveredApp = null; }}
|
||||
onclick={() => openModal(index)}
|
||||
>
|
||||
<!-- Status Indicator -->
|
||||
<div
|
||||
class="absolute top-3 right-3 w-3 h-3 rounded-full status-indicator"
|
||||
style="background-color: {getStatusColor(app.status)}; box-shadow: 0 0 8px {getStatusColor(app.status)};"
|
||||
></div>
|
||||
|
||||
<!-- App Icon -->
|
||||
<div
|
||||
class="mb-2 flex h-20 w-20 mx-auto items-center justify-center rounded-xl transition-transform group-hover:scale-110"
|
||||
>
|
||||
{#if app.icon}
|
||||
<img src={app.icon} alt={app.name} class="w-16 h-16 object-contain" />
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded font-bold text-lg"
|
||||
style="color: {app.color};"
|
||||
>
|
||||
{app.name.charAt(0)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- App Name -->
|
||||
<h4
|
||||
class="text-base font-semibold text-center"
|
||||
style="color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
{app.name}
|
||||
</h4>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for App Details -->
|
||||
{#if selectedApp !== null}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background-color: rgba(0, 0, 0, 0.85);"
|
||||
onclick={closeModal}
|
||||
>
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
onclick={closeModal}
|
||||
class="absolute top-6 right-6 rounded-full p-2 transition-all hover:bg-white/10 z-10"
|
||||
>
|
||||
<svg class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="color: #ffffff;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Full-screen Scrollable Container -->
|
||||
<div
|
||||
bind:this={modalScrollContainer}
|
||||
class="absolute inset-0 flex items-center overflow-x-auto scrollbar-hide snap-x snap-mandatory scroll-smooth"
|
||||
>
|
||||
<div class="flex gap-6 px-8 py-8 mx-auto" style="perspective: 1000px;">
|
||||
{#each apps as app, index}
|
||||
<div
|
||||
class="flex-shrink-0 rounded-3xl p-8 snap-center shadow-2xl card-3d relative"
|
||||
style="min-width: 360px; max-width: 360px; background-color: {hoveredApp === index ? (isDark ? '#2A2A2A' : '#F5F5F5') : (isDark ? '#1E1E1E' : '#ffffff')}; border: 3px solid {app.color}40; transform: perspective(1000px) rotateX({cardRotations[index]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY || 0}deg); transform-style: preserve-3d; transition: transform 0.1s ease-out, background-color 0.2s ease-out; opacity: 1;"
|
||||
onclick={(e) => { e.stopPropagation(); selectedApp = index; }}
|
||||
onmouseenter={() => hoveredApp = index}
|
||||
onmousemove={(e) => handleCardMouseMove(e, index, e.currentTarget)}
|
||||
onmouseleave={() => { handleCardMouseLeave(index); hoveredApp = null; }}
|
||||
>
|
||||
<!-- Status Indicator with Label -->
|
||||
<div class="absolute top-4 right-4 flex items-center gap-2">
|
||||
<span class="text-xs font-medium" style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};">
|
||||
{$t(`app_slider.status_${app.status}`)}
|
||||
</span>
|
||||
<div
|
||||
class="w-4 h-4 rounded-full status-indicator"
|
||||
style="background-color: {getStatusColor(app.status)}; box-shadow: 0 0 12px {getStatusColor(app.status)};"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- App Icon Large -->
|
||||
{#if app.icon}
|
||||
<img src={app.icon} alt={app.name} class="w-28 h-28 object-contain mb-3 mx-auto" />
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-16 w-16 items-center justify-center rounded font-bold text-3xl mb-3 mx-auto"
|
||||
style="color: {app.color};"
|
||||
>
|
||||
{app.name.charAt(0)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- App Name -->
|
||||
<h3 class="text-2xl font-bold mb-2 text-center" style="color: {isDark ? '#ffffff' : '#000000'};">
|
||||
{app.name}
|
||||
</h3>
|
||||
|
||||
<!-- Short Description -->
|
||||
<p class="text-sm mb-4 text-center font-medium" style="color: {app.color};">
|
||||
{app.description}
|
||||
</p>
|
||||
|
||||
<!-- Long Description -->
|
||||
<p class="text-sm leading-relaxed mb-6 text-center" style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};">
|
||||
{app.longDescription}
|
||||
</p>
|
||||
|
||||
<!-- Coming Soon Badge or Action Button -->
|
||||
<div class="text-center">
|
||||
{#if app.comingSoon}
|
||||
<div
|
||||
class="inline-block rounded-full px-5 py-2.5 text-sm font-medium"
|
||||
style="background-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}; color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};"
|
||||
>
|
||||
{$t('app_slider.coming_soon')}
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="rounded-xl px-8 py-3 text-sm font-semibold transition-all hover:opacity-80 border-2 text-white"
|
||||
style="background-color: {app.color}60; border-color: {app.color};"
|
||||
>
|
||||
{$t('app_slider.download')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/* Status indicator pulse animation */
|
||||
.status-indicator {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<AppSlider
|
||||
{apps}
|
||||
title={$t('app_slider.title')}
|
||||
{isDark}
|
||||
{statusLabels}
|
||||
comingSoonLabel={$t('app_slider.coming_soon')}
|
||||
openAppLabel={$t('app_slider.download')}
|
||||
onAppClick={handleAppClick}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,167 +0,0 @@
|
|||
{
|
||||
"subscriptions": [
|
||||
{
|
||||
"id": "free",
|
||||
"name": "Mana Stream Free",
|
||||
"nameEn": "Mana Stream Free",
|
||||
"nameIt": "Mana Stream Free",
|
||||
"price": 0,
|
||||
"priceUnit": "",
|
||||
"monthlyMana": 150,
|
||||
"canGiftMana": false,
|
||||
"popular": false,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Stream_Small_v1",
|
||||
"name": "Kleiner Mana Stream",
|
||||
"nameEn": "Small Mana Stream",
|
||||
"nameIt": "Piccolo Mana Stream",
|
||||
"price": 5.99,
|
||||
"priceUnit": "/ Monat",
|
||||
"monthlyMana": 600,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Stream_Small_Yearly_v1",
|
||||
"name": "Kleiner Mana Stream",
|
||||
"nameEn": "Small Mana Stream",
|
||||
"nameIt": "Piccolo Mana Stream",
|
||||
"price": 47.99,
|
||||
"priceUnit": "/ Jahr",
|
||||
"priceBreakdown": "(entspricht 3,99€ / Monat, 33% Rabatt)",
|
||||
"monthlyMana": 600,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "yearly",
|
||||
"monthlyEquivalent": 3.99,
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Stream_Medium_v1",
|
||||
"name": "Mittlerer Mana Stream",
|
||||
"nameEn": "Medium Mana Stream",
|
||||
"nameIt": "Medio Mana Stream",
|
||||
"price": 14.99,
|
||||
"priceUnit": "/ Monat",
|
||||
"monthlyMana": 1500,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Stream_Medium_Yearly_v1",
|
||||
"name": "Mittlerer Mana Stream",
|
||||
"nameEn": "Medium Mana Stream",
|
||||
"nameIt": "Medio Mana Stream",
|
||||
"price": 119.99,
|
||||
"priceUnit": "/ Jahr",
|
||||
"priceBreakdown": "(entspricht 9,99€ / Monat, 33% Rabatt)",
|
||||
"monthlyMana": 1500,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "yearly",
|
||||
"monthlyEquivalent": 9.99,
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Stream_Large_v1",
|
||||
"name": "Großer Mana Stream",
|
||||
"nameEn": "Large Mana Stream",
|
||||
"nameIt": "Grande Mana Stream",
|
||||
"price": 29.99,
|
||||
"priceUnit": "/ Monat",
|
||||
"monthlyMana": 3000,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Stream_Large_Yearly_v1",
|
||||
"name": "Großer Mana Stream",
|
||||
"nameEn": "Large Mana Stream",
|
||||
"nameIt": "Grande Mana Stream",
|
||||
"price": 239.99,
|
||||
"priceUnit": "/ Jahr",
|
||||
"priceBreakdown": "(entspricht 19,99€ / Monat, 33% Rabatt)",
|
||||
"monthlyMana": 3000,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "yearly",
|
||||
"monthlyEquivalent": 19.99,
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Stream_Giant_v1",
|
||||
"name": "Riesiger Mana Stream",
|
||||
"nameEn": "Giant Mana Stream",
|
||||
"nameIt": "Gigante Mana Stream",
|
||||
"price": 49.99,
|
||||
"priceUnit": "/ Monat",
|
||||
"monthlyMana": 5000,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "monthly",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"id": "Mana_Stream_Giant_Yearly_v1",
|
||||
"name": "Riesiger Mana Stream",
|
||||
"nameEn": "Giant Mana Stream",
|
||||
"nameIt": "Gigante Mana Stream",
|
||||
"price": 399.99,
|
||||
"priceUnit": "/ Jahr",
|
||||
"priceBreakdown": "(entspricht 33,33€ / Monat, 33% Rabatt)",
|
||||
"monthlyMana": 5000,
|
||||
"canGiftMana": true,
|
||||
"popular": false,
|
||||
"billingCycle": "yearly",
|
||||
"monthlyEquivalent": 33.33,
|
||||
"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.99,
|
||||
"popular": false
|
||||
},
|
||||
{
|
||||
"id": "Mana_Potion_Medium_v1",
|
||||
"name": "Mittlerer Mana Trank",
|
||||
"nameEn": "Medium Mana Potion",
|
||||
"nameIt": "Media Pozione di Mana",
|
||||
"manaAmount": 700,
|
||||
"price": 9.99,
|
||||
"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.99,
|
||||
"popular": false
|
||||
},
|
||||
{
|
||||
"id": "Mana_Potion_Giant_v2",
|
||||
"name": "Riesiger Mana Trank",
|
||||
"nameEn": "Giant Mana Potion",
|
||||
"nameIt": "Pozione di Mana Gigante",
|
||||
"manaAmount": 2800,
|
||||
"price": 39.99,
|
||||
"popular": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"usage": {
|
||||
"total": 50,
|
||||
"lastWeek": 350,
|
||||
"lastMonth": 1200,
|
||||
"currentMana": 785,
|
||||
"maxMana": 1000,
|
||||
"history": [
|
||||
{ "date": "2025-04-25", "amount": 50 },
|
||||
{ "date": "2025-04-26", "amount": 70 },
|
||||
{ "date": "2025-04-27", "amount": 30 },
|
||||
{ "date": "2025-04-28", "amount": 45 },
|
||||
{ "date": "2025-04-29", "amount": 80 },
|
||||
{ "date": "2025-04-30", "amount": 55 },
|
||||
{ "date": "2025-05-01", "amount": 20 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +1,6 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
BillingToggle,
|
||||
SubscriptionCard,
|
||||
PackageCard,
|
||||
UsageCard,
|
||||
CostCard,
|
||||
type BillingCycle,
|
||||
type SubscriptionPlan,
|
||||
type ManaPackage,
|
||||
type UsageData,
|
||||
type CostItem
|
||||
} from '@manacore/shared-subscription-ui';
|
||||
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
|
||||
|
||||
import subscriptionData from '$lib/data/subscriptionData.json';
|
||||
import appCostsData from '$lib/data/appCosts.json';
|
||||
import usageData from '$lib/data/usageData.json';
|
||||
|
||||
// State
|
||||
let billingCycle = $state<BillingCycle>('monthly');
|
||||
|
||||
// Data from JSON files
|
||||
const subscriptionOptions = subscriptionData.subscriptions as SubscriptionPlan[];
|
||||
const manaPackages = subscriptionData.packages as ManaPackage[];
|
||||
const appCosts = appCostsData.costs as CostItem[];
|
||||
const usage = usageData.usage as UsageData;
|
||||
|
||||
// Get all subscription plans for current billing cycle
|
||||
function getAllSubscriptionPlans() {
|
||||
return subscriptionOptions.filter(
|
||||
(plan) => plan.id !== 'free' && plan.billingCycle === billingCycle
|
||||
);
|
||||
}
|
||||
|
||||
// Get all mana packages
|
||||
function getAllManaPackages() {
|
||||
return manaPackages;
|
||||
}
|
||||
|
||||
// Handlers
|
||||
function handleSubscribe(planId: string) {
|
||||
alert(`Subscribe to plan: ${planId}\n\nThis would trigger RevenueCat purchase flow.`);
|
||||
}
|
||||
|
|
@ -47,60 +10,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Mana - Memoro</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Content Area -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="mx-auto max-w-5xl pb-12">
|
||||
<h1 class="mb-8 text-3xl font-bold text-theme">Mana kaufen</h1>
|
||||
|
||||
<!-- Active Section (Usage & Costs) -->
|
||||
<section class="mb-8">
|
||||
<div class="mb-4">
|
||||
<UsageCard usageData={usage} currentPlan="Free" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<CostCard costs={appCosts} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Billing Toggle -->
|
||||
<BillingToggle {billingCycle} onChange={(cycle) => (billingCycle = cycle)} yearlyDiscount="33%" />
|
||||
|
||||
<!-- Subscriptions Section -->
|
||||
<section class="mb-12 pt-2">
|
||||
<h2 class="mb-6 text-2xl font-bold text-theme">Abonnements</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<!-- Free Tier -->
|
||||
<SubscriptionCard
|
||||
plan={subscriptionOptions.find((plan) => plan.id === 'free')!}
|
||||
onSelect={handleSubscribe}
|
||||
isCurrentPlan={true}
|
||||
/>
|
||||
|
||||
<!-- All Paid Subscriptions -->
|
||||
{#each getAllSubscriptionPlans() as plan}
|
||||
<SubscriptionCard {plan} onSelect={handleSubscribe} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- One-time Purchases Section -->
|
||||
<section class="mb-12">
|
||||
<h2 class="mb-6 text-2xl font-bold text-theme">Einmalkäufe</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- All Mana Packages -->
|
||||
{#each getAllManaPackages() as pkg}
|
||||
<PackageCard package={pkg} onSelect={handleBuyPackage} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SubscriptionPage
|
||||
appName="Memoro"
|
||||
onSubscribe={handleSubscribe}
|
||||
onBuyPackage={handleBuyPackage}
|
||||
currentPlanId="free"
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue