diff --git a/shop/app/admin/wysiwyg/components/inline-input.tsx b/shop/app/admin/wysiwyg/components/inline-input.tsx new file mode 100644 index 0000000..c7f2e7d --- /dev/null +++ b/shop/app/admin/wysiwyg/components/inline-input.tsx @@ -0,0 +1,63 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' + +interface InlineInputProps { + value: string + onSave: (val: string) => Promise + className?: string + type?: 'text' | 'number' +} + +export function InlineInput({ value, onSave, className, type = 'text' }: InlineInputProps) { + const [isEditing, setIsEditing] = useState(false) + const [currentValue, setCurrentValue] = useState(value) + const inputRef = useRef(null) + + useEffect(() => { + setCurrentValue(value) + }, [value]) + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus() + } + }, [isEditing]) + + const handleBlur = async () => { + setIsEditing(false) + if (currentValue !== value) { + await onSave(currentValue) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleBlur() + } + } + + if (isEditing) { + return ( + setCurrentValue(e.target.value)} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + className={`bg-white/10 text-white border border-primary px-1 rounded outline-none w-full ${className}`} + /> + ) + } + + return ( + setIsEditing(true)} + className={`cursor-pointer hover:bg-white/5 rounded px-1 transition ${className}`} + > + {value || Klicken zum Bearbeiten} + + ) +} diff --git a/shop/app/admin/wysiwyg/page.tsx b/shop/app/admin/wysiwyg/page.tsx new file mode 100644 index 0000000..a6ad821 --- /dev/null +++ b/shop/app/admin/wysiwyg/page.tsx @@ -0,0 +1,33 @@ +import { getProducts, getCategories } from '@/lib/actions/products' +import { WysiwygAdminClient } from './wysiwyg-client' +import { Edit, Sparkles } from 'lucide-react' +import { Suspense } from 'react' + +export const dynamic = 'force-dynamic' + +export default async function WysiwygAdminPage() { + return ( +
+
+
+

+ + WYSIWYG Live-Editor +

+

+ Kategorien und Produkte visuell im Wizard-Layout bearbeiten. +

+
+
+ }> + + +
+ ) +} + +async function WysiwygDataWrapper() { + const products = await getProducts().catch(() => []) + const categories = await getCategories().catch(() => []) + return +} diff --git a/shop/app/admin/wysiwyg/wysiwyg-client.tsx b/shop/app/admin/wysiwyg/wysiwyg-client.tsx new file mode 100644 index 0000000..4fd7e55 --- /dev/null +++ b/shop/app/admin/wysiwyg/wysiwyg-client.tsx @@ -0,0 +1,180 @@ +'use client' + +import { useState, useTransition } from 'react' +import { Category, Product } from '@/lib/types' +import { updateProduct, createProduct, updateCategory, createCategory } from '@/lib/actions/products' +import { InlineInput } from './components/inline-input' + +export function WysiwygAdminClient({ + initialCategories, + initialProducts +}: { + initialCategories: Category[] + initialProducts: Product[] +}) { + const [mode, setMode] = useState<'purchase' | 'subscription'>('purchase') + const [categories, setCategories] = useState(initialCategories) + const [products, setProducts] = useState(initialProducts) + const [isPending, startTransition] = useTransition() + + // Filterung der Kategorien nach aktivem Modus + const filteredCategories = categories.filter(cat => + mode === 'purchase' ? cat.show_in_kauf : cat.show_in_abo + ) + + // Filterung der Produkte nach aktivem Modus + const filteredProducts = products.filter(p => + mode === 'purchase' ? p.billing_interval === 'one_time' : p.billing_interval === 'monthly' + ) + + const handleAddCategory = async () => { + startTransition(async () => { + try { + const newCat = await createCategory({ + name: 'Neue Kategorie', + description: 'Beschreibung hier...', + sort_order: categories.length + 1, + is_required: false, + allow_multiselect: false, + free_items_limit: 0, + preselect: false, + show_in_kauf: mode === 'purchase', + show_in_abo: mode === 'subscription' + }) + setCategories([...categories, newCat]) + } catch (e) { + console.error(e) + } + }) + } + + const handleAddProduct = async (categoryId: string) => { + startTransition(async () => { + try { + const newProd = await createProduct({ + name: 'Neues Produkt', + description: 'Produktbeschreibung...', + base_price: 19.99, + tax_rate: 19, + billing_interval: mode === 'purchase' ? 'one_time' : 'monthly', + category_id: categoryId, + show_in_kauf: mode === 'purchase', + show_in_abo: mode === 'subscription' + }, []) + setProducts([...products, newProd]) + } catch (e) { + console.error(e) + } + }) + } + + return ( +
+
+
+ + +
+ {isPending && Speichere Änderungen...} +
+ +
+ {filteredCategories + .sort((a, b) => a.sort_order - b.sort_order) + .map(cat => { + const catProducts = filteredProducts.filter(p => p.category_id === cat.id) + return ( +
+
+
+ { + await updateCategory(cat.id, { name: newName }) + setCategories(categories.map(c => c.id === cat.id ? { ...c, name: newName } : c)) + }} + className="text-2xl font-bold text-white block w-full" + /> + { + await updateCategory(cat.id, { description: newDesc }) + setCategories(categories.map(c => c.id === cat.id ? { ...c, description: newDesc } : c)) + }} + className="text-slate-400 text-sm mt-1 block w-full" + /> +
+
+ +
+ {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" + /> +
+ { + 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" + /> + +
+
+ ))} + + +
+
+ ) + })} + + +
+
+ ) +}