Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
This commit is contained in:
@@ -1,179 +1,179 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Pencil, Check, X } from 'lucide-react'
|
||||
|
||||
type InlineEditableTextVariant = 'h1' | 'h2' | 'body' | 'mono'
|
||||
|
||||
type InlineEditableTextProps = {
|
||||
value: string
|
||||
onSave: (newValue: string) => void | Promise<void>
|
||||
variant?: InlineEditableTextVariant
|
||||
placeholder?: string
|
||||
multiline?: boolean
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const variantStyles: Record<InlineEditableTextVariant, string> = {
|
||||
h1: 'text-xl font-bold',
|
||||
h2: 'text-base font-semibold',
|
||||
body: 'text-sm',
|
||||
mono: 'text-sm font-mono text-muted-foreground',
|
||||
}
|
||||
|
||||
export function InlineEditableText({
|
||||
value,
|
||||
onSave,
|
||||
variant = 'body',
|
||||
placeholder = 'Click to edit...',
|
||||
multiline = false,
|
||||
disabled = false,
|
||||
className,
|
||||
}: InlineEditableTextProps) {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editValue, setEditValue] = useState(value)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
inputRef.current.select()
|
||||
}
|
||||
}, [isEditing])
|
||||
|
||||
useEffect(() => {
|
||||
setEditValue(value)
|
||||
}, [value])
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
const trimmed = editValue.trim()
|
||||
if (trimmed === value) {
|
||||
setIsEditing(false)
|
||||
return
|
||||
}
|
||||
if (!trimmed) {
|
||||
setEditValue(value)
|
||||
setIsEditing(false)
|
||||
return
|
||||
}
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await onSave(trimmed)
|
||||
setIsEditing(false)
|
||||
} catch {
|
||||
setEditValue(value)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}, [editValue, value, onSave])
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setEditValue(value)
|
||||
setIsEditing(false)
|
||||
}, [value])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleCancel()
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
if (multiline && !e.ctrlKey) return
|
||||
e.preventDefault()
|
||||
handleSave()
|
||||
}
|
||||
},
|
||||
[handleCancel, handleSave, multiline]
|
||||
)
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<span className={cn(variantStyles[variant], className)}>
|
||||
{value || <span className="text-muted-foreground italic">{placeholder}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{isEditing ? (
|
||||
<motion.div
|
||||
key="editing"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className={cn('flex items-center gap-1.5', className)}
|
||||
>
|
||||
{multiline ? (
|
||||
<Textarea
|
||||
ref={inputRef as React.RefObject<HTMLTextAreaElement>}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
disabled={isSaving}
|
||||
placeholder={placeholder}
|
||||
className={cn(variantStyles[variant], 'min-h-[60px] resize-y')}
|
||||
rows={3}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef as React.RefObject<HTMLInputElement>}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
disabled={isSaving}
|
||||
placeholder={placeholder}
|
||||
className={cn(variantStyles[variant], 'h-auto py-1')}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="shrink-0 rounded p-1 text-emerald-600 hover:bg-emerald-50 transition-colors"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
disabled={isSaving}
|
||||
className="shrink-0 rounded p-1 text-muted-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.button
|
||||
key="viewing"
|
||||
type="button"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
onClick={() => setIsEditing(true)}
|
||||
className={cn(
|
||||
'group inline-flex items-center gap-1.5 rounded-md px-1.5 py-0.5 -mx-1.5 -my-0.5',
|
||||
'hover:bg-muted/60 transition-colors text-left cursor-text',
|
||||
variantStyles[variant],
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className={cn(!value && 'text-muted-foreground italic')}>
|
||||
{value || placeholder}
|
||||
</span>
|
||||
<Pencil className="h-3 w-3 shrink-0 opacity-30 sm:opacity-0 sm:group-hover:opacity-50 transition-opacity" />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Pencil, Check, X } from 'lucide-react'
|
||||
|
||||
type InlineEditableTextVariant = 'h1' | 'h2' | 'body' | 'mono'
|
||||
|
||||
type InlineEditableTextProps = {
|
||||
value: string
|
||||
onSave: (newValue: string) => void | Promise<void>
|
||||
variant?: InlineEditableTextVariant
|
||||
placeholder?: string
|
||||
multiline?: boolean
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const variantStyles: Record<InlineEditableTextVariant, string> = {
|
||||
h1: 'text-xl font-bold',
|
||||
h2: 'text-base font-semibold',
|
||||
body: 'text-sm',
|
||||
mono: 'text-sm font-mono text-muted-foreground',
|
||||
}
|
||||
|
||||
export function InlineEditableText({
|
||||
value,
|
||||
onSave,
|
||||
variant = 'body',
|
||||
placeholder = 'Click to edit...',
|
||||
multiline = false,
|
||||
disabled = false,
|
||||
className,
|
||||
}: InlineEditableTextProps) {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editValue, setEditValue] = useState(value)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
inputRef.current.select()
|
||||
}
|
||||
}, [isEditing])
|
||||
|
||||
useEffect(() => {
|
||||
setEditValue(value)
|
||||
}, [value])
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
const trimmed = editValue.trim()
|
||||
if (trimmed === value) {
|
||||
setIsEditing(false)
|
||||
return
|
||||
}
|
||||
if (!trimmed) {
|
||||
setEditValue(value)
|
||||
setIsEditing(false)
|
||||
return
|
||||
}
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await onSave(trimmed)
|
||||
setIsEditing(false)
|
||||
} catch {
|
||||
setEditValue(value)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}, [editValue, value, onSave])
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setEditValue(value)
|
||||
setIsEditing(false)
|
||||
}, [value])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleCancel()
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
if (multiline && !e.ctrlKey) return
|
||||
e.preventDefault()
|
||||
handleSave()
|
||||
}
|
||||
},
|
||||
[handleCancel, handleSave, multiline]
|
||||
)
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<span className={cn(variantStyles[variant], className)}>
|
||||
{value || <span className="text-muted-foreground italic">{placeholder}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{isEditing ? (
|
||||
<motion.div
|
||||
key="editing"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className={cn('flex items-center gap-1.5', className)}
|
||||
>
|
||||
{multiline ? (
|
||||
<Textarea
|
||||
ref={inputRef as React.RefObject<HTMLTextAreaElement>}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
disabled={isSaving}
|
||||
placeholder={placeholder}
|
||||
className={cn(variantStyles[variant], 'min-h-[60px] resize-y')}
|
||||
rows={3}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef as React.RefObject<HTMLInputElement>}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
disabled={isSaving}
|
||||
placeholder={placeholder}
|
||||
className={cn(variantStyles[variant], 'h-auto py-1')}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="shrink-0 rounded p-1 text-emerald-600 hover:bg-emerald-50 transition-colors"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
disabled={isSaving}
|
||||
className="shrink-0 rounded p-1 text-muted-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.button
|
||||
key="viewing"
|
||||
type="button"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
onClick={() => setIsEditing(true)}
|
||||
className={cn(
|
||||
'group inline-flex items-center gap-1.5 rounded-md px-1.5 py-0.5 -mx-1.5 -my-0.5',
|
||||
'hover:bg-muted/60 transition-colors text-left cursor-text',
|
||||
variantStyles[variant],
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className={cn(!value && 'text-muted-foreground italic')}>
|
||||
{value || placeholder}
|
||||
</span>
|
||||
<Pencil className="h-3 w-3 shrink-0 opacity-30 sm:opacity-0 sm:group-hover:opacity-50 transition-opacity" />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user