refactor: adopt shared Input/Textarea components (#101)
* refactor: adopt shared Input/Textarea components across 15 files Replace 42 raw <input>/<textarea> elements with <Input>/<Textarea> from components/ui/. Consistent focus states, error handling, and styling across all form fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: replace hardcoded rgba/hex colors with Tailwind tokens - rgba(255,255,255,0.xx) → bg-white/[0.xx], border-white/[0.xx] - rgba(6,182,212,0.3) → border-primary/30 (focus states) - #0a0a0a → bg-background - Inline style hex colors → var(--color-primary), var(--color-brand-gradient-to) - 28 files updated, zero hardcoded rgba() patterns remaining Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add PageMeta to 16 pages for SEO and proper browser tab titles Public pages (Login, Register, Forgot/Reset Password, Verify Email, Survey Thank You) get descriptions for SEO. Authenticated pages (Dashboard, Flow Library, My Flows, Session History, AI Assistant, Account Settings, Step Library, My Shares, Feedback, Guides) get proper tab titles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add page transitions and staggered list animations - ViewTransitionOutlet: wraps Outlet with fade-in-up animation keyed to route path. Sidebar/topbar stay still, only content area animates. - StaggerList: reusable component that cascades children with incremental delay (50ms default). Pure CSS via @utility stagger-item. - Applied stagger to TreeGridView, MyTreesPage cards, SessionHistoryPage. - New stagger-fade-in keyframe in @theme block. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: ViewTransitionOutlet needs h-full for React Flow canvas The wrapper div broke the height chain needed by TreeEditorPage's h-full layout, causing React Flow canvas to collapse to zero height. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: main-content flex layout for tree editor + scrollable pages Main content area is now flex-col so the ViewTransitionOutlet wrapper gets an explicit computed height via flex-1 min-h-0. This makes h-full resolve correctly in the tree editor (React Flow canvas) while still allowing overflow-y-auto scrolling for normal pages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve ESLint errors in Button and Skeleton components - Button: suppress react-refresh/only-export-components for buttonVariants re-export - Skeleton: replace empty interface with type alias, replace Math.random() with static widths array Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add PageMeta, animation classes, and layout fixes to remaining pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #101.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText, UserCog, AlertTriangle, Clock } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { accountsApi } from '@/api/accounts'
|
||||
import type { Account, AccountMember, AccountInvite } from '@/types'
|
||||
import { TransferOwnershipModal } from '@/components/account/TransferOwnershipModal'
|
||||
@@ -159,6 +160,8 @@ export function AccountSettingsPage() {
|
||||
const sub = subscription?.subscription
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Account Settings" />
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -678,6 +681,7 @@ export function AccountSettingsPage() {
|
||||
<DeleteAccountModal onClose={() => setShowDeleteModal(false)} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Sparkles, Send, Loader2, Flag } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { assistantChatApi } from '@/api/assistantChat'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { ChatSidebar } from '@/components/assistant/ChatSidebar'
|
||||
@@ -179,6 +180,8 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="AI Assistant" />
|
||||
<div className="flex h-[calc(100vh-3.5rem)]">
|
||||
{/* Sidebar */}
|
||||
<ChatSidebar
|
||||
@@ -223,7 +226,7 @@ export default function AssistantChatPage() {
|
||||
<div className="w-8 h-8 rounded-full bg-primary/15 flex items-center justify-center">
|
||||
<Sparkles size={14} className="text-primary" />
|
||||
</div>
|
||||
<div className="bg-[rgba(255,255,255,0.04)] border border-brand-border rounded-2xl px-4 py-3">
|
||||
<div className="bg-white/[0.04] border border-brand-border rounded-2xl px-4 py-3">
|
||||
<Loader2 size={16} className="animate-spin text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,7 +245,7 @@ export default function AssistantChatPage() {
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask about IT, networking, troubleshooting..."
|
||||
rows={3}
|
||||
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]"
|
||||
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-hidden focus:border-primary/30"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
disabled={loading}
|
||||
/>
|
||||
@@ -303,5 +306,6 @@ export default function AssistantChatPage() {
|
||||
chatTitle={chats.find(c => c.id === activeChatId)?.title ?? 'Chat'}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function BatchStatusPage() {
|
||||
const progressPercent = total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-3xl space-y-6 px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="overflow-y-auto h-full container mx-auto max-w-3xl space-y-6 px-4 py-6 sm:px-6 sm:py-8">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { MessageSquareText, Send, CheckCircle2, ChevronDown } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { feedbackApi } from '@/api'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -130,6 +131,8 @@ export function FeedbackPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full">
|
||||
<PageMeta title="Feedback" />
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
{/* Page header */}
|
||||
<div className="mb-8">
|
||||
@@ -273,6 +276,7 @@ export function FeedbackPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function ForgotPasswordPage() {
|
||||
@@ -25,6 +26,8 @@ export function ForgotPasswordPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Forgot Password" description="Reset your ResolutionFlow password" />
|
||||
<div className="flex min-h-screen items-center justify-center bg-card px-4">
|
||||
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
|
||||
|
||||
@@ -109,6 +112,7 @@ export function ForgotPasswordPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function GuideDetailPage() {
|
||||
|
||||
if (!guide) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center p-6">
|
||||
<div className="flex flex-col items-center justify-center h-full text-center p-6 overflow-y-auto">
|
||||
<h2 className="text-lg font-heading font-semibold text-foreground mb-2">Guide Not Found</h2>
|
||||
<p className="text-sm text-muted-foreground mb-4">The guide you're looking for doesn't exist.</p>
|
||||
<Link
|
||||
@@ -25,7 +25,7 @@ export default function GuideDetailPage() {
|
||||
const Icon = guide.icon
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-3xl mx-auto">
|
||||
<div className="overflow-y-auto h-full p-6 max-w-3xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-1.5 text-xs text-muted-foreground mb-6">
|
||||
<Link to="/guides" className="hover:text-primary transition-colors">
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { BookOpen } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { guides } from '@/data/guides'
|
||||
import { GuideCard } from '@/components/guides/GuideCard'
|
||||
|
||||
export default function GuidesHubPage() {
|
||||
return (
|
||||
<div className="overflow-y-auto h-full">
|
||||
<PageMeta title="Guides" />
|
||||
<div className="p-6 max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
@@ -25,5 +28,6 @@ export default function GuidesHubPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { PasswordInput } from '@/components/common/PasswordInput'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function LoginPage() {
|
||||
@@ -40,6 +41,8 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Sign In" description="Sign in to your ResolutionFlow account" />
|
||||
<div className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||
{/* Atmosphere orbs */}
|
||||
<div
|
||||
@@ -106,7 +109,7 @@ export function LoginPage() {
|
||||
className={cn(
|
||||
'block w-full rounded-[10px] border border-border bg-card px-3 py-2.5',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'transition-colors'
|
||||
)}
|
||||
placeholder="you@example.com"
|
||||
@@ -127,7 +130,7 @@ export function LoginPage() {
|
||||
className={cn(
|
||||
'block w-full rounded-[10px] border border-border bg-card px-3 py-2.5',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
||||
'transition-colors'
|
||||
)}
|
||||
placeholder="••••••••••"
|
||||
@@ -164,6 +167,7 @@ export function LoginPage() {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function MaintenanceFlowDetailPage() {
|
||||
const hasActiveBatch = recentSessions.some(s => s.started_at && !s.completed_at && s.batch_id)
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl space-y-6 px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="overflow-y-auto h-full container mx-auto max-w-4xl space-y-6 px-4 py-6 sm:px-6 sm:py-8">
|
||||
{/* Active batch banner */}
|
||||
{hasActiveBatch && !bannerDismissed && (
|
||||
<ActiveBatchBanner
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { Globe, Users, Copy, Check, Link2, ExternalLink, Trash2, ArrowLeft } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
@@ -121,6 +122,8 @@ export default function MySharesPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full">
|
||||
<PageMeta title="My Shares" />
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
{/* Back link */}
|
||||
<Link
|
||||
@@ -241,5 +244,6 @@ export default function MySharesPage() {
|
||||
confirmLabel="Revoke"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, ChevronDown, Wrench } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { StaggerList } from '@/components/common/StaggerList'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
@@ -116,6 +118,8 @@ export function MyTreesPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full">
|
||||
<PageMeta title="My Flows" />
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-6 flex items-center justify-between sm:mb-8">
|
||||
<div>
|
||||
@@ -214,7 +218,7 @@ export function MyTreesPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<StaggerList className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{trees.map((tree) => (
|
||||
<div
|
||||
key={tree.id}
|
||||
@@ -349,7 +353,7 @@ export function MyTreesPage() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</StaggerList>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
@@ -389,6 +393,7 @@ export function MyTreesPage() {
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Search, Loader2, Star, ChevronLeft, ChevronRight, GitBranch } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
import type { TreeListItem, TreeFilters } from '@/types'
|
||||
@@ -277,6 +278,8 @@ export function QuickStartPage() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full">
|
||||
<PageMeta title="Dashboard" />
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Greeting */}
|
||||
<div className="fade-in" style={{ animationDelay: '100ms' }}>
|
||||
@@ -646,6 +649,7 @@ export function QuickStartPage() {
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuthStore } from '@/store/authStore'
|
||||
import { inviteApi } from '@/api/invite'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { PasswordInput } from '@/components/common/PasswordInput'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function RegisterPage() {
|
||||
@@ -76,6 +77,8 @@ export function RegisterPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Create Account" description="Create your ResolutionFlow account to start building guided troubleshooting flows" />
|
||||
<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%)]" />
|
||||
@@ -251,6 +254,7 @@ export function RegisterPage() {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { authApi } from '@/api/auth'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { PasswordInput } from '@/components/common/PasswordInput'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function ResetPasswordPage() {
|
||||
@@ -72,6 +73,8 @@ export function ResetPasswordPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Reset Password" description="Set a new password for your ResolutionFlow account" />
|
||||
<div className="flex min-h-screen items-center justify-center bg-black px-4">
|
||||
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
|
||||
|
||||
@@ -180,6 +183,7 @@ export function ResetPasswordPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ export function SessionDetailPage() {
|
||||
const outcomeConfig = session.outcome ? OUTCOME_CONFIG[session.outcome] : null
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="overflow-y-auto h-full container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
{/* Back nav */}
|
||||
<button
|
||||
onClick={() => navigate('/sessions')}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { StaggerList } from '@/components/common/StaggerList'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import type { Session, TreeListItem } from '@/types'
|
||||
@@ -154,6 +156,8 @@ export function SessionHistoryPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full">
|
||||
<PageMeta title="Session History" />
|
||||
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-heading font-bold text-foreground sm:text-3xl">Session History</h1>
|
||||
@@ -210,7 +214,8 @@ export function SessionHistoryPage() {
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<>
|
||||
<StaggerList className="space-y-4">
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
@@ -298,18 +303,20 @@ export function SessionHistoryPage() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{hasMore ? (
|
||||
<p className="text-center text-sm text-muted-foreground py-4">
|
||||
Showing the 50 most recent sessions
|
||||
</p>
|
||||
) : sessions.length > 0 ? (
|
||||
<p className="text-center text-sm text-muted-foreground py-4">
|
||||
Showing all {sessions.length} sessions
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</StaggerList>
|
||||
{hasMore ? (
|
||||
<p className="text-center text-sm text-muted-foreground py-4">
|
||||
Showing the 50 most recent sessions
|
||||
</p>
|
||||
) : sessions.length > 0 ? (
|
||||
<p className="text-center text-sm text-muted-foreground py-4">
|
||||
Showing all {sessions.length} sessions
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Bookmark, Trash2 } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
@@ -87,6 +88,8 @@ export default function StepLibraryPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Step Library" />
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
@@ -167,5 +170,6 @@ export default function StepLibraryPage() {
|
||||
)}
|
||||
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -274,13 +274,13 @@ export default function SurveyPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="pointer-events-none fixed inset-0 overflow-hidden" aria-hidden="true">
|
||||
<div className="absolute -top-32 right-0 h-[500px] w-[500px] rounded-full opacity-[0.03]" style={{ background: 'radial-gradient(circle, #06b6d4, transparent 70%)' }} />
|
||||
<div className="absolute -top-32 right-0 h-[500px] w-[500px] rounded-full opacity-[0.03]" style={{ background: 'radial-gradient(circle, var(--color-primary), transparent 70%)' }} />
|
||||
<div className="absolute -bottom-32 left-0 h-[400px] w-[400px] rounded-full opacity-[0.02]" style={{ background: 'radial-gradient(circle, #a855f7, transparent 70%)' }} />
|
||||
</div>
|
||||
<div className="relative z-10 mx-auto max-w-[680px] px-4 sm:px-5">
|
||||
<div className="text-center pt-20 sm:pt-32 animate-fade-in-up">
|
||||
<div className="w-16 h-16 mx-auto mb-5 rounded-full flex items-center justify-center" style={{ background: 'rgba(6, 182, 212, 0.1)' }}>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
</div>
|
||||
<h2 className="font-heading text-2xl font-bold mb-2.5">Already Submitted</h2>
|
||||
<p className="text-muted-foreground text-sm max-w-[440px] mx-auto leading-relaxed mb-3">
|
||||
@@ -341,7 +341,7 @@ export default function SurveyPage() {
|
||||
{/* Hero — visible only on first slide */}
|
||||
{currentSlide === 0 && !isComplete && (
|
||||
<div className="text-center pt-10 pb-8 sm:pt-[72px] sm:pb-10 animate-fade-in-up">
|
||||
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] sm:text-[11px] font-semibold font-label uppercase tracking-widest mb-4 sm:mb-5" style={{ background: 'rgba(6, 182, 212, 0.1)', border: '1px solid rgba(6, 182, 212, 0.15)', color: '#06b6d4' }}>
|
||||
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] sm:text-[11px] font-semibold font-label uppercase tracking-widest mb-4 sm:mb-5" style={{ background: 'rgba(6, 182, 212, 0.1)', border: '1px solid rgba(6, 182, 212, 0.15)', color: 'var(--color-primary)' }}>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
FlowPilot Research
|
||||
</div>
|
||||
@@ -353,15 +353,15 @@ export default function SurveyPage() {
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-4 sm:gap-7 mt-4 sm:mt-5 text-[11px] sm:text-[12px] text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
||||
~5 minutes
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
Confidential
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
16 questions
|
||||
</span>
|
||||
</div>
|
||||
@@ -376,7 +376,7 @@ export default function SurveyPage() {
|
||||
key={i}
|
||||
className="flex-1 h-1 sm:h-[3px] rounded-full transition-colors duration-300"
|
||||
style={{
|
||||
background: i < currentSlide ? '#34d399' : i === currentSlide ? 'linear-gradient(90deg, #06b6d4, #22d3ee)' : 'var(--color-border)',
|
||||
background: i < currentSlide ? 'oklch(0.76 0.15 163)' : i === currentSlide ? 'linear-gradient(90deg, var(--color-primary), var(--color-brand-gradient-to))' : 'var(--color-border)',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -424,7 +424,7 @@ export default function SurveyPage() {
|
||||
{isComplete && (
|
||||
<div className="text-center pt-10 sm:pt-16 animate-fade-in-up">
|
||||
<div className="w-16 h-16 mx-auto mb-5 rounded-full flex items-center justify-center" style={{ background: 'rgba(52, 211, 153, 0.1)' }}>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#34d399" strokeWidth="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="oklch(0.76 0.15 163)" strokeWidth="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
</div>
|
||||
<h2 className="font-heading text-2xl font-bold mb-2.5">Response Submitted!</h2>
|
||||
<p className="text-muted-foreground text-sm max-w-[440px] mx-auto mb-6 sm:mb-8 leading-relaxed">
|
||||
@@ -445,7 +445,7 @@ export default function SurveyPage() {
|
||||
placeholder="your@email.com"
|
||||
className="flex-1 rounded-[9px] px-3.5 py-2.5 text-sm text-foreground placeholder:text-brand-text-muted focus:outline-hidden"
|
||||
style={{ background: 'rgba(16, 17, 20, 0.6)', border: '1px solid var(--glass-border)' }}
|
||||
onFocus={e => { e.currentTarget.style.borderColor = 'rgba(6, 182, 212, 0.4)' }}
|
||||
onFocus={e => { e.currentTarget.style.borderColor = 'var(--color-primary)' }}
|
||||
onBlur={e => { e.currentTarget.style.borderColor = 'var(--glass-border)' }}
|
||||
disabled={emailSending}
|
||||
/>
|
||||
@@ -519,8 +519,8 @@ export default function SurveyPage() {
|
||||
|
||||
function ScenarioBox({ scenario }: { scenario: { title: string; symptom: string; details: string } }) {
|
||||
return (
|
||||
<div className="rounded-[10px] p-3.5 px-4 sm:p-4 sm:px-5 mb-4 text-[13px]" style={{ background: 'linear-gradient(135deg, rgba(6, 182, 212, 0.06), rgba(139, 92, 246, 0.03))', border: '1px solid rgba(6, 182, 212, 0.12)' }}>
|
||||
<div className="font-label text-[10px] uppercase tracking-widest mb-2 font-semibold" style={{ color: '#06b6d4' }}>{scenario.title}</div>
|
||||
<div className="rounded-[10px] p-3.5 px-4 sm:p-4 sm:px-5 mb-4 text-[13px]" style={{ background: 'linear-gradient(135deg, rgba(6, 182, 212, 0.06), rgba(139, 92, 246, 0.03))', border: '1px solid color-mix(in srgb, var(--color-primary) 12%, transparent)' }}>
|
||||
<div className="font-label text-[10px] uppercase tracking-widest mb-2 font-semibold" style={{ color: 'var(--color-primary)' }}>{scenario.title}</div>
|
||||
<div className="sm:flex gap-2 mb-1">
|
||||
<span className="text-muted-foreground font-medium whitespace-nowrap">Symptom:</span>
|
||||
<span className="text-muted-foreground/80">{scenario.symptom}</span>
|
||||
@@ -535,8 +535,8 @@ function ScenarioBox({ scenario }: { scenario: { title: string; symptom: string;
|
||||
|
||||
function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQuestion; answer?: string | string[]; setAnswer: (id: string, val: string | string[]) => void }) {
|
||||
return (
|
||||
<div className="glass-card-static p-4 sm:p-7 mb-3 sm:mb-4 transition-[border-color] duration-200 focus-within:border-[rgba(6,182,212,0.25)]!">
|
||||
<div className="font-label text-[11px] mb-1.5 font-medium" style={{ color: '#06b6d4' }}>Q{q.num}</div>
|
||||
<div className="glass-card-static p-4 sm:p-7 mb-3 sm:mb-4 transition-[border-color] duration-200 focus-within:border-primary/25!">
|
||||
<div className="font-label text-[11px] mb-1.5 font-medium" style={{ color: 'var(--color-primary)' }}>Q{q.num}</div>
|
||||
<div className="font-heading text-[14px] sm:text-[15px] font-semibold text-foreground leading-snug mb-1">{q.text}</div>
|
||||
{q.hint && <div className="text-[12px] text-muted-foreground mb-3 sm:mb-4 leading-snug">{q.hint}</div>}
|
||||
{!q.hint && <div className="mb-3 sm:mb-4" />}
|
||||
@@ -550,12 +550,12 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
|
||||
className="flex items-start gap-3 px-3.5 py-3 sm:px-4 rounded-[9px] text-left text-[13px] sm:text-sm transition-all duration-150 select-none"
|
||||
style={{
|
||||
background: answer === opt ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)',
|
||||
border: `1px solid ${answer === opt ? '#06b6d4' : 'var(--glass-border)'}`,
|
||||
border: `1px solid ${answer === opt ? 'var(--color-primary)' : 'var(--glass-border)'}`,
|
||||
color: answer === opt ? 'var(--color-foreground)' : 'var(--color-muted-foreground)',
|
||||
}}
|
||||
>
|
||||
<div className="w-[18px] h-[18px] rounded-full shrink-0 flex items-center justify-center transition-all duration-150 mt-0.5" style={{ border: `2px solid ${answer === opt ? '#06b6d4' : 'var(--glass-border)'}` }}>
|
||||
{answer === opt && <div className="w-2 h-2 rounded-full" style={{ background: '#06b6d4' }} />}
|
||||
<div className="w-[18px] h-[18px] rounded-full shrink-0 flex items-center justify-center transition-all duration-150 mt-0.5" style={{ border: `2px solid ${answer === opt ? 'var(--color-primary)' : 'var(--glass-border)'}` }}>
|
||||
{answer === opt && <div className="w-2 h-2 rounded-full" style={{ background: 'var(--color-primary)' }} />}
|
||||
</div>
|
||||
<span className="leading-snug">{opt}</span>
|
||||
</button>
|
||||
@@ -577,11 +577,11 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
|
||||
className="flex items-start gap-3 px-3.5 py-3 sm:px-4 rounded-[9px] text-left text-[13px] sm:text-sm transition-all duration-150 select-none"
|
||||
style={{
|
||||
background: selected ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)',
|
||||
border: `1px solid ${selected ? '#06b6d4' : 'var(--glass-border)'}`,
|
||||
border: `1px solid ${selected ? 'var(--color-primary)' : 'var(--glass-border)'}`,
|
||||
color: selected ? 'var(--color-foreground)' : 'var(--color-muted-foreground)',
|
||||
}}
|
||||
>
|
||||
<div className="w-[18px] h-[18px] rounded-[5px] shrink-0 flex items-center justify-center text-[11px] transition-all duration-150 mt-0.5" style={{ border: `2px solid ${selected ? '#06b6d4' : 'var(--glass-border)'}`, background: selected ? '#06b6d4' : 'transparent', color: selected ? 'white' : 'transparent' }}>
|
||||
<div className="w-[18px] h-[18px] rounded-[5px] shrink-0 flex items-center justify-center text-[11px] transition-all duration-150 mt-0.5" style={{ border: `2px solid ${selected ? 'var(--color-primary)' : 'var(--glass-border)'}`, background: selected ? 'var(--color-primary)' : 'transparent', color: selected ? 'white' : 'transparent' }}>
|
||||
{'\u2713'}
|
||||
</div>
|
||||
<span className="leading-snug">{opt}</span>
|
||||
@@ -605,7 +605,7 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
|
||||
background: 'rgba(16, 17, 20, 0.6)',
|
||||
border: '1px solid var(--glass-border)',
|
||||
}}
|
||||
onFocus={e => { e.currentTarget.style.borderColor = 'rgba(6, 182, 212, 0.4)'; e.currentTarget.style.boxShadow = '0 0 0 3px rgba(6, 182, 212, 0.1)' }}
|
||||
onFocus={e => { e.currentTarget.style.borderColor = 'var(--color-primary)'; e.currentTarget.style.boxShadow = '0 0 0 3px rgba(6, 182, 212, 0.1)' }}
|
||||
onBlur={e => { e.currentTarget.style.borderColor = 'var(--glass-border)'; e.currentTarget.style.boxShadow = 'none' }}
|
||||
/>
|
||||
)}
|
||||
@@ -621,7 +621,7 @@ function RangeInput({ question: q, value, onChange }: { question: SurveyQuestion
|
||||
const numVal = value ? parseInt(value) : q.min || 0
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="text-center font-label text-2xl font-semibold mb-3" style={{ color: '#06b6d4' }}>
|
||||
<div className="text-center font-label text-2xl font-semibold mb-3" style={{ color: 'var(--color-primary)' }}>
|
||||
{numVal}{q.suffix || ''}
|
||||
</div>
|
||||
<input
|
||||
@@ -633,7 +633,7 @@ function RangeInput({ question: q, value, onChange }: { question: SurveyQuestion
|
||||
onChange={e => onChange(e.target.value + (q.suffix || ''))}
|
||||
className="w-full h-2 sm:h-1 rounded-full appearance-none cursor-pointer touch-none"
|
||||
style={{
|
||||
background: `linear-gradient(to right, #06b6d4 0%, #06b6d4 ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, var(--color-border) ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, var(--color-border) 100%)`,
|
||||
background: `linear-gradient(to right, var(--color-primary) 0%, var(--color-primary) ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, var(--color-border) ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, var(--color-border) 100%)`,
|
||||
}}
|
||||
/>
|
||||
<div className="flex justify-between text-[11px] text-muted-foreground mt-2.5">
|
||||
@@ -730,7 +730,7 @@ function DragRank({ items, onChange }: { items: string[]; onChange: (items: stri
|
||||
className="flex items-center gap-2.5 sm:gap-3 px-3 py-3 sm:px-4 sm:py-2.5 rounded-[9px] text-[13px] sm:text-sm transition-all duration-150 select-none"
|
||||
style={{
|
||||
background: overIdx === idx ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)',
|
||||
border: `1px solid ${overIdx === idx || draggingIdx === idx ? '#06b6d4' : 'var(--glass-border)'}`,
|
||||
border: `1px solid ${overIdx === idx || draggingIdx === idx ? 'var(--color-primary)' : 'var(--glass-border)'}`,
|
||||
opacity: draggingIdx === idx ? 0.5 : 1,
|
||||
cursor: 'grab',
|
||||
color: 'var(--color-muted-foreground)',
|
||||
@@ -739,7 +739,7 @@ function DragRank({ items, onChange }: { items: string[]; onChange: (items: stri
|
||||
<div className="shrink-0 text-brand-text-muted">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg>
|
||||
</div>
|
||||
<div className="font-label text-[11px] font-semibold w-5 text-center shrink-0" style={{ color: '#06b6d4' }}>{idx + 1}</div>
|
||||
<div className="font-label text-[11px] font-semibold w-5 text-center shrink-0" style={{ color: 'var(--color-primary)' }}>{idx + 1}</div>
|
||||
<div className="flex-1 leading-snug">{item}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
|
||||
export default function SurveyThankYouPage() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Thank You" description="Thank you for your feedback on ResolutionFlow" />
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
{/* Atmosphere orbs */}
|
||||
<div className="pointer-events-none fixed inset-0 z-0 overflow-hidden" aria-hidden="true">
|
||||
@@ -29,7 +32,7 @@ export default function SurveyThankYouPage() {
|
||||
<div className="text-center pt-[120px] animate-fade-in-up">
|
||||
{/* Success icon */}
|
||||
<div className="w-20 h-20 mx-auto mb-6 rounded-full flex items-center justify-center" style={{ background: 'rgba(52, 211, 153, 0.1)', border: '1px solid rgba(52, 211, 153, 0.15)' }}>
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#34d399" strokeWidth="2">
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="oklch(0.76 0.15 163)" strokeWidth="2">
|
||||
<path d="M20 6L9 17l-5-5"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -52,7 +55,7 @@ export default function SurveyThankYouPage() {
|
||||
className="glass-card-static p-6 text-center max-w-[480px] mx-auto"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2 mb-3">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
<span className="font-label text-[0.625rem] uppercase tracking-widest text-primary font-semibold">
|
||||
@@ -72,5 +75,6 @@ export default function SurveyThankYouPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { X, RotateCcw, Play, FileUp } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { categoriesApi } from '@/api/categories'
|
||||
@@ -265,6 +266,8 @@ export function TreeLibraryPage() {
|
||||
selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full">
|
||||
<PageMeta title="Flow Library" />
|
||||
<div className="min-h-full">
|
||||
<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">
|
||||
@@ -596,6 +599,7 @@ export function TreeLibraryPage() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useSearchParams, Link } from 'react-router-dom'
|
||||
import { CheckCircle2, XCircle, Loader2 } from 'lucide-react'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function VerifyEmailPage() {
|
||||
@@ -23,6 +24,8 @@ export function VerifyEmailPage() {
|
||||
}, [token])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Verify Email" description="Verify your ResolutionFlow email address" />
|
||||
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
||||
<div className="glass-card-static w-full max-w-md p-8 text-center">
|
||||
{status === 'loading' && (
|
||||
@@ -55,8 +58,8 @@ export function VerifyEmailPage() {
|
||||
<Link
|
||||
to="/"
|
||||
className={cn(
|
||||
'mt-6 inline-flex items-center rounded-[10px] bg-[rgba(255,255,255,0.04)] border border-brand-border px-6 py-2 text-sm font-medium text-foreground',
|
||||
'hover:border-[rgba(255,255,255,0.12)]'
|
||||
'mt-6 inline-flex items-center rounded-[10px] bg-white/[0.04] border border-brand-border px-6 py-2 text-sm font-medium text-foreground',
|
||||
'hover:border-white/[0.12]'
|
||||
)}
|
||||
>
|
||||
Go to Dashboard
|
||||
@@ -65,6 +68,7 @@ export function VerifyEmailPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function ChatRetentionSettingsPage() {
|
||||
onChange={e => setRetentionDays(e.target.value)}
|
||||
min={1}
|
||||
max={365}
|
||||
className="w-full rounded-xl border bg-card text-foreground text-sm px-4 py-2.5 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]"
|
||||
className="w-full rounded-xl border bg-card text-foreground text-sm px-4 py-2.5 focus:outline-hidden focus:border-primary/30"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
@@ -91,7 +91,7 @@ export default function ChatRetentionSettingsPage() {
|
||||
onChange={e => setMaxCount(e.target.value)}
|
||||
min={10}
|
||||
max={10000}
|
||||
className="w-full rounded-xl border bg-card text-foreground text-sm px-4 py-2.5 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]"
|
||||
className="w-full rounded-xl border bg-card text-foreground text-sm px-4 py-2.5 focus:outline-hidden focus:border-primary/30"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { UserUpdate } from '@/types'
|
||||
const inputClass = cn(
|
||||
'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
||||
)
|
||||
|
||||
export function ProfileSettingsPage() {
|
||||
@@ -145,8 +145,8 @@ export function ProfileSettingsPage() {
|
||||
to="/change-password"
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-[10px] px-4 py-2 text-sm font-medium',
|
||||
'bg-[rgba(255,255,255,0.04)] border border-brand-border text-foreground',
|
||||
'hover:border-[rgba(255,255,255,0.12)]'
|
||||
'bg-white/[0.04] border border-brand-border text-foreground',
|
||||
'hover:border-white/[0.12]'
|
||||
)}
|
||||
>
|
||||
Change Password
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
import { PageHeader } from '@/components/common/PageHeader'
|
||||
@@ -79,8 +79,6 @@ export function TeamCategoriesPage() {
|
||||
setForm({ name: cat.name, slug: cat.slug, description: cat.description || '' })
|
||||
}
|
||||
|
||||
const inputCn = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
@@ -141,15 +139,15 @@ export function TeamCategoriesPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">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} />
|
||||
<Input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} />
|
||||
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} />
|
||||
<Input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -166,15 +164,15 @@ export function TeamCategoriesPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={inputCn} />
|
||||
<Input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} />
|
||||
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} />
|
||||
<Input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Plus, Trash2, ToggleLeft } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
|
||||
import type { Column } from '@/components/admin'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
@@ -146,7 +147,7 @@ export function FeatureFlagsPage() {
|
||||
},
|
||||
]
|
||||
|
||||
const inputCn = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
|
||||
const selectClass = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -197,15 +198,15 @@ export function FeatureFlagsPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">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} />
|
||||
<Input type="text" value={createForm.flag_key} onChange={(e) => setCreateForm({ ...createForm, flag_key: e.target.value })} placeholder="e.g. custom_branding" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Display Name</label>
|
||||
<input type="text" value={createForm.display_name} onChange={(e) => setCreateForm({ ...createForm, display_name: e.target.value })} placeholder="e.g. Custom Branding" className={inputCn} />
|
||||
<Input type="text" value={createForm.display_name} onChange={(e) => setCreateForm({ ...createForm, display_name: e.target.value })} placeholder="e.g. Custom Branding" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
|
||||
<Input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -222,11 +223,11 @@ export function FeatureFlagsPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">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} />
|
||||
<Input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Feature Flag</label>
|
||||
<select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={inputCn}>
|
||||
<select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={selectClass}>
|
||||
<option value="">Select a flag...</option>
|
||||
{flags.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
|
||||
</select>
|
||||
@@ -237,7 +238,7 @@ export function FeatureFlagsPage() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
||||
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" className={inputCn} />
|
||||
<Input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin'
|
||||
import type { Column } from '@/components/admin'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { AdminCategory, GlobalCategoryCreate } from '@/types/admin'
|
||||
|
||||
export function GlobalCategoriesPage() {
|
||||
@@ -88,8 +88,6 @@ export function GlobalCategoriesPage() {
|
||||
},
|
||||
]
|
||||
|
||||
const inputCn = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
@@ -127,15 +125,15 @@ export function GlobalCategoriesPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">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} />
|
||||
<Input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} />
|
||||
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
|
||||
<Input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -156,15 +154,15 @@ export function GlobalCategoriesPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Networking" className={inputCn} />
|
||||
<Input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Networking" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
||||
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} />
|
||||
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
||||
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
|
||||
<Input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Plus, Copy, Trash2, Ticket, Mail, MailCheck, RefreshCw } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
|
||||
import type { Column } from '@/components/admin'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
@@ -108,7 +109,7 @@ export function InviteCodesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const inputClass = cn(
|
||||
const selectClass = cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)
|
||||
@@ -254,12 +255,11 @@ export function InviteCodesPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Recipient Email</label>
|
||||
<input
|
||||
<Input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Optional — will send invite email"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -273,7 +273,7 @@ export function InviteCodesPage() {
|
||||
setAssignedPlan(plan)
|
||||
if (plan === 'free') setTrialDays('')
|
||||
}}
|
||||
className={inputClass}
|
||||
className={selectClass}
|
||||
>
|
||||
{PLAN_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
@@ -284,14 +284,13 @@ export function InviteCodesPage() {
|
||||
{assignedPlan !== 'free' && (
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Trial Duration (days)</label>
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
value={trialDays}
|
||||
onChange={(e) => setTrialDays(e.target.value)}
|
||||
placeholder="e.g. 14 (1-90)"
|
||||
min={1}
|
||||
max={90}
|
||||
className={inputClass}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">Leave empty for no trial — account gets full plan immediately.</p>
|
||||
</div>
|
||||
@@ -299,23 +298,21 @@ export function InviteCodesPage() {
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Expires in (days)</label>
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
value={expiresInDays}
|
||||
onChange={(e) => setExpiresInDays(e.target.value)}
|
||||
placeholder="Leave empty for no expiry"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder="Optional note (e.g. who this is for)"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Plus, Trash2, Gauge } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin'
|
||||
import type { Column } from '@/components/admin'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { PlanLimitConfig, AccountOverrideResponse, AccountOverrideCreate } from '@/types/admin'
|
||||
|
||||
export function PlanLimitsPage() {
|
||||
@@ -109,11 +109,6 @@ export function PlanLimitsPage() {
|
||||
},
|
||||
]
|
||||
|
||||
const inputCn = cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<PageHeader title="Plan Limits" description="Configure plan tier limits and account-specific overrides" />
|
||||
@@ -161,15 +156,15 @@ export function PlanLimitsPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">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} />
|
||||
<Input type="number" value={editPlan.max_trees ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_trees: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month (empty = unlimited)</label>
|
||||
<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} />
|
||||
<Input type="number" value={editPlan.max_sessions_per_month ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Users (empty = unlimited)</label>
|
||||
<input type="number" value={editPlan.max_users ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_users: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
|
||||
<Input type="number" value={editPlan.max_users ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_users: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -191,23 +186,23 @@ export function PlanLimitsPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<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} />
|
||||
<Input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees Override</label>
|
||||
<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} />
|
||||
<Input type="number" value={overrideForm.override_max_trees ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_trees: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month Override</label>
|
||||
<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} />
|
||||
<Input type="number" value={overrideForm.override_max_sessions_per_month ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Users Override</label>
|
||||
<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} />
|
||||
<Input type="number" value={overrideForm.override_max_users ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_users: e.target.value ? parseInt(e.target.value) : null })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
||||
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" className={inputCn} />
|
||||
<Input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PageHeader } from '@/components/admin'
|
||||
import { Textarea } from '@/components/ui/Textarea'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -93,15 +94,11 @@ export function SettingsPage() {
|
||||
{maintenanceMode && (
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Maintenance Message</label>
|
||||
<textarea
|
||||
<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-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function SurveyInvitesPage() {
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="John Smith"
|
||||
className="w-full rounded-[10px] border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden"
|
||||
className="w-full rounded-[10px] border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary/30 focus:outline-hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
@@ -94,7 +94,7 @@ export default function SurveyInvitesPage() {
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="john@example.com"
|
||||
className="w-full rounded-[10px] border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden"
|
||||
className="w-full rounded-[10px] border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary/30 focus:outline-hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -109,7 +109,7 @@ export default function SurveyInvitesPage() {
|
||||
<button
|
||||
onClick={() => handleCreate(true)}
|
||||
disabled={creating || !name.trim() || !email.trim()}
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-[rgba(255,255,255,0.04)] border border-brand-border px-4 py-2 text-sm font-medium text-foreground hover:border-[rgba(255,255,255,0.12)] active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-white/[0.04] border border-brand-border px-4 py-2 text-sm font-medium text-foreground hover:border-white/[0.12] active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
{creating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
Send Email
|
||||
@@ -122,7 +122,7 @@ export default function SurveyInvitesPage() {
|
||||
)}
|
||||
|
||||
{lastCreated && (
|
||||
<div className="mt-4 rounded-[10px] border border-[rgba(6,182,212,0.15)] bg-[rgba(6,182,212,0.04)] p-4">
|
||||
<div className="mt-4 rounded-[10px] border border-primary/[0.15] bg-primary/[0.04] p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-muted-foreground mb-1">
|
||||
@@ -163,7 +163,7 @@ export default function SurveyInvitesPage() {
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-muted-foreground">No invites yet</td></tr>
|
||||
) : (
|
||||
invites.map(invite => (
|
||||
<tr key={invite.id} className="border-b border-border/50 hover:bg-[rgba(255,255,255,0.02)] transition-colors">
|
||||
<tr key={invite.id} className="border-b border-border/50 hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-4 py-3 text-sm text-foreground">{invite.recipient_name}</td>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground">{invite.recipient_email || '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
|
||||
@@ -152,7 +152,7 @@ function ResponseRow({
|
||||
className={cn(
|
||||
'border-b border-border/50 transition-colors cursor-pointer',
|
||||
!response.is_read && 'bg-primary/3',
|
||||
'hover:bg-[rgba(255,255,255,0.02)]'
|
||||
'hover:bg-white/[0.02]'
|
||||
)}
|
||||
>
|
||||
{/* Checkbox */}
|
||||
@@ -227,14 +227,14 @@ function ResponseRow({
|
||||
>
|
||||
<button
|
||||
onClick={() => { onMarkRead(); setShowMenu(false) }}
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.04)] transition-colors"
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/[0.04] transition-colors"
|
||||
>
|
||||
{response.is_read ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
{response.is_read ? 'Mark Unread' : 'Mark Read'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { onArchive(); setShowMenu(false) }}
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.04)] transition-colors"
|
||||
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/[0.04] transition-colors"
|
||||
>
|
||||
{response.archived_at ? <ArchiveRestore className="h-3.5 w-3.5" /> : <Archive className="h-3.5 w-3.5" />}
|
||||
{response.archived_at ? 'Unarchive' : 'Archive'}
|
||||
@@ -437,7 +437,7 @@ export default function SurveyResponsesPage() {
|
||||
'inline-flex items-center gap-2 rounded-[10px] px-3 py-2 text-xs font-medium transition-colors border',
|
||||
showArchived
|
||||
? 'bg-primary/10 text-primary border-primary/20'
|
||||
: 'bg-[rgba(255,255,255,0.04)] text-muted-foreground border-brand-border hover:border-[rgba(255,255,255,0.12)]'
|
||||
: 'bg-white/[0.04] text-muted-foreground border-brand-border hover:border-white/[0.12]'
|
||||
)}
|
||||
>
|
||||
<Archive className="h-3.5 w-3.5" />
|
||||
@@ -446,7 +446,7 @@ export default function SurveyResponsesPage() {
|
||||
<button
|
||||
onClick={handleExport}
|
||||
disabled={exporting || responses.length === 0}
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-[rgba(255,255,255,0.04)] border border-brand-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-[rgba(255,255,255,0.12)] disabled:opacity-50"
|
||||
className="inline-flex items-center gap-2 rounded-[10px] bg-white/[0.04] border border-brand-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-white/[0.12] disabled:opacity-50"
|
||||
>
|
||||
{exporting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { ArrowLeft, Shield, Crown, UserCheck, UserX, Clock, Ticket, KeyRound, Copy, Check, Archive, ArchiveRestore, Trash2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { StatusBadge } from '@/components/admin'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
@@ -187,7 +188,7 @@ export function UserDetailPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const inputClass = cn(
|
||||
const selectClass = cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)
|
||||
@@ -534,7 +535,7 @@ export function UserDetailPage() {
|
||||
aria-label="Subscription plan"
|
||||
value={selectedPlan}
|
||||
onChange={(e) => setSelectedPlan(e.target.value)}
|
||||
className={inputClass}
|
||||
className={selectClass}
|
||||
>
|
||||
{PLAN_OPTIONS.map(p => (
|
||||
<option key={p} value={p}>{p.charAt(0).toUpperCase() + p.slice(1)}</option>
|
||||
@@ -646,13 +647,12 @@ export function UserDetailPage() {
|
||||
>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Days to add</label>
|
||||
<input
|
||||
<Input
|
||||
type="number"
|
||||
value={trialDays}
|
||||
onChange={(e) => setTrialDays(e.target.value)}
|
||||
min={1}
|
||||
max={90}
|
||||
className={inputClass}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">1-90 days. Will convert to trialing status if not already.</p>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { UserCheck, UserX, Shield, ArrowRightLeft, ExternalLink, UserPlus, Copy, Check, Mail } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { DataTable, Pagination, SearchInput, PageHeader, StatusBadge, ActionMenu } from '@/components/admin'
|
||||
import type { Column } from '@/components/admin'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
@@ -361,15 +362,11 @@ export function UsersPage() {
|
||||
</p>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<input
|
||||
<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-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -393,28 +390,20 @@ export function UsersPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={createForm.name}
|
||||
onChange={(e) => setCreateForm(f => ({ ...f, name: e.target.value }))}
|
||||
placeholder="Full name"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
|
||||
<input
|
||||
<Input
|
||||
type="email"
|
||||
value={createForm.email}
|
||||
onChange={(e) => setCreateForm(f => ({ ...f, email: e.target.value }))}
|
||||
placeholder="user@example.com"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -435,15 +424,11 @@ export function UsersPage() {
|
||||
<>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={createForm.account_display_code}
|
||||
onChange={(e) => setCreateForm(f => ({ ...f, account_display_code: e.target.value }))}
|
||||
placeholder="e.g. ABC12345"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -532,28 +517,20 @@ export function UsersPage() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
|
||||
<input
|
||||
<Input
|
||||
type="email"
|
||||
value={inviteForm.email}
|
||||
onChange={(e) => setInviteForm(f => ({ ...f, email: e.target.value }))}
|
||||
placeholder="user@example.com"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={inviteForm.account_display_code}
|
||||
onChange={(e) => setInviteForm(f => ({ ...f, account_display_code: e.target.value }))}
|
||||
placeholder="e.g. ABC12345"
|
||||
className={cn(
|
||||
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user