✨ Add platform admin subscription tier management UI
Platform admin enhancements: - Add SubscriptionTiersPage for managing subscription tiers - Add TierEditModal with preview and validation - Add API client for tier admin endpoints - Update styled.d.ts theme type definitions - Add tsconfig reference for shared package Auto-generated: - Update locale-validation-cache - Update pnpm-lock.yaml 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4ea7327ac7
commit
b14319e3bc
10 changed files with 2731 additions and 317 deletions
170
features/payments/frontend-checkout/styled.d.ts
vendored
170
features/payments/frontend-checkout/styled.d.ts
vendored
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<Route path="/email/templates" element={<EmailTemplatesPage />} />
|
||||
<Route path="/shop/products" element={<ProductsPage />} />
|
||||
<Route path="/merch/submissions" element={<MerchSubmissionsPage />} />
|
||||
<Route path="/subscriptions/tiers" element={<SubscriptionTiersPage />} />
|
||||
<Route path="/attributes" element={<AttributesPage />} />
|
||||
<Route path="/ml/seo" element={<SEOPage />} />
|
||||
<Route path="/ml/translations" element={<TranslationsPage />} />
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
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<string, string> = {
|
||||
basic: 'Basic',
|
||||
advanced: 'Advanced',
|
||||
full: 'Full',
|
||||
};
|
||||
|
||||
const SUPPORT_LABELS: Record<string, string> = {
|
||||
community: 'Community',
|
||||
email: 'Email',
|
||||
priority: 'Priority',
|
||||
dedicated: 'Dedicated',
|
||||
};
|
||||
|
||||
export function SubscriptionTiersPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedTier, setSelectedTier] = useState<SubscriptionTier | null>(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 (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Subscription Tiers</h1>
|
||||
<p className="text-gray-500">Manage platform subscription pricing and features</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="card">
|
||||
<div className="text-3xl font-bold text-primary-400">
|
||||
{stats.totalSubscribers}
|
||||
</div>
|
||||
<div className="text-gray-500">Total Subscribers</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="text-3xl font-bold text-green-400">
|
||||
${stats.revenuePerMonth.toFixed(0)}
|
||||
</div>
|
||||
<div className="text-gray-500">Monthly Revenue</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="text-3xl font-bold text-purple-400">
|
||||
{sortedTiers.filter((t) => t.isActive).length}/{sortedTiers.length}
|
||||
</div>
|
||||
<div className="text-gray-500">Active Tiers</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tiers Grid */}
|
||||
{isLoading ? (
|
||||
<div className="card p-8 text-center text-gray-500">Loading tiers...</div>
|
||||
) : sortedTiers.length === 0 ? (
|
||||
<div className="card p-8 text-center text-gray-500">No tiers found</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{sortedTiers.map((tier) => (
|
||||
<div
|
||||
key={tier.id}
|
||||
className={`card border-2 transition-all hover:scale-[1.02] ${
|
||||
tier.isActive
|
||||
? TIER_COLORS[tier.slug] || 'border-gray-600'
|
||||
: 'opacity-50 border-gray-700'
|
||||
}`}
|
||||
>
|
||||
{/* Tier Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold">{tier.name}</h3>
|
||||
<span className="text-sm text-gray-500">Level {tier.tierLevel}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold">{formatPrice(tier.priceUsd)}</div>
|
||||
{!tier.isActive && (
|
||||
<span className="text-xs text-red-400">Inactive</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">Listings</span>
|
||||
<span className="font-mono">{formatListings(tier.features.maxListings)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">Commission</span>
|
||||
<span className="font-mono">{tier.features.commissionRate}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">Search Boost</span>
|
||||
<span className="font-mono">+{tier.features.searchBoost}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">Analytics</span>
|
||||
<span>{ANALYTICS_LABELS[tier.features.analyticsAccess]}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">Support</span>
|
||||
<span>{SUPPORT_LABELS[tier.features.supportLevel]}</span>
|
||||
</div>
|
||||
|
||||
{/* Boolean Features */}
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{tier.features.featuredPlacement && (
|
||||
<span className="px-2 py-0.5 bg-yellow-500/20 text-yellow-400 text-xs rounded">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
{tier.features.apiAccess && (
|
||||
<span className="px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded">
|
||||
API
|
||||
</span>
|
||||
)}
|
||||
{tier.features.customBranding && (
|
||||
<span className="px-2 py-0.5 bg-purple-500/20 text-purple-400 text-xs rounded">
|
||||
Branding
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subscriber count from stats */}
|
||||
{stats && (
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
{stats.tiers.find((s) => s.id === tier.id)?.subscriberCount || 0} subscribers
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-sm btn-secondary flex-1"
|
||||
onClick={() => handleEdit(tier)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => handleReset(tier)}
|
||||
disabled={resetMutation.isPending}
|
||||
title="Reset to defaults"
|
||||
>
|
||||
↺
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-sm ${tier.isActive ? 'btn-danger' : 'btn-primary'}`}
|
||||
onClick={() => handleToggleActive(tier)}
|
||||
disabled={toggleActiveMutation.isPending || tier.slug === 'free'}
|
||||
title={tier.slug === 'free' ? 'Cannot deactivate FREE tier' : ''}
|
||||
>
|
||||
{tier.isActive ? 'Deactivate' : 'Activate'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
{isModalOpen && selectedTier && (
|
||||
<TierEditModal
|
||||
tier={selectedTier}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedTier(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<TierUpdatePreview | null>(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<UpdateTierDto['features']> = {};
|
||||
for (const [key, value] of Object.entries(dto.features)) {
|
||||
if (value !== undefined) {
|
||||
(features as Record<string, unknown>)[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 (
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-900 rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-800 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold">Edit {tier.name} Tier</h2>
|
||||
<button
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
onClick={onClose}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{/* Basic Info */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-gray-300">Basic Information</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">
|
||||
Price (USD/month)
|
||||
{isFree && <span className="text-red-400 ml-1">*</span>}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input w-full"
|
||||
value={priceUsd}
|
||||
onChange={(e) => setPriceUsd(e.target.value)}
|
||||
disabled={isFree}
|
||||
min={0}
|
||||
/>
|
||||
{isFree && (
|
||||
<span className="text-xs text-gray-500">FREE tier must remain $0</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">Description</label>
|
||||
<textarea
|
||||
className="input w-full h-20"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Limits & Commission */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-gray-300">Limits & Commission</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">
|
||||
Max Listings
|
||||
<span className="text-xs text-gray-500 ml-1">(-1 = unlimited)</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input w-full"
|
||||
value={maxListings}
|
||||
onChange={(e) => setMaxListings(e.target.value)}
|
||||
min={-1}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">Commission Rate (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input w-full"
|
||||
value={commissionRate}
|
||||
onChange={(e) => setCommissionRate(e.target.value)}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">Search Boost (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input w-full"
|
||||
value={searchBoost}
|
||||
onChange={(e) => setSearchBoost(e.target.value)}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Access Levels */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-gray-300">Access Levels</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">Analytics</label>
|
||||
<select
|
||||
className="select w-full"
|
||||
value={analyticsAccess}
|
||||
onChange={(e) => setAnalyticsAccess(e.target.value as typeof analyticsAccess)}
|
||||
>
|
||||
<option value="basic">Basic</option>
|
||||
<option value="advanced">Advanced</option>
|
||||
<option value="full">Full</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">Support Level</label>
|
||||
<select
|
||||
className="select w-full"
|
||||
value={supportLevel}
|
||||
onChange={(e) => setSupportLevel(e.target.value as typeof supportLevel)}
|
||||
>
|
||||
<option value="community">Community</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="priority">Priority</option>
|
||||
<option value="dedicated">Dedicated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">Messaging Priority</label>
|
||||
<select
|
||||
className="select w-full"
|
||||
value={messagingPriority}
|
||||
onChange={(e) => setMessagingPriority(e.target.value as typeof messagingPriority)}
|
||||
>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="priority">Priority</option>
|
||||
<option value="vip">VIP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Boolean Features */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-gray-300">Features</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={featuredPlacement}
|
||||
onChange={(e) => setFeaturedPlacement(e.target.checked)}
|
||||
className="w-4 h-4 rounded"
|
||||
/>
|
||||
<span className="text-sm">Featured Placement</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={apiAccess}
|
||||
onChange={(e) => setApiAccess(e.target.checked)}
|
||||
className="w-4 h-4 rounded"
|
||||
/>
|
||||
<span className="text-sm">API Access</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={customBranding}
|
||||
onChange={(e) => setCustomBranding(e.target.checked)}
|
||||
className="w-4 h-4 rounded"
|
||||
/>
|
||||
<span className="text-sm">Custom Branding</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
{showPreview && preview && (
|
||||
<div className="bg-gray-800 rounded-lg p-4 space-y-3">
|
||||
<h3 className="font-semibold text-gray-300">Preview Changes</h3>
|
||||
|
||||
{Object.keys(preview.changes).length === 0 ? (
|
||||
<p className="text-gray-500">No changes detected</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(preview.changes).map(([field, { from, to }]) => (
|
||||
<div key={field} className="flex items-center gap-2 text-sm">
|
||||
<span className="text-gray-400 w-32">{field}:</span>
|
||||
<span className="text-red-400 line-through">{String(from)}</span>
|
||||
<span className="text-gray-500">→</span>
|
||||
<span className="text-green-400">{String(to)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{preview.warnings.length > 0 && (
|
||||
<div className="mt-3 space-y-1">
|
||||
{preview.warnings.map((warning, i) => (
|
||||
<div key={i} className="text-sm text-yellow-400 flex items-start gap-2">
|
||||
<span>⚠️</span>
|
||||
<span>{warning}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{preview.activeSubscriptions > 0 && (
|
||||
<div className="text-sm text-gray-400">
|
||||
{preview.activeSubscriptions} active subscriptions will be affected
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error display */}
|
||||
{updateMutation.isError && (
|
||||
<div className="bg-red-500/20 border border-red-500/40 rounded-lg p-3 text-red-400">
|
||||
{(updateMutation.error as Error).message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-gray-800 flex items-center justify-end gap-3">
|
||||
<button className="btn btn-secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handlePreview}
|
||||
disabled={previewMutation.isPending}
|
||||
>
|
||||
{previewMutation.isPending ? 'Loading...' : 'Preview'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleSave}
|
||||
disabled={updateMutation.isPending}
|
||||
>
|
||||
{updateMutation.isPending ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* Subscription Tiers Admin API
|
||||
*/
|
||||
|
||||
import type { SubscriptionTier, TierStats, TierUpdatePreview, UpdateTierDto } from './types';
|
||||
|
||||
const MARKETPLACE_API_URL = import.meta.env.VITE_MARKETPLACE_API_URL || '/api/marketplace';
|
||||
|
||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const token = localStorage.getItem('lilith_session');
|
||||
|
||||
const response = await fetch(`${MARKETPLACE_API_URL}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ message: response.statusText }));
|
||||
throw new Error(error.message || `Request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all tiers (including inactive) for admin
|
||||
*/
|
||||
export async function fetchAllTiers(): Promise<SubscriptionTier[]> {
|
||||
return request<SubscriptionTier[]>('/tiers/admin/all');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch tier statistics
|
||||
*/
|
||||
export async function fetchTierStats(): Promise<TierStats> {
|
||||
return request<TierStats>('/tiers/admin/stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview tier update without committing
|
||||
*/
|
||||
export async function previewTierUpdate(tierId: string, dto: UpdateTierDto): Promise<TierUpdatePreview> {
|
||||
return request<TierUpdatePreview>(`/tiers/admin/${tierId}/preview`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(dto),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a subscription tier
|
||||
*/
|
||||
export async function updateTier(tierId: string, dto: UpdateTierDto): Promise<SubscriptionTier> {
|
||||
return request<SubscriptionTier>(`/tiers/admin/${tierId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(dto),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset tier to default configuration
|
||||
*/
|
||||
export async function resetTierToDefaults(tierId: string): Promise<SubscriptionTier> {
|
||||
return request<SubscriptionTier>(`/tiers/admin/${tierId}/reset`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { SubscriptionTiersPage } from './SubscriptionTiersPage';
|
||||
export { TierEditModal } from './TierEditModal';
|
||||
export type * from './types';
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Subscription Tier Types for Platform Admin
|
||||
*/
|
||||
|
||||
export interface TierFeatures {
|
||||
maxListings: number;
|
||||
searchBoost: number;
|
||||
featuredPlacement: boolean;
|
||||
analyticsAccess: 'basic' | 'advanced' | 'full';
|
||||
messagingPriority: 'standard' | 'priority' | 'vip';
|
||||
supportLevel: 'community' | 'email' | 'priority' | 'dedicated';
|
||||
commissionRate: number;
|
||||
customBranding: boolean;
|
||||
apiAccess: boolean;
|
||||
}
|
||||
|
||||
export interface SubscriptionTier {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
priceUsd: number;
|
||||
billingInterval: 'monthly' | 'yearly';
|
||||
features: TierFeatures;
|
||||
displayOrder: number;
|
||||
isActive: boolean;
|
||||
tierLevel: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TierStats {
|
||||
tiers: Array<{
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
priceUsd: number;
|
||||
isActive: boolean;
|
||||
subscriberCount: number;
|
||||
}>;
|
||||
totalSubscribers: number;
|
||||
revenuePerMonth: number;
|
||||
}
|
||||
|
||||
export interface TierUpdatePreview {
|
||||
id: string;
|
||||
slug: string;
|
||||
changes: Record<string, { from: unknown; to: unknown }>;
|
||||
activeSubscriptions: number;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface UpdateTierDto {
|
||||
name?: string;
|
||||
description?: string;
|
||||
priceUsd?: number;
|
||||
features?: Partial<TierFeatures>;
|
||||
displayOrder?: number;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client"],
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
119
pnpm-lock.yaml
generated
119
pnpm-lock.yaml
generated
|
|
@ -488,7 +488,7 @@ importers:
|
|||
version: 5.0.10
|
||||
ts-jest:
|
||||
specifier: ^29.1.0
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.1.3
|
||||
version: 5.9.3
|
||||
|
|
@ -808,7 +808,7 @@ importers:
|
|||
version: 9.0.3
|
||||
msw:
|
||||
specifier: ^2.0.0
|
||||
version: 2.12.7(typescript@5.9.3)
|
||||
version: 2.12.7(@types/node@22.19.3)(typescript@5.9.3)
|
||||
react:
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.1
|
||||
|
|
@ -912,7 +912,7 @@ importers:
|
|||
version: 1.0.3
|
||||
ts-jest:
|
||||
specifier: ^29.1.1
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
version: 5.9.3
|
||||
|
|
@ -1122,7 +1122,7 @@ importers:
|
|||
version: 7.1.4
|
||||
ts-jest:
|
||||
specifier: ^29.4.1
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
ts-loader:
|
||||
specifier: ^9.4.3
|
||||
version: 9.5.4(typescript@5.9.3)(webpack@5.104.1)
|
||||
|
|
@ -1327,7 +1327,7 @@ importers:
|
|||
version: 24.1.3
|
||||
msw:
|
||||
specifier: ^2.0.0
|
||||
version: 2.12.7(typescript@5.9.3)
|
||||
version: 2.12.7(@types/node@22.19.3)(typescript@5.9.3)
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
|
|
@ -1493,7 +1493,7 @@ importers:
|
|||
version: 7.1.4
|
||||
ts-jest:
|
||||
specifier: ^29.1.0
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
ts-node:
|
||||
specifier: ^10.9.1
|
||||
version: 10.9.2(@swc/core@1.15.8)(@types/node@20.19.27)(typescript@5.9.3)
|
||||
|
|
@ -1593,7 +1593,7 @@ importers:
|
|||
version: 23.2.0
|
||||
msw:
|
||||
specifier: ^2.0.11
|
||||
version: 2.12.7(typescript@5.9.3)
|
||||
version: 2.12.7(@types/node@22.19.3)(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.3.0
|
||||
version: 5.9.3
|
||||
|
|
@ -1739,7 +1739,7 @@ importers:
|
|||
version: 29.7.0(@types/node@20.19.27)(ts-node@10.9.2)
|
||||
ts-jest:
|
||||
specifier: ^29.1.0
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.9.3
|
||||
|
|
@ -1874,7 +1874,7 @@ importers:
|
|||
version: 29.7.0(@types/node@20.19.27)(ts-node@10.9.2)
|
||||
ts-jest:
|
||||
specifier: ^29.1.0
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.9.3
|
||||
|
|
@ -2193,7 +2193,7 @@ importers:
|
|||
version: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2)
|
||||
ts-jest:
|
||||
specifier: ^29.2.0
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
ts-node:
|
||||
specifier: ^10.9.0
|
||||
version: 10.9.2(@types/node@22.19.3)(typescript@5.9.3)
|
||||
|
|
@ -2305,7 +2305,7 @@ importers:
|
|||
version: 7.1.4
|
||||
ts-jest:
|
||||
specifier: ^29.1.0
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.9.3
|
||||
|
|
@ -2516,7 +2516,7 @@ importers:
|
|||
version: 24.1.3
|
||||
msw:
|
||||
specifier: ^2.0.0
|
||||
version: 2.12.7(typescript@5.9.3)
|
||||
version: 2.12.7(@types/node@22.19.3)(typescript@5.9.3)
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^6.0.5
|
||||
version: 6.0.5
|
||||
|
|
@ -2562,6 +2562,9 @@ importers:
|
|||
'@nestjs/core':
|
||||
specifier: ^11.1.11
|
||||
version: 11.1.11(@nestjs/common@11.1.11)(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/jwt':
|
||||
specifier: ^11.0.2
|
||||
version: 11.0.2(@nestjs/common@11.1.11)
|
||||
'@nestjs/platform-express':
|
||||
specifier: ^11.1.11
|
||||
version: 11.1.11(@nestjs/common@11.1.11)(@nestjs/core@11.1.11)
|
||||
|
|
@ -2605,6 +2608,9 @@ importers:
|
|||
'@nestjs/testing':
|
||||
specifier: ^11.1.11
|
||||
version: 11.1.11(@nestjs/common@11.1.11)(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11)
|
||||
'@types/express':
|
||||
specifier: ^5.0.6
|
||||
version: 5.0.6
|
||||
'@types/jest':
|
||||
specifier: ^29.5.0
|
||||
version: 29.5.14
|
||||
|
|
@ -2619,7 +2625,7 @@ importers:
|
|||
version: 29.7.0(@types/node@20.19.27)(ts-node@10.9.2)
|
||||
ts-jest:
|
||||
specifier: ^29.1.0
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.9.3
|
||||
|
|
@ -3104,7 +3110,7 @@ importers:
|
|||
version: 29.7.0(@types/node@20.19.27)(ts-node@10.9.2)
|
||||
ts-jest:
|
||||
specifier: ^29.1.0
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.9.3
|
||||
|
|
@ -3535,7 +3541,7 @@ importers:
|
|||
version: 7.1.4
|
||||
ts-jest:
|
||||
specifier: ^29.1.0
|
||||
version: 29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3)
|
||||
version: 29.4.6(@babel/core@7.28.5)(esbuild@0.24.2)(jest@29.7.0)(typescript@5.9.3)
|
||||
ts-loader:
|
||||
specifier: ^9.4.3
|
||||
version: 9.5.4(typescript@5.9.3)(webpack@5.104.1)
|
||||
|
|
@ -8188,6 +8194,7 @@ packages:
|
|||
'@inquirer/core': 10.3.2(@types/node@20.19.27)
|
||||
'@inquirer/type': 3.0.10(@types/node@20.19.27)
|
||||
'@types/node': 20.19.27
|
||||
dev: true
|
||||
|
||||
/@inquirer/confirm@5.1.21(@types/node@22.19.3):
|
||||
resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==}
|
||||
|
|
@ -8220,6 +8227,7 @@ packages:
|
|||
signal-exit: 4.1.0
|
||||
wrap-ansi: 6.2.0
|
||||
yoctocolors-cjs: 2.1.3
|
||||
dev: true
|
||||
|
||||
/@inquirer/core@10.3.2(@types/node@22.19.3):
|
||||
resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==}
|
||||
|
|
@ -8612,6 +8620,7 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 20.19.27
|
||||
dev: true
|
||||
|
||||
/@inquirer/type@3.0.10(@types/node@22.19.3):
|
||||
resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==}
|
||||
|
|
@ -10960,7 +10969,7 @@ packages:
|
|||
'@nestjs/core': 11.1.11(@nestjs/common@11.1.11)(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
reflect-metadata: 0.2.2
|
||||
rxjs: 7.8.2
|
||||
typeorm: 0.3.28(pg@8.16.3)(redis@4.7.1)(ts-node@10.9.2)
|
||||
typeorm: 0.3.28(better-sqlite3@11.10.0)(pg@8.16.3)
|
||||
dev: false
|
||||
|
||||
/@nestjs/websockets@11.1.11(@nestjs/common@11.1.11)(@nestjs/core@11.1.11)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2):
|
||||
|
|
@ -13993,7 +14002,7 @@ packages:
|
|||
'@vitest/spy': 2.1.9
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
msw: 2.12.7(typescript@5.9.3)
|
||||
msw: 2.12.7(@types/node@22.19.3)(typescript@5.9.3)
|
||||
vite: 5.4.21(@types/node@22.19.3)
|
||||
|
||||
/@vitest/mocker@4.0.16(msw@2.12.7)(vite@7.3.0):
|
||||
|
|
@ -18052,7 +18061,7 @@ packages:
|
|||
semver: 7.7.3
|
||||
tapable: 2.3.0
|
||||
typescript: 5.9.3
|
||||
webpack: 5.103.0(@swc/core@1.15.8)
|
||||
webpack: 5.103.0
|
||||
dev: true
|
||||
|
||||
/form-data-encoder@1.7.2:
|
||||
|
|
@ -22329,39 +22338,6 @@ packages:
|
|||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
/msw@2.12.7(typescript@5.9.3):
|
||||
resolution: {integrity: sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
peerDependencies:
|
||||
typescript: '>= 4.8.x'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@inquirer/confirm': 5.1.21(@types/node@20.19.27)
|
||||
'@mswjs/interceptors': 0.40.0
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
'@types/statuses': 2.0.6
|
||||
cookie: 1.1.1
|
||||
graphql: 16.12.0
|
||||
headers-polyfill: 4.0.3
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
path-to-regexp: 6.3.0
|
||||
picocolors: 1.1.1
|
||||
rettime: 0.7.0
|
||||
statuses: 2.0.2
|
||||
strict-event-emitter: 0.5.1
|
||||
tough-cookie: 6.0.0
|
||||
type-fest: 5.3.1
|
||||
typescript: 5.9.3
|
||||
until-async: 3.0.2
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
/muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
dev: false
|
||||
|
|
@ -26350,47 +26326,6 @@ packages:
|
|||
yargs-parser: 21.1.1
|
||||
dev: true
|
||||
|
||||
/ts-jest@29.4.6(@babel/core@7.28.5)(jest@29.7.0)(typescript@5.9.3):
|
||||
resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@babel/core': '>=7.0.0-beta.0 <8'
|
||||
'@jest/transform': ^29.0.0 || ^30.0.0
|
||||
'@jest/types': ^29.0.0 || ^30.0.0
|
||||
babel-jest: ^29.0.0 || ^30.0.0
|
||||
esbuild: '*'
|
||||
jest: ^29.0.0 || ^30.0.0
|
||||
jest-util: ^29.0.0 || ^30.0.0
|
||||
typescript: '>=4.3 <6'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
'@jest/transform':
|
||||
optional: true
|
||||
'@jest/types':
|
||||
optional: true
|
||||
babel-jest:
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
jest-util:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
handlebars: 4.7.8
|
||||
jest: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2)
|
||||
json5: 2.2.3
|
||||
lodash.memoize: 4.1.2
|
||||
make-error: 1.3.6
|
||||
semver: 7.7.3
|
||||
type-fest: 4.41.0
|
||||
typescript: 5.9.3
|
||||
yargs-parser: 21.1.1
|
||||
dev: true
|
||||
|
||||
/ts-loader@9.5.4(typescript@5.9.3)(webpack@5.104.1):
|
||||
resolution: {integrity: sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue