refactor(symbol-manager): remove template import, focus on library UX
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 12m43s

This commit is contained in:
Pepe Ziberi
2026-05-21 07:41:17 +02:00
parent 56895be16f
commit 40cea9a9be
3 changed files with 1728 additions and 161 deletions

123
build-log.txt Normal file
View File

@@ -0,0 +1,123 @@
▲ Next.js 15.5.12
- Environments: .env
- Experiments (use with caution):
· serverActions
Creating an optimized production build ...
✓ Compiled successfully in 5.8s
Skipping validation of types
Linting ...
Collecting page data ...
⚠ Using edge runtime on a page currently disables static generation for that page
Generating static pages (0/53) ...
Generating static pages (13/53)
Generating static pages (26/53)
Generating static pages (39/53)
✓ Generating static pages (53/53)
Finalizing page optimization ...
Collecting build traces ...
Route (app) Size First Load JS
┌ ○ / 3.03 kB 361 kB
├ ○ /_not-found 1.02 kB 340 kB
├ ƒ /[slug] 3.52 kB 348 kB
├ ○ /admin 35.3 kB 424 kB
├ ƒ /api/admin/categories 325 B 340 kB
├ ƒ /api/admin/categories/[id] 325 B 340 kB
├ ƒ /api/admin/icons 325 B 340 kB
├ ƒ /api/admin/icons/[id] 325 B 340 kB
├ ƒ /api/admin/icons/upload 325 B 340 kB
├ ƒ /api/admin/projects 325 B 340 kB
├ ƒ /api/admin/settings 325 B 340 kB
├ ƒ /api/admin/tenants 325 B 340 kB
├ ƒ /api/admin/tenants/[id] 325 B 340 kB
├ ƒ /api/admin/tenants/[id]/logo 325 B 340 kB
├ ƒ /api/admin/tenants/[id]/logo/serve 325 B 340 kB
├ ƒ /api/admin/tenants/[id]/members 325 B 340 kB
├ ƒ /api/admin/trial-reminders 325 B 340 kB
├ ƒ /api/admin/users 325 B 340 kB
├ ƒ /api/admin/users/[id] 325 B 340 kB
├ ƒ /api/admin/users/[id]/reset-password 325 B 340 kB
├ ƒ /api/auth/change-password 325 B 340 kB
├ ƒ /api/auth/delete-account 325 B 340 kB
├ ƒ /api/auth/forgot-password 325 B 340 kB
├ ƒ /api/auth/login 325 B 340 kB
├ ƒ /api/auth/logout 325 B 340 kB
├ ƒ /api/auth/me 325 B 340 kB
├ ƒ /api/auth/register 325 B 340 kB
├ ƒ /api/auth/resend-verification 325 B 340 kB
├ ƒ /api/auth/reset-password 325 B 340 kB
├ ƒ /api/auth/verify-email 325 B 340 kB
├ ƒ /api/contact 325 B 340 kB
├ ƒ /api/demo 325 B 340 kB
├ ƒ /api/dictionary 325 B 340 kB
├ ƒ /api/dictionary/[id] 325 B 340 kB
├ ƒ /api/donate/checkout 325 B 340 kB
├ ƒ /api/donate/config 325 B 340 kB
├ ƒ /api/donate/webhook 325 B 340 kB
├ ƒ /api/hose-types 325 B 340 kB
├ ƒ /api/hose-types/[id] 325 B 340 kB
├ ƒ /api/icons 325 B 340 kB
├ ƒ /api/icons/[id]/image 325 B 340 kB
├ ƒ /api/icons/[id]/toggle-visibility 325 B 340 kB
├ ƒ /api/icons/upload 325 B 340 kB
├ ƒ /api/projects 325 B 340 kB
├ ƒ /api/projects/[id] 325 B 340 kB
├ ƒ /api/projects/[id]/editing 325 B 340 kB
├ ƒ /api/projects/[id]/export 325 B 340 kB
├ ƒ /api/projects/[id]/features 325 B 340 kB
├ ƒ /api/projects/[id]/journal 325 B 340 kB
├ ƒ /api/projects/[id]/journal/check-items 325 B 340 kB
├ ƒ /api/projects/[id]/journal/check-items/[itemId] 325 B 340 kB
├ ƒ /api/projects/[id]/journal/entries 325 B 340 kB
├ ƒ /api/projects/[id]/journal/entries/[entryId] 325 B 340 kB
├ ƒ /api/projects/[id]/journal/pendenzen 325 B 340 kB
├ ƒ /api/projects/[id]/journal/pendenzen/[pendenzId] 325 B 340 kB
├ ƒ /api/projects/[id]/journal/send-report 325 B 340 kB
├ ƒ /api/projects/[id]/plan-image 325 B 340 kB
├ ƒ /api/projects/[id]/plan-image/serve 325 B 340 kB
├ ƒ /api/rapports 325 B 340 kB
├ ƒ /api/rapports/[token] 325 B 340 kB
├ ƒ /api/rapports/[token]/pdf 325 B 340 kB
├ ƒ /api/rapports/[token]/send 325 B 340 kB
├ ƒ /api/settings/public 325 B 340 kB
├ ƒ /api/templates 325 B 340 kB
├ ƒ /api/templates/import 325 B 340 kB
├ ƒ /api/tenant/categories 325 B 340 kB
├ ƒ /api/tenant/delete 325 B 340 kB
├ ƒ /api/tenant/info 325 B 340 kB
├ ƒ /api/tenant/logo 325 B 340 kB
├ ƒ /api/tenant/soma-templates 325 B 340 kB
├ ƒ /api/tenant/symbols 325 B 340 kB
├ ƒ /api/tenant/symbols/[id]/image 325 B 340 kB
├ ƒ /api/tenants/[tenantId]/suggestions 325 B 340 kB
├ ƒ /api/tenants/by-slug/[slug] 325 B 340 kB
├ ƒ /api/upgrade-requests 325 B 340 kB
├ ƒ /api/upgrade-requests/[id] 325 B 340 kB
├ ○ /app 281 kB 879 kB
├ ○ /datenschutz 3.18 kB 347 kB
├ ○ /demo 8.78 kB 555 kB
├ ○ /forgot-password 3.88 kB 361 kB
├ ○ /impressum 3.01 kB 353 kB
├ ○ /login 5.74 kB 363 kB
├ ƒ /opengraph-image 325 B 340 kB
├ ƒ /rapport/[token] 4.83 kB 344 kB
├ ○ /register 5.48 kB 363 kB
├ ○ /reset-password 4.01 kB 362 kB
├ ○ /robots.txt 325 B 340 kB
├ ○ /settings 4.28 kB 356 kB
├ ○ /sitemap.xml 325 B 340 kB
├ ○ /spenden 5.26 kB 363 kB
└ ○ /spenden/danke 2.54 kB 360 kB
+ First Load JS shared by all 339 kB
├ chunks/1255-94429a3f41c08b44.js 65.5 kB
├ chunks/4bd1b696-100b9d70ed4e49c1.js 54.2 kB
├ chunks/ed9f2dc4-1b30afa125168b53.js 217 kB
└ other shared chunks (total) 2.21 kB
ƒ Middleware 40.1 kB
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand

File diff suppressed because one or more lines are too long

View File

@@ -32,9 +32,7 @@ import {
LayoutGrid,
ImageIcon,
FolderOpen,
Download,
AlertCircle,
Package,
Library,
} from 'lucide-react'
@@ -63,14 +61,6 @@ interface SymbolGroup {
symbols: TenantSymbol[]
}
interface TemplatePackage {
packageId: string
packageName: string
categoryCount: number
symbolCount: number
previewSymbols: { name: string; svgPath: string }[]
}
/* ─── Component ─── */
export function SymbolManager() {
const { toast } = useToast()
@@ -80,12 +70,11 @@ export function SymbolManager() {
const [categories, setCategories] = useState<TenantCategory[]>([])
const [symbolGroups, setSymbolGroups] = useState<SymbolGroup[]>([])
const [flatSymbols, setFlatSymbols] = useState<TenantSymbol[]>([])
const [templates, setTemplates] = useState<TemplatePackage[]>([])
/* -- UI state -- */
const [search, setSearch] = useState('')
const [expandedCats, setExpandedCats] = useState<Set<string>>(new Set())
const [activeTab, setActiveTab] = useState<'symbols' | 'categories' | 'import' | 'library'>('symbols')
const [activeTab, setActiveTab] = useState<'symbols' | 'categories' | 'library'>('library')
/* -- Symbol editing -- */
const [editingSymbolId, setEditingSymbolId] = useState<string | null>(null)
@@ -103,10 +92,6 @@ export function SymbolManager() {
const [uploading, setUploading] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
/* -- Import dialog -- */
const [importOpen, setImportOpen] = useState(false)
const [importingPkg, setImportingPkg] = useState<string | null>(null)
/* -- Library -- */
const [libraryIcons, setLibraryIcons] = useState<any[]>([])
const [librarySearch, setLibrarySearch] = useState('')
@@ -117,10 +102,9 @@ export function SymbolManager() {
const fetchData = useCallback(async () => {
setLoading(true)
try {
const [catRes, symRes, tplRes] = await Promise.all([
const [catRes, symRes] = await Promise.all([
fetch('/api/tenant/categories'),
fetch('/api/tenant/symbols?grouped=true'),
fetch('/api/templates'),
])
if (catRes.ok) {
const c = await catRes.json()
@@ -133,10 +117,6 @@ export function SymbolManager() {
const all = (s.categories || []).flatMap((c: any) => c.symbols || [])
setFlatSymbols(all)
}
if (tplRes.ok) {
const t = await tplRes.json()
setTemplates(t.packages || [])
}
} catch {
toast({ title: 'Fehler beim Laden', variant: 'destructive' })
}
@@ -281,30 +261,6 @@ export function SymbolManager() {
toast({ title: `${success} Datei(en) hochgeladen` })
}
/* ─── Import ─── */
const importPackage = async (packageId: string) => {
setImportingPkg(packageId)
try {
const res = await fetch('/api/templates/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ packageId }),
})
const data = await res.json().catch(() => ({}))
if (!res.ok) {
toast({ title: data.error || 'Import fehlgeschlagen', variant: 'destructive' })
} else {
toast({ title: `${data.imported} Symbole importiert` })
await fetchData()
setImportOpen(false)
setActiveTab('symbols')
}
} catch {
toast({ title: 'Import fehlgeschlagen', variant: 'destructive' })
}
setImportingPkg(null)
}
/* ─── Library ─── */
const fetchLibrary = useCallback(async () => {
setLibraryLoading(true)
@@ -374,7 +330,6 @@ export function SymbolManager() {
{ key: 'symbols', label: 'Meine Symbole', icon: LayoutGrid },
{ key: 'categories', label: 'Kategorien', icon: FolderOpen },
{ key: 'library', label: 'Bibliothek', icon: Library },
{ key: 'import', label: 'Vorlagen importieren', icon: Download },
] as const).map(t => (
<button
key={t.key}
@@ -393,9 +348,6 @@ export function SymbolManager() {
<Button size="sm" variant="outline" onClick={() => setUploadOpen(true)}>
<Upload className="w-4 h-4 mr-1.5" /> Upload
</Button>
<Button size="sm" variant="outline" onClick={() => setImportOpen(true)}>
<Download className="w-4 h-4 mr-1.5" /> Import
</Button>
</div>
</div>
@@ -461,15 +413,15 @@ export function SymbolManager() {
<div className="text-center py-12 border rounded-lg">
<ImageIcon className="w-10 h-10 mx-auto text-muted-foreground/40 mb-3" />
<p className="text-sm text-muted-foreground">
{search ? 'Keine Symbole gefunden.' : 'Noch keine Symbole vorhanden.'}
{search ? 'Keine Symbole gefunden.' : 'Noch keine Symbole. Füge Symbole aus der Bibliothek hinzu oder lade eigene hoch.'}
</p>
<div className="flex justify-center gap-2 mt-3">
<Button size="sm" variant="outline" onClick={() => setActiveTab('library')}>
<Library className="w-4 h-4 mr-1" /> Bibliothek durchsuchen
</Button>
<Button size="sm" variant="outline" onClick={() => setUploadOpen(true)}>
<Upload className="w-4 h-4 mr-1" /> Hochladen
</Button>
<Button size="sm" variant="outline" onClick={() => setImportOpen(true)}>
<Download className="w-4 h-4 mr-1" /> Importieren
</Button>
</div>
</div>
)}
@@ -564,62 +516,6 @@ export function SymbolManager() {
</div>
)}
{/* ===== TAB: Import ===== */}
{activeTab === 'import' && (
<div className="space-y-4">
{templates.length === 0 ? (
<div className="text-center py-12 border rounded-lg text-muted-foreground">
<Package className="w-10 h-10 mx-auto mb-3 opacity-40" />
<p className="text-sm">Keine Vorlagen-Pakete verfügbar.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{templates.map(pkg => (
<div key={pkg.packageId} className="border rounded-lg p-4 space-y-3 hover:border-primary/30 transition-colors">
<div className="flex items-start justify-between">
<div>
<h4 className="font-medium text-sm">{pkg.packageName}</h4>
<p className="text-xs text-muted-foreground mt-0.5">
{pkg.categoryCount} Kategorien · {pkg.symbolCount} Symbole
</p>
</div>
<Package className="w-5 h-5 text-muted-foreground" />
</div>
{/* Preview */}
<div className="flex gap-2">
{pkg.previewSymbols.slice(0, 4).map((p, i) => (
<div key={i} className="w-10 h-10 border rounded flex items-center justify-center bg-muted/30">
<img
src={`/signaturen/${p.svgPath.replace(/^.*[\\/]/, '')}`}
alt={p.name}
className="w-8 h-8 object-contain"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
/>
</div>
))}
</div>
<Button
size="sm"
className="w-full"
onClick={() => importPackage(pkg.packageId)}
disabled={importingPkg === pkg.packageId}
>
{importingPkg === pkg.packageId ? (
<Loader2 className="w-4 h-4 animate-spin mr-1.5" />
) : (
<Download className="w-4 h-4 mr-1.5" />
)}
{importingPkg === pkg.packageId ? 'Importiere...' : 'Importieren'}
</Button>
</div>
))}
</div>
)}
</div>
)}
{/* ===== TAB: Bibliothek ===== */}
{activeTab === 'library' && (
<div className="space-y-4">
@@ -749,57 +645,6 @@ export function SymbolManager() {
</DialogContent>
</Dialog>
{/* ===== IMPORT DIALOG ===== */}
<Dialog open={importOpen} onOpenChange={setImportOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Aus Vorlagen importieren</DialogTitle>
<DialogDescription>
Wähle ein Vorlagen-Paket aus, das als Mandanten-Symbole importiert werden soll.
</DialogDescription>
</DialogHeader>
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
{templates.map(pkg => (
<div key={pkg.packageId} className="flex items-center gap-3 border rounded-lg p-3">
<div className="flex -space-x-2">
{pkg.previewSymbols.slice(0, 3).map((p, i) => (
<div key={i} className="w-9 h-9 border-2 border-background rounded bg-muted/30 flex items-center justify-center">
<img
src={`/signaturen/${p.svgPath.replace(/^.*[\\/]/, '')}`}
alt=""
className="w-7 h-7 object-contain"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
/>
</div>
))}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{pkg.packageName}</p>
<p className="text-xs text-muted-foreground">{pkg.symbolCount} Symbole · {pkg.categoryCount} Kategorien</p>
</div>
<Button
size="sm"
onClick={() => importPackage(pkg.packageId)}
disabled={importingPkg === pkg.packageId}
>
{importingPkg === pkg.packageId ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Download className="w-4 h-4" />
)}
</Button>
</div>
))}
{templates.length === 0 && (
<div className="text-center py-6 text-sm text-muted-foreground">
Keine Vorlagen verfügbar.
</div>
)}
</div>
</DialogContent>
</Dialog>
</div>
)
}