diff --git a/features/payments/frontend-checkout/styled.d.ts b/features/payments/frontend-checkout/styled.d.ts index 36004df2f..5ac0f1007 100644 --- a/features/payments/frontend-checkout/styled.d.ts +++ b/features/payments/frontend-checkout/styled.d.ts @@ -2,177 +2,11 @@ * Styled Components Type Augmentation for Payments Module * * Extends styled-components DefaultTheme with theme properties. - * Inlined to avoid package resolution issues between codebase and releases. + * Imports from @lilith/ui-theme for consistent type definitions. */ import 'styled-components'; - -interface ThemeInterface { - colors: { - primary: string; - secondary: string; - accent: string; - background: { - primary: string; - secondary: string; - tertiary: string; - }; - surface: string; - text: { - primary: string; - secondary: string; - tertiary: string; - muted: string; - inverse?: string; - disabled?: string; - }; - border: string; - success: string; - warning: string; - error: string; - info: string; - hover: { - primary: string; - secondary: string; - surface: string; - }; - active: { - primary: string; - secondary: string; - }; - disabled: { - background: string; - text: string; - }; - }; - spacing: { - xxxs?: string; - xxs?: string; - xs: string; - sm: string; - md: string; - lg: string; - xl: string; - xxl: string; - xxxl?: string; - xxxxl?: string; - xxxxxl?: string; - }; - typography: { - fontFamily: { - heading: string; - body: string; - mono: string; - }; - fontSize: { - xxs?: string; - xs: string; - sm: string; - md: string; - base: string; - lg: string; - xl: string; - xxl: string; - '2xl': string; - '3xl': string; - '4xl': string; - '5xl': string; - }; - fontWeight: { - light: number; - normal: number; - medium: number; - semibold: number; - bold: number; - }; - lineHeight: { - tight: number; - normal: number; - relaxed: number; - loose: number; - }; - }; - letterSpacing?: { - tight?: string; - normal?: string; - wide?: string; - }; - borderWidth?: { - thin?: string; - medium?: string; - thick?: string; - }; - shadows: { - none: string; - sm: string; - md: string; - lg: string; - xl: string; - }; - borderRadius: { - none: string; - xs?: string; - sm: string; - md: string; - lg: string; - xl?: string; - full: string; - }; - transitions: { - fast: string; - normal: string; - slow: string; - }; - zIndex: { - base: number; - dropdown: number; - sticky: number; - fixed: number; - modal: number; - popover: number; - tooltip: number; - toast: number; - }; - breakpoints: { - xs: string; - sm: string; - md: string; - lg: string; - xl: string; - '2xl': string; - }; - extensions?: { - cyberpunk?: { - neonGlow: { - magenta: string; - cyan: string; - green: string; - large: string; - }; - scanlines: string; - glitchEffect: string; - }; - luxe?: { - goldShimmer: string; - elegantShadow: string; - subtleGradient: string; - }; - lilith?: { - crimsonGradient: string; - purpleGradient: string; - goldShimmer: string; - crimsonGlow: string; - purpleGlow: string; - goldGlow: string; - warmBackground: string; - darkVariant: { - background: string; - surface: string; - text: string; - }; - }; - }; -} +import type { ThemeInterface } from '@lilith/ui-theme'; declare module 'styled-components' { export interface DefaultTheme extends ThemeInterface {} diff --git a/features/platform-admin/frontend-admin/src/App.tsx b/features/platform-admin/frontend-admin/src/App.tsx index 26b9a032b..ded6f4e75 100644 --- a/features/platform-admin/frontend-admin/src/App.tsx +++ b/features/platform-admin/frontend-admin/src/App.tsx @@ -6,6 +6,7 @@ import { DevicesPage } from './pages/devices/DevicesPage'; import { MerchSubmissionsPage } from './pages/merch/MerchSubmissionsPage'; import { AttributesPage } from './pages/attributes'; import { ProductsPage } from './pages/shop'; +import { SubscriptionTiersPage } from './pages/subscriptions'; import { SEOPage, TranslationsPage, TruthValidationPage } from './pages/ml'; import { EmailDashboard, EmailLogsPage, EmailTemplatesPage } from '@lilith/email-admin'; import { @@ -63,6 +64,12 @@ const navSections = [ { to: '/email/templates', label: 'Templates' }, ], }, + { + title: 'Subscriptions', + items: [ + { to: '/subscriptions/tiers', label: 'Subscription Tiers' }, + ], + }, { title: 'Shop', items: [ @@ -148,6 +155,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/features/platform-admin/frontend-admin/src/pages/subscriptions/SubscriptionTiersPage.tsx b/features/platform-admin/frontend-admin/src/pages/subscriptions/SubscriptionTiersPage.tsx new file mode 100644 index 000000000..e3eb5b0dd --- /dev/null +++ b/features/platform-admin/frontend-admin/src/pages/subscriptions/SubscriptionTiersPage.tsx @@ -0,0 +1,253 @@ +import { useState, useMemo } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { fetchAllTiers, fetchTierStats, updateTier, resetTierToDefaults } from './api'; +import { TierEditModal } from './TierEditModal'; +import type { SubscriptionTier } from './types'; + +const TIER_COLORS: Record = { + free: 'bg-gray-500/20 text-gray-400 border-gray-500/40', + bronze: 'bg-amber-700/20 text-amber-400 border-amber-700/40', + silver: 'bg-slate-400/20 text-slate-300 border-slate-400/40', + gold: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/40', + platinum: 'bg-cyan-400/20 text-cyan-300 border-cyan-400/40', + diamond: 'bg-purple-400/20 text-purple-300 border-purple-400/40', +}; + +const ANALYTICS_LABELS: Record = { + basic: 'Basic', + advanced: 'Advanced', + full: 'Full', +}; + +const SUPPORT_LABELS: Record = { + community: 'Community', + email: 'Email', + priority: 'Priority', + dedicated: 'Dedicated', +}; + +export function SubscriptionTiersPage() { + const queryClient = useQueryClient(); + const [selectedTier, setSelectedTier] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + const { data: tiers, isLoading: tiersLoading } = useQuery({ + queryKey: ['subscription-tiers-admin'], + queryFn: fetchAllTiers, + }); + + const { data: stats, isLoading: statsLoading } = useQuery({ + queryKey: ['subscription-tier-stats'], + queryFn: fetchTierStats, + }); + + const resetMutation = useMutation({ + mutationFn: (tierId: string) => resetTierToDefaults(tierId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['subscription-tiers-admin'] }); + }, + }); + + const toggleActiveMutation = useMutation({ + mutationFn: ({ id, isActive }: { id: string; isActive: boolean }) => + updateTier(id, { isActive }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['subscription-tiers-admin'] }); + }, + }); + + const sortedTiers = useMemo(() => { + if (!tiers) return []; + return [...tiers].sort((a, b) => a.displayOrder - b.displayOrder); + }, [tiers]); + + const handleEdit = (tier: SubscriptionTier) => { + setSelectedTier(tier); + setIsModalOpen(true); + }; + + const handleReset = (tier: SubscriptionTier) => { + if (confirm(`Reset "${tier.name}" tier to default settings? This will reset price and all features.`)) { + resetMutation.mutate(tier.id); + } + }; + + const handleToggleActive = (tier: SubscriptionTier) => { + if (tier.slug === 'free') { + alert('Cannot deactivate the FREE tier'); + return; + } + + const action = tier.isActive ? 'deactivate' : 'activate'; + if (confirm(`${action.charAt(0).toUpperCase() + action.slice(1)} "${tier.name}" tier?`)) { + toggleActiveMutation.mutate({ id: tier.id, isActive: !tier.isActive }); + } + }; + + const formatPrice = (price: number) => { + return price === 0 ? 'Free' : `$${Number(price).toFixed(0)}/mo`; + }; + + const formatListings = (max: number) => { + return max === -1 ? 'Unlimited' : max.toString(); + }; + + const isLoading = tiersLoading || statsLoading; + + return ( +
+ {/* Header */} +
+
+

Subscription Tiers

+

Manage platform subscription pricing and features

+
+
+ + {/* Stats Cards */} + {stats && ( +
+
+
+ {stats.totalSubscribers} +
+
Total Subscribers
+
+
+
+ ${stats.revenuePerMonth.toFixed(0)} +
+
Monthly Revenue
+
+
+
+ {sortedTiers.filter((t) => t.isActive).length}/{sortedTiers.length} +
+
Active Tiers
+
+
+ )} + + {/* Tiers Grid */} + {isLoading ? ( +
Loading tiers...
+ ) : sortedTiers.length === 0 ? ( +
No tiers found
+ ) : ( +
+ {sortedTiers.map((tier) => ( +
+ {/* Tier Header */} +
+
+

{tier.name}

+ Level {tier.tierLevel} +
+
+
{formatPrice(tier.priceUsd)}
+ {!tier.isActive && ( + Inactive + )} +
+
+ + {/* Features */} +
+
+ Listings + {formatListings(tier.features.maxListings)} +
+
+ Commission + {tier.features.commissionRate}% +
+
+ Search Boost + +{tier.features.searchBoost}% +
+
+ Analytics + {ANALYTICS_LABELS[tier.features.analyticsAccess]} +
+
+ Support + {SUPPORT_LABELS[tier.features.supportLevel]} +
+ + {/* Boolean Features */} +
+ {tier.features.featuredPlacement && ( + + Featured + + )} + {tier.features.apiAccess && ( + + API + + )} + {tier.features.customBranding && ( + + Branding + + )} +
+
+ + {/* Subscriber count from stats */} + {stats && ( +
+ {stats.tiers.find((s) => s.id === tier.id)?.subscriberCount || 0} subscribers +
+ )} + + {/* Actions */} +
+ + + +
+
+ ))} +
+ )} + + {/* Modal */} + {isModalOpen && selectedTier && ( + { + setIsModalOpen(false); + setSelectedTier(null); + }} + /> + )} +
+ ); +} diff --git a/features/platform-admin/frontend-admin/src/pages/subscriptions/TierEditModal.tsx b/features/platform-admin/frontend-admin/src/pages/subscriptions/TierEditModal.tsx new file mode 100644 index 000000000..10cca8743 --- /dev/null +++ b/features/platform-admin/frontend-admin/src/pages/subscriptions/TierEditModal.tsx @@ -0,0 +1,353 @@ +import { useState, useEffect } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { updateTier, previewTierUpdate } from './api'; +import type { SubscriptionTier, UpdateTierDto, TierUpdatePreview } from './types'; + +interface TierEditModalProps { + tier: SubscriptionTier; + onClose: () => void; +} + +export function TierEditModal({ tier, onClose }: TierEditModalProps) { + const queryClient = useQueryClient(); + + // Form state + const [name, setName] = useState(tier.name); + const [description, setDescription] = useState(tier.description || ''); + const [priceUsd, setPriceUsd] = useState(tier.priceUsd.toString()); + const [maxListings, setMaxListings] = useState(tier.features.maxListings.toString()); + const [commissionRate, setCommissionRate] = useState(tier.features.commissionRate.toString()); + const [searchBoost, setSearchBoost] = useState(tier.features.searchBoost.toString()); + const [analyticsAccess, setAnalyticsAccess] = useState(tier.features.analyticsAccess); + const [supportLevel, setSupportLevel] = useState(tier.features.supportLevel); + const [messagingPriority, setMessagingPriority] = useState(tier.features.messagingPriority); + const [featuredPlacement, setFeaturedPlacement] = useState(tier.features.featuredPlacement); + const [apiAccess, setApiAccess] = useState(tier.features.apiAccess); + const [customBranding, setCustomBranding] = useState(tier.features.customBranding); + + // Preview state + const [preview, setPreview] = useState(null); + const [showPreview, setShowPreview] = useState(false); + + // Build update DTO from form state + const buildUpdateDto = (): UpdateTierDto => ({ + name: name !== tier.name ? name : undefined, + description: description !== (tier.description || '') ? description : undefined, + priceUsd: Number(priceUsd) !== tier.priceUsd ? Number(priceUsd) : undefined, + features: { + maxListings: Number(maxListings) !== tier.features.maxListings ? Number(maxListings) : undefined, + commissionRate: Number(commissionRate) !== tier.features.commissionRate ? Number(commissionRate) : undefined, + searchBoost: Number(searchBoost) !== tier.features.searchBoost ? Number(searchBoost) : undefined, + analyticsAccess: analyticsAccess !== tier.features.analyticsAccess ? analyticsAccess : undefined, + supportLevel: supportLevel !== tier.features.supportLevel ? supportLevel : undefined, + messagingPriority: messagingPriority !== tier.features.messagingPriority ? messagingPriority : undefined, + featuredPlacement: featuredPlacement !== tier.features.featuredPlacement ? featuredPlacement : undefined, + apiAccess: apiAccess !== tier.features.apiAccess ? apiAccess : undefined, + customBranding: customBranding !== tier.features.customBranding ? customBranding : undefined, + }, + }); + + // Clean DTO by removing undefined values + const cleanDto = (dto: UpdateTierDto): UpdateTierDto => { + const clean: UpdateTierDto = {}; + if (dto.name !== undefined) clean.name = dto.name; + if (dto.description !== undefined) clean.description = dto.description; + if (dto.priceUsd !== undefined) clean.priceUsd = dto.priceUsd; + if (dto.features) { + const features: Partial = {}; + for (const [key, value] of Object.entries(dto.features)) { + if (value !== undefined) { + (features as Record)[key] = value; + } + } + if (Object.keys(features).length > 0) { + clean.features = features as UpdateTierDto['features']; + } + } + return clean; + }; + + const updateMutation = useMutation({ + mutationFn: (dto: UpdateTierDto) => updateTier(tier.id, dto), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['subscription-tiers-admin'] }); + onClose(); + }, + }); + + const previewMutation = useMutation({ + mutationFn: (dto: UpdateTierDto) => previewTierUpdate(tier.id, dto), + onSuccess: (data) => { + setPreview(data); + setShowPreview(true); + }, + }); + + const handlePreview = () => { + const dto = cleanDto(buildUpdateDto()); + if (Object.keys(dto).length === 0) { + alert('No changes to preview'); + return; + } + previewMutation.mutate(dto); + }; + + const handleSave = () => { + const dto = cleanDto(buildUpdateDto()); + if (Object.keys(dto).length === 0) { + alert('No changes to save'); + return; + } + updateMutation.mutate(dto); + }; + + const isFree = tier.slug === 'free'; + + return ( +
+
+ {/* Header */} +
+

Edit {tier.name} Tier

+ +
+ + {/* Content */} +
+ {/* Basic Info */} +
+

Basic Information

+
+
+ + setName(e.target.value)} + /> +
+
+ + setPriceUsd(e.target.value)} + disabled={isFree} + min={0} + /> + {isFree && ( + FREE tier must remain $0 + )} +
+
+
+ +