feat: implement monochrome design system across entire frontend
Migrate all 84 frontend files from the old themed/colored design to a monochrome glass-morphism design system. Pure black backgrounds, white text with opacity levels, glass-card components with backdrop-blur, and functional color reserved for status indicators only. Foundation: remap CSS variables to monochrome, simplify Tailwind config, remove theme toggle, convert brand logo/wordmark to white. Pages: all 14 pages updated. Components: all common, library, session, step-library, tree-editor, tree-preview, admin, and subscription components converted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -110,7 +110,7 @@ export function AccountSettingsPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -118,7 +118,7 @@ export function AccountSettingsPage() {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
<div className="rounded-md border border-red-400/20 bg-red-400/10 p-4 text-red-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
{error}
|
||||
@@ -134,23 +134,23 @@ export function AccountSettingsPage() {
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<Building2 className="h-8 w-8 text-primary" />
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Account Settings</h1>
|
||||
<Building2 className="h-8 w-8 text-white/50" />
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">Account Settings</h1>
|
||||
</div>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<p className="mt-2 text-white/40">
|
||||
Manage your account, subscription, and team
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* Account Info Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Account Information</h2>
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Account Information</h2>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* Account Name */}
|
||||
<div>
|
||||
<label className="block font-label text-sm font-medium text-card-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Account Name
|
||||
</label>
|
||||
{isEditingName ? (
|
||||
@@ -160,9 +160,9 @@ export function AccountSettingsPage() {
|
||||
value={editedName}
|
||||
onChange={(e) => setEditedName(e.target.value)}
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
@@ -177,8 +177,8 @@ export function AccountSettingsPage() {
|
||||
onClick={handleSaveName}
|
||||
disabled={isSavingName}
|
||||
className={cn(
|
||||
'rounded-md bg-primary p-2 text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50'
|
||||
'rounded-md bg-white p-2 text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
{isSavingName ? (
|
||||
@@ -192,18 +192,18 @@ export function AccountSettingsPage() {
|
||||
setEditedName(account?.name ?? '')
|
||||
setIsEditingName(false)
|
||||
}}
|
||||
className="rounded-md border border-input p-2 text-muted-foreground hover:bg-accent"
|
||||
className="rounded-md border border-white/10 p-2 text-white/40 hover:bg-white/10"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<span className="text-sm text-foreground">{account?.name}</span>
|
||||
<span className="text-sm text-white">{account?.name}</span>
|
||||
{isAccountOwner && (
|
||||
<button
|
||||
onClick={() => setIsEditingName(true)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
className="text-xs text-white hover:underline"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
@@ -214,10 +214,10 @@ export function AccountSettingsPage() {
|
||||
|
||||
{/* Display Code */}
|
||||
<div>
|
||||
<label className="block font-label text-sm font-medium text-card-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Display Code
|
||||
</label>
|
||||
<p className="mt-1 text-sm font-mono text-muted-foreground">
|
||||
<p className="mt-1 text-sm font-mono text-white/40">
|
||||
{account?.display_code}
|
||||
</p>
|
||||
</div>
|
||||
@@ -225,8 +225,8 @@ export function AccountSettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Subscription Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Subscription</h2>
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Subscription</h2>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* Plan & Status */}
|
||||
@@ -234,9 +234,9 @@ export function AccountSettingsPage() {
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium',
|
||||
plan === 'free' && 'bg-secondary text-secondary-foreground',
|
||||
plan === 'pro' && 'bg-primary/10 text-primary',
|
||||
plan === 'team' && 'bg-primary/20 text-primary'
|
||||
plan === 'free' && 'bg-white/10 text-white/70',
|
||||
plan === 'pro' && 'bg-white/10 text-white',
|
||||
plan === 'team' && 'bg-white/10 text-white'
|
||||
)}
|
||||
>
|
||||
<Crown className="h-3.5 w-3.5" />
|
||||
@@ -246,11 +246,11 @@ export function AccountSettingsPage() {
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium',
|
||||
sub.status === 'active' && 'bg-green-500/10 text-green-600',
|
||||
sub.status === 'trialing' && 'bg-blue-500/10 text-blue-600',
|
||||
sub.status === 'past_due' && 'bg-yellow-500/10 text-yellow-600',
|
||||
sub.status === 'canceled' && 'bg-destructive/10 text-destructive',
|
||||
sub.status === 'orphaned' && 'bg-muted text-muted-foreground'
|
||||
sub.status === 'active' && 'bg-green-500/10 text-emerald-400',
|
||||
sub.status === 'trialing' && 'bg-blue-500/10 text-blue-400',
|
||||
sub.status === 'past_due' && 'bg-yellow-500/10 text-yellow-400',
|
||||
sub.status === 'canceled' && 'bg-red-400/10 text-red-400',
|
||||
sub.status === 'orphaned' && 'bg-white/10 text-white/40'
|
||||
)}
|
||||
>
|
||||
{sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
|
||||
@@ -259,7 +259,7 @@ export function AccountSettingsPage() {
|
||||
</div>
|
||||
|
||||
{sub?.current_period_end && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-white/40">
|
||||
Current period ends: {new Date(sub.current_period_end).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
@@ -302,45 +302,45 @@ export function AccountSettingsPage() {
|
||||
|
||||
{/* Team Members Section (owners only) */}
|
||||
{isAccountOwner && (
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Team Members</h2>
|
||||
<Users className="h-5 w-5 text-white/50" />
|
||||
<h2 className="text-lg font-semibold text-white">Team Members</h2>
|
||||
</div>
|
||||
|
||||
{members.length === 0 ? (
|
||||
<p className="mt-4 text-sm text-muted-foreground">No team members yet.</p>
|
||||
<p className="mt-4 text-sm text-white/40">No team members yet.</p>
|
||||
) : (
|
||||
<div className="mt-4 divide-y divide-border">
|
||||
<div className="mt-4 divide-y divide-white/[0.06]">
|
||||
{members.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{member.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{member.email}</p>
|
||||
<p className="text-sm font-medium text-white">{member.name}</p>
|
||||
<p className="text-xs text-white/40">{member.email}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-full px-2.5 py-0.5 text-xs font-medium',
|
||||
member.account_role === 'owner' && 'bg-primary/10 text-primary',
|
||||
member.account_role === 'engineer' && 'bg-secondary text-secondary-foreground',
|
||||
member.account_role === 'viewer' && 'bg-muted text-muted-foreground'
|
||||
member.account_role === 'owner' && 'bg-white/10 text-white',
|
||||
member.account_role === 'engineer' && 'bg-white/10 text-white/70',
|
||||
member.account_role === 'viewer' && 'bg-white/10 text-white/40'
|
||||
)}
|
||||
>
|
||||
{member.account_role}
|
||||
</span>
|
||||
{!member.is_active && (
|
||||
<span className="rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
|
||||
<span className="rounded-full bg-red-400/10 px-2 py-0.5 text-xs text-red-400">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
{member.account_role !== 'owner' && (
|
||||
<button
|
||||
onClick={() => handleRemoveMember(member.id)}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
className="text-white/40 hover:text-red-400"
|
||||
title="Remove member"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -356,10 +356,10 @@ export function AccountSettingsPage() {
|
||||
|
||||
{/* Invite Member Section (owners only) */}
|
||||
{isAccountOwner && (
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Invite Member</h2>
|
||||
<Mail className="h-5 w-5 text-white/50" />
|
||||
<h2 className="text-lg font-semibold text-white">Invite Member</h2>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleInvite} className="mt-4 space-y-3">
|
||||
@@ -371,17 +371,17 @@ export function AccountSettingsPage() {
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
required
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
<select
|
||||
value={inviteRole}
|
||||
onChange={(e) => setInviteRole(e.target.value)}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="engineer">Engineer</option>
|
||||
@@ -391,8 +391,8 @@ export function AccountSettingsPage() {
|
||||
type="submit"
|
||||
disabled={isInviting || !inviteEmail.trim()}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{isInviting ? (
|
||||
@@ -407,18 +407,18 @@ export function AccountSettingsPage() {
|
||||
</div>
|
||||
|
||||
{inviteError && (
|
||||
<p className="text-sm text-destructive">{inviteError}</p>
|
||||
<p className="text-sm text-red-400">{inviteError}</p>
|
||||
)}
|
||||
{inviteSuccess && (
|
||||
<p className="text-sm text-green-600">{inviteSuccess}</p>
|
||||
<p className="text-sm text-emerald-400">{inviteSuccess}</p>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{/* Pending Invites */}
|
||||
{invites.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-sm font-medium text-card-foreground">Pending Invites</h3>
|
||||
<div className="mt-2 divide-y divide-border">
|
||||
<h3 className="text-sm font-medium text-white">Pending Invites</h3>
|
||||
<div className="mt-2 divide-y divide-white/[0.06]">
|
||||
{invites
|
||||
.filter((inv) => !inv.used_at)
|
||||
.map((invite) => (
|
||||
@@ -427,12 +427,12 @@ export function AccountSettingsPage() {
|
||||
className="flex items-center justify-between py-2"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm text-foreground">{invite.email}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-sm text-white">{invite.email}</p>
|
||||
<p className="text-xs text-white/40">
|
||||
Expires {new Date(invite.expires_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-secondary px-2.5 py-0.5 text-xs text-secondary-foreground">
|
||||
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs text-white/70">
|
||||
{invite.role}
|
||||
</span>
|
||||
</div>
|
||||
@@ -463,25 +463,25 @@ function UsageStat({
|
||||
const isAtLimit = !isUnlimited && current >= max
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-background p-3">
|
||||
<p className="text-xs font-medium text-muted-foreground">{label}</p>
|
||||
<div className="glass-stat rounded-md p-3">
|
||||
<p className="text-xs font-medium text-white/40">{label}</p>
|
||||
<p
|
||||
className={cn(
|
||||
'mt-1 text-lg font-semibold',
|
||||
isAtLimit ? 'text-destructive' : isNearLimit ? 'text-yellow-600' : 'text-foreground'
|
||||
isAtLimit ? 'text-red-400' : isNearLimit ? 'text-yellow-400' : 'text-white'
|
||||
)}
|
||||
>
|
||||
{current}
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
<span className="text-sm font-normal text-white/40">
|
||||
{' '}/ {isUnlimited ? 'Unlimited' : max}
|
||||
</span>
|
||||
</p>
|
||||
{!isUnlimited && (
|
||||
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
|
||||
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-white/10">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all',
|
||||
isAtLimit ? 'bg-destructive' : isNearLimit ? 'bg-yellow-500' : 'bg-primary'
|
||||
isAtLimit ? 'bg-red-400' : isNearLimit ? 'bg-yellow-500' : 'bg-white'
|
||||
)}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
|
||||
@@ -144,7 +144,7 @@ export function AdminCategoriesPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -154,18 +154,18 @@ export function AdminCategoriesPage() {
|
||||
{/* Header */}
|
||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">
|
||||
Step Categories
|
||||
</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<p className="mt-2 text-white/40">
|
||||
Manage categories for organizing step library
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -180,16 +180,16 @@ export function AdminCategoriesPage() {
|
||||
type="checkbox"
|
||||
checked={includeArchived}
|
||||
onChange={(e) => setIncludeArchived(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-input text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
className="h-4 w-4 rounded border-white/10 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">Show archived categories</span>
|
||||
<span className="text-sm text-white/40">Show archived categories</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Categories List */}
|
||||
{categories.length === 0 ? (
|
||||
<div className="rounded-lg border border-border bg-card p-12 text-center">
|
||||
<p className="text-muted-foreground">
|
||||
<div className="glass-card rounded-2xl p-12 text-center">
|
||||
<p className="text-white/40">
|
||||
No categories found. Create your first category to get started.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState } from 'react'
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function LoginPage() {
|
||||
@@ -35,33 +34,38 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="flex min-h-screen items-center justify-center bg-black px-4">
|
||||
{/* Subtle radial overlay */}
|
||||
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
|
||||
|
||||
<div className="relative w-full max-w-md space-y-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex justify-center sm:mb-6">
|
||||
<BrandLogo size="lg" className="h-12 w-12 sm:h-16 sm:w-16" />
|
||||
<div className="w-16 h-16 rounded-2xl bg-white flex items-center justify-center sm:w-20 sm:h-20">
|
||||
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
|
||||
</div>
|
||||
</div>
|
||||
<h1>
|
||||
<BrandWordmark size="lg" />
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">
|
||||
ResolutionFlow
|
||||
</h1>
|
||||
<p className="mt-2 text-base font-medium text-gradient-brand sm:mt-3 sm:text-lg">
|
||||
<p className="mt-2 text-base font-medium text-white/60 sm:mt-3 sm:text-lg">
|
||||
Decision Tree Platform
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground sm:mt-2">
|
||||
<p className="mt-1 text-sm text-white/40 sm:mt-2">
|
||||
Sign in to your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
|
||||
<div className="space-y-4 rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div className="glass-card rounded-2xl p-6 space-y-4">
|
||||
{(error || localError) && (
|
||||
<div className="rounded-md border border-destructive/20 bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
|
||||
{localError || error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="mb-1 block text-sm font-medium text-foreground">
|
||||
<label htmlFor="email" className="mb-1 block text-sm font-medium text-white">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
@@ -73,9 +77,9 @@ export function LoginPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={cn(
|
||||
'block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20',
|
||||
'block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'transition-colors'
|
||||
)}
|
||||
placeholder="you@example.com"
|
||||
@@ -83,7 +87,7 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="mb-1 block text-sm font-medium text-foreground">
|
||||
<label htmlFor="password" className="mb-1 block text-sm font-medium text-white">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
@@ -95,9 +99,9 @@ export function LoginPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={cn(
|
||||
'block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20',
|
||||
'block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
|
||||
'transition-colors'
|
||||
)}
|
||||
placeholder="••••••••••"
|
||||
@@ -108,20 +112,20 @@ export function LoginPage() {
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'w-full rounded-md px-4 py-2.5 text-sm font-semibold text-white btn-press',
|
||||
'bg-gradient-brand hover:bg-gradient-brand-hover',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
||||
'w-full rounded-xl px-4 py-2.5 text-sm font-semibold btn-press',
|
||||
'bg-white text-black hover:bg-white/90',
|
||||
'focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-black',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'shadow-lg shadow-primary/20'
|
||||
'transition-all'
|
||||
)}
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
<p className="text-center text-sm text-white/40">
|
||||
Don't have an account?{' '}
|
||||
<Link to="/register" className="font-medium text-gradient-brand hover:underline">
|
||||
<Link to="/register" className="font-medium text-white hover:underline">
|
||||
Register
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -108,10 +108,8 @@ export function MyTreesPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h1 className="font-heading text-3xl font-bold sm:text-4xl">
|
||||
<span className="text-gradient-brand">My Trees</span>
|
||||
</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">My Trees</h1>
|
||||
<p className="mt-2 text-white/40">
|
||||
Your forked and custom decision trees
|
||||
</p>
|
||||
</div>
|
||||
@@ -119,20 +117,20 @@ export function MyTreesPage() {
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
) : trees.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border bg-card/50 px-4 py-12 text-center">
|
||||
<FolderTree className="mx-auto mb-4 h-12 w-12 text-muted-foreground opacity-50" />
|
||||
<h2 className="mb-2 text-lg font-semibold text-foreground">No personal trees yet</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
<div className="rounded-lg border border-dashed border-white/10 bg-white/[0.02] px-4 py-12 text-center">
|
||||
<FolderTree className="mx-auto mb-4 h-12 w-12 text-white/20" />
|
||||
<h2 className="mb-2 text-lg font-semibold text-white">No personal trees yet</h2>
|
||||
<p className="mb-4 text-sm text-white/40">
|
||||
Fork a tree from the library to customize it for your workflow
|
||||
</p>
|
||||
<Link
|
||||
to="/trees"
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'inline-flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Browse Trees
|
||||
@@ -143,32 +141,32 @@ export function MyTreesPage() {
|
||||
{trees.map((tree) => (
|
||||
<div
|
||||
key={tree.id}
|
||||
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md sm:p-6"
|
||||
className="glass-card rounded-2xl p-4 transition-all hover:glass-card-hover sm:p-6"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="mb-3 flex items-start justify-between gap-2">
|
||||
<h3 className="font-semibold text-card-foreground">{tree.name}</h3>
|
||||
<h3 className="font-semibold text-white">{tree.name}</h3>
|
||||
{tree.category_info && (
|
||||
<span className="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">
|
||||
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs text-white/70">
|
||||
{tree.category_info.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="mb-3 text-sm text-muted-foreground line-clamp-2">
|
||||
<p className="mb-3 text-sm text-white/40 line-clamp-2">
|
||||
{tree.description || 'No description available'}
|
||||
</p>
|
||||
|
||||
{/* Fork Badge */}
|
||||
{tree.parent_tree_id && (
|
||||
<div className="mb-3 flex items-center gap-2 rounded-md bg-accent/50 px-2 py-1.5 text-sm">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
<div className="mb-3 flex items-center gap-2 rounded-md bg-white/5 px-2 py-1.5 text-sm">
|
||||
<GitBranch className="h-4 w-4 text-white/40" />
|
||||
<span className="text-white/40">
|
||||
Forked from{' '}
|
||||
<Link
|
||||
to={`/trees/${tree.parent_tree_id}/navigate`}
|
||||
className="font-medium text-primary hover:underline"
|
||||
className="font-medium text-white hover:underline"
|
||||
>
|
||||
original
|
||||
</Link>
|
||||
@@ -184,7 +182,7 @@ export function MyTreesPage() {
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mb-4 flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<div className="mb-4 flex items-center gap-4 text-xs text-white/30">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>{formatDate(tree.lastUsed)}</span>
|
||||
@@ -201,8 +199,8 @@ export function MyTreesPage() {
|
||||
type="button"
|
||||
onClick={() => handleStartSession(tree.id)}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
@@ -212,8 +210,8 @@ export function MyTreesPage() {
|
||||
<Link
|
||||
to={`/trees/${tree.id}/edit`}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-2 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-2 text-white/40',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Edit tree"
|
||||
>
|
||||
@@ -227,8 +225,8 @@ export function MyTreesPage() {
|
||||
setShowShareModal(true)
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-2 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 p-2 text-white/40',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
title="Share tree"
|
||||
>
|
||||
@@ -241,8 +239,8 @@ export function MyTreesPage() {
|
||||
setShowDeleteConfirm(true)
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-md border border-input p-2 text-muted-foreground',
|
||||
'hover:bg-destructive/10 hover:text-destructive'
|
||||
'rounded-md border border-white/10 p-2 text-white/40',
|
||||
'hover:bg-red-400/10 hover:text-red-400'
|
||||
)}
|
||||
title="Delete tree"
|
||||
>
|
||||
|
||||
315
frontend/src/pages/QuickStartPage-Enhanced.tsx
Normal file
315
frontend/src/pages/QuickStartPage-Enhanced.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { Search, Clock, ArrowRight, Play, Loader2, TrendingUp, Sparkles, Zap } from 'lucide-react'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
import type { TreeListItem } from '@/types'
|
||||
import type { Session } from '@/types/session'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const now = Date.now()
|
||||
const then = new Date(dateStr).getTime()
|
||||
const diffMs = now - then
|
||||
const minutes = Math.floor(diffMs / 60000)
|
||||
if (minutes < 1) return 'just now'
|
||||
if (minutes < 60) return `${minutes}m ago`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `${hours}h ago`
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}d ago`
|
||||
}
|
||||
|
||||
export function QuickStartPage() {
|
||||
const navigate = useNavigate()
|
||||
const [query, setQuery] = useState('')
|
||||
const [searchResults, setSearchResults] = useState<TreeListItem[]>([])
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [showResults, setShowResults] = useState(false)
|
||||
const [activeSessions, setActiveSessions] = useState<Session[]>([])
|
||||
const [recentTrees, setRecentTrees] = useState<{ tree_id: string; name: string; lastUsed: string }[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const searchRef = useRef<HTMLDivElement>(null)
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
// Load sessions on mount
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
try {
|
||||
const [active, recent] = await Promise.all([
|
||||
sessionsApi.list({ completed: false, size: 5 }),
|
||||
sessionsApi.list({ size: 10 }),
|
||||
])
|
||||
setActiveSessions(active.slice(0, 3))
|
||||
|
||||
// Deduplicate recent sessions by tree_id, max 5
|
||||
const seen = new Set<string>()
|
||||
const deduped: { tree_id: string; name: string; lastUsed: string }[] = []
|
||||
for (const s of recent) {
|
||||
if (!seen.has(s.tree_id) && deduped.length < 5) {
|
||||
seen.add(s.tree_id)
|
||||
deduped.push({
|
||||
tree_id: s.tree_id,
|
||||
name: s.tree_snapshot?.name || 'Unnamed Tree',
|
||||
lastUsed: s.started_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
setRecentTrees(deduped)
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions:', err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
// Debounced search
|
||||
useEffect(() => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current)
|
||||
|
||||
if (query.length < 2) {
|
||||
setSearchResults([])
|
||||
setShowResults(false)
|
||||
setIsSearching(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsSearching(true)
|
||||
setShowResults(true)
|
||||
debounceRef.current = setTimeout(async () => {
|
||||
try {
|
||||
const results = await treesApi.search(query, 8)
|
||||
setSearchResults(results)
|
||||
} catch (err) {
|
||||
console.error('Search failed:', err)
|
||||
setSearchResults([])
|
||||
} finally {
|
||||
setIsSearching(false)
|
||||
}
|
||||
}, 300)
|
||||
|
||||
return () => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current)
|
||||
}
|
||||
}, [query])
|
||||
|
||||
// Close dropdown on outside click
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
|
||||
setShowResults(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
return () => document.removeEventListener('mousedown', handleClick)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
|
||||
{/* Animated background grid */}
|
||||
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_right,#4f4f4f12_1px,transparent_1px),linear-gradient(to_bottom,#4f4f4f12_1px,transparent_1px)] bg-[size:64px_64px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_110%)]" />
|
||||
|
||||
<div className="relative container mx-auto px-4 py-12">
|
||||
{/* Hero Section */}
|
||||
<div className="mx-auto max-w-3xl">
|
||||
{/* Badge */}
|
||||
<div className="flex justify-center mb-6 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-violet-500/10 to-purple-500/10 px-4 py-2 border border-violet-500/20 backdrop-blur-sm">
|
||||
<Sparkles className="h-4 w-4 text-violet-400" />
|
||||
<span className="text-sm font-medium text-violet-300">AI-Powered Troubleshooting</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="font-heading text-5xl font-bold text-center bg-clip-text text-transparent bg-gradient-to-br from-white via-slate-200 to-slate-400 leading-tight animate-in fade-in slide-in-from-bottom-4 duration-700 delay-100">
|
||||
What are you troubleshooting?
|
||||
</h1>
|
||||
|
||||
<p className="text-center text-slate-400 mt-4 text-lg animate-in fade-in slide-in-from-bottom-4 duration-700 delay-200">
|
||||
Search our library of proven decision trees or continue where you left off
|
||||
</p>
|
||||
|
||||
{/* Enhanced Search Bar */}
|
||||
<div ref={searchRef} className="relative mt-8 animate-in fade-in slide-in-from-bottom-4 duration-700 delay-300">
|
||||
<div className="relative group">
|
||||
{/* Glow effect */}
|
||||
<div className="absolute -inset-0.5 bg-gradient-to-r from-violet-600 to-purple-600 rounded-xl blur opacity-20 group-hover:opacity-40 transition duration-300" />
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-5 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400 transition-colors group-hover:text-violet-400" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => query.length >= 2 && setShowResults(true)}
|
||||
placeholder="Paste ticket subject or search for a tree..."
|
||||
className={cn(
|
||||
'w-full rounded-xl border border-slate-700/50 bg-slate-900/90 backdrop-blur-xl py-5 pl-14 pr-5 text-lg',
|
||||
'text-white placeholder:text-slate-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-violet-500/50 focus:border-violet-500/50',
|
||||
'transition-all duration-300'
|
||||
)}
|
||||
/>
|
||||
{query && (
|
||||
<Zap className="absolute right-5 top-1/2 h-5 w-5 -translate-y-1/2 text-violet-400 animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Search Results Dropdown */}
|
||||
{showResults && (
|
||||
<div className="absolute z-10 mt-2 w-full rounded-xl border border-slate-700/50 bg-slate-900/95 backdrop-blur-xl shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200">
|
||||
{isSearching ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-violet-400" />
|
||||
</div>
|
||||
) : searchResults.length === 0 ? (
|
||||
<div className="px-4 py-8 text-center">
|
||||
<div className="text-slate-400 text-sm">No results found</div>
|
||||
<div className="text-slate-500 text-xs mt-1">Try a different search term</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="max-h-96 overflow-y-auto py-2">
|
||||
{searchResults.map((tree, idx) => (
|
||||
<li key={tree.id} style={{ animationDelay: `${idx * 50}ms` }} className="animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
<button
|
||||
onClick={() => navigate(`/trees/${tree.id}/navigate`)}
|
||||
className="w-full px-5 py-4 text-left transition-all hover:bg-slate-800/50 group border-b border-slate-800/50 last:border-0"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold text-white group-hover:text-violet-300 transition-colors">
|
||||
{tree.name}
|
||||
</div>
|
||||
{tree.description && (
|
||||
<div className="mt-1 line-clamp-2 text-xs text-slate-400">
|
||||
{tree.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 flex-shrink-0 text-slate-600 group-hover:text-violet-400 group-hover:translate-x-1 transition-all" />
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Continue Session Section */}
|
||||
{activeSessions.length > 0 && (
|
||||
<div className="mx-auto mt-16 max-w-6xl animate-in fade-in slide-in-from-bottom-4 duration-700 delay-500">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-1 bg-gradient-to-b from-violet-500 to-purple-600 rounded-full" />
|
||||
<h2 className="font-heading text-xl font-bold text-white">
|
||||
Continue Session
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-slate-700/50 to-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{activeSessions.map((session, idx) => (
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() =>
|
||||
navigate(`/trees/${session.tree_id}/navigate`, {
|
||||
state: { sessionId: session.id },
|
||||
})
|
||||
}
|
||||
style={{ animationDelay: `${(idx + 5) * 100}ms` }}
|
||||
className="group relative rounded-xl border border-slate-700/50 bg-slate-900/50 backdrop-blur-sm p-5 text-left transition-all hover:border-violet-500/50 hover:shadow-lg hover:shadow-violet-500/10 hover:-translate-y-1 animate-in fade-in slide-in-from-bottom-2 duration-500"
|
||||
>
|
||||
{/* Animated corner accent */}
|
||||
<div className="absolute top-0 right-0 h-16 w-16 bg-gradient-to-bl from-violet-500/20 to-transparent rounded-tr-xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-base font-semibold text-white group-hover:text-violet-300 transition-colors">
|
||||
{session.tree_snapshot?.name || 'Unnamed Tree'}
|
||||
</div>
|
||||
{(session.ticket_number || session.client_name) && (
|
||||
<div className="mt-1.5 truncate text-sm text-slate-400">
|
||||
{[session.ticket_number, session.client_name]
|
||||
.filter(Boolean)
|
||||
.join(' • ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0 rounded-full bg-violet-500/10 p-2 group-hover:bg-violet-500/20 transition-colors">
|
||||
<Play className="h-4 w-4 text-violet-400 group-hover:scale-110 transition-transform" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center gap-2 text-xs text-slate-500">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>Started {timeAgo(session.started_at)}</span>
|
||||
</div>
|
||||
|
||||
{/* Progress indicator (optional - you can remove this) */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-1 bg-slate-800/50 rounded-b-xl overflow-hidden">
|
||||
<div className="h-full bg-gradient-to-r from-violet-500 to-purple-600 w-2/3" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent Trees Section */}
|
||||
{!isLoading && recentTrees.length > 0 && (
|
||||
<div className="mx-auto mt-12 max-w-6xl animate-in fade-in slide-in-from-bottom-4 duration-700 delay-700">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5 text-violet-400" />
|
||||
<h2 className="font-heading text-xl font-bold text-white">
|
||||
Recent Trees
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-slate-700/50 to-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
|
||||
{recentTrees.map((tree, idx) => (
|
||||
<button
|
||||
key={tree.tree_id}
|
||||
onClick={() => navigate(`/trees/${tree.tree_id}/navigate`)}
|
||||
style={{ animationDelay: `${(idx + 8) * 100}ms` }}
|
||||
className="group relative rounded-xl border border-slate-700/50 bg-slate-900/30 backdrop-blur-sm p-4 text-left transition-all hover:border-violet-500/50 hover:bg-slate-900/50 hover:-translate-y-1 animate-in fade-in slide-in-from-bottom-2 duration-500"
|
||||
>
|
||||
<div className="truncate text-sm font-medium text-white group-hover:text-violet-300 transition-colors">
|
||||
{tree.name}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs text-slate-500">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{timeAgo(tree.lastUsed)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer CTA */}
|
||||
<div className="mx-auto mt-16 max-w-4xl text-center animate-in fade-in duration-700 delay-1000">
|
||||
<Link
|
||||
to="/trees"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-gradient-to-r from-violet-600 to-purple-600 text-white font-medium hover:from-violet-500 hover:to-purple-500 transition-all hover:shadow-lg hover:shadow-violet-500/25 hover:scale-105"
|
||||
>
|
||||
Browse All Trees
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuickStartPage
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { Search, Clock, ArrowRight, Play, Loader2 } from 'lucide-react'
|
||||
import { Search, Clock, ArrowRight, Play, Loader2, Sparkles } from 'lucide-react'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
import type { TreeListItem } from '@/types'
|
||||
import type { Session } from '@/types/session'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const now = Date.now()
|
||||
@@ -107,39 +107,61 @@ export function QuickStartPage() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
{/* Hero Section */}
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h1 className="font-heading text-3xl font-bold text-foreground">
|
||||
What are you troubleshooting?
|
||||
<div className="mb-16 text-center max-w-4xl mx-auto">
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8">
|
||||
<Sparkles className="w-4 h-4 text-cyan-400" />
|
||||
<span className="text-sm text-white/70 font-medium">DECISION TREE PLATFORM</span>
|
||||
</div>
|
||||
|
||||
{/* Main heading */}
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-white mb-6 tracking-tight leading-tight">
|
||||
What are you<br />
|
||||
<span className="text-white/60">troubleshooting?</span>
|
||||
</h1>
|
||||
<div ref={searchRef} className="relative mt-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => query.length >= 2 && setShowResults(true)}
|
||||
placeholder="Paste ticket subject or search for a tree..."
|
||||
className={cn(
|
||||
'w-full rounded-lg border border-border bg-card py-3 pl-12 pr-4 text-lg',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary/50'
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-lg text-white/40 mb-10 max-w-2xl mx-auto leading-relaxed">
|
||||
Search our library of proven decision trees or continue where you left off
|
||||
</p>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div ref={searchRef} className="relative max-w-2xl mx-auto group">
|
||||
<div className="absolute inset-0 bg-white/5 rounded-2xl blur-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="relative glass-card rounded-2xl p-1">
|
||||
<div className="flex items-center bg-black/50 rounded-xl">
|
||||
<Search className="ml-5 w-5 h-5 text-blue-400" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => query.length >= 2 && setShowResults(true)}
|
||||
placeholder="Paste ticket subject or search for a tree..."
|
||||
className="flex-1 bg-transparent py-4 px-4 text-white placeholder:text-white/30 focus:outline-none"
|
||||
/>
|
||||
{query.length >= 2 && (
|
||||
<button
|
||||
onClick={() => {/* search already fires on type */}}
|
||||
className="mr-2 px-5 py-2.5 bg-white text-black font-semibold rounded-lg hover:bg-white/90 transition-all"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Results Dropdown */}
|
||||
{showResults && (
|
||||
<div className="absolute z-10 mt-1 w-full rounded-lg border border-border bg-card shadow-lg">
|
||||
<div className="absolute z-10 mt-2 w-full glass-card rounded-2xl shadow-[0_0_40px_rgba(0,0,0,0.5)] overflow-hidden">
|
||||
{isSearching ? (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-white/40" />
|
||||
</div>
|
||||
) : searchResults.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
<div className="px-4 py-8 text-center text-sm text-white/40">
|
||||
No results found
|
||||
</div>
|
||||
) : (
|
||||
@@ -148,13 +170,13 @@ export function QuickStartPage() {
|
||||
<li key={tree.id}>
|
||||
<button
|
||||
onClick={() => navigate(`/trees/${tree.id}/navigate`)}
|
||||
className="w-full px-4 py-3 text-left transition-colors hover:bg-accent"
|
||||
className="w-full px-5 py-3.5 text-left transition-all hover:bg-white/[0.06]"
|
||||
>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
<div className="text-sm font-medium text-white">
|
||||
{tree.name}
|
||||
</div>
|
||||
{tree.description && (
|
||||
<div className="mt-0.5 line-clamp-1 text-xs text-muted-foreground">
|
||||
<div className="mt-0.5 line-clamp-1 text-xs text-white/40">
|
||||
{tree.description}
|
||||
</div>
|
||||
)}
|
||||
@@ -170,65 +192,115 @@ export function QuickStartPage() {
|
||||
|
||||
{/* Continue Session Section */}
|
||||
{activeSessions.length > 0 && (
|
||||
<div className="mx-auto mt-12 max-w-4xl">
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">
|
||||
Continue Session
|
||||
</h2>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{activeSessions.map((session) => (
|
||||
<div className="mx-auto max-w-4xl mb-12">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">Active Sessions</h2>
|
||||
</div>
|
||||
|
||||
{/* Primary active session — Bright Glow card */}
|
||||
<div className="glass-card-glow backdrop-blur-xl rounded-2xl p-8 mb-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-white/15 border border-white/30 flex items-center justify-center">
|
||||
<Play className="w-6 h-6 text-violet-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-white/70 font-semibold uppercase tracking-wider mb-1">
|
||||
Active Session
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white">
|
||||
{activeSessions[0].tree_snapshot?.name || 'Unnamed Tree'}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() =>
|
||||
navigate(`/trees/${session.tree_id}/navigate`, {
|
||||
state: { sessionId: session.id },
|
||||
navigate(`/trees/${activeSessions[0].tree_id}/navigate`, {
|
||||
state: { sessionId: activeSessions[0].id },
|
||||
})
|
||||
}
|
||||
className="rounded-lg border border-border bg-card p-4 text-left transition-colors hover:border-primary/50 hover:bg-accent"
|
||||
className="px-5 py-2.5 bg-white text-black rounded-xl font-semibold hover:bg-white/90 transition-all hover:scale-105"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-foreground">
|
||||
{session.tree_snapshot?.name || 'Unnamed Tree'}
|
||||
</div>
|
||||
{(session.ticket_number || session.client_name) && (
|
||||
<div className="mt-1 truncate text-xs text-muted-foreground">
|
||||
{[session.ticket_number, session.client_name]
|
||||
.filter(Boolean)
|
||||
.join(' - ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Play className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{timeAgo(session.started_at)}</span>
|
||||
</div>
|
||||
Continue
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-white/50 mt-1">
|
||||
{[activeSessions[0].ticket_number, activeSessions[0].client_name]
|
||||
.filter(Boolean)
|
||||
.join(' \u2022 ')}
|
||||
{activeSessions[0].started_at && ` \u2022 Started ${timeAgo(activeSessions[0].started_at)}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Additional active sessions */}
|
||||
{activeSessions.length > 1 && (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{activeSessions.slice(1).map((session) => (
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() =>
|
||||
navigate(`/trees/${session.tree_id}/navigate`, {
|
||||
state: { sessionId: session.id },
|
||||
})
|
||||
}
|
||||
className="glass-card hover:glass-card-hover rounded-2xl p-5 text-left transition-all hover:scale-[1.02] cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-bold text-white">
|
||||
{session.tree_snapshot?.name || 'Unnamed Tree'}
|
||||
</div>
|
||||
{(session.ticket_number || session.client_name) && (
|
||||
<div className="mt-1 truncate text-xs text-white/40">
|
||||
{[session.ticket_number, session.client_name]
|
||||
.filter(Boolean)
|
||||
.join(' - ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Play className="mt-0.5 h-4 w-4 flex-shrink-0 text-violet-400" />
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-1.5 text-xs text-white/30">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>{timeAgo(session.started_at)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent Trees Section */}
|
||||
{!isLoading && recentTrees.length > 0 && (
|
||||
<div className="mx-auto mt-10 max-w-4xl">
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">
|
||||
Recent Trees
|
||||
</h2>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="mx-auto max-w-4xl mb-12">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">Recent Trees</h2>
|
||||
<Link
|
||||
to="/trees"
|
||||
className="text-sm text-white/60 hover:text-white font-medium transition-colors"
|
||||
>
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{recentTrees.map((tree) => (
|
||||
<button
|
||||
key={tree.tree_id}
|
||||
onClick={() => navigate(`/trees/${tree.tree_id}/navigate`)}
|
||||
className="rounded-lg border border-border bg-card p-4 text-left transition-colors hover:border-primary/50 hover:bg-accent"
|
||||
className="glass-card hover:glass-card-hover rounded-2xl p-5 text-left transition-all hover:scale-[1.02] cursor-pointer"
|
||||
>
|
||||
<div className="truncate text-sm font-medium text-foreground">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center">
|
||||
<Search className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="truncate text-sm font-bold text-white mb-2">
|
||||
{tree.name}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{timeAgo(tree.lastUsed)}</span>
|
||||
<div className="flex items-center gap-1.5 text-xs text-white/30">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>Last used {timeAgo(tree.lastUsed)}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -237,10 +309,10 @@ export function QuickStartPage() {
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mx-auto mt-12 max-w-4xl text-center">
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<Link
|
||||
to="/trees"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-white/10 border border-white/20 text-white font-medium rounded-xl hover:bg-white/20 transition-all"
|
||||
>
|
||||
Browse All Trees
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { inviteApi } from '@/api'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function RegisterPage() {
|
||||
@@ -76,33 +75,38 @@ export function RegisterPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="flex min-h-screen items-center justify-center bg-black px-4">
|
||||
{/* Subtle radial overlay */}
|
||||
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
|
||||
|
||||
<div className="relative w-full max-w-md space-y-8">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 flex justify-center sm:mb-6">
|
||||
<BrandLogo size="lg" className="h-12 w-12 sm:h-16 sm:w-16" />
|
||||
<div className="w-16 h-16 rounded-2xl bg-white flex items-center justify-center sm:w-20 sm:h-20">
|
||||
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
|
||||
</div>
|
||||
</div>
|
||||
<h1>
|
||||
<BrandWordmark size="lg" />
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">
|
||||
ResolutionFlow
|
||||
</h1>
|
||||
<p className="mt-2 text-base font-medium text-gradient-brand sm:mt-3 sm:text-lg">
|
||||
<p className="mt-2 text-base font-medium text-white/60 sm:mt-3 sm:text-lg">
|
||||
Decision Tree Platform
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground sm:mt-2">
|
||||
<p className="mt-1 text-sm text-white/40 sm:mt-2">
|
||||
Create your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
|
||||
<div className="space-y-4 rounded-lg border border-border bg-card p-6 shadow-sm">
|
||||
<div className="glass-card rounded-2xl p-6 space-y-4">
|
||||
{(error || localError) && (
|
||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
|
||||
{localError || error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="inviteCode" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="inviteCode" className="block text-sm font-medium text-white">
|
||||
Invite code
|
||||
</label>
|
||||
<input
|
||||
@@ -116,29 +120,29 @@ export function RegisterPage() {
|
||||
}}
|
||||
onBlur={(e) => validateInviteCode(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border bg-background px-3 py-2 font-mono tracking-wider',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'mt-1 block w-full rounded-xl border bg-black/50 px-3 py-2 font-mono tracking-wider',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:outline-none focus:ring-1',
|
||||
inviteCodeStatus === 'valid' && 'border-green-500 focus:border-green-500 focus:ring-green-500',
|
||||
inviteCodeStatus === 'invalid' && 'border-destructive focus:border-destructive focus:ring-destructive',
|
||||
inviteCodeStatus === 'idle' && 'border-input focus:border-primary focus:ring-primary',
|
||||
inviteCodeStatus === 'checking' && 'border-input focus:border-primary focus:ring-primary'
|
||||
inviteCodeStatus === 'valid' && 'border-emerald-400/50 focus:border-emerald-400 focus:ring-emerald-400/30',
|
||||
inviteCodeStatus === 'invalid' && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/30',
|
||||
inviteCodeStatus === 'idle' && 'border-white/10 focus:border-white/30 focus:ring-white/20',
|
||||
inviteCodeStatus === 'checking' && 'border-white/10 focus:border-white/30 focus:ring-white/20'
|
||||
)}
|
||||
placeholder="ABCD1234"
|
||||
/>
|
||||
{inviteCodeStatus === 'checking' && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Validating...</p>
|
||||
<p className="mt-1 text-xs text-white/40">Validating...</p>
|
||||
)}
|
||||
{inviteCodeStatus === 'valid' && (
|
||||
<p className="mt-1 text-xs text-green-600">{inviteCodeMessage}</p>
|
||||
<p className="mt-1 text-xs text-emerald-400">{inviteCodeMessage}</p>
|
||||
)}
|
||||
{inviteCodeStatus === 'invalid' && (
|
||||
<p className="mt-1 text-xs text-destructive">{inviteCodeMessage}</p>
|
||||
<p className="mt-1 text-xs text-red-400">{inviteCodeMessage}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-white">
|
||||
Full name
|
||||
</label>
|
||||
<input
|
||||
@@ -150,16 +154,16 @@ export function RegisterPage() {
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
placeholder="John Smith"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-white">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
@@ -171,16 +175,16 @@ export function RegisterPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-white">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
@@ -192,19 +196,19 @@ export function RegisterPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
placeholder="••••••••••"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-white/30">
|
||||
Must be at least 10 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground">
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-white">
|
||||
Confirm password
|
||||
</label>
|
||||
<input
|
||||
@@ -216,9 +220,9 @@ export function RegisterPage() {
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/30',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
placeholder="••••••••••"
|
||||
/>
|
||||
@@ -228,20 +232,20 @@ export function RegisterPage() {
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'w-full rounded-md px-4 py-2.5 text-sm font-semibold text-white btn-press',
|
||||
'bg-gradient-brand hover:bg-gradient-brand-hover',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
||||
'w-full rounded-xl px-4 py-2.5 text-sm font-semibold btn-press',
|
||||
'bg-white text-black hover:bg-white/90',
|
||||
'focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-black',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'shadow-lg shadow-primary/20'
|
||||
'transition-all'
|
||||
)}
|
||||
>
|
||||
{isLoading ? 'Creating account...' : 'Create account'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
<p className="text-center text-sm text-white/40">
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" className="font-medium text-gradient-brand hover:underline">
|
||||
<Link to="/login" className="font-medium text-white hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -223,7 +223,7 @@ export function SessionDetailPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -231,12 +231,12 @@ export function SessionDetailPage() {
|
||||
if (error || !session) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
<div className="rounded-md border border-red-400/20 bg-red-400/10 p-4 text-red-400">
|
||||
{error || 'Session not found'}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/sessions')}
|
||||
className="mt-4 text-primary hover:underline"
|
||||
className="mt-4 text-white hover:underline"
|
||||
>
|
||||
Back to sessions
|
||||
</button>
|
||||
@@ -252,18 +252,18 @@ export function SessionDetailPage() {
|
||||
<div>
|
||||
<button
|
||||
onClick={() => navigate('/sessions')}
|
||||
className="mb-2 text-sm text-muted-foreground hover:text-foreground"
|
||||
className="mb-2 text-sm text-white/40 hover:text-white"
|
||||
>
|
||||
← Back to sessions
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">
|
||||
{session.ticket_number || 'Session Details'}
|
||||
</h1>
|
||||
<div className="mt-2 flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="mt-2 flex items-center gap-4 text-sm text-white/40">
|
||||
<span
|
||||
className={cn(
|
||||
'flex items-center gap-1',
|
||||
session.completed_at ? 'text-green-600' : 'text-yellow-600'
|
||||
session.completed_at ? 'text-emerald-400' : 'text-yellow-400'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
@@ -286,8 +286,8 @@ export function SessionDetailPage() {
|
||||
onClick={() => setShowSaveAsTreeModal(true)}
|
||||
disabled={isSavingTree}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 bg-transparent px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
@@ -299,8 +299,8 @@ export function SessionDetailPage() {
|
||||
<button
|
||||
onClick={handleCopyForTicket}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
{copiedPsa ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
@@ -314,8 +314,8 @@ export function SessionDetailPage() {
|
||||
onChange={(e) => setExportFormat(e.target.value as typeof exportFormat)}
|
||||
aria-label="Export format"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm sm:w-auto',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white sm:w-auto',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="markdown">Markdown</option>
|
||||
@@ -328,18 +328,18 @@ export function SessionDetailPage() {
|
||||
disabled={isExporting}
|
||||
title="Copy to clipboard"
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background p-2 text-muted-foreground',
|
||||
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
|
||||
'rounded-md border border-white/10 bg-transparent p-2 text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePreview}
|
||||
disabled={isExporting}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50'
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
@@ -352,37 +352,37 @@ export function SessionDetailPage() {
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="mb-8">
|
||||
<h2 className="mb-4 text-lg font-semibold text-foreground">Decision Timeline</h2>
|
||||
<h2 className="mb-4 text-lg font-semibold text-white">Decision Timeline</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="h-3 w-3 rounded-full bg-primary" />
|
||||
<span className="text-muted-foreground">
|
||||
<span className="h-3 w-3 rounded-full bg-white" />
|
||||
<span className="text-white/40">
|
||||
Session started: {formatDate(session.started_at)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{session.decisions.map((decision, index) => (
|
||||
<div key={index} className="ml-1 border-l-2 border-border pl-6">
|
||||
<div key={index} className="ml-1 border-l-2 border-white/[0.06] pl-6">
|
||||
<div className="relative">
|
||||
<span className="absolute -left-[1.625rem] top-1 h-2 w-2 rounded-full bg-border" />
|
||||
<div className="rounded-lg border border-border bg-card p-4">
|
||||
<span className="absolute -left-[1.625rem] top-1 h-2 w-2 rounded-full bg-white/20" />
|
||||
<div className="glass-card rounded-xl p-4">
|
||||
{decision.question && (
|
||||
<p className="font-medium text-card-foreground">{decision.question}</p>
|
||||
<p className="font-medium text-white">{decision.question}</p>
|
||||
)}
|
||||
{decision.answer && (
|
||||
<p className="mt-1 text-sm text-primary">Answer: {decision.answer}</p>
|
||||
<p className="mt-1 text-sm text-white">Answer: {decision.answer}</p>
|
||||
)}
|
||||
{decision.action_performed && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
Action: {decision.action_performed}
|
||||
</p>
|
||||
)}
|
||||
{decision.notes && (
|
||||
<p className="mt-2 rounded bg-muted/50 p-2 text-sm text-muted-foreground">
|
||||
<p className="mt-2 rounded bg-white/5 p-2 text-sm text-white/40">
|
||||
Notes: {decision.notes}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
<p className="mt-2 text-xs text-white/40">
|
||||
{formatDate(decision.timestamp)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -393,7 +393,7 @@ export function SessionDetailPage() {
|
||||
{session.completed_at && (
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="h-3 w-3 rounded-full bg-green-500" />
|
||||
<span className="text-green-600">
|
||||
<span className="text-emerald-400">
|
||||
Session completed: {formatDate(session.completed_at)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -142,14 +142,14 @@ export function SessionHistoryPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Session History</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">Session History</h1>
|
||||
<p className="mt-2 text-white/40">
|
||||
Search and filter your troubleshooting sessions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="mb-6 flex gap-2 border-b border-border">
|
||||
<div className="mb-6 flex gap-2 border-b border-white/[0.06]">
|
||||
{(['all', 'active', 'completed'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
@@ -157,8 +157,8 @@ export function SessionHistoryPage() {
|
||||
className={cn(
|
||||
'px-4 py-2 text-sm font-medium transition-colors',
|
||||
filter === tab
|
||||
? 'border-b-2 border-primary text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
? 'border-b-2 border-white text-white'
|
||||
: 'text-white/40 hover:text-white'
|
||||
)}
|
||||
>
|
||||
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
||||
@@ -179,22 +179,22 @@ export function SessionHistoryPage() {
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<div className="py-12 text-center text-white/40">
|
||||
No sessions found.{' '}
|
||||
{filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from ? (
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="text-primary hover:underline"
|
||||
className="text-white hover:underline"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className="text-primary hover:underline"
|
||||
className="text-white hover:underline"
|
||||
>
|
||||
Start a new session
|
||||
</button>
|
||||
@@ -205,7 +205,7 @@ export function SessionHistoryPage() {
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md"
|
||||
className="glass-card rounded-2xl p-4 transition-all hover:glass-card-hover"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex-1">
|
||||
@@ -217,23 +217,23 @@ export function SessionHistoryPage() {
|
||||
session.completed_at ? 'bg-green-500' : 'bg-yellow-500'
|
||||
)}
|
||||
/>
|
||||
<span className="font-medium text-card-foreground">
|
||||
<span className="font-medium text-white">
|
||||
{session.ticket_number || 'No ticket'}
|
||||
</span>
|
||||
{session.client_name && (
|
||||
<span className="rounded-full bg-accent px-2.5 py-0.5 text-xs font-medium">
|
||||
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs font-medium text-white">
|
||||
{session.client_name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tree Name */}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
<span className="font-medium">Tree:</span> {getTreeName(session)}
|
||||
</p>
|
||||
|
||||
{/* Timestamps */}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
Started: {formatDate(session.started_at)}
|
||||
{session.completed_at && (
|
||||
<> · Completed: {formatDate(session.completed_at)}</>
|
||||
@@ -241,7 +241,7 @@ export function SessionHistoryPage() {
|
||||
</p>
|
||||
|
||||
{/* Stats */}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
{session.decisions.length} decision{session.decisions.length !== 1 ? 's' : ''} recorded
|
||||
{session.scratchpad && session.scratchpad.trim() && (
|
||||
<span> · Has notes</span>
|
||||
@@ -254,8 +254,8 @@ export function SessionHistoryPage() {
|
||||
<button
|
||||
onClick={() => navigate(`/sessions/${session.id}`)}
|
||||
className={cn(
|
||||
'rounded-md border border-input px-3 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-md border border-white/10 px-3 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
View Details
|
||||
@@ -264,8 +264,8 @@ export function SessionHistoryPage() {
|
||||
<button
|
||||
onClick={() => navigate(`/trees/${session.tree_id}/navigate`, { state: { sessionId: session.id } })}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Resume
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Settings } from 'lucide-react'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import { useThemeStore } from '@/store/themeStore'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ThemeToggle } from '@/components/common/ThemeToggle'
|
||||
import { toast } from '@/lib/toast'
|
||||
|
||||
export function SettingsPage() {
|
||||
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
|
||||
const { theme } = useThemeStore()
|
||||
|
||||
const handleExportFormatChange = (format: 'markdown' | 'text' | 'html') => {
|
||||
setDefaultExportFormat(format)
|
||||
@@ -18,50 +15,30 @@ export function SettingsPage() {
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings className="h-8 w-8 text-primary" />
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Settings</h1>
|
||||
<Settings className="h-8 w-8 text-white/50" />
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">Settings</h1>
|
||||
</div>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<p className="mt-2 text-white/40">
|
||||
Manage your application preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-2xl space-y-6">
|
||||
{/* Appearance Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Appearance</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Customize how ResolutionFlow looks on your device
|
||||
</p>
|
||||
|
||||
<div className="mt-4">
|
||||
<label className="block font-label text-sm font-medium text-card-foreground">
|
||||
Theme
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Current: {theme.charAt(0).toUpperCase() + theme.slice(1)}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export Preferences Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Export Preferences</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Export Preferences</h2>
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
Configure default settings for session exports
|
||||
</p>
|
||||
|
||||
<div className="mt-4">
|
||||
<label
|
||||
htmlFor="export-format"
|
||||
className="block font-label text-sm font-medium text-card-foreground"
|
||||
className="block text-sm font-medium text-white"
|
||||
>
|
||||
Default Export Format
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-white/40">
|
||||
This format will be pre-selected when exporting sessions
|
||||
</p>
|
||||
<select
|
||||
@@ -69,9 +46,9 @@ export function SettingsPage() {
|
||||
value={defaultExportFormat}
|
||||
onChange={(e) => handleExportFormatChange(e.target.value as 'markdown' | 'text' | 'html')}
|
||||
className={cn(
|
||||
'mt-2 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-sm text-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-2 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-sm text-white',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="markdown">Markdown (.md)</option>
|
||||
@@ -82,12 +59,12 @@ export function SettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* About Section */}
|
||||
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-card-foreground">About</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<h2 className="text-lg font-semibold text-white">About</h2>
|
||||
<p className="mt-1 text-sm text-white/40">
|
||||
ResolutionFlow - Decision Tree Platform
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
<p className="mt-2 text-sm text-white/40">
|
||||
Transform troubleshooting into guided workflows
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -248,7 +248,7 @@ export function TreeEditorPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -257,16 +257,16 @@ export function TreeEditorPage() {
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] flex-col items-center justify-center px-6 text-center">
|
||||
<Monitor className="mb-4 h-12 w-12 text-muted-foreground" />
|
||||
<h2 className="mb-2 text-xl font-semibold text-foreground">Desktop Required</h2>
|
||||
<p className="mb-6 max-w-sm text-sm text-muted-foreground">
|
||||
<Monitor className="mb-4 h-12 w-12 text-white/50" />
|
||||
<h2 className="mb-2 text-xl font-semibold text-white">Desktop Required</h2>
|
||||
<p className="mb-6 max-w-sm text-sm text-white/40">
|
||||
The tree editor requires a larger screen for the best experience. Please open this page on a desktop or tablet in landscape mode.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Back to Library
|
||||
@@ -280,18 +280,18 @@ export function TreeEditorPage() {
|
||||
|
||||
{/* Draft Restore Prompt */}
|
||||
{showDraftPrompt && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<h2 className="mb-2 text-lg font-semibold">Restore Draft?</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="glass-card rounded-2xl w-full max-w-md p-6 shadow-lg">
|
||||
<h2 className="mb-2 text-lg font-semibold text-white">Restore Draft?</h2>
|
||||
<p className="mb-4 text-sm text-white/40">
|
||||
You have an unsaved draft from a previous session. Would you like to restore it?
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleRestoreDraft}
|
||||
className={cn(
|
||||
'flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex-1 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Restore Draft
|
||||
@@ -299,8 +299,8 @@ export function TreeEditorPage() {
|
||||
<button
|
||||
onClick={handleDiscardDraft}
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
|
||||
'hover:bg-accent'
|
||||
'flex-1 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
Start Fresh
|
||||
@@ -312,18 +312,18 @@ export function TreeEditorPage() {
|
||||
|
||||
{/* Unsaved Changes Dialog */}
|
||||
{blocker.state === 'blocked' && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<h2 className="mb-2 text-lg font-semibold">Unsaved Changes</h2>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="glass-card rounded-2xl w-full max-w-md p-6 shadow-lg">
|
||||
<h2 className="mb-2 text-lg font-semibold text-white">Unsaved Changes</h2>
|
||||
<p className="mb-4 text-sm text-white/40">
|
||||
You have unsaved changes. Are you sure you want to leave?
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleBlockerReset}
|
||||
className={cn(
|
||||
'flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex-1 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Stay
|
||||
@@ -331,8 +331,8 @@ export function TreeEditorPage() {
|
||||
<button
|
||||
onClick={handleBlockerProceed}
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium text-destructive',
|
||||
'hover:bg-accent'
|
||||
'flex-1 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-red-400',
|
||||
'hover:bg-white/10'
|
||||
)}
|
||||
>
|
||||
Leave Without Saving
|
||||
@@ -343,17 +343,17 @@ export function TreeEditorPage() {
|
||||
)}
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between border-b border-border bg-card px-4 py-2">
|
||||
<div className="flex items-center justify-between border-b border-white/[0.06] bg-black px-4 py-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
className="text-sm text-white/50 hover:text-white"
|
||||
>
|
||||
← Back to Library
|
||||
</button>
|
||||
<h1 className="text-lg font-semibold">
|
||||
<h1 className="text-lg font-semibold text-white">
|
||||
{isEditMode ? 'Edit Tree' : 'Create New Tree'}
|
||||
{name && <span className="ml-2 text-muted-foreground">- {name}</span>}
|
||||
{name && <span className="ml-2 text-white/40">- {name}</span>}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
{treeStatus === 'draft' && (
|
||||
@@ -372,7 +372,7 @@ export function TreeEditorPage() {
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Undo/Redo */}
|
||||
<div className="flex items-center rounded-md border border-border">
|
||||
<div className="flex items-center rounded-md border border-white/[0.06]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => undo()}
|
||||
@@ -381,13 +381,13 @@ export function TreeEditorPage() {
|
||||
className={cn(
|
||||
'rounded-l-md p-2 transition-colors',
|
||||
pastStates.length > 0
|
||||
? 'text-foreground hover:bg-accent'
|
||||
: 'text-muted-foreground/40 cursor-not-allowed'
|
||||
? 'text-white hover:bg-white/[0.06]'
|
||||
: 'text-white/20 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<Undo2 className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="h-6 w-px bg-border" />
|
||||
<div className="h-6 w-px bg-white/[0.06]" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => redo()}
|
||||
@@ -396,15 +396,15 @@ export function TreeEditorPage() {
|
||||
className={cn(
|
||||
'rounded-r-md p-2 transition-colors',
|
||||
futureStates.length > 0
|
||||
? 'text-foreground hover:bg-accent'
|
||||
: 'text-muted-foreground/40 cursor-not-allowed'
|
||||
? 'text-white hover:bg-white/[0.06]'
|
||||
: 'text-white/20 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<Redo2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mx-2 h-6 w-px bg-border" />
|
||||
<div className="mx-2 h-6 w-px bg-white/[0.06]" />
|
||||
|
||||
{/* Validate */}
|
||||
<button
|
||||
@@ -412,8 +412,8 @@ export function TreeEditorPage() {
|
||||
disabled={isSaving}
|
||||
title="Validate tree structure (checks for errors and warnings)"
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm font-medium',
|
||||
'hover:bg-accent disabled:opacity-50'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
@@ -426,8 +426,8 @@ export function TreeEditorPage() {
|
||||
disabled={isSaving || !isDirty}
|
||||
title="Save as draft (Ctrl+S when draft or has errors)"
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium',
|
||||
'hover:bg-accent disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
@@ -440,8 +440,8 @@ export function TreeEditorPage() {
|
||||
disabled={isSaving || !isDirty || hasBlockingErrors}
|
||||
title={hasBlockingErrors ? 'Fix validation errors before publishing (Ctrl+S when no errors)' : 'Publish tree (Ctrl+S when no errors)'}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
|
||||
@@ -199,8 +199,8 @@ export function TreeLibraryPage() {
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-6 flex flex-col gap-4 sm:mb-8 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Decision Trees</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">Decision Trees</h1>
|
||||
<p className="mt-2 text-white/40">
|
||||
Select a troubleshooting tree to start a new session
|
||||
</p>
|
||||
</div>
|
||||
@@ -208,8 +208,8 @@ export function TreeLibraryPage() {
|
||||
<Link
|
||||
to="/trees/new"
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -225,9 +225,9 @@ export function TreeLibraryPage() {
|
||||
<button
|
||||
onClick={() => setMobileFolderOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input px-3 py-2 text-sm font-medium md:hidden',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
||||
selectedFolderId && 'border-primary text-primary'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 px-3 py-2 text-sm font-medium md:hidden',
|
||||
'text-white/40 hover:bg-white/10 hover:text-white',
|
||||
selectedFolderId && 'border-white/30 text-white'
|
||||
)}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
@@ -241,16 +241,16 @@ export function TreeLibraryPage() {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Search
|
||||
@@ -262,8 +262,8 @@ export function TreeLibraryPage() {
|
||||
onChange={(e) => setSelectedCategoryId(e.target.value)}
|
||||
aria-label="Filter by category"
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
@@ -284,9 +284,9 @@ export function TreeLibraryPage() {
|
||||
type="checkbox"
|
||||
checked={showDrafts}
|
||||
onChange={(e) => setShowDrafts(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-input text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
className="h-4 w-4 rounded border-white/20 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-2"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">Show my drafts</span>
|
||||
<span className="text-sm text-white/40">Show my drafts</span>
|
||||
</label>
|
||||
</div>
|
||||
<ViewToggle view={treeLibraryView} onChange={setTreeLibraryView} />
|
||||
@@ -296,24 +296,24 @@ export function TreeLibraryPage() {
|
||||
{/* Active Filters */}
|
||||
{hasActiveFilters && (
|
||||
<div className="mb-6 flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Filters:</span>
|
||||
<span className="text-sm text-white/40">Filters:</span>
|
||||
{selectedFolderId && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white">
|
||||
Folder
|
||||
<button
|
||||
onClick={() => setSelectedFolderId(null)}
|
||||
className="rounded-full p-0.5 hover:bg-accent-foreground/10"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{selectedCategoryId && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-secondary px-3 py-1 text-sm">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white">
|
||||
{categories.find((c) => c.id === selectedCategoryId)?.name}
|
||||
<button
|
||||
onClick={() => setSelectedCategoryId('')}
|
||||
className="rounded-full p-0.5 hover:bg-secondary-foreground/10"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -322,12 +322,12 @@ export function TreeLibraryPage() {
|
||||
{selectedTags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-3 py-1 text-sm text-primary"
|
||||
className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => removeTagFilter(tag)}
|
||||
className="rounded-full p-0.5 hover:bg-primary/20"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -335,7 +335,7 @@ export function TreeLibraryPage() {
|
||||
))}
|
||||
<button
|
||||
onClick={clearAllFilters}
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
className="text-sm text-white/40 hover:text-white"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
@@ -345,10 +345,10 @@ export function TreeLibraryPage() {
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
) : trees.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
<div className="py-12 text-center text-white/40">
|
||||
No trees found.{' '}
|
||||
{(searchQuery || hasActiveFilters) && 'Try adjusting your filters.'}
|
||||
</div>
|
||||
|
||||
@@ -267,7 +267,7 @@ export function TreeNavigationPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -275,12 +275,12 @@ export function TreeNavigationPage() {
|
||||
if (error || !tree) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
<div className="rounded-md bg-red-400/10 p-4 text-red-400">
|
||||
{error || 'Tree not found'}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className="mt-4 text-primary hover:underline"
|
||||
className="mt-4 text-white/50 hover:text-white hover:underline"
|
||||
>
|
||||
Back to trees
|
||||
</button>
|
||||
@@ -292,17 +292,17 @@ export function TreeNavigationPage() {
|
||||
if (showMetadataForm) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-lg px-4 py-8">
|
||||
<h1 className="mb-2 text-2xl font-bold text-foreground">{tree.name}</h1>
|
||||
<p className="mb-6 text-muted-foreground">{tree.description}</p>
|
||||
<h1 className="mb-2 text-2xl font-bold text-white">{tree.name}</h1>
|
||||
<p className="mb-6 text-white/40">{tree.description}</p>
|
||||
|
||||
<div className="space-y-4 rounded-lg border border-border bg-card p-6">
|
||||
<h2 className="font-semibold text-card-foreground">Session Details</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="glass-card rounded-2xl space-y-4 p-6">
|
||||
<h2 className="font-semibold text-white">Session Details</h2>
|
||||
<p className="text-sm text-white/40">
|
||||
Optional: Add ticket and client info for easier tracking
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Ticket Number
|
||||
</label>
|
||||
<input
|
||||
@@ -311,15 +311,15 @@ export function TreeNavigationPage() {
|
||||
onChange={(e) => setTicketNumber(e.target.value)}
|
||||
placeholder="e.g., INC0012345"
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Client Name
|
||||
</label>
|
||||
<input
|
||||
@@ -328,9 +328,9 @@ export function TreeNavigationPage() {
|
||||
onChange={(e) => setClientName(e.target.value)}
|
||||
placeholder="e.g., Acme Corp"
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -338,8 +338,8 @@ export function TreeNavigationPage() {
|
||||
<button
|
||||
onClick={startSession}
|
||||
className={cn(
|
||||
'w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'w-full rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Start Troubleshooting
|
||||
@@ -352,7 +352,7 @@ export function TreeNavigationPage() {
|
||||
if (!currentNode && !currentCustomStep) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
<div className="rounded-md bg-red-400/10 p-4 text-red-400">
|
||||
Invalid tree structure
|
||||
</div>
|
||||
</div>
|
||||
@@ -367,9 +367,9 @@ export function TreeNavigationPage() {
|
||||
{/* Header */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">{tree.name}</h1>
|
||||
<h1 className="text-xl font-bold text-white">{tree.name}</h1>
|
||||
{(ticketNumber || clientName) && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-sm text-white/40">
|
||||
{ticketNumber && `Ticket: ${ticketNumber}`}
|
||||
{ticketNumber && clientName && ' · '}
|
||||
{clientName && `Client: ${clientName}`}
|
||||
@@ -378,7 +378,7 @@ export function TreeNavigationPage() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/sessions')}
|
||||
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
className="rounded-md px-3 py-1.5 text-sm text-white/50 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
Exit
|
||||
</button>
|
||||
@@ -392,12 +392,12 @@ export function TreeNavigationPage() {
|
||||
const label = node?.question || node?.title || customStep?.step_data.title || nodeId
|
||||
return (
|
||||
<span key={nodeId} className="flex items-center gap-2 whitespace-nowrap">
|
||||
{index > 0 && <span className="text-muted-foreground">→</span>}
|
||||
{index > 0 && <span className="text-white/40">→</span>}
|
||||
<span
|
||||
className={cn(
|
||||
index === pathTaken.length - 1
|
||||
? 'font-medium text-foreground'
|
||||
: 'text-muted-foreground'
|
||||
? 'font-medium text-white'
|
||||
: 'text-white/40'
|
||||
)}
|
||||
>
|
||||
{label.length > 30 ? `${label.slice(0, 30)}...` : label}
|
||||
@@ -408,15 +408,15 @@ export function TreeNavigationPage() {
|
||||
</div>
|
||||
|
||||
{/* Current Node */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
|
||||
<div className="glass-card rounded-2xl p-6 shadow-sm">
|
||||
{/* Decision Node */}
|
||||
{currentNode && currentNode.type === 'decision' && (
|
||||
<>
|
||||
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
|
||||
<h2 className="mb-2 text-xl font-semibold text-white">
|
||||
{currentNode.question}
|
||||
</h2>
|
||||
{currentNode.help_text && (
|
||||
<div className="mb-4 text-sm text-muted-foreground">
|
||||
<div className="mb-4 text-sm text-white/50">
|
||||
<MarkdownContent content={currentNode.help_text} />
|
||||
</div>
|
||||
)}
|
||||
@@ -426,13 +426,13 @@ export function TreeNavigationPage() {
|
||||
key={option.id}
|
||||
onClick={() => handleSelectOption(option.id, option.label, option.next_node_id)}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-input p-3 text-left transition-colors',
|
||||
'hover:border-primary hover:bg-accent',
|
||||
'w-full rounded-md border border-white/10 p-3 text-left text-white transition-colors',
|
||||
'hover:border-white/30 hover:bg-white/[0.06]',
|
||||
'flex items-center gap-3'
|
||||
)}
|
||||
>
|
||||
{index < 9 && (
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-muted text-xs font-medium text-muted-foreground">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10 text-xs font-medium text-white/50">
|
||||
{index + 1}
|
||||
</span>
|
||||
)}
|
||||
@@ -443,7 +443,7 @@ export function TreeNavigationPage() {
|
||||
{/* Previously-created custom steps at this node */}
|
||||
{customStepFlow.customSteps.filter(cs => cs.inserted_after_node_id === currentNodeId).length > 0 && (
|
||||
<div className="mt-2 space-y-2">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-white/40">
|
||||
Your Custom Steps
|
||||
</p>
|
||||
{customStepFlow.customSteps
|
||||
@@ -454,13 +454,12 @@ export function TreeNavigationPage() {
|
||||
key={cs.id}
|
||||
onClick={() => customStepFlow.handleNavigateToCustomStep(cs)}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-purple-300 bg-purple-50 p-3 text-left transition-colors',
|
||||
'hover:border-purple-500 hover:bg-purple-100',
|
||||
'dark:border-purple-700 dark:bg-purple-900/20 dark:hover:border-purple-500 dark:hover:bg-purple-900/40',
|
||||
'w-full rounded-md border border-purple-700 bg-purple-900/20 p-3 text-left text-white transition-colors',
|
||||
'hover:border-purple-500 hover:bg-purple-900/40',
|
||||
'flex items-center gap-3'
|
||||
)}
|
||||
>
|
||||
<span className="flex-shrink-0 rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-100">
|
||||
<span className="flex-shrink-0 rounded-full bg-purple-900 px-2 py-0.5 text-xs font-medium text-purple-100">
|
||||
Custom
|
||||
</span>
|
||||
<span>{cs.step_data.title}</span>
|
||||
@@ -472,7 +471,7 @@ export function TreeNavigationPage() {
|
||||
{/* Add Custom Step Button */}
|
||||
<button
|
||||
onClick={() => customStepFlow.setShowCustomStepModal(true)}
|
||||
className="mt-2 inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-sm text-primary hover:bg-primary/10"
|
||||
className="mt-2 inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-sm text-white/50 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
Add Custom Step
|
||||
@@ -482,18 +481,18 @@ export function TreeNavigationPage() {
|
||||
|
||||
{/* Custom Step Node */}
|
||||
{currentCustomStep && (
|
||||
<div className="rounded-lg border border-purple-200 bg-purple-50 p-4 dark:border-purple-800 dark:bg-purple-900/20">
|
||||
<div className="rounded-lg border border-purple-800 bg-purple-900/20 p-4">
|
||||
{/* Custom Step Badge */}
|
||||
<span className="mb-2 inline-block rounded-full bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-100">
|
||||
<span className="mb-2 inline-block rounded-full bg-purple-900 px-2 py-1 text-xs font-medium text-purple-100">
|
||||
Custom Step
|
||||
</span>
|
||||
|
||||
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
|
||||
<h2 className="mb-2 text-xl font-semibold text-white">
|
||||
{currentCustomStep.step_data.title}
|
||||
</h2>
|
||||
|
||||
{currentCustomStep.step_data.content.instructions && (
|
||||
<div className="mb-4 text-muted-foreground">
|
||||
<div className="mb-4 text-white/60">
|
||||
<MarkdownContent content={currentCustomStep.step_data.content.instructions} />
|
||||
</div>
|
||||
)}
|
||||
@@ -506,12 +505,12 @@ export function TreeNavigationPage() {
|
||||
|
||||
{currentCustomStep.step_data.content.commands && currentCustomStep.step_data.content.commands.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 text-sm font-medium text-foreground">Commands:</p>
|
||||
<p className="mb-2 text-sm font-medium text-white">Commands:</p>
|
||||
<div className="space-y-2">
|
||||
{currentCustomStep.step_data.content.commands.map((cmd, index) => (
|
||||
<div key={index}>
|
||||
<p className="mb-1 text-xs text-muted-foreground">{cmd.label}</p>
|
||||
<code className="block rounded bg-muted p-2 text-sm font-mono">
|
||||
<p className="mb-1 text-xs text-white/40">{cmd.label}</p>
|
||||
<code className="block rounded bg-white/10 p-2 text-sm font-mono">
|
||||
{cmd.command}
|
||||
</code>
|
||||
</div>
|
||||
@@ -525,13 +524,13 @@ export function TreeNavigationPage() {
|
||||
const targetNode = findNode(customStepFlow.pendingContinuationNodeId, tree?.tree_structure)
|
||||
const targetLabel = targetNode?.question || targetNode?.title || 'next step'
|
||||
return (
|
||||
<div className="mt-6 border-t border-purple-200 pt-4 dark:border-purple-700">
|
||||
<div className="mt-6 border-t border-purple-700 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={customStepFlow.handleContinueToDescendant}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between rounded-md bg-primary px-4 py-3 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'flex w-full items-center justify-between rounded-md bg-white px-4 py-3 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
<span>Continue to: {targetLabel.length > 50 ? `${targetLabel.slice(0, 50)}...` : targetLabel}</span>
|
||||
@@ -543,16 +542,16 @@ export function TreeNavigationPage() {
|
||||
|
||||
{/* Custom Branch Controls */}
|
||||
{customStepFlow.customBranchMode && (
|
||||
<div className="mt-6 border-t border-purple-200 pt-4 dark:border-purple-700">
|
||||
<p className="mb-3 text-sm text-amber-600 dark:text-amber-400">
|
||||
<div className="mt-6 border-t border-purple-700 pt-4">
|
||||
<p className="mb-3 text-sm text-amber-400">
|
||||
Building custom branch - add steps until the issue is resolved
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
onClick={() => customStepFlow.setShowCustomStepModal(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md border border-input px-4 py-2 text-sm font-medium',
|
||||
'bg-background hover:bg-accent hover:text-accent-foreground'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -578,22 +577,22 @@ export function TreeNavigationPage() {
|
||||
{/* Action Node */}
|
||||
{currentNode && currentNode.type === 'action' && (
|
||||
<>
|
||||
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
|
||||
<h2 className="mb-2 text-xl font-semibold text-white">
|
||||
{currentNode.title}
|
||||
</h2>
|
||||
{currentNode.description && (
|
||||
<div className="mb-4 text-muted-foreground">
|
||||
<div className="mb-4 text-white/60">
|
||||
<MarkdownContent content={currentNode.description} />
|
||||
</div>
|
||||
)}
|
||||
{currentNode.commands && currentNode.commands.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 text-sm font-medium text-foreground">Commands:</p>
|
||||
<p className="mb-2 text-sm font-medium text-white">Commands:</p>
|
||||
<div className="space-y-1">
|
||||
{currentNode.commands.map((cmd, index) => (
|
||||
<code
|
||||
key={index}
|
||||
className="block rounded bg-muted p-2 text-sm font-mono"
|
||||
className="block rounded bg-white/10 p-2 text-sm font-mono"
|
||||
>
|
||||
{cmd}
|
||||
</code>
|
||||
@@ -602,7 +601,7 @@ export function TreeNavigationPage() {
|
||||
</div>
|
||||
)}
|
||||
{currentNode.expected_outcome && (
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
<p className="mb-4 text-sm text-white/40">
|
||||
<strong>Expected outcome:</strong> {currentNode.expected_outcome}
|
||||
</p>
|
||||
)}
|
||||
@@ -610,8 +609,8 @@ export function TreeNavigationPage() {
|
||||
<button
|
||||
onClick={() => handleContinue()}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Continue
|
||||
@@ -624,22 +623,22 @@ export function TreeNavigationPage() {
|
||||
{currentNode && currentNode.type === 'solution' && (
|
||||
<>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<span className="rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800">
|
||||
<span className="rounded-full bg-green-900/30 px-2 py-1 text-xs font-medium text-green-400">
|
||||
Solution
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
|
||||
<h2 className="mb-2 text-xl font-semibold text-white">
|
||||
{currentNode.title}
|
||||
</h2>
|
||||
{currentNode.description && (
|
||||
<div className="mb-4 text-muted-foreground">
|
||||
<div className="mb-4 text-white/60">
|
||||
<MarkdownContent content={currentNode.description} />
|
||||
</div>
|
||||
)}
|
||||
{currentNode.resolution_steps && currentNode.resolution_steps.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 text-sm font-medium text-foreground">Resolution steps:</p>
|
||||
<ol className="list-inside list-decimal space-y-1 text-sm text-muted-foreground">
|
||||
<p className="mb-2 text-sm font-medium text-white">Resolution steps:</p>
|
||||
<ol className="list-inside list-decimal space-y-1 text-sm text-white/40">
|
||||
{currentNode.resolution_steps.map((step, index) => (
|
||||
<li key={index}>{step}</li>
|
||||
))}
|
||||
@@ -660,8 +659,8 @@ export function TreeNavigationPage() {
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
<div className="mt-6 border-t border-border pt-4">
|
||||
<label className="block text-sm font-medium text-foreground">
|
||||
<div className="mt-6 border-t border-white/[0.06] pt-4">
|
||||
<label className="block text-sm font-medium text-white">
|
||||
Notes (optional)
|
||||
</label>
|
||||
<textarea
|
||||
@@ -670,9 +669,9 @@ export function TreeNavigationPage() {
|
||||
placeholder="Add any notes for this step..."
|
||||
rows={2}
|
||||
className={cn(
|
||||
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
|
||||
'text-white placeholder:text-white/40',
|
||||
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -681,7 +680,7 @@ export function TreeNavigationPage() {
|
||||
{pathTaken.length > 1 && (
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className="mt-4 text-sm text-muted-foreground hover:text-foreground"
|
||||
className="mt-4 text-sm text-white/50 hover:text-white"
|
||||
>
|
||||
← Go back
|
||||
</button>
|
||||
@@ -689,7 +688,7 @@ export function TreeNavigationPage() {
|
||||
|
||||
{/* Keyboard Shortcuts Hint */}
|
||||
{currentNode && (
|
||||
<div className="mt-4 border-t border-border pt-3 text-xs text-muted-foreground">
|
||||
<div className="mt-4 border-t border-white/[0.06] pt-3 text-xs text-white/40">
|
||||
<span className="font-medium">Keyboard:</span>{' '}
|
||||
{currentNode.type === 'decision' && currentOptions.length > 0 && (
|
||||
<span>1-{Math.min(currentOptions.length, 9)} select option</span>
|
||||
|
||||
@@ -77,16 +77,16 @@ export function TeamCategoriesPage() {
|
||||
setForm({ name: cat.name, slug: cat.slug, description: cat.description || '' })
|
||||
}
|
||||
|
||||
const inputCn = cn('w-full rounded-md border border-border bg-background px-3 py-2 text-sm', 'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring')
|
||||
const inputCn = cn('w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white', 'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20')
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="font-heading text-2xl font-bold text-foreground">Team Categories</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Manage tree categories for your team</p>
|
||||
<h1 className="text-2xl font-bold text-white">Team Categories</h1>
|
||||
<p className="mt-1 text-sm text-white/40">Manage tree categories for your team</p>
|
||||
</div>
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Category
|
||||
</button>
|
||||
@@ -95,30 +95,30 @@ export function TeamCategoriesPage() {
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="h-16 animate-pulse rounded-lg bg-muted" />
|
||||
<div key={i} className="h-16 animate-pulse rounded-lg bg-white/10" />
|
||||
))}
|
||||
</div>
|
||||
) : categories.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-border bg-card py-16">
|
||||
<FolderTree className="h-12 w-12 text-muted-foreground/50" />
|
||||
<h3 className="mt-4 font-medium text-foreground">No team categories</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Create categories to organize your team's trees.</p>
|
||||
<div className="flex flex-col items-center justify-center glass-card rounded-2xl py-16">
|
||||
<FolderTree className="h-12 w-12 text-white/30" />
|
||||
<h3 className="mt-4 font-medium text-white">No team categories</h3>
|
||||
<p className="mt-1 text-sm text-white/40">Create categories to organize your team's trees.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{categories.map((cat) => (
|
||||
<div key={cat.id} className="flex items-center justify-between rounded-lg border border-border bg-card px-4 py-3">
|
||||
<div key={cat.id} className="flex items-center justify-between rounded-lg border border-white/[0.06] px-4 py-3">
|
||||
<div>
|
||||
<span className="font-medium text-foreground">{cat.name}</span>
|
||||
<span className="ml-3 text-sm text-muted-foreground">{cat.slug}</span>
|
||||
{cat.description && <span className="ml-3 text-sm text-muted-foreground">- {cat.description}</span>}
|
||||
<span className="ml-3 text-xs text-muted-foreground">{cat.tree_count} trees</span>
|
||||
<span className="font-medium text-white">{cat.name}</span>
|
||||
<span className="ml-3 text-sm text-white/40">{cat.slug}</span>
|
||||
{cat.description && <span className="ml-3 text-sm text-white/40">- {cat.description}</span>}
|
||||
<span className="ml-3 text-xs text-white/40">{cat.tree_count} trees</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => openEdit(cat)} className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground">
|
||||
<button onClick={() => openEdit(cat)} className="rounded-md p-1.5 text-white/50 hover:bg-white/[0.06] hover:text-white">
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(cat.id)} className="rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive">
|
||||
<button onClick={() => handleDelete(cat.id)} className="rounded-md p-1.5 text-white/50 hover:bg-red-400/10 hover:text-red-400">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -131,22 +131,22 @@ export function TeamCategoriesPage() {
|
||||
<Modal isOpen={createOpen} onClose={() => setCreateOpen(false)} title="Create Category" size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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-white">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" className={inputCn} />
|
||||
</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-white">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} />
|
||||
</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-white">Description</label>
|
||||
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,22 +156,22 @@ export function TeamCategoriesPage() {
|
||||
<Modal isOpen={!!editCategory} onClose={() => setEditCategory(null)} title="Edit Category" size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setEditCategory(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Save</button>
|
||||
<button onClick={() => setEditCategory(null)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Save</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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-white">Name</label>
|
||||
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={inputCn} />
|
||||
</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-white">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} />
|
||||
</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-white">Description</label>
|
||||
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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-white/50 hover:text-white"
|
||||
>
|
||||
{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-white">{log.action}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'resource',
|
||||
header: 'Resource',
|
||||
render: (log) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
{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-white/40">{log.user_email || 'System'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
header: 'Time',
|
||||
render: (log) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
{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-card-foreground hover:bg-accent'
|
||||
'flex items-center gap-2 rounded-md border border-white/10 px-4 py-2 text-sm font-medium',
|
||||
'text-white/60 hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<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-background px-3 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'h-9 rounded-md border border-white/10 bg-black/50 px-3 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/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-background px-3 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'h-9 rounded-md border border-white/10 bg-black/50 px-3 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/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-muted/30 p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-foreground">Details</h4>
|
||||
<pre className="overflow-x-auto rounded bg-muted p-3 text-xs text-muted-foreground">
|
||||
<div className="rounded-md border border-white/[0.06] bg-white/[0.02] p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-white">Details</h4>
|
||||
<pre className="overflow-x-auto rounded bg-black/50 p-3 text-xs text-white/40">
|
||||
{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="rounded-lg border border-border bg-card p-6">
|
||||
<div className="glass-card rounded-2xl 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-white/40">{label}</p>
|
||||
<p className="mt-1 text-3xl font-bold text-white">{value}</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-muted/50 p-3 text-muted-foreground">{icon}</div>
|
||||
<div className="rounded-lg bg-white/[0.06] p-3 text-white/50">{icon}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -56,7 +56,7 @@ export function DashboardPage() {
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="h-32 animate-pulse rounded-lg bg-muted" />
|
||||
<div key={i} className="h-32 animate-pulse rounded-lg bg-white/10" />
|
||||
))}
|
||||
</div>
|
||||
) : metrics && (
|
||||
@@ -71,18 +71,18 @@ export function DashboardPage() {
|
||||
{/* Recent Activity */}
|
||||
{activity.length > 0 && (
|
||||
<div>
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Recent Activity</h2>
|
||||
<h2 className="text-lg font-semibold text-white">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 bg-card px-4 py-3 text-sm">
|
||||
<div key={entry.id} className="flex items-center justify-between rounded-md border border-white/[0.06] 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-white">{entry.action}</span>
|
||||
<span className="ml-2 text-white/40">{entry.resource_type}</span>
|
||||
{entry.user_email && (
|
||||
<span className="ml-2 text-muted-foreground">by {entry.user_email}</span>
|
||||
<span className="ml-2 text-white/40">by {entry.user_email}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-xs text-white/40">
|
||||
{new Date(entry.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -93,18 +93,18 @@ export function DashboardPage() {
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Quick Links</h2>
|
||||
<h2 className="text-lg font-semibold text-white">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 rounded-lg border border-border bg-card p-4',
|
||||
'text-sm font-medium text-foreground transition-colors hover:bg-accent'
|
||||
'flex items-center gap-3 glass-card rounded-2xl p-4',
|
||||
'text-sm font-medium text-white transition-colors hover:bg-white/[0.06]'
|
||||
)}
|
||||
>
|
||||
<link.icon className="h-5 w-5 text-muted-foreground" />
|
||||
<link.icon className="h-5 w-5 text-white/50" />
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -93,11 +93,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-white">{f.display_name}</div>
|
||||
<div className="text-xs text-white/40">{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-white/40">{f.description || '-'}</span> },
|
||||
...PLANS.map(plan => ({
|
||||
key: plan,
|
||||
header: plan.charAt(0).toUpperCase() + plan.slice(1),
|
||||
@@ -109,7 +109,7 @@ export function FeatureFlagsPage() {
|
||||
onClick={() => handleTogglePlan(f.id, plan, enabled)}
|
||||
className={cn(
|
||||
'h-6 w-10 rounded-full transition-colors',
|
||||
enabled ? 'bg-green-500' : 'bg-muted'
|
||||
enabled ? 'bg-emerald-400' : 'bg-white/10'
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
@@ -131,10 +131,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-white">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
|
||||
{ key: 'flag', header: 'Flag', render: (o) => <span className="text-sm text-white/40">{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-white/40">{o.note || '-'}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (o) => (
|
||||
@@ -145,7 +145,7 @@ export function FeatureFlagsPage() {
|
||||
},
|
||||
]
|
||||
|
||||
const inputCn = cn('w-full rounded-md border border-border bg-background px-3 py-2 text-sm', 'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring')
|
||||
const inputCn = cn('w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white', 'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20')
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -153,7 +153,7 @@ export function FeatureFlagsPage() {
|
||||
title="Feature Flags"
|
||||
description="Manage feature availability per plan and account"
|
||||
action={
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Flag
|
||||
</button>
|
||||
@@ -161,7 +161,7 @@ export function FeatureFlagsPage() {
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Feature Matrix</h2>
|
||||
<h2 className="text-lg font-semibold text-white">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." />}
|
||||
@@ -171,8 +171,8 @@ export function FeatureFlagsPage() {
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Account Overrides</h2>
|
||||
<button onClick={() => setOverrideOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
|
||||
<h2 className="text-lg font-semibold text-white">Account Overrides</h2>
|
||||
<button onClick={() => setOverrideOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Override
|
||||
</button>
|
||||
@@ -188,22 +188,22 @@ export function FeatureFlagsPage() {
|
||||
<Modal isOpen={createOpen} onClose={() => setCreateOpen(false)} title="Create Feature Flag" size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!createForm.flag_key || !createForm.display_name} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!createForm.flag_key || !createForm.display_name} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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-white">Flag Key</label>
|
||||
<input type="text" value={createForm.flag_key} onChange={(e) => setCreateForm({ ...createForm, flag_key: e.target.value })} placeholder="e.g. custom_branding" className={inputCn} />
|
||||
</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-white">Display Name</label>
|
||||
<input type="text" value={createForm.display_name} onChange={(e) => setCreateForm({ ...createForm, display_name: e.target.value })} placeholder="e.g. Custom Branding" className={inputCn} />
|
||||
</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-white">Description</label>
|
||||
<input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,29 +213,29 @@ export function FeatureFlagsPage() {
|
||||
<Modal isOpen={overrideOpen} onClose={() => setOverrideOpen(false)} title="Add Account Override" size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setOverrideOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code || !overrideForm.flag_id} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
|
||||
<button onClick={() => setOverrideOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code || !overrideForm.flag_id} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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-white">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" className={inputCn} />
|
||||
</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-white">Feature Flag</label>
|
||||
<select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={inputCn}>
|
||||
<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-white/10" />
|
||||
<label htmlFor="override-enabled" className="text-sm font-medium text-white">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-white">Note</label>
|
||||
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,10 +72,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-white">{c.name}</span> },
|
||||
{ key: 'slug', header: 'Slug', render: (c) => <span className="text-sm text-white/40">{c.slug}</span> },
|
||||
{ key: 'description', header: 'Description', render: (c) => <span className="text-sm text-white/40">{c.description || '-'}</span> },
|
||||
{ key: 'tree_count', header: 'Trees', render: (c) => <span className="text-sm text-white/40">{c.tree_count}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (c) => (
|
||||
@@ -87,7 +87,7 @@ export function GlobalCategoriesPage() {
|
||||
},
|
||||
]
|
||||
|
||||
const inputCn = cn('w-full rounded-md border border-border bg-background px-3 py-2 text-sm', 'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring')
|
||||
const inputCn = cn('w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white', 'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20')
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -95,7 +95,7 @@ export function GlobalCategoriesPage() {
|
||||
title="Global Categories"
|
||||
description="Manage tree categories available to all accounts"
|
||||
action={
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
|
||||
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Category
|
||||
</button>
|
||||
@@ -118,22 +118,22 @@ export function GlobalCategoriesPage() {
|
||||
size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
|
||||
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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-white">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" className={inputCn} />
|
||||
</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-white">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} />
|
||||
</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-white">Description</label>
|
||||
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,22 +147,22 @@ export function GlobalCategoriesPage() {
|
||||
size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setEditCategory(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Save</button>
|
||||
<button onClick={() => setEditCategory(null)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Save</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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-white">Name</label>
|
||||
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Networking" className={inputCn} />
|
||||
</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-white">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} />
|
||||
</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-white">Description</label>
|
||||
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function InviteCodesPage() {
|
||||
key: 'code',
|
||||
header: 'Code',
|
||||
render: (c) => (
|
||||
<code className="rounded bg-muted px-2 py-1 text-sm font-mono">{c.code}</code>
|
||||
<code className="rounded bg-white/10 px-2 py-1 text-sm font-mono text-white/70">{c.code}</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -89,7 +89,7 @@ export function InviteCodesPage() {
|
||||
key: 'expires_at',
|
||||
header: 'Expires',
|
||||
render: (c) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
{c.expires_at ? new Date(c.expires_at).toLocaleDateString() : 'Never'}
|
||||
</span>
|
||||
),
|
||||
@@ -98,7 +98,7 @@ export function InviteCodesPage() {
|
||||
key: 'created_at',
|
||||
header: 'Created',
|
||||
render: (c) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
{new Date(c.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
@@ -135,7 +135,7 @@ export function InviteCodesPage() {
|
||||
onClick={() => setCreateOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium',
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||
'bg-white text-black hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -167,13 +167,13 @@ export function InviteCodesPage() {
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setCreateOpen(false)}
|
||||
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent"
|
||||
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
@@ -182,15 +182,15 @@ export function InviteCodesPage() {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<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-white">Expires in (days)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={expiresInDays}
|
||||
onChange={(e) => setExpiresInDays(e.target.value)}
|
||||
placeholder="Leave empty for no expiry"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -75,16 +75,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-white capitalize">{p.plan}</span> },
|
||||
{ key: 'max_trees', header: 'Max Trees', render: (p) => <span className="text-sm text-white/40">{p.max_trees ?? 'Unlimited'}</span> },
|
||||
{ key: 'max_sessions', header: 'Sessions/Month', render: (p) => <span className="text-sm text-white/40">{p.max_sessions_per_month ?? 'Unlimited'}</span> },
|
||||
{ key: 'max_users', header: 'Max Users', render: (p) => <span className="text-sm text-white/40">{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-white/50 hover:bg-white/[0.06] hover:text-white"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
@@ -93,11 +93,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-white">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
|
||||
{ key: 'max_trees', header: 'Max Trees', render: (o) => <span className="text-sm text-white/40">{o.override_max_trees ?? '-'}</span> },
|
||||
{ key: 'max_sessions', header: 'Sessions/Month', render: (o) => <span className="text-sm text-white/40">{o.override_max_sessions_per_month ?? '-'}</span> },
|
||||
{ key: 'max_users', header: 'Max Users', render: (o) => <span className="text-sm text-white/40">{o.override_max_users ?? '-'}</span> },
|
||||
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-white/40">{o.note || '-'}</span> },
|
||||
{
|
||||
key: 'actions', header: '', className: 'w-12',
|
||||
render: (o) => (
|
||||
@@ -109,8 +109,8 @@ export function PlanLimitsPage() {
|
||||
]
|
||||
|
||||
const inputCn = cn(
|
||||
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -118,7 +118,7 @@ export function PlanLimitsPage() {
|
||||
<PageHeader title="Plan Limits" description="Configure plan tier limits and account-specific overrides" />
|
||||
|
||||
<div>
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Plan Defaults</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Plan Defaults</h2>
|
||||
<div className="mt-3">
|
||||
<DataTable columns={planColumns} data={plans} keyExtractor={(p) => p.plan} isLoading={loading} />
|
||||
</div>
|
||||
@@ -126,10 +126,10 @@ export function PlanLimitsPage() {
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-heading text-lg font-semibold text-foreground">Account Overrides</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Account Overrides</h2>
|
||||
<button
|
||||
onClick={() => setCreateOverride(true)}
|
||||
className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}
|
||||
className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Override
|
||||
@@ -154,23 +154,23 @@ export function PlanLimitsPage() {
|
||||
size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setEditPlan(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleSavePlan} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">Save</button>
|
||||
<button onClick={() => setEditPlan(null)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleSavePlan} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90">Save</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{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-white">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 })} className={inputCn} />
|
||||
</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-white">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 })} className={inputCn} />
|
||||
</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-white">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 })} className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,30 +185,30 @@ export function PlanLimitsPage() {
|
||||
size="sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setCreateOverride(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
|
||||
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
|
||||
<button onClick={() => setCreateOverride(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
|
||||
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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-white">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" className={inputCn} />
|
||||
</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-white">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 })} className={inputCn} />
|
||||
</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-white">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 })} className={inputCn} />
|
||||
</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-white">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 })} className={inputCn} />
|
||||
</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-white">Note</label>
|
||||
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" className={inputCn} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function SettingsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="Platform Settings" description="Global platform configuration" />
|
||||
<div className="h-40 animate-pulse rounded-lg bg-muted" />
|
||||
<div className="h-40 animate-pulse rounded-lg bg-white/10" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -45,11 +45,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 rounded-lg border border-border bg-card p-6">
|
||||
<div className="max-w-xl space-y-6 glass-card rounded-2xl p-6">
|
||||
<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-white">Maintenance Mode</h3>
|
||||
<p className="text-sm text-white/40">
|
||||
When enabled, users will see a maintenance message instead of the app.
|
||||
</p>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ export function SettingsPage() {
|
||||
onClick={() => setSettings({ ...settings, maintenance_mode: !maintenanceMode })}
|
||||
className={cn(
|
||||
'h-6 w-10 rounded-full transition-colors',
|
||||
maintenanceMode ? 'bg-destructive' : 'bg-muted'
|
||||
maintenanceMode ? 'bg-red-400' : 'bg-white/10'
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
@@ -69,27 +69,27 @@ 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-white">Maintenance Message</label>
|
||||
<textarea
|
||||
value={maintenanceMessage}
|
||||
onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })}
|
||||
rows={3}
|
||||
placeholder="We're performing scheduled maintenance. Please check back later."
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-border pt-4">
|
||||
<div className="border-t border-white/[0.06] pt-4">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className={cn(
|
||||
'rounded-md px-4 py-2 text-sm font-medium',
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
'bg-white text-black hover:bg-white/90',
|
||||
'disabled:opacity-50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -98,8 +98,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-white">{u.name}</div>
|
||||
<div className="text-xs text-white/40">{u.email}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -129,7 +129,7 @@ export function UsersPage() {
|
||||
header: 'Joined',
|
||||
sortable: true,
|
||||
render: (u) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-sm text-white/40">
|
||||
{new Date(u.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
@@ -197,13 +197,13 @@ export function UsersPage() {
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setRoleModalUser(null)}
|
||||
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent"
|
||||
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRoleChange}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@@ -211,15 +211,15 @@ 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-white/70">
|
||||
Changing role for <span className="font-medium text-white">{roleModalUser?.name}</span>
|
||||
</p>
|
||||
<select
|
||||
value={newRole}
|
||||
onChange={(e) => setNewRole(e.target.value)}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
>
|
||||
<option value="engineer">Engineer</option>
|
||||
@@ -238,14 +238,14 @@ export function UsersPage() {
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setMoveModalUser(null)}
|
||||
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent"
|
||||
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMoveAccount}
|
||||
disabled={!displayCode}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50"
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
@@ -253,19 +253,19 @@ 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-white/70">
|
||||
Moving <span className="font-medium text-white">{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-white">Account Display Code</label>
|
||||
<input
|
||||
type="text"
|
||||
value={displayCode}
|
||||
onChange={(e) => setDisplayCode(e.target.value)}
|
||||
placeholder="e.g. ABC-1234"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
|
||||
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user