Add account hierarchy framework with migrations, RLS policies, and UI components
This commit is contained in:
@@ -1,14 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { CreatePageSchema } from '../schema/site.schema';
|
||||
import { createPage } from '../server/actions/site-builder-actions';
|
||||
|
||||
@@ -35,7 +45,10 @@ export function CreatePageForm({ accountId, account }: Props) {
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9äöüß\s-]+/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss')
|
||||
.replace(/ä/g, 'ae')
|
||||
.replace(/ö/g, 'oe')
|
||||
.replace(/ü/g, 'ue')
|
||||
.replace(/ß/g, 'ss')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
const { execute, isPending } = useAction(createPage, {
|
||||
@@ -45,50 +58,102 @@ export function CreatePageForm({ accountId, account }: Props) {
|
||||
router.push(`/home/${account}/site-builder/${data.data.id}/edit`);
|
||||
}
|
||||
},
|
||||
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler beim Erstellen'),
|
||||
onError: ({ error }) =>
|
||||
toast.error(error.serverError ?? 'Fehler beim Erstellen'),
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit((data) => execute({ ...data, slug: data.slug || autoSlug }))} className="space-y-6 max-w-xl">
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) =>
|
||||
execute({ ...data, slug: data.slug || autoSlug }),
|
||||
)}
|
||||
className="max-w-xl space-y-6"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Neue Seite erstellen</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Neue Seite erstellen</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<FormField control={form.control} name="title" render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Seitentitel *</FormLabel>
|
||||
<FormControl><Input placeholder="z.B. Startseite, Über uns, Kontakt" {...field} /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="slug" render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>URL-Pfad</FormLabel>
|
||||
<FormControl><Input placeholder={autoSlug || 'wird-automatisch-generiert'} {...field} /></FormControl>
|
||||
<p className="text-xs text-muted-foreground">Leer lassen für automatische Generierung aus dem Titel</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="metaDescription" render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Beschreibung (SEO)</FormLabel>
|
||||
<FormControl><Input placeholder="Kurze Beschreibung für Suchmaschinen" {...field} /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="isHomepage" render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-3">
|
||||
<FormControl>
|
||||
<input type="checkbox" checked={field.value} onChange={field.onChange} className="h-4 w-4" />
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Als Startseite festlegen</FormLabel>
|
||||
</FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Seitentitel *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="z.B. Startseite, Über uns, Kontakt"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>URL-Pfad</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={autoSlug || 'wird-automatisch-generiert'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Leer lassen für automatische Generierung aus dem Titel
|
||||
</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="metaDescription"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Beschreibung (SEO)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Kurze Beschreibung für Suchmaschinen"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isHomepage"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-3">
|
||||
<FormControl>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">
|
||||
Als Startseite festlegen
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>Abbrechen</Button>
|
||||
<Button type="submit" disabled={isPending}>{isPending ? 'Wird erstellt...' : 'Seite erstellen & Editor öffnen'}</Button>
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? 'Wird erstellt...' : 'Seite erstellen & Editor öffnen'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { CreatePostSchema } from '../schema/site.schema';
|
||||
import { createPost } from '../server/actions/site-builder-actions';
|
||||
|
||||
@@ -34,47 +44,127 @@ export function CreatePostForm({ accountId, account }: Props) {
|
||||
|
||||
// Auto-generate slug from title
|
||||
const watchTitle = form.watch('title');
|
||||
const autoSlug = watchTitle.toLowerCase().replace(/[^a-z0-9äöüß]+/g, '-').replace(/^-|-$/g, '').replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss');
|
||||
const autoSlug = watchTitle
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9äöüß]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.replace(/ä/g, 'ae')
|
||||
.replace(/ö/g, 'oe')
|
||||
.replace(/ü/g, 'ue')
|
||||
.replace(/ß/g, 'ss');
|
||||
|
||||
const { execute, isPending } = useAction(createPost, {
|
||||
onSuccess: () => { toast.success('Beitrag erstellt'); router.push(`/home/${account}/site-builder/posts`); },
|
||||
onSuccess: () => {
|
||||
toast.success('Beitrag erstellt');
|
||||
router.push(`/home/${account}/site-builder/posts`);
|
||||
},
|
||||
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit((data) => execute({ ...data, slug: data.slug || autoSlug }))} className="space-y-6 max-w-3xl">
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) =>
|
||||
execute({ ...data, slug: data.slug || autoSlug }),
|
||||
)}
|
||||
className="max-w-3xl space-y-6"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Beitrag</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Beitrag</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<FormField control={form.control} name="title" render={({ field }) => (
|
||||
<FormItem><FormLabel>Titel *</FormLabel><FormControl><Input placeholder="Vereinsnachrichten..." {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="slug" render={({ field }) => (
|
||||
<FormItem><FormLabel>URL-Slug</FormLabel><FormControl><Input placeholder={autoSlug || 'wird-automatisch-generiert'} {...field} /></FormControl>
|
||||
<p className="text-xs text-muted-foreground">Leer lassen für automatische Generierung</p><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="excerpt" render={({ field }) => (
|
||||
<FormItem><FormLabel>Kurzfassung</FormLabel><FormControl><Input placeholder="Kurze Zusammenfassung..." {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="content" render={({ field }) => (
|
||||
<FormItem><FormLabel>Inhalt</FormLabel><FormControl>
|
||||
<textarea {...field} rows={12} className="w-full rounded-md border px-3 py-2 text-sm font-mono" placeholder="Beitragsinhalt (HTML erlaubt)..." />
|
||||
</FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="status" render={({ field }) => (
|
||||
<FormItem><FormLabel>Status</FormLabel><FormControl>
|
||||
<select {...field} className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<option value="draft">Entwurf</option>
|
||||
<option value="published">Veröffentlicht</option>
|
||||
</select>
|
||||
</FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Titel *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Vereinsnachrichten..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>URL-Slug</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={autoSlug || 'wird-automatisch-generiert'}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Leer lassen für automatische Generierung
|
||||
</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="excerpt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Kurzfassung</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Kurze Zusammenfassung..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Inhalt</FormLabel>
|
||||
<FormControl>
|
||||
<textarea
|
||||
{...field}
|
||||
rows={12}
|
||||
className="w-full rounded-md border px-3 py-2 font-mono text-sm"
|
||||
placeholder="Beitragsinhalt (HTML erlaubt)..."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="draft">Entwurf</option>
|
||||
<option value="published">Veröffentlicht</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>Abbrechen</Button>
|
||||
<Button type="submit" disabled={isPending}>{isPending ? 'Wird erstellt...' : 'Beitrag erstellen'}</Button>
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? 'Wird erstellt...' : 'Beitrag erstellen'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { Provider } from '@supabase/supabase-js';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Shield, LogIn, AlertCircle } from 'lucide-react';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import { OauthProviderLogoImage } from '@kit/ui/oauth-provider-logo-image';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { Shield, LogIn, AlertCircle } from 'lucide-react';
|
||||
|
||||
const OAUTH_PROVIDERS: { id: Provider; label: string }[] = [
|
||||
{ id: 'google', label: 'Google' },
|
||||
{ id: 'apple', label: 'Apple' },
|
||||
{ id: 'azure', label: 'Microsoft' },
|
||||
{ id: 'github', label: 'GitHub' },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
function getSupabaseClient() {
|
||||
return createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!,
|
||||
);
|
||||
}
|
||||
|
||||
export function PortalLoginForm({ slug, accountName }: Props) {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [oauthLoading, setOauthLoading] = useState<string | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
@@ -28,18 +50,19 @@ export function PortalLoginForm({ slug, accountName }: Props) {
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLIC_KEY!,
|
||||
const supabase = getSupabaseClient();
|
||||
|
||||
const { data, error: authError } = await supabase.auth.signInWithPassword(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
},
|
||||
);
|
||||
|
||||
const { data, error: authError } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (authError) {
|
||||
setError('Ungültige Anmeldedaten. Bitte überprüfen Sie E-Mail und Passwort.');
|
||||
setError(
|
||||
'Ungültige Anmeldedaten. Bitte überprüfen Sie E-Mail und Passwort.',
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -56,19 +79,42 @@ export function PortalLoginForm({ slug, accountName }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOAuthLogin = async (provider: Provider) => {
|
||||
setOauthLoading(provider);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const supabase = getSupabaseClient();
|
||||
const redirectTo = `${window.location.origin}/club/${slug}/portal`;
|
||||
|
||||
const { error: oauthError } = await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
options: { redirectTo },
|
||||
});
|
||||
|
||||
if (oauthError) {
|
||||
setError(`Anmeldung fehlgeschlagen: ${oauthError.message}`);
|
||||
setOauthLoading(null);
|
||||
}
|
||||
} catch {
|
||||
setError('Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||||
setOauthLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md mx-auto">
|
||||
<Card className="mx-auto w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
||||
<Shield className="h-6 w-6 text-primary" />
|
||||
<div className="bg-primary/10 mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<Shield className="text-primary h-6 w-6" />
|
||||
</div>
|
||||
<CardTitle>Mitgliederbereich</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{accountName}</p>
|
||||
<p className="text-muted-foreground text-sm">{accountName}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="bg-destructive/10 text-destructive flex items-center gap-2 rounded-md p-3 text-sm">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
{error}
|
||||
</div>
|
||||
@@ -103,10 +149,39 @@ export function PortalLoginForm({ slug, accountName }: Props) {
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-xs text-center text-muted-foreground">
|
||||
Zugangsdaten erhalten Sie per Einladung von Ihrem Verein.
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<div className="relative my-4">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background text-muted-foreground px-2">
|
||||
oder
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{OAUTH_PROVIDERS.map(({ id, label }) => (
|
||||
<Button
|
||||
key={id}
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
disabled={!!oauthLoading}
|
||||
onClick={() => handleOAuthLogin(id)}
|
||||
>
|
||||
<OauthProviderLogoImage providerId={id} />
|
||||
{oauthLoading === id
|
||||
? 'Wird weitergeleitet...'
|
||||
: `Mit ${label} anmelden`}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-center text-xs">
|
||||
Zugangsdaten erhalten Sie per Einladung von Ihrem Verein.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Puck } from '@measured/puck';
|
||||
|
||||
import '@measured/puck/puck.css';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { clubPuckConfig } from '../config/puck-config';
|
||||
import { publishPage } from '../server/actions/site-builder-actions';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Render } from '@measured/puck';
|
||||
|
||||
import { clubPuckConfig } from '../config/puck-config';
|
||||
import { SiteDataProvider, type SiteData } from '../context/site-data-context';
|
||||
|
||||
@@ -10,7 +11,12 @@ interface Props {
|
||||
}
|
||||
|
||||
export function SiteRenderer({ data, siteData }: Props) {
|
||||
const defaultData: SiteData = { accountId: '', events: [], courses: [], posts: [] };
|
||||
const defaultData: SiteData = {
|
||||
accountId: '',
|
||||
events: [],
|
||||
courses: [],
|
||||
posts: [],
|
||||
};
|
||||
|
||||
return (
|
||||
<SiteDataProvider data={siteData ?? defaultData}>
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
|
||||
import { SiteSettingsSchema } from '../schema/site.schema';
|
||||
import { updateSiteSettings } from '../server/actions/site-builder-actions';
|
||||
|
||||
@@ -41,79 +51,210 @@ export function SiteSettingsForm({ accountId, account, settings }: Props) {
|
||||
});
|
||||
|
||||
const { execute, isPending } = useAction(updateSiteSettings, {
|
||||
onSuccess: () => { toast.success('Einstellungen gespeichert'); router.refresh(); },
|
||||
onSuccess: () => {
|
||||
toast.success('Einstellungen gespeichert');
|
||||
router.refresh();
|
||||
},
|
||||
onError: ({ error }) => toast.error(error.serverError ?? 'Fehler'),
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit((data) => execute(data))} className="space-y-6 max-w-3xl">
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => execute(data))}
|
||||
className="max-w-3xl space-y-6"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Allgemein</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Allgemein</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField control={form.control} name="siteName" render={({ field }) => (
|
||||
<FormItem><FormLabel>Website-Name</FormLabel><FormControl><Input placeholder="Mein Verein" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="fontFamily" render={({ field }) => (
|
||||
<FormItem><FormLabel>Schriftart</FormLabel><FormControl>
|
||||
<select {...field} className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<option value="Inter">Inter</option>
|
||||
<option value="system-ui">System</option>
|
||||
<option value="Georgia">Georgia</option>
|
||||
<option value="Roboto">Roboto</option>
|
||||
</select>
|
||||
</FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="primaryColor" render={({ field }) => (
|
||||
<FormItem><FormLabel>Primärfarbe</FormLabel><FormControl><Input type="color" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="secondaryColor" render={({ field }) => (
|
||||
<FormItem><FormLabel>Sekundärfarbe</FormLabel><FormControl><Input type="color" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Website-Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Mein Verein" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fontFamily"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Schriftart</FormLabel>
|
||||
<FormControl>
|
||||
<select
|
||||
{...field}
|
||||
className="border-input bg-background flex h-10 w-full rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="Inter">Inter</option>
|
||||
<option value="system-ui">System</option>
|
||||
<option value="Georgia">Georgia</option>
|
||||
<option value="Roboto">Roboto</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="primaryColor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Primärfarbe</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="color" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secondaryColor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Sekundärfarbe</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="color" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Kontakt</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Kontakt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField control={form.control} name="contactEmail" render={({ field }) => (
|
||||
<FormItem><FormLabel>E-Mail</FormLabel><FormControl><Input type="email" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="contactPhone" render={({ field }) => (
|
||||
<FormItem><FormLabel>Telefon</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="contactAddress" render={({ field }) => (
|
||||
<FormItem className="col-span-full"><FormLabel>Adresse</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contactEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>E-Mail</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contactPhone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telefon</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contactAddress"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-full">
|
||||
<FormLabel>Adresse</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Rechtliches</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Rechtliches</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<FormField control={form.control} name="impressum" render={({ field }) => (
|
||||
<FormItem><FormLabel>Impressum</FormLabel><FormControl><textarea {...field} rows={6} className="w-full rounded-md border px-3 py-2 text-sm" /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="datenschutz" render={({ field }) => (
|
||||
<FormItem><FormLabel>Datenschutzerklärung</FormLabel><FormControl><textarea {...field} rows={6} className="w-full rounded-md border px-3 py-2 text-sm" /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="impressum"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Impressum</FormLabel>
|
||||
<FormControl>
|
||||
<textarea
|
||||
{...field}
|
||||
rows={6}
|
||||
className="w-full rounded-md border px-3 py-2 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="datenschutz"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Datenschutzerklärung</FormLabel>
|
||||
<FormControl>
|
||||
<textarea
|
||||
{...field}
|
||||
rows={6}
|
||||
className="w-full rounded-md border px-3 py-2 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Veröffentlichung</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle>Veröffentlichung</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FormField control={form.control} name="isPublic" render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-3">
|
||||
<FormControl><input type="checkbox" checked={field.value} onChange={field.onChange} className="h-4 w-4" /></FormControl>
|
||||
<div>
|
||||
<FormLabel>Website öffentlich zugänglich</FormLabel>
|
||||
<p className="text-xs text-muted-foreground">Wenn aktiviert, ist Ihre Website unter /club/{account} erreichbar.</p>
|
||||
</div>
|
||||
</FormItem>
|
||||
)} />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isPublic"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-3">
|
||||
<FormControl>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value}
|
||||
onChange={field.onChange}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
</FormControl>
|
||||
<div>
|
||||
<FormLabel>Website öffentlich zugänglich</FormLabel>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Wenn aktiviert, ist Ihre Website unter /club/{account}{' '}
|
||||
erreichbar.
|
||||
</p>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>Abbrechen</Button>
|
||||
<Button type="submit" disabled={isPending}>{isPending ? 'Wird gespeichert...' : 'Einstellungen speichern'}</Button>
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? 'Wird gespeichert...' : 'Einstellungen speichern'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,11 @@ import { z } from 'zod';
|
||||
|
||||
export const CreatePageSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
slug: z.string().min(1).max(128).regex(/^[a-z0-9-]+$/),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(128)
|
||||
.regex(/^[a-z0-9-]+$/),
|
||||
title: z.string().min(1).max(256),
|
||||
puckData: z.record(z.string(), z.unknown()).default({}),
|
||||
isHomepage: z.boolean().default(false),
|
||||
@@ -29,7 +33,9 @@ export const SiteSettingsSchema = z.object({
|
||||
secondaryColor: z.string().default('#64748b'),
|
||||
fontFamily: z.string().default('Inter'),
|
||||
customCss: z.string().optional(),
|
||||
navigation: z.array(z.object({ label: z.string(), href: z.string() })).default([]),
|
||||
navigation: z
|
||||
.array(z.object({ label: z.string(), href: z.string() }))
|
||||
.default([]),
|
||||
footerText: z.string().optional(),
|
||||
contactEmail: z.string().optional(),
|
||||
contactPhone: z.string().optional(),
|
||||
@@ -42,7 +48,10 @@ export const SiteSettingsSchema = z.object({
|
||||
export const CreatePostSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
title: z.string().min(1),
|
||||
slug: z.string().min(1).regex(/^[a-z0-9-]+$/),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(/^[a-z0-9-]+$/),
|
||||
content: z.string().optional(),
|
||||
excerpt: z.string().optional(),
|
||||
coverImage: z.string().optional(),
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authActionClient } from '@kit/next/safe-action';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { CreatePageSchema, UpdatePageSchema, SiteSettingsSchema, CreatePostSchema, UpdatePostSchema, NewsletterSubscribeSchema } from '../../schema/site.schema';
|
||||
|
||||
import {
|
||||
CreatePageSchema,
|
||||
UpdatePageSchema,
|
||||
SiteSettingsSchema,
|
||||
CreatePostSchema,
|
||||
UpdatePostSchema,
|
||||
NewsletterSubscribeSchema,
|
||||
} from '../../schema/site.schema';
|
||||
import { createSiteBuilderApi } from '../api';
|
||||
|
||||
export const createPage = authActionClient
|
||||
@@ -21,7 +30,11 @@ export const saveDraft = authActionClient
|
||||
.action(async ({ parsedInput: input, ctx }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const api = createSiteBuilderApi(client);
|
||||
const data = await api.updatePage(input.pageId, { ...input, isPublished: false }, ctx.user.id);
|
||||
const data = await api.updatePage(
|
||||
input.pageId,
|
||||
{ ...input, isPublished: false },
|
||||
ctx.user.id,
|
||||
);
|
||||
return { success: true, data };
|
||||
});
|
||||
|
||||
@@ -30,7 +43,11 @@ export const publishPage = authActionClient
|
||||
.action(async ({ parsedInput: input, ctx }) => {
|
||||
const client = getSupabaseServerClient();
|
||||
const api = createSiteBuilderApi(client);
|
||||
const data = await api.updatePage(input.pageId, { ...input, isPublished: true }, ctx.user.id);
|
||||
const data = await api.updatePage(
|
||||
input.pageId,
|
||||
{ ...input, isPublished: true },
|
||||
ctx.user.id,
|
||||
);
|
||||
return { success: true, data };
|
||||
});
|
||||
|
||||
|
||||
@@ -1,42 +1,90 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import type { Database } from '@kit/supabase/database';
|
||||
|
||||
export function createSiteBuilderApi(client: SupabaseClient<Database>) {
|
||||
return {
|
||||
// Pages
|
||||
async listPages(accountId: string) {
|
||||
const { data, error } = await client.from('site_pages').select('*')
|
||||
.eq('account_id', accountId).order('sort_order');
|
||||
const { data, error } = await client
|
||||
.from('site_pages')
|
||||
.select('*')
|
||||
.eq('account_id', accountId)
|
||||
.order('sort_order');
|
||||
if (error) throw error;
|
||||
return data ?? [];
|
||||
},
|
||||
async getPage(pageId: string) {
|
||||
const { data, error } = await client.from('site_pages').select('*').eq('id', pageId).single();
|
||||
const { data, error } = await client
|
||||
.from('site_pages')
|
||||
.select('*')
|
||||
.eq('id', pageId)
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
async getPageBySlug(accountId: string, slug: string) {
|
||||
const { data, error } = await client.from('site_pages').select('*')
|
||||
.eq('account_id', accountId).eq('slug', slug).single();
|
||||
const { data, error } = await client
|
||||
.from('site_pages')
|
||||
.select('*')
|
||||
.eq('account_id', accountId)
|
||||
.eq('slug', slug)
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
async getHomepage(accountId: string) {
|
||||
const { data, error } = await client.from('site_pages').select('*')
|
||||
.eq('account_id', accountId).eq('is_homepage', true).eq('is_published', true).maybeSingle();
|
||||
const { data, error } = await client
|
||||
.from('site_pages')
|
||||
.select('*')
|
||||
.eq('account_id', accountId)
|
||||
.eq('is_homepage', true)
|
||||
.eq('is_published', true)
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
async createPage(input: { accountId: string; slug: string; title: string; puckData?: Record<string, unknown>; isHomepage?: boolean; metaDescription?: string }, userId: string) {
|
||||
const { data, error } = await client.from('site_pages').insert({
|
||||
account_id: input.accountId, slug: input.slug, title: input.title,
|
||||
puck_data: (input.puckData ?? {}) as any, is_homepage: input.isHomepage ?? false,
|
||||
meta_description: input.metaDescription, created_by: userId, updated_by: userId,
|
||||
}).select().single();
|
||||
async createPage(
|
||||
input: {
|
||||
accountId: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
puckData?: Record<string, unknown>;
|
||||
isHomepage?: boolean;
|
||||
metaDescription?: string;
|
||||
},
|
||||
userId: string,
|
||||
) {
|
||||
const { data, error } = await client
|
||||
.from('site_pages')
|
||||
.insert({
|
||||
account_id: input.accountId,
|
||||
slug: input.slug,
|
||||
title: input.title,
|
||||
puck_data: (input.puckData ?? {}) as any,
|
||||
is_homepage: input.isHomepage ?? false,
|
||||
meta_description: input.metaDescription,
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
async updatePage(pageId: string, input: { title?: string; slug?: string; puckData?: Record<string, unknown>; isPublished?: boolean; isHomepage?: boolean; metaDescription?: string; metaImage?: string }, userId: string) {
|
||||
async updatePage(
|
||||
pageId: string,
|
||||
input: {
|
||||
title?: string;
|
||||
slug?: string;
|
||||
puckData?: Record<string, unknown>;
|
||||
isPublished?: boolean;
|
||||
isHomepage?: boolean;
|
||||
metaDescription?: string;
|
||||
metaImage?: string;
|
||||
},
|
||||
userId: string,
|
||||
) {
|
||||
const update: Record<string, unknown> = { updated_by: userId };
|
||||
if (input.title !== undefined) update.title = input.title;
|
||||
if (input.slug !== undefined) update.slug = input.slug;
|
||||
@@ -46,65 +94,129 @@ export function createSiteBuilderApi(client: SupabaseClient<Database>) {
|
||||
if (input.isPublished) update.published_at = new Date().toISOString();
|
||||
}
|
||||
if (input.isHomepage !== undefined) update.is_homepage = input.isHomepage;
|
||||
if (input.metaDescription !== undefined) update.meta_description = input.metaDescription;
|
||||
if (input.metaDescription !== undefined)
|
||||
update.meta_description = input.metaDescription;
|
||||
if (input.metaImage !== undefined) update.meta_image = input.metaImage;
|
||||
const { data, error } = await client.from('site_pages').update(update).eq('id', pageId).select().single();
|
||||
const { data, error } = await client
|
||||
.from('site_pages')
|
||||
.update(update)
|
||||
.eq('id', pageId)
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
async deletePage(pageId: string) {
|
||||
const { error } = await client.from('site_pages').delete().eq('id', pageId);
|
||||
const { error } = await client
|
||||
.from('site_pages')
|
||||
.delete()
|
||||
.eq('id', pageId);
|
||||
if (error) throw error;
|
||||
},
|
||||
// Settings
|
||||
async getSiteSettings(accountId: string) {
|
||||
const { data, error } = await client.from('site_settings').select('*').eq('account_id', accountId).maybeSingle();
|
||||
const { data, error } = await client
|
||||
.from('site_settings')
|
||||
.select('*')
|
||||
.eq('account_id', accountId)
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
async upsertSiteSettings(accountId: string, input: Record<string, unknown>) {
|
||||
async upsertSiteSettings(
|
||||
accountId: string,
|
||||
input: Record<string, unknown>,
|
||||
) {
|
||||
const row: Record<string, unknown> = { account_id: accountId };
|
||||
if (input.siteName !== undefined) row.site_name = input.siteName;
|
||||
if (input.siteLogo !== undefined) row.site_logo = input.siteLogo;
|
||||
if (input.primaryColor !== undefined) row.primary_color = input.primaryColor;
|
||||
if (input.secondaryColor !== undefined) row.secondary_color = input.secondaryColor;
|
||||
if (input.primaryColor !== undefined)
|
||||
row.primary_color = input.primaryColor;
|
||||
if (input.secondaryColor !== undefined)
|
||||
row.secondary_color = input.secondaryColor;
|
||||
if (input.fontFamily !== undefined) row.font_family = input.fontFamily;
|
||||
if (input.navigation !== undefined) row.navigation = input.navigation;
|
||||
if (input.footerText !== undefined) row.footer_text = input.footerText;
|
||||
if (input.contactEmail !== undefined) row.contact_email = input.contactEmail;
|
||||
if (input.contactPhone !== undefined) row.contact_phone = input.contactPhone;
|
||||
if (input.contactAddress !== undefined) row.contact_address = input.contactAddress;
|
||||
if (input.contactEmail !== undefined)
|
||||
row.contact_email = input.contactEmail;
|
||||
if (input.contactPhone !== undefined)
|
||||
row.contact_phone = input.contactPhone;
|
||||
if (input.contactAddress !== undefined)
|
||||
row.contact_address = input.contactAddress;
|
||||
if (input.impressum !== undefined) row.impressum = input.impressum;
|
||||
if (input.datenschutz !== undefined) row.datenschutz = input.datenschutz;
|
||||
if (input.isPublic !== undefined) row.is_public = input.isPublic;
|
||||
const { data, error } = await client.from('site_settings').upsert(row as any).select().single();
|
||||
const { data, error } = await client
|
||||
.from('site_settings')
|
||||
.upsert(row as any)
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
// Posts
|
||||
async listPosts(accountId: string, status?: string) {
|
||||
let query = client.from('cms_posts').select('*').eq('account_id', accountId).order('created_at', { ascending: false });
|
||||
let query = client
|
||||
.from('cms_posts')
|
||||
.select('*')
|
||||
.eq('account_id', accountId)
|
||||
.order('created_at', { ascending: false });
|
||||
if (status) query = query.eq('status', status);
|
||||
const { data, error } = await query;
|
||||
if (error) throw error;
|
||||
return data ?? [];
|
||||
},
|
||||
async getPost(postId: string) {
|
||||
const { data, error } = await client.from('cms_posts').select('*').eq('id', postId).single();
|
||||
const { data, error } = await client
|
||||
.from('cms_posts')
|
||||
.select('*')
|
||||
.eq('id', postId)
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
async createPost(input: { accountId: string; title: string; slug: string; content?: string; excerpt?: string; coverImage?: string; status?: string }, userId: string) {
|
||||
const { data, error } = await client.from('cms_posts').insert({
|
||||
account_id: input.accountId, title: input.title, slug: input.slug,
|
||||
content: input.content, excerpt: input.excerpt, cover_image: input.coverImage,
|
||||
status: input.status ?? 'draft', author_id: userId,
|
||||
published_at: input.status === 'published' ? new Date().toISOString() : null,
|
||||
}).select().single();
|
||||
async createPost(
|
||||
input: {
|
||||
accountId: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content?: string;
|
||||
excerpt?: string;
|
||||
coverImage?: string;
|
||||
status?: string;
|
||||
},
|
||||
userId: string,
|
||||
) {
|
||||
const { data, error } = await client
|
||||
.from('cms_posts')
|
||||
.insert({
|
||||
account_id: input.accountId,
|
||||
title: input.title,
|
||||
slug: input.slug,
|
||||
content: input.content,
|
||||
excerpt: input.excerpt,
|
||||
cover_image: input.coverImage,
|
||||
status: input.status ?? 'draft',
|
||||
author_id: userId,
|
||||
published_at:
|
||||
input.status === 'published' ? new Date().toISOString() : null,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
async updatePost(postId: string, input: { title?: string; slug?: string; content?: string; excerpt?: string; coverImage?: string; status?: string }) {
|
||||
async updatePost(
|
||||
postId: string,
|
||||
input: {
|
||||
title?: string;
|
||||
slug?: string;
|
||||
content?: string;
|
||||
excerpt?: string;
|
||||
coverImage?: string;
|
||||
status?: string;
|
||||
},
|
||||
) {
|
||||
const update: Record<string, unknown> = {};
|
||||
if (input.title !== undefined) update.title = input.title;
|
||||
if (input.slug !== undefined) update.slug = input.slug;
|
||||
@@ -113,22 +225,38 @@ export function createSiteBuilderApi(client: SupabaseClient<Database>) {
|
||||
if (input.coverImage !== undefined) update.cover_image = input.coverImage;
|
||||
if (input.status !== undefined) {
|
||||
update.status = input.status;
|
||||
if (input.status === 'published') update.published_at = new Date().toISOString();
|
||||
if (input.status === 'published')
|
||||
update.published_at = new Date().toISOString();
|
||||
}
|
||||
const { data, error } = await client.from('cms_posts').update(update).eq('id', postId).select().single();
|
||||
const { data, error } = await client
|
||||
.from('cms_posts')
|
||||
.update(update)
|
||||
.eq('id', postId)
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
async deletePost(postId: string) {
|
||||
const { error } = await client.from('cms_posts').delete().eq('id', postId);
|
||||
const { error } = await client
|
||||
.from('cms_posts')
|
||||
.delete()
|
||||
.eq('id', postId);
|
||||
if (error) throw error;
|
||||
},
|
||||
// Newsletter
|
||||
async subscribe(accountId: string, email: string, name?: string) {
|
||||
const token = crypto.randomUUID();
|
||||
const { error } = await client.from('newsletter_subscriptions').upsert({
|
||||
account_id: accountId, email, name, confirmation_token: token, is_active: true,
|
||||
}, { onConflict: 'account_id,email' });
|
||||
const { error } = await client.from('newsletter_subscriptions').upsert(
|
||||
{
|
||||
account_id: accountId,
|
||||
email,
|
||||
name,
|
||||
confirmation_token: token,
|
||||
is_active: true,
|
||||
},
|
||||
{ onConflict: 'account_id,email' },
|
||||
);
|
||||
if (error) throw error;
|
||||
return token;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user