refactor: migrate page components to Design System v4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,7 +64,7 @@ export function AuditLogsPage() {
|
||||
render: (log) => (
|
||||
<button
|
||||
onClick={() => setExpandedId(expandedId === log.id ? null : log.id)}
|
||||
className="p-1 text-muted-foreground hover:text-foreground"
|
||||
className="p-1 text-[#848b9b] hover:text-[#e2e5eb]"
|
||||
>
|
||||
{expandedId === log.id ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
@@ -78,14 +78,14 @@ export function AuditLogsPage() {
|
||||
key: 'action',
|
||||
header: 'Action',
|
||||
render: (log) => (
|
||||
<span className="text-sm font-medium text-foreground">{log.action}</span>
|
||||
<span className="text-sm font-medium text-[#e2e5eb]">{log.action}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'resource',
|
||||
header: 'Resource',
|
||||
render: (log) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-[#848b9b]">
|
||||
{log.resource_type}{log.resource_id ? ` (${log.resource_id.slice(0, 8)}...)` : ''}
|
||||
</span>
|
||||
),
|
||||
@@ -94,14 +94,14 @@ export function AuditLogsPage() {
|
||||
key: 'user',
|
||||
header: 'User',
|
||||
render: (log) => (
|
||||
<span className="text-sm text-muted-foreground">{log.user_email || 'System'}</span>
|
||||
<span className="text-sm text-[#848b9b]">{log.user_email || 'System'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
header: 'Time',
|
||||
render: (log) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-[#848b9b]">
|
||||
{new Date(log.created_at).toLocaleString()}
|
||||
</span>
|
||||
),
|
||||
@@ -117,8 +117,8 @@ export function AuditLogsPage() {
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium',
|
||||
'text-muted-foreground hover:bg-accent hover:text-foreground'
|
||||
'flex items-center gap-2 rounded-md border border-[#1e2130] px-4 py-2 text-sm font-medium',
|
||||
'text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]'
|
||||
)}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
@@ -134,8 +134,8 @@ export function AuditLogsPage() {
|
||||
onChange={(e) => { setActionFilter(e.target.value); setPage(1) }}
|
||||
placeholder="Filter by action..."
|
||||
className={cn(
|
||||
'h-9 rounded-md border border-border bg-card px-3 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
'h-9 rounded-md border border-[#1e2130] bg-[#14161d] px-3 text-sm text-[#e2e5eb]',
|
||||
'placeholder:text-[#848b9b] focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
<input
|
||||
@@ -144,8 +144,8 @@ export function AuditLogsPage() {
|
||||
onChange={(e) => { setResourceFilter(e.target.value); setPage(1) }}
|
||||
placeholder="Filter by resource type..."
|
||||
className={cn(
|
||||
'h-9 rounded-md border border-border bg-card px-3 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
'h-9 rounded-md border border-[#1e2130] bg-[#14161d] px-3 text-sm text-[#e2e5eb]',
|
||||
'placeholder:text-[#848b9b] focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -166,9 +166,9 @@ export function AuditLogsPage() {
|
||||
|
||||
{/* Expanded details row */}
|
||||
{expandedId && logs.find(l => l.id === expandedId)?.details && (
|
||||
<div className="rounded-md border border-border bg-accent p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-foreground">Details</h4>
|
||||
<pre className="overflow-x-auto rounded bg-card p-3 text-xs text-muted-foreground">
|
||||
<div className="rounded-md border border-[#1e2130] bg-accent p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-[#e2e5eb]">Details</h4>
|
||||
<pre className="overflow-x-auto rounded bg-[#14161d] p-3 text-xs text-[#848b9b]">
|
||||
{JSON.stringify(logs.find(l => l.id === expandedId)?.details, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -14,13 +14,13 @@ interface MetricCardProps {
|
||||
|
||||
function MetricCard({ label, value, icon }: MetricCardProps) {
|
||||
return (
|
||||
<div className="bg-card border border-border rounded-xl p-6">
|
||||
<div className="bg-[#14161d] border border-[#1e2130] rounded-xl p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">{label}</p>
|
||||
<p className="mt-1 text-3xl font-bold text-foreground">{value}</p>
|
||||
<p className="text-sm text-[#848b9b]">{label}</p>
|
||||
<p className="mt-1 text-3xl font-bold text-[#e2e5eb]">{value}</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-accent p-3 text-muted-foreground">{icon}</div>
|
||||
<div className="rounded-lg bg-accent p-3 text-[#848b9b]">{icon}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -71,18 +71,18 @@ export function DashboardPage() {
|
||||
{/* Recent Activity */}
|
||||
{activity.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">Recent Activity</h2>
|
||||
<h2 className="text-lg font-semibold text-[#e2e5eb]">Recent Activity</h2>
|
||||
<div className="mt-3 space-y-2">
|
||||
{activity.slice(0, 10).map((entry) => (
|
||||
<div key={entry.id} className="flex items-center justify-between rounded-md border border-border px-4 py-3 text-sm">
|
||||
<div key={entry.id} className="flex items-center justify-between rounded-md border border-[#1e2130] px-4 py-3 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-foreground">{entry.action}</span>
|
||||
<span className="ml-2 text-muted-foreground">{entry.resource_type}</span>
|
||||
<span className="font-medium text-[#e2e5eb]">{entry.action}</span>
|
||||
<span className="ml-2 text-[#848b9b]">{entry.resource_type}</span>
|
||||
{entry.user_email && (
|
||||
<span className="ml-2 text-muted-foreground">by {entry.user_email}</span>
|
||||
<span className="ml-2 text-[#848b9b]">by {entry.user_email}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-xs text-[#848b9b]">
|
||||
{new Date(entry.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -93,18 +93,18 @@ export function DashboardPage() {
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">Quick Links</h2>
|
||||
<h2 className="text-lg font-semibold text-[#e2e5eb]">Quick Links</h2>
|
||||
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{quickLinks.map((link) => (
|
||||
<Link
|
||||
key={link.to}
|
||||
to={link.to}
|
||||
className={cn(
|
||||
'flex items-center gap-3 bg-card border border-border rounded-xl p-4',
|
||||
'text-sm font-medium text-foreground transition-colors hover:bg-accent'
|
||||
'flex items-center gap-3 bg-[#14161d] border border-[#1e2130] rounded-xl p-4',
|
||||
'text-sm font-medium text-[#e2e5eb] transition-colors hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<link.icon className="h-5 w-5 text-muted-foreground" />
|
||||
<link.icon className="h-5 w-5 text-[#848b9b]" />
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -95,11 +95,11 @@ export function FeatureFlagsPage() {
|
||||
const flagColumns: Column<FeatureFlagResponse>[] = [
|
||||
{ key: 'name', header: 'Name', render: (f) => (
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{f.display_name}</div>
|
||||
<div className="text-xs text-muted-foreground">{f.flag_key}</div>
|
||||
<div className="font-medium text-[#e2e5eb]">{f.display_name}</div>
|
||||
<div className="text-xs text-[#848b9b]">{f.flag_key}</div>
|
||||
</div>
|
||||
)},
|
||||
{ key: 'description', header: 'Description', render: (f) => <span className="text-sm text-muted-foreground">{f.description || '-'}</span> },
|
||||
{ key: 'description', header: 'Description', render: (f) => <span className="text-sm text-[#848b9b]">{f.description || '-'}</span> },
|
||||
...PLANS.map(plan => ({
|
||||
key: plan,
|
||||
header: plan.charAt(0).toUpperCase() + plan.slice(1),
|
||||
@@ -133,10 +133,10 @@ export function FeatureFlagsPage() {
|
||||
]
|
||||
|
||||
const overrideColumns: Column<AccountFeatureOverrideResponse>[] = [
|
||||
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-foreground">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
|
||||
{ key: 'flag', header: 'Flag', render: (o) => <span className="text-sm text-muted-foreground">{o.flag_display_name || o.flag_key || o.flag_id.slice(0, 8)}</span> },
|
||||
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-[#e2e5eb]">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
|
||||
{ key: 'flag', header: 'Flag', render: (o) => <span className="text-sm text-[#848b9b]">{o.flag_display_name || o.flag_key || o.flag_id.slice(0, 8)}</span> },
|
||||
{ key: 'enabled', header: 'Enabled', render: (o) => <StatusBadge variant={o.enabled ? 'success' : 'destructive'}>{o.enabled ? 'Yes' : 'No'}</StatusBadge> },
|
||||
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-muted-foreground">{o.note || '-'}</span> },
|
||||
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-[#848b9b]">{o.note || '-'}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (o) => (
|
||||
@@ -147,7 +147,7 @@ export function FeatureFlagsPage() {
|
||||
},
|
||||
]
|
||||
|
||||
const selectClass = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
|
||||
const selectClass = cn('w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]', 'placeholder:text-[#848b9b] focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -163,7 +163,7 @@ export function FeatureFlagsPage() {
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">Feature Matrix</h2>
|
||||
<h2 className="text-lg font-semibold text-[#e2e5eb]">Feature Matrix</h2>
|
||||
<div className="mt-3">
|
||||
<DataTable columns={flagColumns} data={flags} keyExtractor={(f) => f.id} isLoading={loading}
|
||||
emptyState={<EmptyState icon={<ToggleLeft className="h-12 w-12" />} title="No feature flags" description="Create feature flags to control availability per plan." />}
|
||||
@@ -173,7 +173,7 @@ export function FeatureFlagsPage() {
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-foreground">Account Overrides</h2>
|
||||
<h2 className="text-lg font-semibold text-[#e2e5eb]">Account Overrides</h2>
|
||||
<Button onClick={() => setOverrideOpen(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Override
|
||||
@@ -197,15 +197,15 @@ export function FeatureFlagsPage() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Flag Key</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Flag Key</label>
|
||||
<Input type="text" value={createForm.flag_key} onChange={(e) => setCreateForm({ ...createForm, flag_key: e.target.value })} placeholder="e.g. custom_branding" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Display Name</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Display Name</label>
|
||||
<Input type="text" value={createForm.display_name} onChange={(e) => setCreateForm({ ...createForm, display_name: e.target.value })} placeholder="e.g. Custom Branding" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Description</label>
|
||||
<Input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,22 +222,22 @@ export function FeatureFlagsPage() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Account Display Code</label>
|
||||
<Input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Feature Flag</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Feature Flag</label>
|
||||
<select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={selectClass}>
|
||||
<option value="">Select a flag...</option>
|
||||
{flags.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="checkbox" id="override-enabled" checked={overrideForm.enabled} onChange={(e) => setOverrideForm({ ...overrideForm, enabled: e.target.checked })} className="h-4 w-4 rounded border-border" />
|
||||
<label htmlFor="override-enabled" className="text-sm font-medium text-foreground">Enabled</label>
|
||||
<input type="checkbox" id="override-enabled" checked={overrideForm.enabled} onChange={(e) => setOverrideForm({ ...overrideForm, enabled: e.target.checked })} className="h-4 w-4 rounded border-[#1e2130]" />
|
||||
<label htmlFor="override-enabled" className="text-sm font-medium text-[#e2e5eb]">Enabled</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Note</label>
|
||||
<Input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,7 +89,7 @@ function SortOrderInput({
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'w-20 rounded-[8px] border border-border bg-card px-2 py-1 text-sm text-foreground',
|
||||
'w-20 rounded-[8px] border border-[#1e2130] bg-[#14161d] px-2 py-1 text-sm text-[#e2e5eb]',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-none',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
@@ -117,19 +117,19 @@ function FilterBar({
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="relative flex-1 min-w-[180px] max-w-xs">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#848b9b] pointer-events-none" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name…"
|
||||
value={search}
|
||||
onChange={e => onSearchChange(e.target.value)}
|
||||
className={cn(
|
||||
'w-full rounded-[10px] border border-border bg-card pl-9 pr-3 py-2 text-sm text-foreground placeholder:text-muted-foreground',
|
||||
'w-full rounded-lg border border-[#1e2130] bg-[#14161d] pl-9 pr-3 py-2 text-sm text-[#e2e5eb] placeholder:text-[#848b9b]',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-none',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex rounded-[10px] border border-border overflow-hidden text-sm">
|
||||
<div className="flex rounded-lg border border-[#1e2130] overflow-hidden text-sm">
|
||||
{(['all', 'featured', 'unfeatured'] as FilterMode[]).map(mode => (
|
||||
<button
|
||||
key={mode}
|
||||
@@ -137,8 +137,8 @@ function FilterBar({
|
||||
className={cn(
|
||||
'px-3 py-1.5 capitalize transition-colors',
|
||||
filter === mode
|
||||
? 'bg-primary text-[#101114] font-semibold'
|
||||
: 'text-muted-foreground hover:text-foreground bg-card',
|
||||
? 'bg-primary text-white font-semibold'
|
||||
: 'text-[#848b9b] hover:text-[#e2e5eb] bg-[#14161d]',
|
||||
)}
|
||||
>
|
||||
{mode}
|
||||
@@ -166,7 +166,7 @@ function FlowsTable({
|
||||
}) {
|
||||
if (flows.length === 0) {
|
||||
return (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">No flows match the current filter.</p>
|
||||
<p className="py-8 text-center text-sm text-[#848b9b]">No flows match the current filter.</p>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -174,19 +174,19 @@ function FlowsTable({
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left">
|
||||
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Name</th>
|
||||
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Type</th>
|
||||
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Featured</th>
|
||||
<th className="pb-3 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Sort Order</th>
|
||||
<tr className="border-b border-[#1e2130] text-left">
|
||||
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Name</th>
|
||||
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Type</th>
|
||||
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Featured</th>
|
||||
<th className="pb-3 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Sort Order</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{flows.map(flow => (
|
||||
<tr key={flow.id} className="group hover:bg-[rgba(255,255,255,0.02)] transition-colors">
|
||||
<td className="py-3 pr-4 text-foreground font-medium">{flow.name}</td>
|
||||
<td className="py-3 pr-4 text-[#e2e5eb] font-medium">{flow.name}</td>
|
||||
<td className="py-3 pr-4">
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] rounded-full px-2 py-0.5 border border-border text-muted-foreground">
|
||||
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] rounded-full px-2 py-0.5 border border-[#1e2130] text-[#848b9b]">
|
||||
{flow.tree_type}
|
||||
</span>
|
||||
</td>
|
||||
@@ -234,7 +234,7 @@ function ScriptsTable({
|
||||
}) {
|
||||
if (scripts.length === 0) {
|
||||
return (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">No scripts match the current filter.</p>
|
||||
<p className="py-8 text-center text-sm text-[#848b9b]">No scripts match the current filter.</p>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -242,24 +242,24 @@ function ScriptsTable({
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left">
|
||||
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Name</th>
|
||||
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Status</th>
|
||||
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Featured</th>
|
||||
<th className="pb-3 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Sort Order</th>
|
||||
<tr className="border-b border-[#1e2130] text-left">
|
||||
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Name</th>
|
||||
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Status</th>
|
||||
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Featured</th>
|
||||
<th className="pb-3 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Sort Order</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{scripts.map(script => (
|
||||
<tr key={script.id} className="group hover:bg-[rgba(255,255,255,0.02)] transition-colors">
|
||||
<td className="py-3 pr-4 text-foreground font-medium">{script.name}</td>
|
||||
<td className="py-3 pr-4 text-[#e2e5eb] font-medium">{script.name}</td>
|
||||
<td className="py-3 pr-4">
|
||||
<span
|
||||
className={cn(
|
||||
'font-label text-[0.625rem] uppercase tracking-[0.1em] rounded-full px-2 py-0.5 border',
|
||||
'font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] rounded-full px-2 py-0.5 border',
|
||||
script.is_active
|
||||
? 'border-emerald-400/30 text-emerald-400 bg-emerald-400/10'
|
||||
: 'border-border text-muted-foreground',
|
||||
: 'border-[#1e2130] text-[#848b9b]',
|
||||
)}
|
||||
>
|
||||
{script.is_active ? 'Active' : 'Inactive'}
|
||||
@@ -417,23 +417,23 @@ export default function GalleryManagementPage() {
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16 text-muted-foreground text-sm">
|
||||
<div className="flex items-center justify-center py-16 text-[#848b9b] text-sm">
|
||||
Loading gallery items…
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
{/* Summary stats */}
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
<div className="glass-card-static rounded-[12px] px-4 py-3 flex items-center gap-3">
|
||||
<div className="card-flat rounded-[12px] px-4 py-3 flex items-center gap-3">
|
||||
<Star className="h-4 w-4 text-amber-400 fill-amber-400" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-foreground font-semibold">{featuredFlowCount}</span> featured flow{featuredFlowCount !== 1 ? 's' : ''}
|
||||
<span className="text-sm text-[#848b9b]">
|
||||
<span className="text-[#e2e5eb] font-semibold">{featuredFlowCount}</span> featured flow{featuredFlowCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="glass-card-static rounded-[12px] px-4 py-3 flex items-center gap-3">
|
||||
<div className="card-flat rounded-[12px] px-4 py-3 flex items-center gap-3">
|
||||
<Star className="h-4 w-4 text-amber-400 fill-amber-400" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-foreground font-semibold">{featuredScriptCount}</span> featured script{featuredScriptCount !== 1 ? 's' : ''}
|
||||
<span className="text-sm text-[#848b9b]">
|
||||
<span className="text-[#e2e5eb] font-semibold">{featuredScriptCount}</span> featured script{featuredScriptCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -442,14 +442,14 @@ export default function GalleryManagementPage() {
|
||||
<section>
|
||||
<div className="mb-4 flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-foreground">Featured Flows</h2>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
<h2 className="text-base font-semibold text-[#e2e5eb]">Featured Flows</h2>
|
||||
<p className="text-xs text-[#848b9b] mt-0.5">
|
||||
Toggle flows to show in the gallery. Lower sort order = shown first.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card-static rounded-[16px] p-5 space-y-4">
|
||||
<div className="card-flat rounded-lg p-5 space-y-4">
|
||||
<FilterBar
|
||||
search={flowSearch}
|
||||
onSearchChange={setFlowSearch}
|
||||
@@ -469,14 +469,14 @@ export default function GalleryManagementPage() {
|
||||
<section>
|
||||
<div className="mb-4 flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-foreground">Featured Scripts</h2>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
<h2 className="text-base font-semibold text-[#e2e5eb]">Featured Scripts</h2>
|
||||
<p className="text-xs text-[#848b9b] mt-0.5">
|
||||
Toggle scripts to show in the gallery. Lower sort order = shown first.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card-static rounded-[16px] p-5 space-y-4">
|
||||
<div className="card-flat rounded-lg p-5 space-y-4">
|
||||
<FilterBar
|
||||
search={scriptSearch}
|
||||
onSearchChange={setScriptSearch}
|
||||
|
||||
@@ -73,10 +73,10 @@ export function GlobalCategoriesPage() {
|
||||
}
|
||||
|
||||
const columns: Column<AdminCategory>[] = [
|
||||
{ key: 'name', header: 'Name', render: (c) => <span className="font-medium text-foreground">{c.name}</span> },
|
||||
{ key: 'slug', header: 'Slug', render: (c) => <span className="text-sm text-muted-foreground">{c.slug}</span> },
|
||||
{ key: 'description', header: 'Description', render: (c) => <span className="text-sm text-muted-foreground">{c.description || '-'}</span> },
|
||||
{ key: 'tree_count', header: 'Trees', render: (c) => <span className="text-sm text-muted-foreground">{c.tree_count}</span> },
|
||||
{ key: 'name', header: 'Name', render: (c) => <span className="font-medium text-[#e2e5eb]">{c.name}</span> },
|
||||
{ key: 'slug', header: 'Slug', render: (c) => <span className="text-sm text-[#848b9b]">{c.slug}</span> },
|
||||
{ key: 'description', header: 'Description', render: (c) => <span className="text-sm text-[#848b9b]">{c.description || '-'}</span> },
|
||||
{ key: 'tree_count', header: 'Trees', render: (c) => <span className="text-sm text-[#848b9b]">{c.tree_count}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (c) => (
|
||||
@@ -124,15 +124,15 @@ export function GlobalCategoriesPage() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Name</label>
|
||||
<Input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Slug</label>
|
||||
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Description</label>
|
||||
<Input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,15 +153,15 @@ export function GlobalCategoriesPage() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Name</label>
|
||||
<Input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Networking" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Slug</label>
|
||||
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Description</label>
|
||||
<Input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -110,8 +110,8 @@ export function InviteCodesPage() {
|
||||
}
|
||||
|
||||
const selectClass = cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
|
||||
'placeholder:text-[#848b9b] focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)
|
||||
|
||||
const columns: Column<InviteCodeResponse>[] = [
|
||||
@@ -119,7 +119,7 @@ export function InviteCodesPage() {
|
||||
key: 'code',
|
||||
header: 'Code',
|
||||
render: (c) => (
|
||||
<code className="rounded bg-accent px-2 py-1 text-sm font-mono text-muted-foreground">{c.code}</code>
|
||||
<code className="rounded bg-accent px-2 py-1 text-sm font-mono text-[#848b9b]">{c.code}</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -130,12 +130,12 @@ export function InviteCodesPage() {
|
||||
{c.email_sent ? (
|
||||
<MailCheck className="h-3.5 w-3.5 text-emerald-400" />
|
||||
) : (
|
||||
<Mail className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Mail className="h-3.5 w-3.5 text-[#848b9b]" />
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground">{c.email}</span>
|
||||
<span className="text-sm text-[#848b9b]">{c.email}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">—</span>
|
||||
<span className="text-sm text-[#848b9b]">—</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -151,9 +151,9 @@ export function InviteCodesPage() {
|
||||
key: 'trial',
|
||||
header: 'Trial',
|
||||
render: (c) => c.has_trial ? (
|
||||
<span className="text-sm text-muted-foreground">{c.trial_duration_days}d</span>
|
||||
<span className="text-sm text-[#848b9b]">{c.trial_duration_days}d</span>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">—</span>
|
||||
<span className="text-sm text-[#848b9b]">—</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -170,7 +170,7 @@ export function InviteCodesPage() {
|
||||
key: 'expires_at',
|
||||
header: 'Expires',
|
||||
render: (c) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-[#848b9b]">
|
||||
{c.expires_at ? new Date(c.expires_at).toLocaleDateString() : 'Never'}
|
||||
</span>
|
||||
),
|
||||
@@ -179,7 +179,7 @@ export function InviteCodesPage() {
|
||||
key: 'created_at',
|
||||
header: 'Created',
|
||||
render: (c) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-[#848b9b]">
|
||||
{new Date(c.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
@@ -254,7 +254,7 @@ export function InviteCodesPage() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Recipient Email</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Recipient Email</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={email}
|
||||
@@ -264,7 +264,7 @@ export function InviteCodesPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Plan</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Plan</label>
|
||||
<select
|
||||
aria-label="Plan"
|
||||
value={assignedPlan}
|
||||
@@ -283,7 +283,7 @@ export function InviteCodesPage() {
|
||||
|
||||
{assignedPlan !== 'free' && (
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Trial Duration (days)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Trial Duration (days)</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={trialDays}
|
||||
@@ -292,12 +292,12 @@ export function InviteCodesPage() {
|
||||
min={1}
|
||||
max={90}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">Leave empty for no trial — account gets full plan immediately.</p>
|
||||
<p className="mt-1 text-xs text-[#848b9b]">Leave empty for no trial — account gets full plan immediately.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Expires in (days)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Expires in (days)</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={expiresInDays}
|
||||
@@ -307,7 +307,7 @@ export function InviteCodesPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Note</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={note}
|
||||
|
||||
@@ -76,16 +76,16 @@ export function PlanLimitsPage() {
|
||||
}
|
||||
|
||||
const planColumns: Column<PlanLimitConfig>[] = [
|
||||
{ key: 'plan', header: 'Plan', render: (p) => <span className="font-medium text-foreground capitalize">{p.plan}</span> },
|
||||
{ key: 'max_trees', header: 'Max Trees', render: (p) => <span className="text-sm text-muted-foreground">{p.max_trees ?? 'Unlimited'}</span> },
|
||||
{ key: 'max_sessions', header: 'Sessions/Month', render: (p) => <span className="text-sm text-muted-foreground">{p.max_sessions_per_month ?? 'Unlimited'}</span> },
|
||||
{ key: 'max_users', header: 'Max Users', render: (p) => <span className="text-sm text-muted-foreground">{p.max_users ?? 'Unlimited'}</span> },
|
||||
{ key: 'plan', header: 'Plan', render: (p) => <span className="font-medium text-[#e2e5eb] capitalize">{p.plan}</span> },
|
||||
{ key: 'max_trees', header: 'Max Trees', render: (p) => <span className="text-sm text-[#848b9b]">{p.max_trees ?? 'Unlimited'}</span> },
|
||||
{ key: 'max_sessions', header: 'Sessions/Month', render: (p) => <span className="text-sm text-[#848b9b]">{p.max_sessions_per_month ?? 'Unlimited'}</span> },
|
||||
{ key: 'max_users', header: 'Max Users', render: (p) => <span className="text-sm text-[#848b9b]">{p.max_users ?? 'Unlimited'}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (p) => (
|
||||
<button
|
||||
onClick={() => setEditPlan({ ...p })}
|
||||
className="rounded-md px-3 py-1 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="rounded-md px-3 py-1 text-sm text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
@@ -94,11 +94,11 @@ export function PlanLimitsPage() {
|
||||
]
|
||||
|
||||
const overrideColumns: Column<AccountOverrideResponse>[] = [
|
||||
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-foreground">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
|
||||
{ key: 'max_trees', header: 'Max Trees', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_trees ?? '-'}</span> },
|
||||
{ key: 'max_sessions', header: 'Sessions/Month', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_sessions_per_month ?? '-'}</span> },
|
||||
{ key: 'max_users', header: 'Max Users', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_users ?? '-'}</span> },
|
||||
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-muted-foreground">{o.note || '-'}</span> },
|
||||
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-[#e2e5eb]">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
|
||||
{ key: 'max_trees', header: 'Max Trees', render: (o) => <span className="text-sm text-[#848b9b]">{o.override_max_trees ?? '-'}</span> },
|
||||
{ key: 'max_sessions', header: 'Sessions/Month', render: (o) => <span className="text-sm text-[#848b9b]">{o.override_max_sessions_per_month ?? '-'}</span> },
|
||||
{ key: 'max_users', header: 'Max Users', render: (o) => <span className="text-sm text-[#848b9b]">{o.override_max_users ?? '-'}</span> },
|
||||
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-[#848b9b]">{o.note || '-'}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (o) => (
|
||||
@@ -114,7 +114,7 @@ export function PlanLimitsPage() {
|
||||
<PageHeader title="Plan Limits" description="Configure plan tier limits and account-specific overrides" />
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">Plan Defaults</h2>
|
||||
<h2 className="text-lg font-semibold text-[#e2e5eb]">Plan Defaults</h2>
|
||||
<div className="mt-3">
|
||||
<DataTable columns={planColumns} data={plans} keyExtractor={(p) => p.plan} isLoading={loading} />
|
||||
</div>
|
||||
@@ -122,7 +122,7 @@ export function PlanLimitsPage() {
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-foreground">Account Overrides</h2>
|
||||
<h2 className="text-lg font-semibold text-[#e2e5eb]">Account Overrides</h2>
|
||||
<Button onClick={() => setCreateOverride(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Override
|
||||
@@ -155,15 +155,15 @@ export function PlanLimitsPage() {
|
||||
{editPlan && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees (empty = unlimited)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Max Trees (empty = unlimited)</label>
|
||||
<Input type="number" value={editPlan.max_trees ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_trees: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month (empty = unlimited)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Max Sessions/Month (empty = unlimited)</label>
|
||||
<Input type="number" value={editPlan.max_sessions_per_month ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Users (empty = unlimited)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Max Users (empty = unlimited)</label>
|
||||
<Input type="number" value={editPlan.max_users ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_users: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,23 +185,23 @@ export function PlanLimitsPage() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Account Display Code</label>
|
||||
<Input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees Override</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Max Trees Override</label>
|
||||
<Input type="number" value={overrideForm.override_max_trees ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_trees: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month Override</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Max Sessions/Month Override</label>
|
||||
<Input type="number" value={overrideForm.override_max_sessions_per_month ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Users Override</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Max Users Override</label>
|
||||
<Input type="number" value={overrideForm.override_max_users ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_users: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Note</label>
|
||||
<Input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,11 +47,11 @@ export function SettingsPage() {
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="Platform Settings" description="Global platform configuration" />
|
||||
|
||||
<div className="max-w-xl space-y-6 bg-card border border-border rounded-xl p-6">
|
||||
<div className="max-w-xl space-y-6 bg-[#14161d] border border-[#1e2130] rounded-xl p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">Email Verification</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<h3 className="font-medium text-[#e2e5eb]">Email Verification</h3>
|
||||
<p className="text-sm text-[#848b9b]">
|
||||
When enabled, unverified users see a banner prompting them to verify their email.
|
||||
</p>
|
||||
</div>
|
||||
@@ -59,7 +59,7 @@ export function SettingsPage() {
|
||||
onClick={() => setSettings({ ...settings, email_verification_enabled: !emailVerificationEnabled })}
|
||||
className={cn(
|
||||
'h-6 w-10 rounded-full transition-colors',
|
||||
emailVerificationEnabled ? 'bg-gradient-brand' : 'bg-accent'
|
||||
emailVerificationEnabled ? 'bg-[#22d3ee]' : 'bg-accent'
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
@@ -71,8 +71,8 @@ export function SettingsPage() {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">Maintenance Mode</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<h3 className="font-medium text-[#e2e5eb]">Maintenance Mode</h3>
|
||||
<p className="text-sm text-[#848b9b]">
|
||||
When enabled, users will see a maintenance message instead of the app.
|
||||
</p>
|
||||
</div>
|
||||
@@ -93,7 +93,7 @@ export function SettingsPage() {
|
||||
|
||||
{maintenanceMode && (
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Maintenance Message</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Maintenance Message</label>
|
||||
<Textarea
|
||||
value={maintenanceMessage}
|
||||
onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })}
|
||||
@@ -103,13 +103,13 @@ export function SettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-border pt-4">
|
||||
<div className="border-t border-[#1e2130] pt-4">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className={cn(
|
||||
'rounded-md px-4 py-2 text-sm font-medium',
|
||||
'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90',
|
||||
'bg-[#22d3ee] text-white hover:brightness-110',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -70,11 +70,11 @@ export default function SurveyInvitesPage() {
|
||||
/>
|
||||
|
||||
{/* Create Invite Section */}
|
||||
<div className="glass-card-static p-6">
|
||||
<h3 className="font-heading text-sm font-semibold text-foreground mb-4">Create Invite</h3>
|
||||
<div className="card-flat p-6">
|
||||
<h3 className="font-heading text-sm font-semibold text-[#e2e5eb] mb-4">Create Invite</h3>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<div className="flex-1">
|
||||
<label className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-1.5 block">
|
||||
<label className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b] mb-1.5 block">
|
||||
Recipient Name
|
||||
</label>
|
||||
<input
|
||||
@@ -82,11 +82,11 @@ export default function SurveyInvitesPage() {
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="John Smith"
|
||||
className="w-full rounded-[10px] border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary/30 focus:outline-hidden"
|
||||
className="w-full rounded-lg border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb] placeholder:text-[#848b9b] focus:border-primary/30 focus:outline-hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-1.5 block">
|
||||
<label className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b] mb-1.5 block">
|
||||
Email (optional)
|
||||
</label>
|
||||
<input
|
||||
@@ -94,14 +94,14 @@ export default function SurveyInvitesPage() {
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="john@example.com"
|
||||
className="w-full rounded-[10px] border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary/30 focus:outline-hidden"
|
||||
className="w-full rounded-lg border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb] placeholder:text-[#848b9b] focus:border-primary/30 focus:outline-hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleCreate(false)}
|
||||
disabled={creating || !name.trim()}
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-4 py-2 text-sm font-semibold text-brand-dark shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-[#22d3ee] px-4 py-2 text-sm font-semibold text-brand-dark hover:brightness-110 active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
{creating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Link2 className="h-4 w-4" />}
|
||||
Generate Link
|
||||
@@ -109,7 +109,7 @@ export default function SurveyInvitesPage() {
|
||||
<button
|
||||
onClick={() => handleCreate(true)}
|
||||
disabled={creating || !name.trim() || !email.trim()}
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-white/[0.04] border border-brand-border px-4 py-2 text-sm font-medium text-foreground hover:border-white/[0.12] active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-white/[0.04] border border-brand-border px-4 py-2 text-sm font-medium text-[#e2e5eb] hover:border-white/[0.12] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
{creating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
Send Email
|
||||
@@ -122,17 +122,17 @@ export default function SurveyInvitesPage() {
|
||||
)}
|
||||
|
||||
{lastCreated && (
|
||||
<div className="mt-4 rounded-[10px] border border-primary/[0.15] bg-primary/[0.04] p-4">
|
||||
<div className="mt-4 rounded-lg border border-primary/[0.15] bg-primary/[0.04] p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-muted-foreground mb-1">
|
||||
<p className="text-xs text-[#848b9b] mb-1">
|
||||
{lastCreated.email_sent ? 'Email sent to ' + lastCreated.recipient_email + '! Link:' : 'Share this link with ' + lastCreated.recipient_name + ':'}
|
||||
</p>
|
||||
<p className="truncate text-sm font-mono text-foreground">{lastCreated.survey_url}</p>
|
||||
<p className="truncate text-sm font-mono text-[#e2e5eb]">{lastCreated.survey_url}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleCopy(lastCreated.survey_url)}
|
||||
className="shrink-0 rounded-lg p-2 text-muted-foreground hover:bg-brand-border hover:text-foreground transition-colors"
|
||||
className="shrink-0 rounded-lg p-2 text-[#848b9b] hover:bg-brand-border hover:text-[#e2e5eb] transition-colors"
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
|
||||
</button>
|
||||
@@ -142,33 +142,33 @@ export default function SurveyInvitesPage() {
|
||||
</div>
|
||||
|
||||
{/* Invites Table */}
|
||||
<div className="glass-card-static overflow-hidden">
|
||||
<div className="card-flat overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Name</th>
|
||||
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Email</th>
|
||||
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Status</th>
|
||||
<th className="px-4 py-3 text-center font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Sent</th>
|
||||
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Created</th>
|
||||
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Completed</th>
|
||||
<th className="px-4 py-3 text-center font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Link</th>
|
||||
<tr className="border-b border-[#1e2130]">
|
||||
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Name</th>
|
||||
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Email</th>
|
||||
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Status</th>
|
||||
<th className="px-4 py-3 text-center font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Sent</th>
|
||||
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Created</th>
|
||||
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Completed</th>
|
||||
<th className="px-4 py-3 text-center font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-muted-foreground">Loading...</td></tr>
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-[#848b9b]">Loading...</td></tr>
|
||||
) : invites.length === 0 ? (
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-muted-foreground">No invites yet</td></tr>
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-[#848b9b]">No invites yet</td></tr>
|
||||
) : (
|
||||
invites.map(invite => (
|
||||
<tr key={invite.id} className="border-b border-border/50 hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-4 py-3 text-sm text-foreground">{invite.recipient_name}</td>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground">{invite.recipient_email || '—'}</td>
|
||||
<tr key={invite.id} className="border-b border-[#1e2130]/50 hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-4 py-3 text-sm text-[#e2e5eb]">{invite.recipient_name}</td>
|
||||
<td className="px-4 py-3 text-sm text-[#848b9b]">{invite.recipient_email || '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={cn(
|
||||
'inline-flex items-center rounded-full px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wider',
|
||||
'inline-flex items-center rounded-full px-2 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wider',
|
||||
invite.status === 'completed'
|
||||
? 'bg-emerald-400/10 text-emerald-400'
|
||||
: 'bg-amber-400/10 text-amber-400'
|
||||
@@ -178,17 +178,17 @@ export default function SurveyInvitesPage() {
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{invite.email_sent ? (
|
||||
<Mail className="mx-auto h-4 w-4 text-muted-foreground" />
|
||||
<Mail className="mx-auto h-4 w-4 text-[#848b9b]" />
|
||||
) : (
|
||||
<span className="text-muted-foreground/50">—</span>
|
||||
<span className="text-[#848b9b]/50">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-label text-xs text-muted-foreground">{formatDate(invite.created_at)}</td>
|
||||
<td className="px-4 py-3 font-label text-xs text-muted-foreground">{invite.completed_at ? formatDate(invite.completed_at) : '—'}</td>
|
||||
<td className="px-4 py-3 font-sans text-xs text-xs text-[#848b9b]">{formatDate(invite.created_at)}</td>
|
||||
<td className="px-4 py-3 font-sans text-xs text-xs text-[#848b9b]">{invite.completed_at ? formatDate(invite.completed_at) : '—'}</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<button
|
||||
onClick={() => handleCopy(invite.survey_url)}
|
||||
className="rounded-lg p-1.5 text-muted-foreground hover:bg-brand-border hover:text-foreground transition-colors"
|
||||
className="rounded-lg p-1.5 text-[#848b9b] hover:bg-brand-border hover:text-[#e2e5eb] transition-colors"
|
||||
title="Copy survey link"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
|
||||
@@ -41,7 +41,7 @@ const QUESTIONS: { id: string; num: string; text: string; type: 'mc' | 'mc-multi
|
||||
|
||||
function AnswerDisplay({ value, type }: { value: string | string[] | undefined; type: string }) {
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||
return <p className="text-sm italic text-muted-foreground/60">No answer</p>
|
||||
return <p className="text-sm italic text-[#848b9b]/60">No answer</p>
|
||||
}
|
||||
|
||||
if (type === 'mc-multi' && Array.isArray(value)) {
|
||||
@@ -50,7 +50,7 @@ function AnswerDisplay({ value, type }: { value: string | string[] | undefined;
|
||||
{value.map((v, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-block rounded-full bg-primary/10 px-2.5 py-0.5 font-label text-[0.625rem] uppercase tracking-wider text-primary"
|
||||
className="inline-block rounded-full bg-[rgba(34,211,238,0.10)] px-2.5 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wider text-[#22d3ee]"
|
||||
>
|
||||
{v}
|
||||
</span>
|
||||
@@ -63,8 +63,8 @@ function AnswerDisplay({ value, type }: { value: string | string[] | undefined;
|
||||
return (
|
||||
<ol className="space-y-1">
|
||||
{value.map((v, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-sm text-foreground/90">
|
||||
<span className="font-label text-xs font-bold text-primary">{i + 1}.</span>
|
||||
<li key={i} className="flex items-start gap-2 text-sm text-[#e2e5eb]/90">
|
||||
<span className="font-sans text-xs text-xs font-bold text-[#22d3ee]">{i + 1}.</span>
|
||||
{v}
|
||||
</li>
|
||||
))}
|
||||
@@ -75,12 +75,12 @@ function AnswerDisplay({ value, type }: { value: string | string[] | undefined;
|
||||
if (type === 'text') {
|
||||
return (
|
||||
<div className="border-l-2 border-primary/30 pl-3">
|
||||
<p className="text-sm text-foreground/90 whitespace-pre-wrap">{String(value)}</p>
|
||||
<p className="text-sm text-[#e2e5eb]/90 whitespace-pre-wrap">{String(value)}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <p className="text-sm text-foreground/90">{String(value)}</p>
|
||||
return <p className="text-sm text-[#e2e5eb]/90">{String(value)}</p>
|
||||
}
|
||||
|
||||
function ExpandedDetail({ response }: { response: SurveyResponseDetail }) {
|
||||
@@ -98,16 +98,16 @@ function ExpandedDetail({ response }: { response: SurveyResponseDetail }) {
|
||||
{QUESTIONS.map((q) => (
|
||||
<div
|
||||
key={q.id}
|
||||
className="rounded-[10px] p-4"
|
||||
className="rounded-lg p-4"
|
||||
style={{
|
||||
background: 'rgba(24, 26, 31, 0.6)',
|
||||
background: '#14161d',
|
||||
border: '1px solid var(--glass-border)',
|
||||
}}
|
||||
>
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-widest text-primary mb-1">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#22d3ee] mb-1">
|
||||
Q{q.num}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mb-2">{q.text}</p>
|
||||
<p className="text-xs text-[#848b9b] mb-2">{q.text}</p>
|
||||
<AnswerDisplay value={response.responses[q.id]} type={q.type} />
|
||||
</div>
|
||||
))}
|
||||
@@ -150,7 +150,7 @@ function ResponseRow({
|
||||
<>
|
||||
<tr
|
||||
className={cn(
|
||||
'border-b border-border/50 transition-colors cursor-pointer',
|
||||
'border-b border-[#1e2130]/50 transition-colors cursor-pointer',
|
||||
!response.is_read && 'bg-primary/3',
|
||||
'hover:bg-white/[0.02]'
|
||||
)}
|
||||
@@ -158,16 +158,16 @@ function ResponseRow({
|
||||
{/* Checkbox */}
|
||||
<td className="px-2 py-3 w-8" onClick={e => { e.stopPropagation(); onSelect() }}>
|
||||
{isSelected ? (
|
||||
<CheckSquare className="h-4 w-4 text-primary cursor-pointer" />
|
||||
<CheckSquare className="h-4 w-4 text-[#22d3ee] cursor-pointer" />
|
||||
) : (
|
||||
<Square className="h-4 w-4 text-muted-foreground/40 cursor-pointer hover:text-muted-foreground" />
|
||||
<Square className="h-4 w-4 text-[#848b9b]/40 cursor-pointer hover:text-[#848b9b]" />
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Unread dot */}
|
||||
<td className="px-1 py-3 w-6" onClick={onToggle}>
|
||||
{!response.is_read && (
|
||||
<Circle className="h-2.5 w-2.5 fill-primary text-primary" />
|
||||
<Circle className="h-2.5 w-2.5 fill-primary text-[#22d3ee]" />
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -175,46 +175,46 @@ function ResponseRow({
|
||||
<td className="px-2 py-3 w-8" onClick={onToggle}>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 text-muted-foreground transition-transform',
|
||||
'h-4 w-4 text-[#848b9b] transition-transform',
|
||||
isExpanded && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-label text-xs text-muted-foreground" onClick={onToggle}>{index + 1}</td>
|
||||
<td className={cn('px-4 py-3 text-sm', !response.is_read ? 'text-foreground font-medium' : 'text-foreground')} onClick={onToggle}>
|
||||
{response.respondent_name || <span className="text-muted-foreground italic">Anonymous</span>}
|
||||
<td className="px-4 py-3 font-sans text-xs text-xs text-[#848b9b]" onClick={onToggle}>{index + 1}</td>
|
||||
<td className={cn('px-4 py-3 text-sm', !response.is_read ? 'text-[#e2e5eb] font-medium' : 'text-[#e2e5eb]')} onClick={onToggle}>
|
||||
{response.respondent_name || <span className="text-[#848b9b] italic">Anonymous</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3" onClick={onToggle}>
|
||||
{response.source === 'invite' ? (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wider bg-primary/10 text-primary">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wider bg-[rgba(34,211,238,0.10)] text-[#22d3ee]">
|
||||
<User className="h-3 w-3" />
|
||||
Invite
|
||||
{response.invite_name && (
|
||||
<span className="text-primary/70">({response.invite_name})</span>
|
||||
<span className="text-[#22d3ee]/70">({response.invite_name})</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wider bg-brand-border text-muted-foreground">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wider bg-brand-border text-[#848b9b]">
|
||||
<Link2 className="h-3 w-3" />
|
||||
Direct
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-label text-xs text-muted-foreground" onClick={onToggle}>
|
||||
<td className="px-4 py-3 font-sans text-xs text-xs text-[#848b9b]" onClick={onToggle}>
|
||||
{new Date(response.created_at).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground" onClick={onToggle}>
|
||||
<td className="px-4 py-3 text-sm text-[#848b9b]" onClick={onToggle}>
|
||||
{answeredCount} / {QUESTIONS.length}
|
||||
</td>
|
||||
{/* Actions */}
|
||||
<td className="px-3 py-3 w-10 relative">
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); setShowMenu(!showMenu) }}
|
||||
className="p-1.5 rounded-lg hover:bg-brand-border text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="p-1.5 rounded-lg hover:bg-brand-border text-[#848b9b] hover:text-[#e2e5eb] transition-colors"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -223,18 +223,18 @@ function ResponseRow({
|
||||
<div className="fixed inset-0 z-40" onClick={() => setShowMenu(false)} />
|
||||
<div
|
||||
className="absolute right-3 top-full z-50 mt-1 w-44 rounded-xl py-1 shadow-xl"
|
||||
style={{ background: 'rgba(24, 26, 31, 0.95)', border: '1px solid var(--glass-border)', backdropFilter: 'blur(16px)' }}
|
||||
style={{ background: '#14161d', border: '1px solid var(--glass-border)', }}
|
||||
>
|
||||
<button
|
||||
onClick={() => { onMarkRead(); setShowMenu(false) }}
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/[0.04] transition-colors"
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-[#848b9b] hover:text-[#e2e5eb] hover:bg-white/[0.04] transition-colors"
|
||||
>
|
||||
{response.is_read ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
{response.is_read ? 'Mark Unread' : 'Mark Read'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { onArchive(); setShowMenu(false) }}
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/[0.04] transition-colors"
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-[#848b9b] hover:text-[#e2e5eb] hover:bg-white/[0.04] transition-colors"
|
||||
>
|
||||
{response.archived_at ? <ArchiveRestore className="h-3.5 w-3.5" /> : <Archive className="h-3.5 w-3.5" />}
|
||||
{response.archived_at ? 'Unarchive' : 'Archive'}
|
||||
@@ -408,7 +408,7 @@ export default function SurveyResponsesPage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
||||
<Loader2 className="h-6 w-6 animate-spin text-[#22d3ee]" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -416,7 +416,7 @@ export default function SurveyResponsesPage() {
|
||||
if (error && !data) {
|
||||
return (
|
||||
<div className="px-6 py-8">
|
||||
<div className="glass-card-static p-6 text-center text-rose-400">{error}</div>
|
||||
<div className="card-flat p-6 text-center text-rose-400">{error}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -434,10 +434,10 @@ export default function SurveyResponsesPage() {
|
||||
<button
|
||||
onClick={() => setShowArchived(!showArchived)}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-[10px] px-3 py-2 text-xs font-medium transition-colors border',
|
||||
'inline-flex items-center gap-2 rounded-lg px-3 py-2 text-xs font-medium transition-colors border',
|
||||
showArchived
|
||||
? 'bg-primary/10 text-primary border-primary/20'
|
||||
: 'bg-white/[0.04] text-muted-foreground border-brand-border hover:border-white/[0.12]'
|
||||
? 'bg-[rgba(34,211,238,0.10)] text-[#22d3ee] border-primary/20'
|
||||
: 'bg-white/[0.04] text-[#848b9b] border-brand-border hover:border-white/[0.12]'
|
||||
)}
|
||||
>
|
||||
<Archive className="h-3.5 w-3.5" />
|
||||
@@ -446,7 +446,7 @@ export default function SurveyResponsesPage() {
|
||||
<button
|
||||
onClick={handleExport}
|
||||
disabled={exporting || responses.length === 0}
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-white/[0.04] border border-brand-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-white/[0.12] disabled:opacity-50"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-white/[0.04] border border-brand-border px-4 py-2 text-sm font-medium text-[#e2e5eb] transition-colors hover:border-white/[0.12] disabled:opacity-50"
|
||||
>
|
||||
{exporting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -460,36 +460,36 @@ export default function SurveyResponsesPage() {
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-[10px] border border-rose-500/20 bg-rose-500/10 px-4 py-2 text-sm text-rose-400">
|
||||
<div className="rounded-lg border border-rose-500/20 bg-rose-500/10 px-4 py-2 text-sm text-rose-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stat cards */}
|
||||
<div className="flex gap-4">
|
||||
<div className="glass-card-static px-5 py-4 flex-1">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-1">
|
||||
<div className="card-flat px-5 py-4 flex-1">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b] mb-1">
|
||||
Total Responses
|
||||
</p>
|
||||
<p className="text-2xl font-heading font-bold text-gradient-brand">
|
||||
<p className="text-2xl font-heading font-bold text-[#67e8f9]">
|
||||
{data?.total ?? 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-card-static px-5 py-4 flex-1">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-1">
|
||||
<div className="card-flat px-5 py-4 flex-1">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b] mb-1">
|
||||
This Week
|
||||
</p>
|
||||
<p className="text-2xl font-heading font-bold text-foreground">
|
||||
<p className="text-2xl font-heading font-bold text-[#e2e5eb]">
|
||||
{data?.this_week ?? 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-card-static px-5 py-4 flex-1">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-1">
|
||||
<div className="card-flat px-5 py-4 flex-1">
|
||||
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b] mb-1">
|
||||
Unread
|
||||
</p>
|
||||
<p className={cn(
|
||||
'text-2xl font-heading font-bold',
|
||||
(data?.unread ?? 0) > 0 ? 'text-primary' : 'text-foreground'
|
||||
(data?.unread ?? 0) > 0 ? 'text-[#22d3ee]' : 'text-[#e2e5eb]'
|
||||
)}>
|
||||
{data?.unread ?? 0}
|
||||
</p>
|
||||
@@ -502,27 +502,27 @@ export default function SurveyResponsesPage() {
|
||||
className="flex items-center gap-3 rounded-xl px-4 py-2.5"
|
||||
style={{ background: 'rgba(6, 182, 212, 0.08)', border: '1px solid rgba(6, 182, 212, 0.15)' }}
|
||||
>
|
||||
<span className="text-sm text-primary font-medium">
|
||||
<span className="text-sm text-[#22d3ee] font-medium">
|
||||
{selectedIds.size} selected
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={() => handleBulkAction('mark_read')}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-brand-border transition-colors"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-[#848b9b] hover:text-[#e2e5eb] hover:bg-brand-border transition-colors"
|
||||
>
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
Mark Read
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleBulkAction('mark_unread')}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-brand-border transition-colors"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-[#848b9b] hover:text-[#e2e5eb] hover:bg-brand-border transition-colors"
|
||||
>
|
||||
<EyeOff className="h-3.5 w-3.5" />
|
||||
Mark Unread
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleBulkAction('archive')}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-brand-border transition-colors"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-[#848b9b] hover:text-[#e2e5eb] hover:bg-brand-border transition-colors"
|
||||
>
|
||||
<Archive className="h-3.5 w-3.5" />
|
||||
Archive
|
||||
@@ -536,7 +536,7 @@ export default function SurveyResponsesPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedIds(new Set())}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-brand-border transition-colors"
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium text-[#848b9b] hover:text-[#e2e5eb] hover:bg-brand-border transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
@@ -544,14 +544,14 @@ export default function SurveyResponsesPage() {
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
<div className="glass-card-static overflow-hidden">
|
||||
<div className="card-flat overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border/50">
|
||||
<tr className="border-b border-[#1e2130]/50">
|
||||
<th className="px-2 py-3 w-8">
|
||||
<button onClick={toggleSelectAll} className="text-muted-foreground/40 hover:text-muted-foreground">
|
||||
<button onClick={toggleSelectAll} className="text-[#848b9b]/40 hover:text-[#848b9b]">
|
||||
{selectedIds.size > 0 && selectedIds.size === responses.length ? (
|
||||
<CheckSquare className="h-4 w-4 text-primary" />
|
||||
<CheckSquare className="h-4 w-4 text-[#22d3ee]" />
|
||||
) : (
|
||||
<Square className="h-4 w-4" />
|
||||
)}
|
||||
@@ -559,19 +559,19 @@ export default function SurveyResponsesPage() {
|
||||
</th>
|
||||
<th className="px-1 py-3 w-6" />
|
||||
<th className="px-2 py-3 w-8" />
|
||||
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">
|
||||
#
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">
|
||||
Respondent
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">
|
||||
Source
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">
|
||||
Date
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
|
||||
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">
|
||||
Answered
|
||||
</th>
|
||||
<th className="px-3 py-3 w-10" />
|
||||
@@ -580,7 +580,7 @@ export default function SurveyResponsesPage() {
|
||||
<tbody>
|
||||
{responses.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={9} className="px-4 py-12 text-center text-sm text-muted-foreground">
|
||||
<td colSpan={9} className="px-4 py-12 text-center text-sm text-[#848b9b]">
|
||||
{showArchived ? 'No archived responses.' : 'No survey responses yet.'}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -189,8 +189,8 @@ export function UserDetailPage() {
|
||||
}
|
||||
|
||||
const selectClass = cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
|
||||
'placeholder:text-[#848b9b] focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
@@ -224,15 +224,15 @@ export function UserDetailPage() {
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => navigate('/admin/users')}
|
||||
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="rounded-md border border-[#1e2130] p-2 text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-xl font-heading font-semibold text-foreground">
|
||||
<h1 className="text-xl font-heading font-semibold text-[#e2e5eb]">
|
||||
{user.full_name || user.email}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">{user.email}</p>
|
||||
<p className="text-sm text-[#848b9b]">{user.email}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{user.is_super_admin && (
|
||||
@@ -252,21 +252,21 @@ export function UserDetailPage() {
|
||||
|
||||
{/* Account & Subscription */}
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="bg-card border border-border rounded-xl p-6">
|
||||
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<div className="bg-[#14161d] border border-[#1e2130] rounded-xl p-6">
|
||||
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-[#848b9b]">
|
||||
Account & Subscription
|
||||
</h2>
|
||||
<dl className="space-y-3">
|
||||
{user.account && (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-muted-foreground">Account</dt>
|
||||
<dd className="text-sm text-foreground">{user.account.name}</dd>
|
||||
<dt className="text-sm text-[#848b9b]">Account</dt>
|
||||
<dd className="text-sm text-[#e2e5eb]">{user.account.name}</dd>
|
||||
</div>
|
||||
{user.account.display_code && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-muted-foreground">Display Code</dt>
|
||||
<dd className="text-sm font-mono text-muted-foreground">{user.account.display_code}</dd>
|
||||
<dt className="text-sm text-[#848b9b]">Display Code</dt>
|
||||
<dd className="text-sm font-mono text-[#848b9b]">{user.account.display_code}</dd>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -274,13 +274,13 @@ export function UserDetailPage() {
|
||||
{user.subscription ? (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-muted-foreground">Plan</dt>
|
||||
<dd className="text-sm font-semibold text-foreground">
|
||||
<dt className="text-sm text-[#848b9b]">Plan</dt>
|
||||
<dd className="text-sm font-semibold text-[#e2e5eb]">
|
||||
{user.subscription.plan.charAt(0).toUpperCase() + user.subscription.plan.slice(1)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-muted-foreground">Status</dt>
|
||||
<dt className="text-sm text-[#848b9b]">Status</dt>
|
||||
<dd>
|
||||
<StatusBadge variant={user.subscription.status === 'trialing' ? 'warning' : 'success'}>
|
||||
{user.subscription.status}
|
||||
@@ -289,24 +289,24 @@ export function UserDetailPage() {
|
||||
</div>
|
||||
{user.subscription.current_period_end && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-muted-foreground">Period End</dt>
|
||||
<dd className="text-sm text-muted-foreground">{fmt(user.subscription.current_period_end)}</dd>
|
||||
<dt className="text-sm text-[#848b9b]">Period End</dt>
|
||||
<dd className="text-sm text-[#848b9b]">{fmt(user.subscription.current_period_end)}</dd>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">No subscription</div>
|
||||
<div className="text-sm text-[#848b9b]">No subscription</div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-muted-foreground">Joined</dt>
|
||||
<dd className="text-sm text-muted-foreground">{fmt(user.created_at)}</dd>
|
||||
<dt className="text-sm text-[#848b9b]">Joined</dt>
|
||||
<dd className="text-sm text-[#848b9b]">{fmt(user.created_at)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{/* Admin Actions */}
|
||||
<div className="bg-card border border-border rounded-xl p-6">
|
||||
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<div className="bg-[#14161d] border border-[#1e2130] rounded-xl p-6">
|
||||
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-[#848b9b]">
|
||||
Admin Actions
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
@@ -317,16 +317,16 @@ export function UserDetailPage() {
|
||||
setSelectedPlan(user.subscription?.plan || 'free')
|
||||
setPlanModalOpen(true)
|
||||
}}
|
||||
className="flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3 text-left text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="flex w-full items-center gap-3 rounded-lg border border-[#1e2130] px-4 py-3 text-left text-sm text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
|
||||
>
|
||||
<Shield className="h-4 w-4 text-muted-foreground" />
|
||||
<Shield className="h-4 w-4 text-[#848b9b]" />
|
||||
Change Plan
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTrialModalOpen(true)}
|
||||
className="flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3 text-left text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="flex w-full items-center gap-3 rounded-lg border border-[#1e2130] px-4 py-3 text-left text-sm text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
|
||||
>
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<Clock className="h-4 w-4 text-[#848b9b]" />
|
||||
{user.subscription?.status === 'trialing' ? 'Extend Trial' : 'Start Trial'}
|
||||
</button>
|
||||
</>
|
||||
@@ -349,9 +349,9 @@ export function UserDetailPage() {
|
||||
setResetTempPassword(null)
|
||||
setResetModalOpen(true)
|
||||
}}
|
||||
className="flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3 text-left text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="flex w-full items-center gap-3 rounded-lg border border-[#1e2130] px-4 py-3 text-left text-sm text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
|
||||
>
|
||||
<KeyRound className="h-4 w-4 text-muted-foreground" />
|
||||
<KeyRound className="h-4 w-4 text-[#848b9b]" />
|
||||
Reset Password
|
||||
</button>
|
||||
<button
|
||||
@@ -402,42 +402,42 @@ export function UserDetailPage() {
|
||||
|
||||
{/* Invite Code Used */}
|
||||
{user.invite_code_used && (
|
||||
<div className="bg-card border border-border rounded-xl p-6">
|
||||
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<div className="bg-[#14161d] border border-[#1e2130] rounded-xl p-6">
|
||||
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-[#848b9b]">
|
||||
<Ticket className="mr-2 inline h-4 w-4" />
|
||||
Invite Code Used
|
||||
</h2>
|
||||
<dl className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<div>
|
||||
<dt className="text-xs text-muted-foreground">Code</dt>
|
||||
<dd className="mt-1 font-mono text-sm text-muted-foreground">{user.invite_code_used.code}</dd>
|
||||
<dt className="text-xs text-[#848b9b]">Code</dt>
|
||||
<dd className="mt-1 font-mono text-sm text-[#848b9b]">{user.invite_code_used.code}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-muted-foreground">Plan Assigned</dt>
|
||||
<dd className="mt-1 text-sm text-muted-foreground">
|
||||
<dt className="text-xs text-[#848b9b]">Plan Assigned</dt>
|
||||
<dd className="mt-1 text-sm text-[#848b9b]">
|
||||
{user.invite_code_used.assigned_plan.charAt(0).toUpperCase() + user.invite_code_used.assigned_plan.slice(1)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-muted-foreground">Trial Days</dt>
|
||||
<dd className="mt-1 text-sm text-muted-foreground">{user.invite_code_used.trial_duration_days ?? '—'}</dd>
|
||||
<dt className="text-xs text-[#848b9b]">Trial Days</dt>
|
||||
<dd className="mt-1 text-sm text-[#848b9b]">{user.invite_code_used.trial_duration_days ?? '—'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-muted-foreground">Created By</dt>
|
||||
<dd className="mt-1 text-sm text-muted-foreground">{user.invite_code_used.created_by_email ?? '—'}</dd>
|
||||
<dt className="text-xs text-[#848b9b]">Created By</dt>
|
||||
<dd className="mt-1 text-sm text-[#848b9b]">{user.invite_code_used.created_by_email ?? '—'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabs: Sessions / Audit Logs */}
|
||||
<div className="bg-card border border-border rounded-xl">
|
||||
<div className="flex border-b border-border">
|
||||
<div className="bg-[#14161d] border border-[#1e2130] rounded-xl">
|
||||
<div className="flex border-b border-[#1e2130]">
|
||||
<button
|
||||
onClick={() => setActiveTab('sessions')}
|
||||
className={cn(
|
||||
'px-6 py-3 text-sm font-medium',
|
||||
activeTab === 'sessions' ? 'border-b-2 border-foreground text-foreground' : 'text-muted-foreground hover:text-foreground'
|
||||
activeTab === 'sessions' ? 'border-b-2 border-foreground text-[#e2e5eb]' : 'text-[#848b9b] hover:text-[#e2e5eb]'
|
||||
)}
|
||||
>
|
||||
Sessions ({user.total_sessions})
|
||||
@@ -446,7 +446,7 @@ export function UserDetailPage() {
|
||||
onClick={() => setActiveTab('audit')}
|
||||
className={cn(
|
||||
'px-6 py-3 text-sm font-medium',
|
||||
activeTab === 'audit' ? 'border-b-2 border-foreground text-foreground' : 'text-muted-foreground hover:text-foreground'
|
||||
activeTab === 'audit' ? 'border-b-2 border-foreground text-[#e2e5eb]' : 'text-[#848b9b] hover:text-[#e2e5eb]'
|
||||
)}
|
||||
>
|
||||
Audit Logs ({user.total_audit_logs})
|
||||
@@ -458,7 +458,7 @@ export function UserDetailPage() {
|
||||
user.recent_sessions.length > 0 ? (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left text-xs text-muted-foreground">
|
||||
<tr className="border-b border-[#1e2130] text-left text-xs text-[#848b9b]">
|
||||
<th className="pb-2 font-medium">Tree</th>
|
||||
<th className="pb-2 font-medium">Started</th>
|
||||
<th className="pb-2 font-medium">Completed</th>
|
||||
@@ -467,17 +467,17 @@ export function UserDetailPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{user.recent_sessions.map(s => (
|
||||
<tr key={s.id} className="border-b border-border">
|
||||
<td className="py-3 text-sm text-muted-foreground">{s.tree_name ?? '—'}</td>
|
||||
<td className="py-3 text-sm text-muted-foreground">{fmtFull(s.started_at)}</td>
|
||||
<td className="py-3 text-sm text-muted-foreground">{fmtFull(s.completed_at)}</td>
|
||||
<tr key={s.id} className="border-b border-[#1e2130]">
|
||||
<td className="py-3 text-sm text-[#848b9b]">{s.tree_name ?? '—'}</td>
|
||||
<td className="py-3 text-sm text-[#848b9b]">{fmtFull(s.started_at)}</td>
|
||||
<td className="py-3 text-sm text-[#848b9b]">{fmtFull(s.completed_at)}</td>
|
||||
<td className="py-3">
|
||||
{s.outcome ? (
|
||||
<StatusBadge variant={s.outcome === 'resolved' ? 'success' : 'default'}>
|
||||
{s.outcome}
|
||||
</StatusBadge>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">—</span>
|
||||
<span className="text-sm text-[#848b9b]">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -485,7 +485,7 @@ export function UserDetailPage() {
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">No sessions yet</div>
|
||||
<div className="py-8 text-center text-sm text-[#848b9b]">No sessions yet</div>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -493,7 +493,7 @@ export function UserDetailPage() {
|
||||
user.recent_audit_logs.length > 0 ? (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left text-xs text-muted-foreground">
|
||||
<tr className="border-b border-[#1e2130] text-left text-xs text-[#848b9b]">
|
||||
<th className="pb-2 font-medium">Action</th>
|
||||
<th className="pb-2 font-medium">Resource</th>
|
||||
<th className="pb-2 font-medium">Time</th>
|
||||
@@ -501,16 +501,16 @@ export function UserDetailPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{user.recent_audit_logs.map(a => (
|
||||
<tr key={a.id} className="border-b border-border">
|
||||
<td className="py-3 text-sm text-muted-foreground">{a.action}</td>
|
||||
<td className="py-3 text-sm text-muted-foreground">{a.resource_type ?? '—'}</td>
|
||||
<td className="py-3 text-sm text-muted-foreground">{fmtFull(a.created_at)}</td>
|
||||
<tr key={a.id} className="border-b border-[#1e2130]">
|
||||
<td className="py-3 text-sm text-[#848b9b]">{a.action}</td>
|
||||
<td className="py-3 text-sm text-[#848b9b]">{a.resource_type ?? '—'}</td>
|
||||
<td className="py-3 text-sm text-[#848b9b]">{fmtFull(a.created_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">No audit logs yet</div>
|
||||
<div className="py-8 text-center text-sm text-[#848b9b]">No audit logs yet</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -530,7 +530,7 @@ export function UserDetailPage() {
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Plan</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Plan</label>
|
||||
<select
|
||||
aria-label="Subscription plan"
|
||||
value={selectedPlan}
|
||||
@@ -560,11 +560,11 @@ export function UserDetailPage() {
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose how to reset the password for <span className="font-medium text-foreground">{user.full_name || user.email}</span>.
|
||||
<p className="text-sm text-[#848b9b]">
|
||||
Choose how to reset the password for <span className="font-medium text-[#e2e5eb]">{user.full_name || user.email}</span>.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-start gap-3 rounded-lg border border-border p-3 cursor-pointer hover:bg-accent">
|
||||
<label className="flex items-start gap-3 rounded-lg border border-[#1e2130] p-3 cursor-pointer hover:bg-accent">
|
||||
<input
|
||||
type="radio"
|
||||
name="reset-mode"
|
||||
@@ -574,11 +574,11 @@ export function UserDetailPage() {
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground">Send Reset Email</div>
|
||||
<div className="text-xs text-muted-foreground">User receives an email with a reset link (30 min expiry)</div>
|
||||
<div className="text-sm font-medium text-[#e2e5eb]">Send Reset Email</div>
|
||||
<div className="text-xs text-[#848b9b]">User receives an email with a reset link (30 min expiry)</div>
|
||||
</div>
|
||||
</label>
|
||||
<label className="flex items-start gap-3 rounded-lg border border-border p-3 cursor-pointer hover:bg-accent">
|
||||
<label className="flex items-start gap-3 rounded-lg border border-[#1e2130] p-3 cursor-pointer hover:bg-accent">
|
||||
<input
|
||||
type="radio"
|
||||
name="reset-mode"
|
||||
@@ -588,8 +588,8 @@ export function UserDetailPage() {
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground">Generate Temp Password</div>
|
||||
<div className="text-xs text-muted-foreground">A temporary password is generated. You share it manually.</div>
|
||||
<div className="text-sm font-medium text-[#e2e5eb]">Generate Temp Password</div>
|
||||
<div className="text-xs text-[#848b9b]">A temporary password is generated. You share it manually.</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -613,18 +613,18 @@ export function UserDetailPage() {
|
||||
This password will not be shown again. Copy it now.
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground font-mono">
|
||||
<code className="flex-1 rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb] font-mono">
|
||||
{resetTempPassword}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopyResetPassword}
|
||||
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
||||
className="rounded-md border border-[#1e2130] p-2 text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb] transition-colors"
|
||||
title="Copy password"
|
||||
>
|
||||
{resetCopied ? <Check className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-[#848b9b]">
|
||||
The user will be required to change this password on next login.
|
||||
</p>
|
||||
</div>
|
||||
@@ -646,7 +646,7 @@ export function UserDetailPage() {
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Days to add</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Days to add</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={trialDays}
|
||||
@@ -654,7 +654,7 @@ export function UserDetailPage() {
|
||||
min={1}
|
||||
max={90}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">1-90 days. Will convert to trialing status if not already.</p>
|
||||
<p className="mt-1 text-xs text-[#848b9b]">1-90 days. Will convert to trialing status if not already.</p>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -712,11 +712,11 @@ export function UserDetailPage() {
|
||||
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
|
||||
This user cannot be deleted because they have dependencies:
|
||||
</div>
|
||||
<ul className="space-y-1 text-sm text-muted-foreground">
|
||||
<ul className="space-y-1 text-sm text-[#848b9b]">
|
||||
{Object.entries(hardDeleteBlockers).map(([key, count]) => (
|
||||
<li key={key} className="flex justify-between">
|
||||
<span>{key.replace(/_/g, ' ')}</span>
|
||||
<span className="font-mono text-muted-foreground">{count}</span>
|
||||
<span className="font-mono text-[#848b9b]">{count}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -190,8 +190,8 @@ export function UsersPage() {
|
||||
sortable: true,
|
||||
render: (u) => (
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{u.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{u.email}</div>
|
||||
<div className="font-medium text-[#e2e5eb]">{u.name}</div>
|
||||
<div className="text-xs text-[#848b9b]">{u.email}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -226,7 +226,7 @@ export function UsersPage() {
|
||||
header: 'Joined',
|
||||
sortable: true,
|
||||
render: (u) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-[#848b9b]">
|
||||
{new Date(u.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
@@ -286,12 +286,12 @@ export function UsersPage() {
|
||||
placeholder="Search by name or email..."
|
||||
className="max-w-sm"
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<label className="flex items-center gap-2 text-sm text-[#848b9b]">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showArchived}
|
||||
onChange={(e) => { setShowArchived(e.target.checked); setPage(1) }}
|
||||
className="rounded border-border bg-card"
|
||||
className="rounded border-[#1e2130] bg-[#14161d]"
|
||||
/>
|
||||
Show archived
|
||||
</label>
|
||||
@@ -326,14 +326,14 @@ export function UsersPage() {
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Changing role for <span className="font-medium text-foreground">{roleModalUser?.name}</span>
|
||||
<p className="text-sm text-[#848b9b]">
|
||||
Changing role for <span className="font-medium text-[#e2e5eb]">{roleModalUser?.name}</span>
|
||||
</p>
|
||||
<select
|
||||
value={newRole}
|
||||
onChange={(e) => setNewRole(e.target.value)}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
|
||||
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
@@ -357,11 +357,11 @@ export function UsersPage() {
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Moving <span className="font-medium text-foreground">{moveModalUser?.name}</span> to a new account.
|
||||
<p className="text-sm text-[#848b9b]">
|
||||
Moving <span className="font-medium text-[#e2e5eb]">{moveModalUser?.name}</span> to a new account.
|
||||
</p>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Account Display Code</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={displayCode}
|
||||
@@ -389,7 +389,7 @@ export function UsersPage() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Name</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={createForm.name}
|
||||
@@ -398,7 +398,7 @@ export function UsersPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Email</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={createForm.email}
|
||||
@@ -407,12 +407,12 @@ export function UsersPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Mode</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Account Mode</label>
|
||||
<select
|
||||
value={createForm.account_mode}
|
||||
onChange={(e) => setCreateForm(f => ({ ...f, account_mode: e.target.value as 'existing' | 'personal' }))}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
|
||||
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
@@ -423,7 +423,7 @@ export function UsersPage() {
|
||||
{createForm.account_mode === 'existing' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Account Display Code</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={createForm.account_display_code}
|
||||
@@ -432,12 +432,12 @@ export function UsersPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Role</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Account Role</label>
|
||||
<select
|
||||
value={createForm.account_role}
|
||||
onChange={(e) => setCreateForm(f => ({ ...f, account_role: e.target.value as 'engineer' | 'viewer' }))}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
|
||||
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
@@ -453,9 +453,9 @@ export function UsersPage() {
|
||||
id="send-email"
|
||||
checked={createForm.send_email}
|
||||
onChange={(e) => setCreateForm(f => ({ ...f, send_email: e.target.checked }))}
|
||||
className="rounded border-border bg-card"
|
||||
className="rounded border-[#1e2130] bg-[#14161d]"
|
||||
/>
|
||||
<label htmlFor="send-email" className="text-sm text-muted-foreground">
|
||||
<label htmlFor="send-email" className="text-sm text-[#848b9b]">
|
||||
Send welcome email with temporary password
|
||||
</label>
|
||||
</div>
|
||||
@@ -479,21 +479,21 @@ export function UsersPage() {
|
||||
This password will not be shown again. Copy it now.
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Temporary Password</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Temporary Password</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground font-mono">
|
||||
<code className="flex-1 rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb] font-mono">
|
||||
{tempPassword}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopyPassword}
|
||||
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
||||
className="rounded-md border border-[#1e2130] p-2 text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb] transition-colors"
|
||||
title="Copy password"
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-[#848b9b]">
|
||||
The user will be required to change this password on first login.
|
||||
</p>
|
||||
</div>
|
||||
@@ -516,7 +516,7 @@ export function UsersPage() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Email</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={inviteForm.email}
|
||||
@@ -525,7 +525,7 @@ export function UsersPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Account Display Code</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={inviteForm.account_display_code}
|
||||
@@ -534,12 +534,12 @@ export function UsersPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Role</label>
|
||||
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Role</label>
|
||||
<select
|
||||
value={inviteForm.role}
|
||||
onChange={(e) => setInviteForm(f => ({ ...f, role: e.target.value as 'engineer' | 'viewer' }))}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
|
||||
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user