SEO: Fix structured data validation errors, delete static sitemap, and update indexing scripts
This commit is contained in:
386
src/app/(main)/(app)/settings/page.tsx
Normal file
386
src/app/(main)/(app)/settings/page.tsx
Normal file
@@ -0,0 +1,386 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user