+
+
)
}
diff --git a/frontend/src/components/admin/EditCategoryModal.tsx b/frontend/src/components/admin/EditCategoryModal.tsx
index 9af88445..c51d6f0c 100644
--- a/frontend/src/components/admin/EditCategoryModal.tsx
+++ b/frontend/src/components/admin/EditCategoryModal.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react'
-import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { StepCategoryListItem } from '@/types'
+import { Modal } from '@/components/common/Modal'
interface EditCategoryModalProps {
isOpen: boolean
@@ -33,7 +33,7 @@ export function EditCategoryModal({
setPrevCategoryId(null)
}
- if (!isOpen || !category) return null
+ if (!category) return null
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@@ -67,102 +67,90 @@ export function EditCategoryModal({
}
return (
-
-
- {/* Header */}
-
-
Edit Category
+
+
-
- {/* Form */}
-
+
+
+
+
+
)
}
diff --git a/frontend/src/components/session/ShareSessionModal.tsx b/frontend/src/components/session/ShareSessionModal.tsx
index 7ea2b23f..338875b3 100644
--- a/frontend/src/components/session/ShareSessionModal.tsx
+++ b/frontend/src/components/session/ShareSessionModal.tsx
@@ -1,15 +1,16 @@
import { useState, useEffect, useCallback } from 'react'
-import { X, Copy, Check, Globe, Users, Clock, Trash2, Link2 } from 'lucide-react'
+import { Copy, Check, Globe, Users, Clock, Trash2, Link2 } from 'lucide-react'
import type { SessionShare, SessionShareVisibility } from '@/types'
import { sessionsApi } from '@/api/sessions'
import { buildSessionShareUrl, filterSharesForSession } from '@/lib/sessionShare'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
import { Spinner } from '@/components/common/Spinner'
+import { Modal } from '@/components/common/Modal'
interface ShareSessionModalProps {
sessionId: string
- sessionLabel: string // e.g. ticket number or "Session Details"
+ sessionLabel: string
isOpen: boolean
onClose: () => void
}
@@ -76,7 +77,6 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
const [isGenerating, setIsGenerating] = useState(false)
const [copiedShareId, setCopiedShareId] = useState
(null)
- // Form state
const [visibility, setVisibility] = useState('account')
const [shareName, setShareName] = useState('')
const [expirationPreset, setExpirationPreset] = useState('never')
@@ -88,7 +88,6 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
try {
const allShares = await sessionsApi.listMyShares()
const sessionShares = filterSharesForSession(allShares, sessionId)
- // Sort newest first
sessionShares.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
setShares(sessionShares)
} catch (err) {
@@ -101,7 +100,6 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
useEffect(() => {
if (isOpen) {
loadShares()
- // Reset form state
setVisibility('account')
setShareName('')
setExpirationPreset('never')
@@ -123,7 +121,6 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
})
setShares([newShare, ...shares])
toast.success('Share link generated')
- // Reset form
setShareName('')
setExpirationPreset('never')
setCustomDatetime('')
@@ -167,8 +164,6 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
}
}
- if (!isOpen) return null
-
const presetButtons: { value: ExpirationPreset; label: string }[] = [
{ value: 'never', label: 'Never' },
{ value: '1day', label: '1 day' },
@@ -178,242 +173,13 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
]
return (
-
- {/* Backdrop */}
-
-
- {/* Modal */}
-
- {/* Header */}
-
-
-
Share Session
-
{sessionLabel}
-
-
-
-
- {/* Body */}
-
- {/* Create Share Form */}
-
- {/* Visibility */}
-
-
-
-
-
-
- {visibilityError && (
-
{visibilityError}
- )}
-
-
- {/* Share Name */}
-
-
- setShareName(e.target.value.slice(0, 100))}
- placeholder="e.g. Training link, Customer escalation"
- className={cn(
- 'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground placeholder-muted-foreground',
- 'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
- )}
- maxLength={100}
- />
-
-
- {/* Expiration */}
-
-
-
- {presetButtons.map((preset) => (
-
- ))}
-
- {expirationPreset === 'custom' && (
-
setCustomDatetime(e.target.value)}
- className={cn(
- 'mt-2 w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
- 'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
- 'scheme-dark'
- )}
- />
- )}
-
-
- {/* Generate Button */}
-
-
-
- {/* Existing Shares */}
- {shares.length > 0 && (
-
-
- Active Shares ({shares.length})
-
-
- {shares.map((share) => {
- const expiration = getExpirationLabel(share.expires_at)
- const isCopied = copiedShareId === share.id
- return (
-
-
-
-
-
- {share.visibility === 'public' ? (
-
- ) : (
-
- )}
- {share.visibility === 'public' ? 'Public' : 'Account'}
-
-
- {share.share_name || 'Untitled share'}
-
-
-
- {getRelativeTime(share.created_at)}
-
- {share.view_count > 0
- ? `${share.view_count} view${share.view_count === 1 ? '' : 's'}`
- : 'Not viewed yet'}
-
-
-
- {expiration.text}
-
-
-
-
-
-
-
-
-
- )
- })}
-
-
- )}
-
- {/* Loading state */}
- {isLoadingShares && shares.length === 0 && (
-
-
-
- )}
-
-
- {/* Footer */}
-
+
+ }
+ >
+ {/* Subtitle */}
+
{sessionLabel}
+
+
+ {/* Create Share Form */}
+
+ {/* Visibility */}
+
+
+
+
+
+
+ {visibilityError && (
+
{visibilityError}
+ )}
+
+
+ {/* Share Name */}
+
+
+ setShareName(e.target.value.slice(0, 100))}
+ placeholder="e.g. Training link, Customer escalation"
+ className={cn(
+ 'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground placeholder-muted-foreground',
+ 'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
+ )}
+ maxLength={100}
+ />
+
+
+ {/* Expiration */}
+
+
+
+ {presetButtons.map((preset) => (
+
+ ))}
+
+ {expirationPreset === 'custom' && (
+
setCustomDatetime(e.target.value)}
+ className={cn(
+ 'mt-2 w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
+ 'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
+ 'scheme-dark'
+ )}
+ />
+ )}
+
+
+ {/* Generate Button */}
+
+
+
+ {/* Existing Shares */}
+ {shares.length > 0 && (
+
+
+ Active Shares ({shares.length})
+
+
+ {shares.map((share) => {
+ const expiration = getExpirationLabel(share.expires_at)
+ const isCopied = copiedShareId === share.id
+ return (
+
+
+
+
+
+ {share.visibility === 'public' ? (
+
+ ) : (
+
+ )}
+ {share.visibility === 'public' ? 'Public' : 'Account'}
+
+
+ {share.share_name || 'Untitled share'}
+
+
+
+ {getRelativeTime(share.created_at)}
+
+ {share.view_count > 0
+ ? `${share.view_count} view${share.view_count === 1 ? '' : 's'}`
+ : 'Not viewed yet'}
+
+
+
+ {expiration.text}
+
+
+
+
+
+
+
+
+
+ )
+ })}
+
+
+ )}
+
+ {/* Loading state */}
+ {isLoadingShares && shares.length === 0 && (
+
+
+
+ )}
-
+
)
}