'use client'; import { useState, useEffect, useCallback } from 'react'; import { Bell, Send, Mail, CheckCircle2, AlertCircle, Loader2, Globe, CreditCard, HardDrive, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { cn } from '@/lib/utils'; interface NotificationSettings { telegramChatId: string | null; telegramEnabled: boolean; emailEnabled: boolean; onNewVideo: boolean; onNewVersion: boolean; onNewComment: boolean; onNewReply: boolean; onApprovalEvents: boolean; timezone: string; } interface BillingOverview { isEnabled: boolean; isConfigured: boolean; status: 'disabled' | 'ready' | 'misconfigured'; checkoutAvailable: boolean; portalAvailable: boolean; subscription: { status: string; label: string; hasActiveSubscription: boolean; hasRecoverableSubscription: boolean; hasActiveTrial: boolean; hasBillingAccess: boolean; isTrialEligible: boolean; priceId: string | null; currentPeriodEnd: string | null; cancelAtPeriodEnd: boolean; cancelAt: string | null; trialEndsAt: string | null; billingAccessEndedAt: string | null; storageCleanupEligibleAt: string | null; }; workspaceCreation: { canCreateWorkspace: boolean; reason: string | null; ownedWorkspaceCount: number; invitedWorkspaceCount: number; }; } interface StorageInfo { usedBytes: string; limitBytes: string; percentage: number; } function formatBytes(bytesStr: string): string { const bytes = Number(bytesStr); if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } function ToggleButton({ enabled, onToggle, label, description, }: { enabled: boolean; onToggle: () => void; label: string; description?: string; }) { return ( ); } export default function SettingsPage({ billingOnly = false }: { billingOnly?: boolean }) { const [settings, setSettings] = useState({ telegramChatId: null, telegramEnabled: false, emailEnabled: false, onNewVideo: true, onNewVersion: true, onNewComment: true, onNewReply: true, onApprovalEvents: true, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(null); const [billing, setBilling] = useState(null); const [billingLoading, setBillingLoading] = useState(true); const [billingAction, setBillingAction] = useState<'checkout' | 'portal' | null>(null); const [storageInfo, setStorageInfo] = useState(null); const [storageLoading, setStorageLoading] = useState(true); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); // Form state for Telegram chat ID (separate from saved settings for editing) const [telegramChatId, setTelegramChatId] = useState(''); const hasScheduledCancellation = Boolean( billing?.subscription.cancelAtPeriodEnd || billing?.subscription.cancelAt ); useEffect(() => { async function fetchSettings() { try { const [settingsRes, billingRes, storageRes] = await Promise.all([ fetch('/api/settings/notifications'), fetch('/api/billing'), fetch('/api/settings/storage'), ]); if (settingsRes.ok) { const data = await settingsRes.json(); setSettings(data.data); setTelegramChatId(data.data.telegramChatId || ''); } if (billingRes.ok) { const data = await billingRes.json(); setBilling(data.data); } if (storageRes.ok) { const data = await storageRes.json(); setStorageInfo(data.data); } } catch { console.error('Failed to fetch settings'); } finally { setLoading(false); setBillingLoading(false); setStorageLoading(false); } } fetchSettings(); }, []); const showMessage = useCallback((type: 'success' | 'error', text: string) => { setMessage({ type, text }); setTimeout(() => setMessage(null), 4000); }, []); const handleSave = useCallback(async () => { setSaving(true); try { const res = await fetch('/api/settings/notifications', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...settings, telegramChatId: telegramChatId || null, }), }); if (res.ok) { const data = await res.json(); setSettings(data.data); showMessage('success', 'Settings saved'); } else { const data = await res.json(); showMessage('error', data.error || 'Failed to save'); } } catch { showMessage('error', 'Failed to save settings'); } finally { setSaving(false); } }, [settings, telegramChatId, showMessage]); const handleTest = useCallback( async (channel: 'telegram' | 'email') => { setTesting(channel); try { const res = await fetch('/api/settings/notifications', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel, telegramChatId, }), }); const data = await res.json(); if (res.ok) { showMessage('success', data.data.message); } else { showMessage('error', data.error || 'Test failed'); } } catch { showMessage('error', 'Test failed'); } finally { setTesting(null); } }, [telegramChatId, showMessage] ); const handleBillingRedirect = useCallback( async (endpoint: '/api/billing/checkout' | '/api/billing/portal') => { setBillingAction(endpoint.endsWith('checkout') ? 'checkout' : 'portal'); try { const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); const data = await res.json(); if (!res.ok) { showMessage('error', data.error || 'Failed to open billing flow'); return; } window.location.href = data.data.url; } catch { showMessage('error', 'Failed to open billing flow'); } finally { setBillingAction(null); } }, [showMessage] ); if (loading) { return (
{Array.from({ length: 4 }).map((_, i) => ( {Array.from({ length: 3 }).map((_, j) => (
))}
))}
); } return (

Settings

{billingOnly ? 'Manage your billing access' : 'Manage your notification preferences'}

{/* Status message */} {message && (
{message.type === 'success' ? ( ) : ( )} {message.text}
)} Billing Manage your paid plan and workspace creation access {billingLoading || !billing ? (
) : !billing.isEnabled ? (
Stripe billing is disabled by this host. Workspace creation is unrestricted in this environment.
) : !billing.isConfigured ? (
Stripe is not configured yet. Add your Stripe environment variables before using billing.
) : ( <> {!billing.subscription.hasActiveSubscription && !billing.subscription.hasActiveTrial && billing.subscription.isTrialEligible && billing.checkoutAvailable ? (

Start your 7-day free trial

Get full access to all features — no charge until the trial ends. Cancel anytime.

) : null}

Current plan

{billing.subscription.hasActiveSubscription ? hasScheduledCancellation ? billing.subscription.hasActiveTrial ? 'Trial canceled. Access remains active until the trial ends.' : 'Subscription canceled. Access remains active until the end of the current billing period.' : 'Paid account with workspace creation unlocked.' : billing.subscription.hasActiveTrial ? 'Trial access is active.' : billing.subscription.isTrialEligible ? "You haven't started your free trial yet." : 'Billing access has ended.'}

{billing.subscription.label}
{billing.subscription.hasRecoverableSubscription && !billing.subscription.hasActiveSubscription ? (

Your latest payment didn't go through. Update your payment method to keep your subscription — starting a new one would create a duplicate.

) : null} {billing.subscription.hasActiveTrial && billing.subscription.trialEndsAt && hasScheduledCancellation ? (

Access ends on {new Date(billing.subscription.trialEndsAt).toLocaleDateString()}.

) : null} {billing.subscription.currentPeriodEnd ? (

{hasScheduledCancellation ? 'Your subscription ends on ' : 'Current billing period ends on '} {new Date(billing.subscription.currentPeriodEnd).toLocaleDateString()}.

) : null} {hasScheduledCancellation && billing.subscription.cancelAt ? (

Cancellation was scheduled on{' '} {new Date(billing.subscription.cancelAt).toLocaleDateString()}.

) : null} {!billing.subscription.hasBillingAccess && billing.subscription.billingAccessEndedAt && billing.subscription.storageCleanupEligibleAt ? (

Stored media cleanup is scheduled after{' '} {new Date(billing.subscription.storageCleanupEligibleAt).toLocaleDateString()}{' '} unless billing is restored first.

) : null} {!billing.workspaceCreation.canCreateWorkspace ? (

Workspace creation

{billing.workspaceCreation.reason || 'Upgrade to create another workspace.'}

) : null}
{billing.subscription.hasRecoverableSubscription && billing.portalAvailable ? ( ) : ( )}
)}
{billing?.subscription.hasBillingAccess && ( Storage Combined usage across video files and media attachments (200 GB limit) {storageLoading || !storageInfo ? (
) : ( <>
{formatBytes(storageInfo.usedBytes)} used of{' '} {formatBytes(storageInfo.limitBytes)} = 90 ? 'text-destructive font-medium' : storageInfo.percentage >= 75 ? 'text-amber-600 dark:text-amber-400 font-medium' : 'text-muted-foreground' } > {storageInfo.percentage < 0.1 ? '<0.1%' : `${storageInfo.percentage.toFixed(1)}%`}
= 90 ? '[&>div]:bg-destructive' : storageInfo.percentage >= 75 ? '[&>div]:bg-amber-500' : '' } /> {storageInfo.percentage >= 90 && (

Storage is almost full. Delete unused files or contact support.

)} )}
)} {!billingOnly && ( <> {/* Event Subscriptions */} Notification Events Choose which events trigger notifications setSettings((s) => ({ ...s, onNewVideo: !s.onNewVideo }))} label="New Video Added" description="When a new video is added to one of your projects" /> setSettings((s) => ({ ...s, onNewVersion: !s.onNewVersion }))} label="New Version Added" description="When a new version is added to an existing video" /> setSettings((s) => ({ ...s, onNewComment: !s.onNewComment }))} label="New Comment" description="When someone leaves a comment on your videos" /> setSettings((s) => ({ ...s, onNewReply: !s.onNewReply }))} label="New Reply" description="When someone replies to a comment thread" /> setSettings((s) => ({ ...s, onApprovalEvents: !s.onApprovalEvents })) } label="Approval Workflow" description="When approval requests are created, responded to, or finalized" /> {/* Telegram */}
Telegram {settings.telegramEnabled ? 'Enabled' : 'Disabled'}
Get instant notifications via Telegram

Setup instructions

  1. Message{' '} @UserInfeBot {' '} on Telegram and send{' '} /start to get your Chat ID
  2. Start{' '} @openframe_bot {' '} and send /start so it can message you
  3. Paste your Chat ID below and enable notifications
setTelegramChatId(e.target.value)} className="mt-1 font-mono text-sm" />
setSettings((s) => ({ ...s, telegramEnabled: !s.telegramEnabled }))} label="Enable Telegram notifications" />
{/* Email */}
Email {settings.emailEnabled ? 'Enabled' : 'Disabled'}
Receive notification emails to your account email address
setSettings((s) => ({ ...s, emailEnabled: !s.emailEnabled }))} label="Enable email notifications" />
{/* Timezone */} Timezone Timestamps in notifications will use this timezone {/* Save button */}
)}
); }