Files
QR-master/src/app/(main)/(app)/settings/page.tsx

387 lines
12 KiB
TypeScript

'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';
export default function SettingsPage() {
const { fetchWithCsrf } = useCsrf();
const [activeTab, setActiveTab] = useState<TabType>('profile');
const [loading, setLoading] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
// 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);
}
} 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 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' };
default:
return { dynamic: 3, price: '€0', period: 'forever' };
}
};
const planLimits = getPlanLimits();
const usagePercentage = (usageStats.dynamicUsed / usageStats.dynamicLimit) * 100;
return (
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Settings</h1>
<p className="text-gray-600 mt-2">Manage your account settings and preferences</p>
</div>
{/* Tabs */}
<div className="border-b border-gray-200 mb-6">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('profile')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'profile'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Profile
</button>
<button
onClick={() => setActiveTab('subscription')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'subscription'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Subscription
</button>
</nav>
</div>
{/* Tab Content */}
{activeTab === 'profile' && (
<div className="space-y-6">
{/* Profile Information */}
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Name
</label>
<input
type="text"
value={name}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email
</label>
<input
type="email"
value={email}
disabled
className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500 cursor-not-allowed"
/>
<p className="text-xs text-gray-500 mt-1">
Email cannot be changed
</p>
</div>
</CardContent>
</Card>
{/* Security */}
<Card>
<CardHeader>
<CardTitle>Security</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900">Password</h3>
<p className="text-sm text-gray-500 mt-1">
Update your password to keep your account secure
</p>
</div>
<Button
variant="outline"
onClick={() => setShowPasswordModal(true)}
>
Change Password
</Button>
</div>
</CardContent>
</Card>
{/* Account Deletion */}
<Card>
<CardHeader>
<CardTitle className="text-red-600">Delete Account</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900">Delete your account</h3>
<p className="text-sm text-gray-500 mt-1">
Permanently delete your account and all data. This action cannot be undone.
</p>
</div>
<Button
variant="outline"
className="border-red-600 text-red-600 hover:bg-red-50"
onClick={handleDeleteAccount}
>
Delete Account
</Button>
</div>
</CardContent>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button
onClick={handleSaveProfile}
disabled={loading}
size="lg"
variant="primary"
>
{loading ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
)}
{activeTab === 'subscription' && (
<div className="space-y-6">
{/* Current Plan */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Current Plan</CardTitle>
<Badge variant={plan === 'FREE' ? 'default' : plan === 'PRO' ? 'info' : 'warning'}>
{plan}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-baseline">
<span className="text-4xl font-bold">{planLimits.price}</span>
<span className="text-gray-600 ml-2">{planLimits.period}</span>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Dynamic QR Codes</span>
<span className="font-medium">
{usageStats.dynamicUsed} of {usageStats.dynamicLimit} used
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary-600 h-2 rounded-full transition-all"
style={{ width: `${Math.min(usagePercentage, 100)}%` }}
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600">Static QR Codes</span>
<span className="font-medium">Unlimited </span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-success-600 h-2 rounded-full" style={{ width: '100%' }} />
</div>
</div>
{plan !== 'FREE' && (
<div className="pt-4 border-t">
<Button
variant="outline"
className="w-full"
onClick={() => window.location.href = '/pricing'}
>
Manage Subscription
</Button>
</div>
)}
{plan === 'FREE' && (
<div className="pt-4 border-t">
<Button variant="primary" className="w-full" onClick={() => window.location.href = '/pricing'}>
Upgrade Plan
</Button>
</div>
)}
</CardContent>
</Card>
</div>
)}
{/* Change Password Modal */}
<ChangePasswordModal
isOpen={showPasswordModal}
onClose={() => setShowPasswordModal(false)}
onSuccess={() => {
setShowPasswordModal(false);
}}
/>
</div>
);
}