presets community
This commit is contained in:
@@ -4,6 +4,7 @@ import { auth } from '@/lib/auth'
|
|||||||
import { supabaseAdmin } from '@/lib/supabase/server'
|
import { supabaseAdmin } from '@/lib/supabase/server'
|
||||||
|
|
||||||
// PUT /api/presets/[id]
|
// PUT /api/presets/[id]
|
||||||
|
// Accepts any subset of: name, data, is_community
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> },
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
@@ -15,25 +16,30 @@ export async function PUT(
|
|||||||
|
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
const { name } = body
|
const { name, data, is_community } = body
|
||||||
|
|
||||||
if (!name) {
|
if (name === undefined && data === undefined && is_community === undefined) {
|
||||||
return NextResponse.json({ error: 'Missing name' }, { status: 400 })
|
return NextResponse.json({ error: 'Nothing to update' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: existing } = await supabaseAdmin
|
const { data: existing } = await supabaseAdmin
|
||||||
.from('presets')
|
.from('presets')
|
||||||
.select('user_id, is_community')
|
.select('user_id')
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
.single()
|
.single()
|
||||||
|
|
||||||
if (!existing || existing.user_id !== session.user.id || existing.is_community) {
|
if (!existing || existing.user_id !== session.user.id) {
|
||||||
return NextResponse.json({ error: 'Not found or forbidden' }, { status: 403 })
|
return NextResponse.json({ error: 'Not found or forbidden' }, { status: 403 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updates: Record<string, unknown> = {}
|
||||||
|
if (name !== undefined) updates.name = name
|
||||||
|
if (data !== undefined) updates.data = data
|
||||||
|
if (is_community !== undefined) updates.is_community = is_community
|
||||||
|
|
||||||
const { data: preset, error } = await supabaseAdmin
|
const { data: preset, error } = await supabaseAdmin
|
||||||
.from('presets')
|
.from('presets')
|
||||||
.update({ name })
|
.update(updates)
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
.select()
|
.select()
|
||||||
.single()
|
.single()
|
||||||
@@ -56,11 +62,11 @@ export async function DELETE(
|
|||||||
|
|
||||||
const { data: existing } = await supabaseAdmin
|
const { data: existing } = await supabaseAdmin
|
||||||
.from('presets')
|
.from('presets')
|
||||||
.select('user_id, is_community, thumbnail_url')
|
.select('user_id, thumbnail_url')
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
.single()
|
.single()
|
||||||
|
|
||||||
if (!existing || existing.user_id !== session.user.id || existing.is_community) {
|
if (!existing || existing.user_id !== session.user.id) {
|
||||||
return NextResponse.json({ error: 'Not found or forbidden' }, { status: 403 })
|
return NextResponse.json({ error: 'Not found or forbidden' }, { status: 403 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export async function GET(req: NextRequest) {
|
|||||||
.select('*')
|
.select('*')
|
||||||
.eq('type', type)
|
.eq('type', type)
|
||||||
.eq('user_id', session.user.id)
|
.eq('user_id', session.user.id)
|
||||||
.eq('is_community', false)
|
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
|
|
||||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
||||||
|
|||||||
@@ -117,9 +117,9 @@ export function DoorPanel() {
|
|||||||
handleUpdate({ segments: updated })
|
handleUpdate({ segments: updated })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSavePreset = useCallback(async (name: string) => {
|
const getDoorPresetData = useCallback(() => {
|
||||||
if (!node) return
|
if (!node) return null
|
||||||
const data = {
|
return {
|
||||||
width: node.width,
|
width: node.width,
|
||||||
height: node.height,
|
height: node.height,
|
||||||
frameThickness: node.frameThickness,
|
frameThickness: node.frameThickness,
|
||||||
@@ -137,12 +137,27 @@ export function DoorPanel() {
|
|||||||
panicBarHeight: node.panicBarHeight,
|
panicBarHeight: node.panicBarHeight,
|
||||||
segments: node.segments,
|
segments: node.segments,
|
||||||
}
|
}
|
||||||
|
}, [node])
|
||||||
|
|
||||||
|
const handleSavePreset = useCallback(async (name: string) => {
|
||||||
|
const data = getDoorPresetData()
|
||||||
|
if (!data) return
|
||||||
await fetch('/api/presets', {
|
await fetch('/api/presets', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ type: 'door', name, data }),
|
body: JSON.stringify({ type: 'door', name, data }),
|
||||||
})
|
})
|
||||||
}, [node])
|
}, [getDoorPresetData])
|
||||||
|
|
||||||
|
const handleOverwritePreset = useCallback(async (id: string) => {
|
||||||
|
const data = getDoorPresetData()
|
||||||
|
if (!data) return
|
||||||
|
await fetch(`/api/presets/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ data }),
|
||||||
|
})
|
||||||
|
}, [getDoorPresetData])
|
||||||
|
|
||||||
const handleApplyPreset = useCallback((data: Record<string, unknown>) => {
|
const handleApplyPreset = useCallback((data: Record<string, unknown>) => {
|
||||||
handleUpdate(data as Partial<DoorNode>)
|
handleUpdate(data as Partial<DoorNode>)
|
||||||
@@ -162,7 +177,7 @@ export function DoorPanel() {
|
|||||||
>
|
>
|
||||||
{/* Presets strip */}
|
{/* Presets strip */}
|
||||||
<div className="px-3 pt-2.5 pb-1.5 border-b border-border/30">
|
<div className="px-3 pt-2.5 pb-1.5 border-b border-border/30">
|
||||||
<PresetsPopover type="door" onApply={handleApplyPreset} onSave={handleSavePreset}>
|
<PresetsPopover type="door" onApply={handleApplyPreset} onSave={handleSavePreset} onOverwrite={handleOverwritePreset}>
|
||||||
<button className="flex w-full items-center gap-2 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-[#3e3e3e] transition-colors">
|
<button className="flex w-full items-center gap-2 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-[#3e3e3e] transition-colors">
|
||||||
<BookMarked className="h-3.5 w-3.5 shrink-0" />
|
<BookMarked className="h-3.5 w-3.5 shrink-0" />
|
||||||
<span>Presets</span>
|
<span>Presets</span>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { BookMarked, Pencil, Plus, Trash2, Users, Check, X } from 'lucide-react'
|
import { BookMarked, Check, Globe, GlobeLock, Pencil, Plus, Save, Trash2, Users, X } from 'lucide-react'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/primitives/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/primitives/popover'
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/primitives/tooltip'
|
||||||
import { useAuth } from '@/features/community/lib/auth/hooks'
|
import { useAuth } from '@/features/community/lib/auth/hooks'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -23,19 +24,23 @@ type Tab = 'community' | 'mine'
|
|||||||
|
|
||||||
interface PresetsPopoverProps {
|
interface PresetsPopoverProps {
|
||||||
type: PresetType
|
type: PresetType
|
||||||
|
/** Apply preset data to the current node */
|
||||||
onApply: (data: Record<string, unknown>) => void
|
onApply: (data: Record<string, unknown>) => void
|
||||||
|
/** Save current node state as a new preset with the given name */
|
||||||
onSave: (name: string) => Promise<void>
|
onSave: (name: string) => Promise<void>
|
||||||
|
/** Overwrite an existing preset's data with the current node state */
|
||||||
|
onOverwrite: (id: string) => Promise<void>
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PresetsPopover({ type, onApply, onSave, children }: PresetsPopoverProps) {
|
export function PresetsPopover({ type, onApply, onSave, onOverwrite, children }: PresetsPopoverProps) {
|
||||||
const { isAuthenticated } = useAuth()
|
const { isAuthenticated } = useAuth()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [tab, setTab] = useState<Tab>('community')
|
const [tab, setTab] = useState<Tab>('community')
|
||||||
const [presets, setPresets] = useState<PresetData[]>([])
|
const [presets, setPresets] = useState<PresetData[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
// Save dialog state
|
// New preset save state
|
||||||
const [showSaveInput, setShowSaveInput] = useState(false)
|
const [showSaveInput, setShowSaveInput] = useState(false)
|
||||||
const [saveName, setSaveName] = useState('')
|
const [saveName, setSaveName] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@@ -47,6 +52,9 @@ export function PresetsPopover({ type, onApply, onSave, children }: PresetsPopov
|
|||||||
// Delete confirmation
|
// Delete confirmation
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null)
|
const [deletingId, setDeletingId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Overwrite feedback (shows check icon briefly after overwrite)
|
||||||
|
const [overwrittenId, setOverwrittenId] = useState<string | null>(null)
|
||||||
|
|
||||||
const fetchPresets = useCallback(async () => {
|
const fetchPresets = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -64,12 +72,11 @@ export function PresetsPopover({ type, onApply, onSave, children }: PresetsPopov
|
|||||||
if (open) fetchPresets()
|
if (open) fetchPresets()
|
||||||
}, [open, fetchPresets])
|
}, [open, fetchPresets])
|
||||||
|
|
||||||
// Switch tab to community if user signs out while on mine tab
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated && tab === 'mine') setTab('community')
|
if (!isAuthenticated && tab === 'mine') setTab('community')
|
||||||
}, [isAuthenticated, tab])
|
}, [isAuthenticated, tab])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSaveNew = async () => {
|
||||||
if (!saveName.trim()) return
|
if (!saveName.trim()) return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
@@ -104,6 +111,23 @@ export function PresetsPopover({ type, onApply, onSave, children }: PresetsPopov
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOverwrite = async (id: string) => {
|
||||||
|
await onOverwrite(id)
|
||||||
|
setOverwrittenId(id)
|
||||||
|
setTimeout(() => setOverwrittenId(null), 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleCommunity = async (id: string, current: boolean) => {
|
||||||
|
const res = await fetch(`/api/presets/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ is_community: !current }),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
setPresets((prev) => prev.map((p) => (p.id === id ? { ...p, is_community: !current } : p)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||||
@@ -123,19 +147,16 @@ export function PresetsPopover({ type, onApply, onSave, children }: PresetsPopov
|
|||||||
</div>
|
</div>
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => { setShowSaveInput((v) => !v); setSaveName('') }}
|
||||||
setShowSaveInput((v) => !v)
|
|
||||||
setSaveName('')
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium text-muted-foreground hover:text-foreground hover:bg-white/10 transition-colors"
|
className="flex items-center gap-1 rounded-md px-2 py-1 text-[11px] font-medium text-muted-foreground hover:text-foreground hover:bg-white/10 transition-colors"
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
Save preset
|
Save new
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Save input */}
|
{/* New preset name input */}
|
||||||
{showSaveInput && (
|
{showSaveInput && (
|
||||||
<div className="flex items-center gap-1.5 px-3 py-2 border-b border-border/50 bg-white/5">
|
<div className="flex items-center gap-1.5 px-3 py-2 border-b border-border/50 bg-white/5">
|
||||||
<input
|
<input
|
||||||
@@ -143,7 +164,7 @@ export function PresetsPopover({ type, onApply, onSave, children }: PresetsPopov
|
|||||||
value={saveName}
|
value={saveName}
|
||||||
onChange={(e) => setSaveName(e.target.value)}
|
onChange={(e) => setSaveName(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') handleSave()
|
if (e.key === 'Enter') handleSaveNew()
|
||||||
if (e.key === 'Escape') { setShowSaveInput(false); setSaveName('') }
|
if (e.key === 'Escape') { setShowSaveInput(false); setSaveName('') }
|
||||||
}}
|
}}
|
||||||
placeholder="Preset name…"
|
placeholder="Preset name…"
|
||||||
@@ -151,7 +172,7 @@ export function PresetsPopover({ type, onApply, onSave, children }: PresetsPopov
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
disabled={!saveName.trim() || saving}
|
disabled={!saveName.trim() || saving}
|
||||||
onClick={handleSave}
|
onClick={handleSaveNew}
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-md bg-primary/20 hover:bg-primary/30 text-primary disabled:opacity-40 transition-colors"
|
className="flex h-6 w-6 items-center justify-center rounded-md bg-primary/20 hover:bg-primary/30 text-primary disabled:opacity-40 transition-colors"
|
||||||
>
|
>
|
||||||
<Check className="h-3.5 w-3.5" />
|
<Check className="h-3.5 w-3.5" />
|
||||||
@@ -173,10 +194,7 @@ export function PresetsPopover({ type, onApply, onSave, children }: PresetsPopov
|
|||||||
</TabButton>
|
</TabButton>
|
||||||
<TabButton
|
<TabButton
|
||||||
active={tab === 'mine'}
|
active={tab === 'mine'}
|
||||||
onClick={() => {
|
onClick={() => { if (isAuthenticated) setTab('mine') }}
|
||||||
if (!isAuthenticated) return
|
|
||||||
setTab('mine')
|
|
||||||
}}
|
|
||||||
disabled={!isAuthenticated}
|
disabled={!isAuthenticated}
|
||||||
>
|
>
|
||||||
<BookMarked className="h-3 w-3" />
|
<BookMarked className="h-3 w-3" />
|
||||||
@@ -202,7 +220,10 @@ export function PresetsPopover({ type, onApply, onSave, children }: PresetsPopov
|
|||||||
renamingId={renamingId}
|
renamingId={renamingId}
|
||||||
renameValue={renameValue}
|
renameValue={renameValue}
|
||||||
deletingId={deletingId}
|
deletingId={deletingId}
|
||||||
|
overwrittenId={overwrittenId}
|
||||||
onApply={() => { onApply(preset.data); setOpen(false) }}
|
onApply={() => { onApply(preset.data); setOpen(false) }}
|
||||||
|
onOverwrite={() => handleOverwrite(preset.id)}
|
||||||
|
onToggleCommunity={() => handleToggleCommunity(preset.id, preset.is_community)}
|
||||||
onStartRename={() => { setRenamingId(preset.id); setRenameValue(preset.name) }}
|
onStartRename={() => { setRenamingId(preset.id); setRenameValue(preset.name) }}
|
||||||
onRenameChange={setRenameValue}
|
onRenameChange={setRenameValue}
|
||||||
onRenameConfirm={() => handleRename(preset.id)}
|
onRenameConfirm={() => handleRename(preset.id)}
|
||||||
@@ -256,7 +277,7 @@ function EmptyState({ tab, isAuthenticated }: { tab: Tab; isAuthenticated: boole
|
|||||||
{tab === 'community'
|
{tab === 'community'
|
||||||
? 'No community presets yet.'
|
? 'No community presets yet.'
|
||||||
: isAuthenticated
|
: isAuthenticated
|
||||||
? 'No presets saved yet. Use "Save preset" to save the current configuration.'
|
? 'No presets saved yet. Use "Save new" to save the current configuration.'
|
||||||
: 'Sign in to save and view your presets.'}
|
: 'Sign in to save and view your presets.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,7 +290,10 @@ interface PresetRowProps {
|
|||||||
renamingId: string | null
|
renamingId: string | null
|
||||||
renameValue: string
|
renameValue: string
|
||||||
deletingId: string | null
|
deletingId: string | null
|
||||||
|
overwrittenId: string | null
|
||||||
onApply: () => void
|
onApply: () => void
|
||||||
|
onOverwrite: () => void
|
||||||
|
onToggleCommunity: () => void
|
||||||
onStartRename: () => void
|
onStartRename: () => void
|
||||||
onRenameChange: (v: string) => void
|
onRenameChange: (v: string) => void
|
||||||
onRenameConfirm: () => void
|
onRenameConfirm: () => void
|
||||||
@@ -285,7 +309,10 @@ function PresetRow({
|
|||||||
renamingId,
|
renamingId,
|
||||||
renameValue,
|
renameValue,
|
||||||
deletingId,
|
deletingId,
|
||||||
|
overwrittenId,
|
||||||
onApply,
|
onApply,
|
||||||
|
onOverwrite,
|
||||||
|
onToggleCommunity,
|
||||||
onStartRename,
|
onStartRename,
|
||||||
onRenameChange,
|
onRenameChange,
|
||||||
onRenameConfirm,
|
onRenameConfirm,
|
||||||
@@ -296,6 +323,7 @@ function PresetRow({
|
|||||||
}: PresetRowProps) {
|
}: PresetRowProps) {
|
||||||
const isRenaming = renamingId === preset.id
|
const isRenaming = renamingId === preset.id
|
||||||
const isDeleting = deletingId === preset.id
|
const isDeleting = deletingId === preset.id
|
||||||
|
const justOverwritten = overwrittenId === preset.id
|
||||||
|
|
||||||
if (isDeleting) {
|
if (isDeleting) {
|
||||||
return (
|
return (
|
||||||
@@ -350,7 +378,7 @@ function PresetRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="group flex items-center gap-2 px-3 py-2.5 hover:bg-white/5 transition-colors">
|
<li className="group flex items-center gap-2 px-3 py-2.5 hover:bg-white/5 transition-colors">
|
||||||
{/* Thumbnail placeholder */}
|
{/* Thumbnail */}
|
||||||
<div className="h-8 w-12 shrink-0 rounded-md border border-border/40 bg-white/5 overflow-hidden">
|
<div className="h-8 w-12 shrink-0 rounded-md border border-border/40 bg-white/5 overflow-hidden">
|
||||||
{preset.thumbnail_url ? (
|
{preset.thumbnail_url ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
@@ -362,32 +390,87 @@ function PresetRow({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
{/* Name + date — clicking applies */}
|
||||||
onClick={onApply}
|
<button onClick={onApply} className="flex-1 min-w-0 text-left">
|
||||||
className="flex-1 min-w-0 text-left"
|
<span className="flex items-center gap-1.5">
|
||||||
>
|
<span className="block truncate text-xs font-medium text-foreground group-hover:text-foreground/90">
|
||||||
<span className="block truncate text-xs font-medium text-foreground group-hover:text-foreground/90">
|
{preset.name}
|
||||||
{preset.name}
|
</span>
|
||||||
|
{preset.is_community && (
|
||||||
|
<Globe className="h-2.5 w-2.5 shrink-0 text-muted-foreground/50" />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="block text-[10px] text-muted-foreground/60">
|
<span className="block text-[10px] text-muted-foreground/60">
|
||||||
{new Date(preset.created_at).toLocaleDateString()}
|
{new Date(preset.created_at).toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Actions — only shown on hover for "My presets" */}
|
||||||
{isMine && (
|
{isMine && (
|
||||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
||||||
<button
|
{/* Save into (overwrite) */}
|
||||||
onClick={onStartRename}
|
<Tooltip>
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-white/10 transition-colors"
|
<TooltipTrigger asChild>
|
||||||
>
|
<button
|
||||||
<Pencil className="h-3 w-3" />
|
onClick={onOverwrite}
|
||||||
</button>
|
className={cn(
|
||||||
<button
|
'flex h-6 w-6 items-center justify-center rounded-md transition-colors',
|
||||||
onClick={onDeleteRequest}
|
justOverwritten
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-red-400 hover:bg-red-500/10 transition-colors"
|
? 'text-green-400 bg-green-500/10'
|
||||||
>
|
: 'text-muted-foreground hover:text-foreground hover:bg-white/10',
|
||||||
<Trash2 className="h-3 w-3" />
|
)}
|
||||||
</button>
|
>
|
||||||
|
{justOverwritten ? <Check className="h-3 w-3" /> : <Save className="h-3 w-3" />}
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Save current config here</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Community toggle */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={onToggleCommunity}
|
||||||
|
className={cn(
|
||||||
|
'flex h-6 w-6 items-center justify-center rounded-md transition-colors',
|
||||||
|
preset.is_community
|
||||||
|
? 'text-blue-400 hover:text-blue-300 hover:bg-blue-500/10'
|
||||||
|
: 'text-muted-foreground hover:text-blue-400 hover:bg-blue-500/10',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{preset.is_community ? <Globe className="h-3 w-3" /> : <GlobeLock className="h-3 w-3" />}
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
{preset.is_community ? 'Remove from community' : 'Share with community'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Rename */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={onStartRename}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-white/10 transition-colors"
|
||||||
|
>
|
||||||
|
<Pencil className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Rename</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Delete */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={onDeleteRequest}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-red-400 hover:bg-red-500/10 transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Delete</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ export function WindowPanel() {
|
|||||||
setSelection({ selectedIds: [] })
|
setSelection({ selectedIds: [] })
|
||||||
}, [node, setMovingNode, setSelection])
|
}, [node, setMovingNode, setSelection])
|
||||||
|
|
||||||
const handleSavePreset = useCallback(async (name: string) => {
|
const getWindowPresetData = useCallback(() => {
|
||||||
if (!node) return
|
if (!node) return null
|
||||||
const data = {
|
return {
|
||||||
width: node.width,
|
width: node.width,
|
||||||
height: node.height,
|
height: node.height,
|
||||||
frameThickness: node.frameThickness,
|
frameThickness: node.frameThickness,
|
||||||
@@ -107,12 +107,27 @@ export function WindowPanel() {
|
|||||||
sillDepth: node.sillDepth,
|
sillDepth: node.sillDepth,
|
||||||
sillThickness: node.sillThickness,
|
sillThickness: node.sillThickness,
|
||||||
}
|
}
|
||||||
|
}, [node])
|
||||||
|
|
||||||
|
const handleSavePreset = useCallback(async (name: string) => {
|
||||||
|
const data = getWindowPresetData()
|
||||||
|
if (!data) return
|
||||||
await fetch('/api/presets', {
|
await fetch('/api/presets', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ type: 'window', name, data }),
|
body: JSON.stringify({ type: 'window', name, data }),
|
||||||
})
|
})
|
||||||
}, [node])
|
}, [getWindowPresetData])
|
||||||
|
|
||||||
|
const handleOverwritePreset = useCallback(async (id: string) => {
|
||||||
|
const data = getWindowPresetData()
|
||||||
|
if (!data) return
|
||||||
|
await fetch(`/api/presets/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ data }),
|
||||||
|
})
|
||||||
|
}, [getWindowPresetData])
|
||||||
|
|
||||||
const handleApplyPreset = useCallback((data: Record<string, unknown>) => {
|
const handleApplyPreset = useCallback((data: Record<string, unknown>) => {
|
||||||
handleUpdate(data as Partial<WindowNode>)
|
handleUpdate(data as Partial<WindowNode>)
|
||||||
@@ -163,7 +178,7 @@ export function WindowPanel() {
|
|||||||
>
|
>
|
||||||
{/* Presets strip */}
|
{/* Presets strip */}
|
||||||
<div className="px-3 pt-2.5 pb-1.5 border-b border-border/30">
|
<div className="px-3 pt-2.5 pb-1.5 border-b border-border/30">
|
||||||
<PresetsPopover type="window" onApply={handleApplyPreset} onSave={handleSavePreset}>
|
<PresetsPopover type="window" onApply={handleApplyPreset} onSave={handleSavePreset} onOverwrite={handleOverwritePreset}>
|
||||||
<button className="flex w-full items-center gap-2 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-[#3e3e3e] transition-colors">
|
<button className="flex w-full items-center gap-2 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-[#3e3e3e] transition-colors">
|
||||||
<BookMarked className="h-3.5 w-3.5 shrink-0" />
|
<BookMarked className="h-3.5 w-3.5 shrink-0" />
|
||||||
<span>Presets</span>
|
<span>Presets</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user