'use client'; import React, { useState, useEffect } from 'react'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { useCsrf } from '@/hooks/useCsrf'; import { showToast } from '@/components/ui/Toast'; import ChangePasswordModal from '@/components/settings/ChangePasswordModal'; type TabType = 'profile' | 'subscription'; type MilestonePost = { channel: string; status: string; postUrl: string | null; error: string | null; postedAt: string | null; }; type MilestoneHistoryItem = { id: string; qrTitle: string; uniqueScans: number; detectedAt: string; promptStatus: string; selfSharedAt: string | null; shareUrl: string | null; posts: MilestonePost[]; }; const CHANNEL_LABELS: Record = { x: 'X', instagram: 'Instagram' }; function milestoneStateLabel(milestone: MilestoneHistoryItem) { if (milestone.posts.some(post => post.status !== 'revoked')) return 'Approved for publishing'; if (milestone.selfSharedAt) return 'Shared by you'; if (milestone.promptStatus === 'declined') return 'Declined'; return 'Waiting for your decision'; } function postStateLabel(post: MilestonePost) { if (post.status === 'posted') return 'published'; if (post.status === 'failed') return 'publishing failed'; if (post.status === 'revoked') return 'revoked before publishing'; return 'queued'; } export default function SettingsPage() { const { fetchWithCsrf } = useCsrf(); const [activeTab, setActiveTab] = useState('profile'); const [loading, setLoading] = useState(false); const [showPasswordModal, setShowPasswordModal] = useState(false); const [socialPromptsEnabled, setSocialPromptsEnabled] = useState(true); const [socialTestResetAvailable, setSocialTestResetAvailable] = useState(false); const [socialSaving, setSocialSaving] = useState(false); const [milestones, setMilestones] = useState([]); const [milestoneBusy, setMilestoneBusy] = useState(null); // Profile states const [name, setName] = useState(''); const [email, setEmail] = useState(''); // Subscription states const [plan, setPlan] = useState('FREE'); const [usageStats, setUsageStats] = useState({ dynamicUsed: 0, dynamicLimit: 3, staticUsed: 0, }); // Load user data useEffect(() => { const fetchUserData = async () => { try { // Load from localStorage const userStr = localStorage.getItem('user'); if (userStr) { const user = JSON.parse(userStr); setName(user.name || ''); setEmail(user.email || ''); } // Fetch plan from API const planResponse = await fetch('/api/user/plan'); if (planResponse.ok) { const data = await planResponse.json(); setPlan(data.plan || 'FREE'); } // Fetch usage stats from API const statsResponse = await fetch('/api/user/stats'); if (statsResponse.ok) { const data = await statsResponse.json(); setUsageStats(data); } const socialResponse = await fetch('/api/social-milestones/preferences'); if (socialResponse.ok) { const data = await socialResponse.json(); setSocialPromptsEnabled(data.promptsEnabled !== false); setSocialTestResetAvailable(data.testResetAvailable === true); } const historyResponse = await fetch('/api/social-milestones/history'); if (historyResponse.ok) { const data = await historyResponse.json(); setMilestones(Array.isArray(data.milestones) ? data.milestones : []); } } catch (e) { console.error('Failed to load user data:', e); } }; fetchUserData(); }, []); const handleSaveProfile = async () => { setLoading(true); try { // Save to backend API const response = await fetchWithCsrf('/api/user/profile', { method: 'PATCH', body: JSON.stringify({ name }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to update profile'); } // Update user data in localStorage const userStr = localStorage.getItem('user'); if (userStr) { const user = JSON.parse(userStr); user.name = name; localStorage.setItem('user', JSON.stringify(user)); } showToast('Profile updated successfully!', 'success'); } catch (error: any) { console.error('Error saving profile:', error); showToast(error.message || 'Failed to update profile', 'error'); } finally { setLoading(false); } }; const updateSocialPrompts = async (action: 'enable' | 'disable' | 'reset_test') => { setSocialSaving(true); try { const response = await fetchWithCsrf('/api/social-milestones/preferences', { method: 'PATCH', body: JSON.stringify({ action }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || 'Could not update milestone prompts'); setSocialPromptsEnabled(data.promptsEnabled !== false); showToast(action === 'reset_test' ? 'Milestone test reset. Open the dashboard to test it again.' : 'Milestone preference updated.', 'success'); } catch (error) { showToast(error instanceof Error ? error.message : 'Could not update milestone prompts', 'error'); } finally { setSocialSaving(false); } }; const updateMilestone = async (id: string, action: 'retry' | 'revoke', channel: string) => { setMilestoneBusy(`${id}:${channel}`); try { const response = await fetchWithCsrf(`/api/social-milestones/${id}`, { method: 'PATCH', body: JSON.stringify({ action, channel }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || 'Could not update this milestone'); setMilestones(current => current.map(milestone => milestone.id === id ? { ...milestone, posts: milestone.posts.map(post => { const next = (data.milestone?.posts || []).find((entry: MilestonePost) => entry.channel === post.channel); return next ? { ...post, status: next.status, postUrl: next.postUrl, error: next.error } : post; }), } : milestone)); showToast(action === 'retry' ? 'Post queued again.' : 'Post revoked. Nothing will be published.', 'success'); } catch (error) { showToast(error instanceof Error ? error.message : 'Could not update this milestone', 'error'); } finally { setMilestoneBusy(null); } }; const handleManageSubscription = async () => { setLoading(true); try { const response = await fetchWithCsrf('/api/stripe/portal', { method: 'POST', }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to open subscription management'); } // Redirect to Stripe Customer Portal window.location.href = data.url; } catch (error: any) { console.error('Error opening portal:', error); showToast(error.message || 'Failed to open subscription management', 'error'); setLoading(false); } }; const handleDeleteAccount = async () => { const confirmed = window.confirm( 'Are you sure you want to delete your account? This will permanently delete all your data, including all QR codes and analytics. This action cannot be undone.' ); if (!confirmed) return; // Double confirmation for safety const doubleConfirmed = window.confirm( 'This is your last warning. Are you absolutely sure you want to permanently delete your account?' ); if (!doubleConfirmed) return; setLoading(true); try { const response = await fetchWithCsrf('/api/user/delete', { method: 'DELETE', }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to delete account'); } // Clear local storage and redirect to login localStorage.clear(); showToast('Account deleted successfully', 'success'); // Redirect to home page after a short delay setTimeout(() => { window.location.href = '/'; }, 1500); } catch (error: any) { console.error('Error deleting account:', error); showToast(error.message || 'Failed to delete account', 'error'); setLoading(false); } }; const getPlanLimits = () => { switch (plan) { case 'PRO': return { dynamic: 50, price: '€9', period: 'per month' }; case 'BUSINESS': return { dynamic: 500, price: '€29', period: 'per month' }; case 'ENTERPRISE': return { dynamic: 99999, price: 'Custom', period: 'per month' }; default: return { dynamic: 3, price: '€0', period: 'forever' }; } }; const planLimits = getPlanLimits(); const usagePercentage = (usageStats.dynamicUsed / usageStats.dynamicLimit) * 100; return (

Settings

Manage your account settings and preferences

{/* Tabs */}
{/* Tab Content */} {activeTab === 'profile' && (
{/* Profile Information */} Profile Information
setName(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" placeholder="Enter your name" />

Email cannot be changed

Milestone sharing

Show scan milestone prompts

Choose whether QR Master may ask you to share verified scan achievements. Nothing is published without your confirmation.

{milestones.length > 0 &&

Your milestones

Every scan milestone we detected and what happened to it.

    {milestones.map(milestone => (
  • {milestone.qrTitle}

    {milestone.uniqueScans.toLocaleString('en-US')} unique scans · {new Date(milestone.detectedAt).toLocaleDateString('en-US')} · {milestoneStateLabel(milestone)}

    {milestone.shareUrl && ( Open card )}
    {/* One line per channel: consent, and everything that can be withdrawn or restarted, is per channel. */} {milestone.posts.map(post => (

    {CHANNEL_LABELS[post.channel] || post.channel} — {postStateLabel(post)}

    {post.status === 'failed' && post.error && (

    {post.error}

    )}
    {post.postUrl && ( View post )} {post.status === 'failed' && ( )} {['approved', 'failed'].includes(post.status) && ( )}
    ))}
  • ))}
} {socialTestResetAvailable &&

Test environment: reopen the latest milestone and clear its publishing state.

}
{/* Security */} Security

Password

Update your password to keep your account secure

{/* Account Deletion */} Delete Account

Delete your account

Permanently delete your account and all data. This action cannot be undone.

{/* Save Button */}
)} {activeTab === 'subscription' && (
{/* Current Plan */}
Current Plan {plan}
{planLimits.price} {planLimits.period}
Dynamic QR Codes {usageStats.dynamicUsed} of {usageStats.dynamicLimit} used
Static QR Codes Unlimited ∞
{plan !== 'FREE' && (
)} {plan === 'FREE' && (
)}
)} {/* Change Password Modal */} setShowPasswordModal(false)} onSuccess={() => { setShowPasswordModal(false); }} />
); }