refactor: clean up dead files, restructure nav, align QuickStartPage to monochrome
- Delete 6 unused files: ThemeToggle, themeStore, AppLayout-original, QuickStartPage-Enhanced, UpgradePrompt, SettingsPage - Restructure Account/Settings navigation: merge Settings into Account page, make AccountSettingsPage the /account index, remove orphaned /account-settings route - Remove "Settings" nav item (consolidated under Account) - Add export preferences and team categories link to AccountSettingsPage - Align QuickStartPage to monochrome design system: replace cyan/blue/violet accent colors with white opacity variants - Replace non-functional search button with search spinner indicator Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
import { Sun, Moon, Monitor } from 'lucide-react'
|
||||
import { useThemeStore } from '@/store/themeStore'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useThemeStore()
|
||||
|
||||
const options = [
|
||||
{ value: 'light' as const, icon: Sun, label: 'Light' },
|
||||
{ value: 'dark' as const, icon: Moon, label: 'Dark' },
|
||||
{ value: 'system' as const, icon: Monitor, label: 'System' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex items-center rounded-md border border-input bg-background p-1">
|
||||
{options.map(({ value, icon: Icon, label }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => setTheme(value)}
|
||||
className={cn(
|
||||
'rounded p-1.5 transition-colors',
|
||||
theme === value
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
aria-label={`Switch to ${label} theme`}
|
||||
aria-pressed={theme === value}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThemeToggle
|
||||
@@ -1,32 +0,0 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubscription } from '@/hooks/useSubscription'
|
||||
|
||||
interface UpgradePromptProps {
|
||||
feature: string // e.g., "create more trees", "start more sessions"
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function UpgradePrompt({ feature, className }: UpgradePromptProps) {
|
||||
const { plan } = useSubscription()
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'glass-card rounded-2xl border border-white/[0.06] p-4',
|
||||
className
|
||||
)}>
|
||||
<h3 className="font-semibold text-white">Plan Limit Reached</h3>
|
||||
<p className="mt-1 text-sm text-white/70">
|
||||
Your {plan} plan doesn't allow you to {feature}. Upgrade your plan to continue.
|
||||
</p>
|
||||
<button
|
||||
className={cn(
|
||||
'mt-3 rounded-xl bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
onClick={() => window.location.href = '/account'}
|
||||
>
|
||||
View Plans
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { ThemeToggle } from '@/components/common/ThemeToggle'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function AppLayout() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { user, logout } = useAuthStore()
|
||||
const { effectiveRole, isSuperAdmin } = usePermissions()
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
const handleLogout = async () => {
|
||||
setMobileMenuOpen(false)
|
||||
await logout()
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
// Close mobile menu on route change - key-based reset
|
||||
const [prevPath, setPrevPath] = useState(location.pathname)
|
||||
if (prevPath !== location.pathname) {
|
||||
setPrevPath(location.pathname)
|
||||
if (mobileMenuOpen) setMobileMenuOpen(false)
|
||||
}
|
||||
|
||||
// Close on Escape
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setMobileMenuOpen(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (mobileMenuOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
}, [mobileMenuOpen, handleKeyDown])
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: 'Home' },
|
||||
{ path: '/trees', label: 'Trees' },
|
||||
{ path: '/my-trees', label: 'My Trees' },
|
||||
{ path: '/sessions', label: 'Sessions' },
|
||||
{ path: '/account', label: 'Account' },
|
||||
{ path: '/settings', label: 'Settings' },
|
||||
...(isSuperAdmin ? [{ path: '/admin', label: 'Admin Panel' }] : []),
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-card backdrop-blur-sm">
|
||||
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
||||
<div className="flex items-center gap-8">
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground sm:hidden"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<BrandLogo size="sm" />
|
||||
<BrandWordmark size="sm" />
|
||||
</Link>
|
||||
<nav className="hidden items-center gap-1 sm:flex">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'relative rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
(item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path))
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="hidden text-sm text-muted-foreground sm:block">
|
||||
{user?.name || user?.email}
|
||||
</span>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span
|
||||
className={cn(
|
||||
'hidden rounded-full px-2 py-0.5 text-xs font-medium sm:inline-block',
|
||||
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'hidden rounded-md px-3 py-1.5 text-sm font-medium sm:block',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Nav Drawer */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="fixed inset-0 z-50 sm:hidden">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-background/80 backdrop-blur-sm animate-fade-in"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<nav className="absolute inset-y-0 left-0 w-72 border-r border-border bg-card shadow-xl animate-slide-in-left">
|
||||
<div className="flex h-16 items-center justify-between border-b border-border px-4">
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<BrandLogo size="sm" />
|
||||
<BrandWordmark size="sm" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col p-4">
|
||||
{/* User info */}
|
||||
<div className="mb-4 border-b border-border pb-4">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{user?.name || user?.email}
|
||||
</p>
|
||||
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||
<span
|
||||
className={cn(
|
||||
'mt-1 inline-block rounded-full px-2 py-0.5 text-xs font-medium',
|
||||
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
effectiveRole === 'owner' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||
)}
|
||||
>
|
||||
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||
effectiveRole === 'owner' ? 'Owner' :
|
||||
'Viewer'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nav items */}
|
||||
<div className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={cn(
|
||||
'block rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
|
||||
(item.path === '/' ? location.pathname === '/' : location.pathname.startsWith(item.path))
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<div className="mt-4 border-t border-border pt-4">
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<span className="text-sm text-muted-foreground">Theme</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="mt-2">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={cn(
|
||||
'w-full rounded-md px-3 py-2.5 text-left text-sm font-medium',
|
||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="animate-fade-in">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppLayout
|
||||
@@ -50,7 +50,6 @@ export function AppLayout() {
|
||||
{ path: '/my-trees', label: 'My Trees' },
|
||||
{ path: '/sessions', label: 'Sessions' },
|
||||
{ path: '/account', label: 'Account' },
|
||||
{ path: '/settings', label: 'Settings' },
|
||||
...(isSuperAdmin ? [{ path: '/admin', label: 'Admin Panel' }] : []),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree } from 'lucide-react'
|
||||
import { accountsApi } from '@/api'
|
||||
import type { Account, AccountMember, AccountInvite } from '@/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { useSubscription } from '@/hooks/useSubscription'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import { CheckoutButton } from '@/components/subscription/CheckoutButton'
|
||||
import { toast } from '@/lib/toast'
|
||||
|
||||
export function AccountSettingsPage() {
|
||||
const { isAccountOwner } = usePermissions()
|
||||
const { plan, limits, usage } = useSubscription()
|
||||
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
|
||||
const subscription = useAuthStore((s) => s.subscription)
|
||||
|
||||
const [account, setAccount] = useState<Account | null>(null)
|
||||
@@ -442,6 +446,60 @@ export function AccountSettingsPage() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Team Categories Link (owners only) */}
|
||||
{isAccountOwner && (
|
||||
<Link
|
||||
to="/account/categories"
|
||||
className="glass-card rounded-2xl p-4 sm:p-6 flex items-center justify-between group hover:border-white/20 transition-all"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderTree className="h-5 w-5 text-white/50" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">Team Categories</h2>
|
||||
<p className="text-sm text-white/40">Manage tree categories for your team</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-white/30 group-hover:text-white transition-colors">→</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Preferences Section */}
|
||||
<div className="glass-card rounded-2xl p-4 sm:p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 text-white/50" />
|
||||
<h2 className="text-lg font-semibold text-white">Preferences</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<label
|
||||
htmlFor="export-format"
|
||||
className="block text-sm font-medium text-white"
|
||||
>
|
||||
Default Export Format
|
||||
</label>
|
||||
<p className="text-sm text-white/40">
|
||||
This format will be pre-selected when exporting sessions
|
||||
</p>
|
||||
<select
|
||||
id="export-format"
|
||||
value={defaultExportFormat}
|
||||
onChange={(e) => {
|
||||
setDefaultExportFormat(e.target.value as 'markdown' | 'text' | 'html')
|
||||
toast.success('Preference saved')
|
||||
}}
|
||||
className={cn(
|
||||
'mt-2 block w-full max-w-xs 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>
|
||||
<option value="text">Plain Text (.txt)</option>
|
||||
<option value="html">HTML (.html)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
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
|
||||
@@ -112,7 +112,7 @@ export function QuickStartPage() {
|
||||
<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" />
|
||||
<Sparkles className="w-4 h-4 text-white/50" />
|
||||
<span className="text-sm text-white/70 font-medium">DECISION TREE PLATFORM</span>
|
||||
</div>
|
||||
|
||||
@@ -132,7 +132,7 @@ export function QuickStartPage() {
|
||||
<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" />
|
||||
<Search className="ml-5 w-5 h-5 text-white/40" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
@@ -142,13 +142,8 @@ export function QuickStartPage() {
|
||||
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>
|
||||
{isSearching && (
|
||||
<Loader2 className="mr-4 h-5 w-5 animate-spin text-white/30" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,7 +197,7 @@ export function QuickStartPage() {
|
||||
<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" />
|
||||
<Play className="w-6 h-6 text-white/50" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-white/70 font-semibold uppercase tracking-wider mb-1">
|
||||
@@ -258,7 +253,7 @@ export function QuickStartPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Play className="mt-0.5 h-4 w-4 flex-shrink-0 text-violet-400" />
|
||||
<Play className="mt-0.5 h-4 w-4 flex-shrink-0 text-white/50" />
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-1.5 text-xs text-white/30">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
@@ -292,7 +287,7 @@ export function QuickStartPage() {
|
||||
>
|
||||
<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" />
|
||||
<Search className="w-5 h-5 text-white/40" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="truncate text-sm font-bold text-white mb-2">
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Settings } from 'lucide-react'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
|
||||
export function SettingsPage() {
|
||||
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
|
||||
|
||||
const handleExportFormatChange = (format: 'markdown' | 'text' | 'html') => {
|
||||
setDefaultExportFormat(format)
|
||||
toast.success('Preferences saved successfully')
|
||||
}
|
||||
|
||||
return (
|
||||
<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-white/50" />
|
||||
<h1 className="text-2xl font-bold text-white sm:text-3xl">Settings</h1>
|
||||
</div>
|
||||
<p className="mt-2 text-white/40">
|
||||
Manage your application preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-2xl space-y-6">
|
||||
{/* Export Preferences Section */}
|
||||
<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 text-sm font-medium text-white"
|
||||
>
|
||||
Default Export Format
|
||||
</label>
|
||||
<p className="text-sm text-white/40">
|
||||
This format will be pre-selected when exporting sessions
|
||||
</p>
|
||||
<select
|
||||
id="export-format"
|
||||
value={defaultExportFormat}
|
||||
onChange={(e) => handleExportFormatChange(e.target.value as 'markdown' | 'text' | 'html')}
|
||||
className={cn(
|
||||
'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>
|
||||
<option value="text">Plain Text (.txt)</option>
|
||||
<option value="html">HTML (.html)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* About Section */}
|
||||
<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-white/40">
|
||||
Transform troubleshooting into guided workflows
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsPage
|
||||
@@ -6,6 +6,5 @@ export { default as TreeNavigationPage } from './TreeNavigationPage'
|
||||
export { default as TreeEditorPage } from './TreeEditorPage'
|
||||
export { default as SessionHistoryPage } from './SessionHistoryPage'
|
||||
export { default as SessionDetailPage } from './SessionDetailPage'
|
||||
export { default as SettingsPage } from './SettingsPage'
|
||||
export { default as AccountSettingsPage } from './AccountSettingsPage'
|
||||
export { default as AdminCategoriesPage } from './AdminCategoriesPage'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createBrowserRouter, Navigate } from 'react-router-dom'
|
||||
import { createBrowserRouter } from 'react-router-dom'
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { AppLayout, ProtectedRoute } from '@/components/layout'
|
||||
import { RouteError } from '@/components/common/RouteError'
|
||||
@@ -16,7 +16,6 @@ const TreeNavigationPage = lazy(() => import('@/pages/TreeNavigationPage'))
|
||||
const TreeEditorPage = lazy(() => import('@/pages/TreeEditorPage'))
|
||||
const SessionHistoryPage = lazy(() => import('@/pages/SessionHistoryPage'))
|
||||
const SessionDetailPage = lazy(() => import('@/pages/SessionDetailPage'))
|
||||
const SettingsPage = lazy(() => import('@/pages/SettingsPage'))
|
||||
const AccountSettingsPage = lazy(() => import('@/pages/AccountSettingsPage'))
|
||||
// Admin pages
|
||||
const AdminLayout = lazy(() => import('@/components/admin/AdminLayout'))
|
||||
@@ -117,22 +116,6 @@ export const router = createBrowserRouter([
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
element: (
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<SettingsPage />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'account-settings',
|
||||
element: (
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<AccountSettingsPage />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
// Admin routes
|
||||
{
|
||||
path: 'admin',
|
||||
@@ -215,21 +198,25 @@ export const router = createBrowserRouter([
|
||||
path: 'account',
|
||||
element: (
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<ProtectedRoute requiredRole="owner">
|
||||
<AccountLayout />
|
||||
</ProtectedRoute>
|
||||
<AccountLayout />
|
||||
</Suspense>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to="/account/categories" replace />,
|
||||
element: (
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<AccountSettingsPage />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
element: (
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<TeamCategoriesPage />
|
||||
<ProtectedRoute requiredRole="owner">
|
||||
<TeamCategoriesPage />
|
||||
</ProtectedRoute>
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system'
|
||||
|
||||
interface ThemeState {
|
||||
theme: Theme
|
||||
resolvedTheme: 'light' | 'dark'
|
||||
setTheme: (theme: Theme) => void
|
||||
}
|
||||
|
||||
const getSystemTheme = (): 'light' | 'dark' => {
|
||||
if (typeof window === 'undefined') return 'light'
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
const applyTheme = (theme: Theme): 'light' | 'dark' => {
|
||||
const resolved = theme === 'system' ? getSystemTheme() : theme
|
||||
const root = document.documentElement
|
||||
|
||||
if (resolved === 'dark') {
|
||||
root.classList.add('dark')
|
||||
} else {
|
||||
root.classList.remove('dark')
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
theme: 'system',
|
||||
resolvedTheme: getSystemTheme(),
|
||||
|
||||
setTheme: (theme: Theme) => {
|
||||
const resolvedTheme = applyTheme(theme)
|
||||
set({ theme, resolvedTheme })
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'theme-storage',
|
||||
onRehydrateStorage: () => (state) => {
|
||||
// Apply theme on initial load after rehydration
|
||||
if (state) {
|
||||
applyTheme(state.theme)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export default useThemeStore
|
||||
Reference in New Issue
Block a user