refactor: shared components, ConfirmDialog migration, pinned flow fixes
- Create shared Spinner component with sm/md/lg sizes - Migrate 13 page-level spinners to shared Spinner - Promote EmptyState to shared component, adopt in MyShares and SessionHistory - Replace window.confirm with ConfirmDialog in 3 files - Fix PinnedFlow.tree_type to include maintenance, update emoji display - Verify sidebar unpin handler already correct (no-op) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ export interface PinnedFlow {
|
||||
id: string
|
||||
tree_id: string
|
||||
tree_name: string
|
||||
tree_type: 'troubleshooting' | 'procedural'
|
||||
tree_type: 'troubleshooting' | 'procedural' | 'maintenance'
|
||||
category_emoji?: string
|
||||
category_name?: string
|
||||
pinned_at: string
|
||||
|
||||
@@ -1,25 +1,2 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface EmptyStateProps {
|
||||
icon?: ReactNode
|
||||
title: string
|
||||
description?: string
|
||||
action?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center justify-center py-12 text-center', className)}>
|
||||
{icon && <div className="mb-4 text-muted-foreground">{icon}</div>}
|
||||
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
|
||||
{description && (
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
{action && <div className="mt-4">{action}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmptyState
|
||||
export { EmptyState } from '@/components/common/EmptyState'
|
||||
export { default } from '@/components/common/EmptyState'
|
||||
|
||||
25
frontend/src/components/common/EmptyState.tsx
Normal file
25
frontend/src/components/common/EmptyState.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface EmptyStateProps {
|
||||
icon?: ReactNode
|
||||
title: string
|
||||
description?: string
|
||||
action?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center justify-center py-12 text-center', className)}>
|
||||
{icon && <div className="mb-4 text-muted-foreground">{icon}</div>}
|
||||
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
|
||||
{description && (
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
{action && <div className="mt-4">{action}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmptyState
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
|
||||
export function PageLoader() {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-black">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-4 border-border border-t-foreground" />
|
||||
<Spinner size="lg" />
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
26
frontend/src/components/common/Spinner.tsx
Normal file
26
frontend/src/components/common/Spinner.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const SIZES = {
|
||||
sm: 'h-4 w-4 border-2',
|
||||
md: 'h-8 w-8 border-4',
|
||||
lg: 'h-12 w-12 border-4',
|
||||
} as const
|
||||
|
||||
interface SpinnerProps {
|
||||
size?: keyof typeof SIZES
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Spinner({ size = 'md', className }: SpinnerProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'animate-spin rounded-full border-border border-t-primary',
|
||||
SIZES[size],
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spinner
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Folder, ChevronDown, ChevronRight, Plus, MoreVertical, Pencil, Trash2, FolderPlus, X } from 'lucide-react'
|
||||
import { foldersApi } from '@/api/folders'
|
||||
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
|
||||
import type { FolderListItem, FolderTreeItem } from '@/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -245,6 +246,7 @@ export function FolderSidebar({
|
||||
const [menuOpenId, setMenuOpenId] = useState<string | null>(null)
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null)
|
||||
const [pendingDelete, setPendingDelete] = useState<{ id: string; message: string } | null>(null)
|
||||
|
||||
const loadFolders = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
@@ -277,15 +279,19 @@ export function FolderSidebar({
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteFolder = async (folderId: string, folderHasChildren: boolean) => {
|
||||
const handleDeleteFolder = (folderId: string, folderHasChildren: boolean) => {
|
||||
const descendantCount = getDescendantIds(folders, folderId).length
|
||||
const message = folderHasChildren
|
||||
? `Are you sure you want to delete this folder and its ${descendantCount} subfolder(s)? The trees in them will not be deleted.`
|
||||
: 'Are you sure you want to delete this folder? The trees in it will not be deleted.'
|
||||
|
||||
if (!confirm(message)) {
|
||||
return
|
||||
}
|
||||
setPendingDelete({ id: folderId, message })
|
||||
}
|
||||
|
||||
const confirmDeleteFolder = async () => {
|
||||
if (!pendingDelete) return
|
||||
const folderId = pendingDelete.id
|
||||
setPendingDelete(null)
|
||||
try {
|
||||
await foldersApi.delete(folderId)
|
||||
// Remove folder and all descendants from local state
|
||||
@@ -494,6 +500,15 @@ export function FolderSidebar({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={!!pendingDelete}
|
||||
onClose={() => setPendingDelete(null)}
|
||||
onConfirm={confirmDeleteFolder}
|
||||
title="Delete Folder"
|
||||
message={pendingDelete?.message || ''}
|
||||
confirmLabel="Delete"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function PinnedFlowsSection({ flows, onUnpin }: PinnedFlowsSectionProps)
|
||||
title={`${flow.tree_name} (right-click to unpin)`}
|
||||
>
|
||||
<span className="text-sm shrink-0">
|
||||
{flow.tree_type === 'procedural' ? '📋' : '🔧'}
|
||||
{flow.tree_type === 'procedural' ? '📋' : flow.tree_type === 'maintenance' ? '🛠️' : '🔧'}
|
||||
</span>
|
||||
<span className="truncate flex-1 text-left">{flow.tree_name}</span>
|
||||
<Pin size={12} className="shrink-0 opacity-0 group-hover:opacity-40 transition-opacity" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTreeEditorStore, findNodeInTree } from '@/store/treeEditorStore'
|
||||
import { NodeFormDecision } from './NodeFormDecision'
|
||||
import { NodeFormAction } from './NodeFormAction'
|
||||
import { NodeFormResolution } from './NodeFormResolution'
|
||||
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { TreeStructure, NodeType } from '@/types'
|
||||
|
||||
@@ -36,6 +37,7 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
|
||||
const [draft, setDraft] = useState<TreeStructure | null>(null)
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Initialize/reset draft when nodeId changes
|
||||
@@ -84,7 +86,8 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (isDirty) {
|
||||
if (!window.confirm('You have unsaved changes. Discard them?')) return
|
||||
setShowDiscardConfirm(true)
|
||||
return
|
||||
}
|
||||
onClose()
|
||||
}, [isDirty, onClose])
|
||||
@@ -236,6 +239,18 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={showDiscardConfirm}
|
||||
onClose={() => setShowDiscardConfirm(false)}
|
||||
onConfirm={() => {
|
||||
setShowDiscardConfirm(false)
|
||||
onClose()
|
||||
}}
|
||||
title="Discard Changes"
|
||||
message="You have unsaved changes. Discard them?"
|
||||
confirmLabel="Discard"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'
|
||||
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText } from 'lucide-react'
|
||||
import { accountsApi } from '@/api/accounts'
|
||||
import type { Account, AccountMember, AccountInvite } from '@/types'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { useSubscription } from '@/hooks/useSubscription'
|
||||
@@ -130,7 +131,7 @@ export function AccountSettingsPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { BarChart3, Loader2, Target, Clock, TrendingUp, CheckCircle } from 'lucide-react'
|
||||
import { BarChart3, Target, Clock, TrendingUp, CheckCircle } from 'lucide-react'
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { analyticsApi } from '@/api'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import type { PersonalAnalyticsResponse, AnalyticsPeriod } from '@/types'
|
||||
@@ -45,7 +46,7 @@ export default function MyAnalyticsPage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<Loader2 size={32} className="animate-spin text-muted-foreground" />
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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 { Spinner } from '@/components/common/Spinner'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
@@ -47,6 +50,7 @@ export default function MySharesPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
const [revokeTarget, setRevokeTarget] = useState<SessionShare | null>(null)
|
||||
|
||||
const fetchShares = useCallback(async () => {
|
||||
try {
|
||||
@@ -77,18 +81,16 @@ export default function MySharesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleRevoke = async (share: SessionShare) => {
|
||||
const confirmed = window.confirm(
|
||||
'Revoke this share link? Anyone with the link will no longer be able to access the session.'
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
const handleRevoke = async () => {
|
||||
if (!revokeTarget) return
|
||||
try {
|
||||
await sessionsApi.revokeShare(share.id)
|
||||
setShares((prev) => prev.filter((s) => s.id !== share.id))
|
||||
await sessionsApi.revokeShare(revokeTarget.id)
|
||||
setShares((prev) => prev.filter((s) => s.id !== revokeTarget.id))
|
||||
toast.success('Share link revoked')
|
||||
} catch {
|
||||
toast.error('Failed to revoke share link')
|
||||
} finally {
|
||||
setRevokeTarget(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +98,7 @@ export default function MySharesPage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-32">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -139,18 +141,20 @@ export default function MySharesPage() {
|
||||
|
||||
{/* Empty state */}
|
||||
{shares.length === 0 ? (
|
||||
<div className="bg-card border border-border rounded-xl p-12 text-center">
|
||||
<Link2 className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h2 className="text-lg font-heading font-semibold text-foreground mb-2">No shared sessions</h2>
|
||||
<p className="text-muted-foreground text-sm mb-6">
|
||||
Share a session from the session detail page to create a link
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/sessions')}
|
||||
className="bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
|
||||
>
|
||||
Go to Sessions
|
||||
</button>
|
||||
<div className="bg-card border border-border rounded-xl">
|
||||
<EmptyState
|
||||
icon={<Link2 className="h-12 w-12" />}
|
||||
title="No shared sessions"
|
||||
description="Share a session from the session detail page to create a link"
|
||||
action={
|
||||
<button
|
||||
onClick={() => navigate('/sessions')}
|
||||
className="bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
|
||||
>
|
||||
Go to Sessions
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
@@ -223,7 +227,7 @@ export default function MySharesPage() {
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => handleRevoke(share)}
|
||||
onClick={() => setRevokeTarget(share)}
|
||||
className="inline-flex items-center gap-1.5 text-red-400 hover:text-red-300 hover:bg-red-400/10 rounded-md px-3 py-1.5 text-sm transition-colors"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
@@ -235,6 +239,15 @@ export default function MySharesPage() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={!!revokeTarget}
|
||||
onClose={() => setRevokeTarget(null)}
|
||||
onConfirm={handleRevoke}
|
||||
title="Revoke Share Link"
|
||||
message="Revoke this share link? Anyone with the link will no longer be able to access the session."
|
||||
confirmLabel="Revoke"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { TreeListItem } from '@/types'
|
||||
import { TagBadges } from '@/components/common/TagBadges'
|
||||
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
|
||||
import { ShareTreeModal } from '@/components/library/ShareTreeModal'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
@@ -177,7 +178,7 @@ export function MyTreesPage() {
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
|
||||
<Spinner />
|
||||
</div>
|
||||
) : trees.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border bg-accent px-4 py-12 text-center">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { MaintenanceScheduleSection } from '@/components/procedural-editor/Maint
|
||||
import { getScheduleSummary } from '@/components/procedural-editor/scheduleUtils'
|
||||
import { StepList } from '@/components/procedural-editor/StepList'
|
||||
import { TagInput } from '@/components/common/TagInput'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { toast } from '@/lib/toast'
|
||||
import type { TreeType, MaintenanceSchedule, TargetList } from '@/types'
|
||||
|
||||
@@ -143,7 +144,7 @@ export function ProceduralEditorPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[50vh] items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { StepDetail } from '@/components/procedural/StepDetail'
|
||||
import { ProgressBar } from '@/components/procedural/ProgressBar'
|
||||
import { CompletionSummary } from '@/components/procedural/CompletionSummary'
|
||||
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { StepFeedback } from '@/components/session/StepFeedback'
|
||||
@@ -290,7 +291,7 @@ export function ProceduralNavigationPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[50vh] items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { MenuAction } from '@/components/common/ActionMenu'
|
||||
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
|
||||
import type { Session, SessionExport, SaveAsTreeRequest, Step, RedactionSummary } from '@/types'
|
||||
import { hasRatedSession, markSessionRated } from '@/lib/sessionRatings'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
|
||||
@@ -289,7 +290,7 @@ export function SessionDetailPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-primary" />
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { Session, TreeListItem } from '@/types'
|
||||
import type { DateRange } from 'react-day-picker'
|
||||
import { SessionFilters } from '@/components/session/SessionFilters'
|
||||
import type { SessionFilterState } from '@/components/session/SessionFilters'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { EmptyState } from '@/components/common/EmptyState'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { getSessionResumePath } from '@/lib/routing'
|
||||
@@ -188,27 +190,22 @@ export function SessionHistoryPage() {
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-primary" />
|
||||
<Spinner />
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
No sessions found.{' '}
|
||||
{filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from ? (
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="text-foreground hover:underline"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className="text-foreground hover:underline"
|
||||
>
|
||||
Start a new session
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<EmptyState
|
||||
title="No sessions found"
|
||||
description={filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from
|
||||
? "Try adjusting your filters"
|
||||
: "Complete a flow to see it here"}
|
||||
action={
|
||||
(filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from) ? (
|
||||
<button onClick={handleClearFilters} className="text-foreground hover:underline text-sm">
|
||||
Clear all filters
|
||||
</button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{sessions.map((session) => (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||
import { Globe, Users, ShieldAlert, FileX, Clock, Loader2 } from 'lucide-react'
|
||||
import { Globe, Users, ShieldAlert, FileX, Clock } from 'lucide-react'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { sessionsApi } from '@/api/sessions'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
import { SessionTimeline } from '@/components/session/SessionTimeline'
|
||||
import { SharedSessionTreePreview } from '@/components/session/SharedSessionTreePreview'
|
||||
@@ -144,7 +145,7 @@ export function SharedSessionPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<Spinner />
|
||||
<p className="text-sm text-muted-foreground">Loading shared session...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link, Navigate } from 'react-router-dom'
|
||||
import { BarChart3, Loader2, Users, Target, Clock, TrendingUp } from 'lucide-react'
|
||||
import { BarChart3, Users, Target, Clock, TrendingUp } from 'lucide-react'
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { analyticsApi } from '@/api'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import type { TeamAnalyticsResponse, AnalyticsPeriod } from '@/types'
|
||||
@@ -51,7 +52,7 @@ export default function TeamAnalyticsPage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<Loader2 size={32} className="animate-spin text-muted-foreground" />
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TreeEditorLayout } from '@/components/tree-editor/TreeEditorLayout'
|
||||
import { ValidationSummary } from '@/components/tree-editor/ValidationSummary'
|
||||
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'
|
||||
import { usePermissions } from '@/hooks/usePermissions'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { cn, safeGetItem } from '@/lib/utils'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { FlowAnalyticsPanel } from '@/components/analytics/FlowAnalyticsPanel'
|
||||
@@ -382,7 +383,7 @@ export function TreeEditorPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
||||
import { CustomStepModal } from '@/components/step-library/CustomStepModal'
|
||||
import { PostStepActionModal, ContinuationModal, ForkTreeModal, ScratchpadSidebar, SessionOutcomeModal } from '@/components/session'
|
||||
import { Plus, CheckCircle, ArrowRight, Clock, Terminal, Clipboard, Check, Copy, HelpCircle, Link2, ChevronDown, Settings } from 'lucide-react'
|
||||
import { Spinner } from '@/components/common/Spinner'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
import { ShareSessionModal } from '@/components/session/ShareSessionModal'
|
||||
@@ -528,7 +529,7 @@ export function TreeNavigationPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user