diff --git a/shop/app/admin/layout.tsx b/shop/app/admin/layout.tsx
index 4c0c367..46da5c5 100644
--- a/shop/app/admin/layout.tsx
+++ b/shop/app/admin/layout.tsx
@@ -1,9 +1,10 @@
import Link from 'next/link'
-import { LayoutDashboard, Package, Settings, Users, LayoutGrid, ShoppingCart, Wrench, Database, Building2 } from 'lucide-react'
+import { LayoutDashboard, Package, Settings, Users, LayoutGrid, ShoppingCart, Wrench, Database, Building2, Sparkles } from 'lucide-react'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import { AdminLogoutButton } from '@/components/admin/logout-menu-item'
import { DemoWrapper } from '@/components/DemoWrapper'
+import { AdminNavLink } from '@/components/admin/admin-nav-link'
export default async function AdminLayout({
children,
@@ -41,53 +42,58 @@ export default async function AdminLayout({
+
diff --git a/shop/app/admin/wysiwyg/components/inline-input.tsx b/shop/app/admin/wysiwyg/components/inline-input.tsx
index c7f2e7d..a7372ba 100644
--- a/shop/app/admin/wysiwyg/components/inline-input.tsx
+++ b/shop/app/admin/wysiwyg/components/inline-input.tsx
@@ -26,8 +26,26 @@ export function InlineInput({ value, onSave, className, type = 'text' }: InlineI
const handleBlur = async () => {
setIsEditing(false)
- if (currentValue !== value) {
- await onSave(currentValue)
+ const trimmed = currentValue.trim()
+
+ if (type === 'number') {
+ const parsed = parseFloat(trimmed)
+ if (isNaN(parsed) || trimmed === '') {
+ setCurrentValue(value)
+ return
+ }
+ const finalVal = parsed.toString()
+ if (finalVal !== value) {
+ await onSave(finalVal)
+ }
+ } else {
+ if (trimmed === '') {
+ setCurrentValue(value)
+ return
+ }
+ if (trimmed !== value) {
+ await onSave(trimmed)
+ }
}
}
diff --git a/shop/app/admin/wysiwyg/components/module-popover.tsx b/shop/app/admin/wysiwyg/components/module-popover.tsx
new file mode 100644
index 0000000..d692bf3
--- /dev/null
+++ b/shop/app/admin/wysiwyg/components/module-popover.tsx
@@ -0,0 +1,118 @@
+'use client'
+
+import { useState, useEffect, useRef } from 'react'
+import { ProductModule } from '@/lib/types'
+import { Plus, Search } from 'lucide-react'
+
+interface ModulePopoverProps {
+ assignedModules: ProductModule[]
+ allModules: ProductModule[]
+ onAddModule: (module: ProductModule) => void
+}
+
+export function ModulePopover({ assignedModules, allModules, onAddModule }: ModulePopoverProps) {
+ const [isOpen, setIsOpen] = useState(false)
+ const [search, setSearch] = useState('')
+ const popoverRef = useRef
(null)
+ const inputRef = useRef(null)
+
+ // Filter modules that are not already assigned to this product
+ // We match by name to treat identical modules across products as the same entity
+ const availableModules = allModules.filter(m => {
+ const isAssigned = assignedModules.some(am => am.name.toLowerCase() === m.name.toLowerCase())
+ const matchesSearch = m.name.toLowerCase().includes(search.toLowerCase()) ||
+ (m.description && m.description.toLowerCase().includes(search.toLowerCase()))
+ return !isAssigned && matchesSearch
+ })
+
+ // Deduplicate available modules by name to show a clean list
+ const uniqueAvailableModules: ProductModule[] = []
+ const seenNames = new Set()
+ for (const m of availableModules) {
+ const nameKey = m.name.toLowerCase()
+ if (!seenNames.has(nameKey)) {
+ seenNames.add(nameKey)
+ uniqueAvailableModules.push(m)
+ }
+ }
+
+ useEffect(() => {
+ if (isOpen && inputRef.current) {
+ inputRef.current.focus()
+ }
+ }, [isOpen])
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
+ setIsOpen(false)
+ }
+ }
+ if (isOpen) {
+ document.addEventListener('mousedown', handleClickOutside)
+ }
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside)
+ }
+ }, [isOpen])
+
+ return (
+
+
+
+ {isOpen && (
+
+
+
+ setSearch(e.target.value)}
+ className="w-full bg-white/5 border border-white/10 rounded-lg pl-9 pr-3 py-1.5 text-xs text-white placeholder-slate-400 outline-none focus:border-primary/50 transition-colors"
+ />
+
+
+
+ {uniqueAvailableModules.length > 0 ? (
+ uniqueAvailableModules.map((mod) => (
+
+ ))
+ ) : (
+
+ Keine passenden Module gefunden.
+
+ )}
+
+
+ )}
+
+ )
+}
diff --git a/shop/app/admin/wysiwyg/page.tsx b/shop/app/admin/wysiwyg/page.tsx
index a6ad821..af94339 100644
--- a/shop/app/admin/wysiwyg/page.tsx
+++ b/shop/app/admin/wysiwyg/page.tsx
@@ -26,8 +26,23 @@ export default async function WysiwygAdminPage() {
)
}
+import { createClient } from '@/lib/supabase/server'
+
async function WysiwygDataWrapper() {
const products = await getProducts().catch(() => [])
const categories = await getCategories().catch(() => [])
- return
+
+ const supabase = await createClient()
+ const { data: allModules } = await supabase
+ .from('product_modules')
+ .select('*')
+ .order('name', { ascending: true })
+
+ return (
+
+ )
}
diff --git a/shop/app/admin/wysiwyg/wysiwyg-client.tsx b/shop/app/admin/wysiwyg/wysiwyg-client.tsx
index 4fd7e55..d12f5d0 100644
--- a/shop/app/admin/wysiwyg/wysiwyg-client.tsx
+++ b/shop/app/admin/wysiwyg/wysiwyg-client.tsx
@@ -1,22 +1,65 @@
'use client'
import { useState, useTransition } from 'react'
-import { Category, Product } from '@/lib/types'
-import { updateProduct, createProduct, updateCategory, createCategory } from '@/lib/actions/products'
+import { Category, Product, ProductModule } from '@/lib/types'
+import { updateProduct, createProduct, updateCategory, createCategory, deleteProduct, deleteCategory } from '@/lib/actions/products'
import { InlineInput } from './components/inline-input'
+import { ModulePopover } from './components/module-popover'
+import { Trash2 } from 'lucide-react'
export function WysiwygAdminClient({
initialCategories,
- initialProducts
+ initialProducts,
+ allModules
}: {
initialCategories: Category[]
initialProducts: Product[]
+ allModules: ProductModule[]
}) {
const [mode, setMode] = useState<'purchase' | 'subscription'>('purchase')
const [categories, setCategories] = useState(initialCategories)
const [products, setProducts] = useState(initialProducts)
const [isPending, startTransition] = useTransition()
+ const handleDeleteCategory = async (catId: string) => {
+ // Cascade-Check: check if there are products belonging to this category
+ const categoryProducts = products.filter(p => p.category_id === catId)
+ if (categoryProducts.length > 0) {
+ alert(`Diese Kategorie enthält noch ${categoryProducts.length} Produkte. Bitte lösche oder verschiebe diese zuerst!`)
+ return
+ }
+
+ if (!window.confirm('Möchten Sie diese Kategorie wirklich löschen?')) {
+ return
+ }
+
+ startTransition(async () => {
+ try {
+ await deleteCategory(catId)
+ setCategories(categories.filter(c => c.id !== catId))
+ } catch (e) {
+ console.error(e)
+ alert('Fehler beim Löschen der Kategorie.')
+ }
+ })
+ }
+
+ const handleDeleteProduct = async (prodId: string) => {
+ if (!window.confirm('Möchten Sie dieses Produkt wirklich löschen?')) {
+ return
+ }
+
+ startTransition(async () => {
+ try {
+ await deleteProduct(prodId)
+ setProducts(products.filter(p => p.id !== prodId))
+ } catch (e) {
+ console.error(e)
+ alert('Fehler beim Löschen des Produkts.')
+ }
+ })
+ }
+
// Filterung der Kategorien nach aktivem Modus
const filteredCategories = categories.filter(cat =>
mode === 'purchase' ? cat.show_in_kauf : cat.show_in_abo
@@ -68,6 +111,17 @@ export function WysiwygAdminClient({
})
}
+ const handleCategoryToggle = async (catId: string, fields: Partial) => {
+ startTransition(async () => {
+ try {
+ setCategories(categories.map(c => c.id === catId ? { ...c, ...fields } : c))
+ await updateCategory(catId, fields)
+ } catch (e) {
+ console.error(e)
+ }
+ })
+ }
+
return (
@@ -89,7 +143,7 @@ export function WysiwygAdminClient({
Abo-Modus (Monatlich)
- {isPending &&
Speichere Änderungen...}
+ {isPending &&
Speichere Änderungen...}
@@ -98,8 +152,8 @@ export function WysiwygAdminClient({
.map(cat => {
const catProducts = filteredProducts.filter(p => p.category_id === cat.id)
return (
-
-
+
+
+
-
+
{catProducts.map(prod => (
-
-
{
- await updateProduct(prod.id, { name: newName }, prod.modules || [])
- setProducts(products.map(p => p.id === prod.id ? { ...p, name: newName } : p))
- }}
- className="font-semibold text-white block"
- />
- {
- await updateProduct(prod.id, { description: newDesc }, prod.modules || [])
- setProducts(products.map(p => p.id === prod.id ? { ...p, description: newDesc } : p))
- }}
- className="text-xs text-slate-400 block"
- />
-
+
+
+
+
+ {
+ await updateProduct(prod.id, { name: newName }, prod.modules || [])
+ setProducts(products.map(p => p.id === prod.id ? { ...p, name: newName } : p))
+ }}
+ className="font-semibold text-white block"
+ />
+
+
+
{
- const priceNum = parseFloat(newPrice) || 0
- await updateProduct(prod.id, { base_price: priceNum }, prod.modules || [])
- setProducts(products.map(p => p.id === prod.id ? { ...p, base_price: priceNum } : p))
+ value={prod.description || ''}
+ onSave={async (newDesc) => {
+ await updateProduct(prod.id, { description: newDesc }, prod.modules || [])
+ setProducts(products.map(p => p.id === prod.id ? { ...p, description: newDesc } : p))
}}
- className="inline-block"
- />
- €
+ className="text-xs text-slate-400 block"
+ />
+
+ {
+ const priceNum = parseFloat(newPrice) || 0
+ await updateProduct(prod.id, { base_price: priceNum }, prod.modules || [])
+ setProducts(products.map(p => p.id === prod.id ? { ...p, base_price: priceNum } : p))
+ }}
+ className="inline-block"
+ />
+ €
+
+
+ {/* Modul-Badges */}
+
+
Module:
+
+ {(prod.modules || []).length > 0 ? (
+ (prod.modules || []).map((m) => (
+
+ {m.name}
+
+
+ ))
+ ) : (
+ Keine Module
+ )}
+
+
+
+
+ {/* Modul hinzufügen Popover */}
+
+ {
+ const newMod = {
+ name: mod.name,
+ description: mod.description,
+ price: mod.price,
+ requirements: mod.requirements || [],
+ exclusions: mod.exclusions || [],
+ has_quantity: mod.has_quantity || false
+ }
+ const updatedModules = [...(prod.modules || []), newMod]
+ setProducts(products.map(p => p.id === prod.id ? { ...p, modules: updatedModules as any } : p))
+ await updateProduct(prod.id, {}, updatedModules)
+ }}
+ />
))}
@@ -160,7 +304,7 @@ export function WysiwygAdminClient({
className="flex flex-col items-center justify-center p-4 border border-dashed border-white/20 hover:border-primary/50 rounded-xl text-slate-400 hover:text-primary transition h-full min-h-[140px]"
>
+
-
Produkt hinzufügen
+
Produkt hinzufügen
@@ -172,7 +316,7 @@ export function WysiwygAdminClient({
className="w-full py-4 border border-dashed border-white/20 hover:border-primary/50 rounded-2xl flex items-center justify-center text-slate-400 hover:text-primary transition"
>
+
-
Neue Kategorie hinzufügen
+
Neue Kategorie hinzufügen
diff --git a/shop/components/admin/admin-nav-link.tsx b/shop/components/admin/admin-nav-link.tsx
new file mode 100644
index 0000000..c9c991a
--- /dev/null
+++ b/shop/components/admin/admin-nav-link.tsx
@@ -0,0 +1,28 @@
+'use client'
+
+import Link from 'next/link'
+import { usePathname } from 'next/navigation'
+import { ReactNode } from 'react'
+
+interface AdminNavLinkProps {
+ href: string
+ children: ReactNode
+}
+
+export function AdminNavLink({ href, children }: AdminNavLinkProps) {
+ const pathname = usePathname()
+ const isActive = pathname === href
+
+ return (
+
+ {children}
+
+ )
+}