Updates Mar6

This commit is contained in:
belviskhoremk
2026-03-06 23:05:33 +00:00
parent f2a0fd1260
commit d07111a4f2
22 changed files with 2390 additions and 479 deletions

View File

@@ -1,35 +1,20 @@
import React, { useState, useEffect, useCallback } from 'react'
import React, { useState, useCallback } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useNavigate, useLocation, Link } from 'react-router-dom'
import { billingAPI } from '@/services/api'
import { billingAPI, authAPI } from '@/services/api'
import { useAuthStore } from '@/store/authStore'
import { Button, Card, Input, Badge } from '@/components/ui'
import { Button, Card, Input } from '@/components/ui'
import { getPlanColor, formatDate } from '@/lib/utils'
import { CreditCard, User, Shield, Download, ExternalLink } from 'lucide-react'
import { CreditCard, User, ExternalLink, AlertTriangle } from 'lucide-react'
export const SettingsPage: React.FC = () => {
// BUG-04 FIX: Removed unused 'updateUser' from destructuring
const { user } = useAuthStore()
const navigate = useNavigate()
const location = useLocation()
// BUG-06 FIX: Sync tab with URL path on mount and when path changes
const getTabFromPath = (pathname: string): 'profile' | 'billing' => {
if (pathname === '/settings/billing') return 'billing'
return 'profile'
}
const [tab, setTab] = useState<'profile' | 'billing'>(getTabFromPath(location.pathname))
const [toast, setToast] = useState('')
// Keep tab in sync if URL changes externally
useEffect(() => {
setTab(getTabFromPath(location.pathname))
}, [location.pathname])
const tab: 'profile' | 'billing' = location.pathname === '/settings/billing' ? 'billing' : 'profile'
// Update URL when tab changes
const handleTabChange = useCallback((newTab: 'profile' | 'billing') => {
setTab(newTab)
const newPath = newTab === 'billing' ? '/settings/billing' : '/settings'
if (location.pathname !== newPath) {
navigate(newPath, { replace: true })
@@ -38,7 +23,7 @@ export const SettingsPage: React.FC = () => {
const showToast = (msg: string) => {
setToast(msg)
setTimeout(() => setToast(''), 3000)
setTimeout(() => setToast(''), 3500)
}
return (
@@ -80,30 +65,153 @@ export const SettingsPage: React.FC = () => {
}
const ProfileSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast }) => {
const { user } = useAuthStore()
const { user, setAuth, token, logout } = useAuthStore()
const navigate = useNavigate()
const [companyName, setCompanyName] = useState(user?.company_name || '')
const [currentPassword, setCurrentPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [saving, setSaving] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [deleteConfirm, setDeleteConfirm] = useState('')
const [deleting, setDeleting] = useState(false)
const handleSave = async () => {
setSaving(true)
try {
const payload: { company_name?: string; current_password?: string; new_password?: string } = {}
if (companyName !== user?.company_name) payload.company_name = companyName
if (newPassword) {
payload.current_password = currentPassword
payload.new_password = newPassword
}
if (Object.keys(payload).length === 0) {
onToast('No changes to save')
return
}
const updated = await authAPI.updateProfile(payload)
setAuth(updated, token || '')
setCurrentPassword('')
setNewPassword('')
onToast('Profile updated successfully')
} catch (err) {
const e = err as { response?: { data?: { detail?: string } } }
onToast(e.response?.data?.detail || 'Failed to update profile')
} finally {
setSaving(false)
}
}
const handleDeleteAccount = async () => {
if (deleteConfirm !== 'DELETE') return
setDeleting(true)
try {
await authAPI.deleteAccount()
logout()
navigate('/')
} catch (err) {
const e = err as { response?: { data?: { detail?: string } } }
onToast(e.response?.data?.detail || 'Failed to delete account')
setDeleting(false)
}
}
return (
<Card className="p-6 space-y-4">
<h2 className="font-semibold text-gray-900">Profile Information</h2>
<Input label="Email" value={user?.email || ''} disabled />
<Input label="Company Name" value={user?.company_name || ''} disabled hint="Contact support to change company name" />
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">Plan</label>
<div className="flex items-center gap-2">
<span className={`px-3 py-1 text-sm font-medium rounded-full capitalize ${getPlanColor(user?.plan || 'free')}`}>
{user?.plan || 'free'}
</span>
<Link to="/pricing" className="text-sm text-primary-600 hover:underline">
Manage plan
</Link>
<div className="space-y-4">
<Card className="p-6 space-y-4">
<h2 className="font-semibold text-gray-900">Profile Information</h2>
<Input label="Email" value={user?.email || ''} disabled hint="Email cannot be changed" />
<Input
label="Company Name"
value={companyName}
onChange={e => setCompanyName(e.target.value)}
placeholder="Your company name"
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1.5">Plan</label>
<div className="flex items-center gap-2">
<span className={`px-3 py-1 text-sm font-medium rounded-full capitalize ${getPlanColor(user?.plan || 'free')}`}>
{user?.plan || 'free'}
</span>
<Link to="/pricing" className="text-sm text-primary-600 hover:underline">
Manage plan
</Link>
</div>
</div>
</div>
</Card>
</Card>
<Card className="p-6 space-y-4">
<h2 className="font-semibold text-gray-900">Change Password</h2>
<Input
label="Current Password"
type="password"
value={currentPassword}
onChange={e => setCurrentPassword(e.target.value)}
placeholder="Enter current password"
/>
<Input
label="New Password"
type="password"
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
placeholder="Min 8 characters"
hint="Leave blank to keep current password"
/>
</Card>
<Button onClick={handleSave} loading={saving}>
Save Changes
</Button>
{/* Danger Zone */}
<Card className="p-6 border-red-200 bg-red-50/30">
<h2 className="font-semibold text-red-800 mb-2 flex items-center gap-1.5">
<AlertTriangle className="w-4 h-4" />
Danger Zone
</h2>
<p className="text-sm text-red-700 mb-4">
Permanently delete your account, all chatbots, documents, and data. This cannot be undone.
</p>
<Button variant="outline" className="border-red-300 text-red-700 hover:bg-red-50" onClick={() => setShowDeleteModal(true)}>
Delete Account
</Button>
</Card>
{/* Delete Account Modal */}
{showDeleteModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md shadow-xl">
<h3 className="text-lg font-bold text-gray-900 mb-2">Delete Account</h3>
<p className="text-sm text-gray-500 mb-4">
This will permanently delete your account and all associated data including chatbots, documents, conversations, and leads.
<strong className="text-red-600"> This action cannot be undone.</strong>
</p>
<p className="text-sm text-gray-700 mb-2">Type <strong>DELETE</strong> to confirm:</p>
<Input
value={deleteConfirm}
onChange={e => setDeleteConfirm(e.target.value)}
placeholder="DELETE"
/>
<div className="flex gap-3 mt-4">
<Button variant="outline" className="flex-1" onClick={() => { setShowDeleteModal(false); setDeleteConfirm('') }}>
Cancel
</Button>
<Button
className="flex-1 bg-red-600 hover:bg-red-700"
disabled={deleteConfirm !== 'DELETE'}
loading={deleting}
onClick={handleDeleteAccount}
>
Delete Account
</Button>
</div>
</div>
</div>
)}
</div>
)
}
const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast }) => {
const { user } = useAuthStore()
const navigate = useNavigate()
const [loading, setLoading] = useState(false)
@@ -117,14 +225,25 @@ const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast
try {
const { url } = await billingAPI.createPortal(window.location.href)
window.location.href = url
} catch (err: any) {
onToast(err.response?.data?.detail || 'Failed to open billing portal')
} catch (err) {
const e = err as { response?: { data?: { detail?: string } } }
onToast(e.response?.data?.detail || 'Failed to open billing portal')
} finally {
setLoading(false)
}
}
const isPaid = subscription?.plan && subscription.plan !== 'free'
const plan = subscription?.plan || 'free'
const isPaid = plan !== 'free'
const planFeatures: Record<string, { published: string; conversations: string; codeExport: string }> = {
free: { published: '1', conversations: '100/month', codeExport: '❌ Agency+ only' },
starter: { published: '1', conversations: '1,500/month', codeExport: '❌ Agency+ only' },
business: { published: '3', conversations: '5,000/month', codeExport: '❌ Agency+ only' },
agency: { published: 'Unlimited', conversations: '20,000/month', codeExport: '✅ Included' },
enterprise: { published: 'Unlimited', conversations: 'Unlimited', codeExport: '✅ Included' },
}
const features = planFeatures[plan] || planFeatures.free
return (
<div className="space-y-4">
@@ -132,13 +251,14 @@ const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast
<h2 className="font-semibold text-gray-900 mb-4">Current Plan</h2>
<div className="flex items-center justify-between mb-4">
<div>
<span className={`px-3 py-1 text-sm font-semibold rounded-full capitalize ${getPlanColor(subscription?.plan || 'free')}`}>
{subscription?.plan || 'free'}
</span>
<p className="text-xs text-gray-500 mt-1">
Status: <span className={subscription?.status === 'active' ? 'text-green-600' : 'text-red-600'}>
{subscription?.status || 'active'}
<span className={`px-3 py-1 text-sm font-semibold rounded-full capitalize ${getPlanColor(plan)}`}>
{plan}
</span>
<p className="text-xs text-gray-500 mt-1">
Status:{' '}
<span className={subscription?.status === 'active' ? 'text-green-600' : 'text-red-600'}>
{subscription?.status || 'active'}
</span>
</p>
</div>
{isPaid && subscription?.current_period_end && (
@@ -168,10 +288,9 @@ const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast
<h3 className="font-semibold text-gray-900 mb-4">Plan Features</h3>
<div className="space-y-3">
{[
{ label: 'Chatbots created', value: 'Unlimited' },
{ label: 'Chatbots published', value: subscription?.plan === 'pro' ? '3' : subscription?.plan === 'starter' ? '1' : '0 (upgrade to publish)' },
{ label: 'Conversations/month', value: subscription?.plan === 'pro' ? '20,000' : subscription?.plan === 'starter' ? '5,000' : 'Unlimited (preview)' },
{ label: 'Code export', value: subscription?.plan === 'pro' || subscription?.plan === 'enterprise' ? '✅ Included' : '❌ Pro+ only' },
{ label: 'Chatbots published', value: features.published },
{ label: 'Conversations/month', value: features.conversations },
{ label: 'Code export', value: features.codeExport },
].map(({ label, value }) => (
<div key={label} className="flex justify-between py-2 border-b border-gray-100 last:border-0">
<span className="text-sm text-gray-600">{label}</span>
@@ -182,4 +301,4 @@ const BillingSettings: React.FC<{ onToast: (msg: string) => void }> = ({ onToast
</Card>
</div>
)
}
}