events werden nun erfolgreich gespeichert, branding wird nun erfolgreich gespeichert, emotionen können nun angelegt werden. Task Ansicht im Event admin verbessert, Buttons in FAB umgewandelt und vereinheitlicht. Teilen-Link Guest PWA schicker gemacht, SynGoogleFonts ausgebaut (mit Einzel-Family-Download).
This commit is contained in:
@@ -15,8 +15,16 @@ import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
|
||||
import { AdminLayout } from '../components/AdminLayout';
|
||||
import { getEmotions, createEmotion, updateEmotion, TenantEmotion, EmotionPayload } from '../api';
|
||||
import {
|
||||
getEmotions,
|
||||
createEmotion,
|
||||
updateEmotion,
|
||||
deleteEmotion,
|
||||
TenantEmotion,
|
||||
EmotionPayload,
|
||||
} from '../api';
|
||||
import { isAuthError } from '../auth/tokens';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
type EmotionFormState = {
|
||||
name: string;
|
||||
@@ -49,6 +57,7 @@ export function EmotionsSection({ embedded = false }: EmotionsSectionProps) {
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = React.useState<TenantEmotion | null>(null);
|
||||
const [saving, setSaving] = React.useState(false);
|
||||
const [form, setForm] = React.useState<EmotionFormState>(INITIAL_FORM_STATE);
|
||||
|
||||
@@ -107,9 +116,11 @@ export function EmotionsSection({ embedded = false }: EmotionsSectionProps) {
|
||||
const created = await createEmotion(payload);
|
||||
setEmotions((prev) => [created, ...prev]);
|
||||
setDialogOpen(false);
|
||||
toast.success(t('emotions.toast.created', 'Emotion erstellt.'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
setError(t('emotions.errors.create'));
|
||||
toast.error(t('emotions.toast.error', 'Emotion konnte nicht erstellt werden.'));
|
||||
}
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -120,13 +131,35 @@ export function EmotionsSection({ embedded = false }: EmotionsSectionProps) {
|
||||
try {
|
||||
const updated = await updateEmotion(emotion.id, { is_active: !emotion.is_active });
|
||||
setEmotions((prev) => prev.map((item) => (item.id === updated.id ? updated : item)));
|
||||
toast.success(
|
||||
updated.is_active
|
||||
? t('emotions.toast.activated', 'Emotion aktiviert.')
|
||||
: t('emotions.toast.deactivated', 'Emotion deaktiviert.')
|
||||
);
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
setError(t('emotions.errors.toggle'));
|
||||
toast.error(t('emotions.toast.errorToggle', 'Emotion konnte nicht aktualisiert werden.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteEmotion(emotion: TenantEmotion) {
|
||||
setSaving(true);
|
||||
try {
|
||||
await deleteEmotion(emotion.id);
|
||||
setEmotions((prev) => prev.filter((item) => item.id !== emotion.id));
|
||||
toast.success(t('emotions.toast.deleted', 'Emotion gelöscht.'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
toast.error(t('emotions.toast.deleteError', 'Emotion konnte nicht gelöscht werden.'));
|
||||
}
|
||||
} finally {
|
||||
setSaving(false);
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}
|
||||
|
||||
const locale = i18n.language.startsWith('en') ? enGB : de;
|
||||
const title = embedded ? t('emotions.title') : t('emotions.title');
|
||||
const subtitle = embedded
|
||||
@@ -165,17 +198,18 @@ export function EmotionsSection({ embedded = false }: EmotionsSectionProps) {
|
||||
) : emotions.length === 0 ? (
|
||||
<EmptyEmotionsState onCreate={openCreateDialog} />
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{emotions.map((emotion) => (
|
||||
<EmotionCard
|
||||
key={emotion.id}
|
||||
emotion={emotion}
|
||||
onToggle={() => toggleEmotion(emotion)}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{emotions.map((emotion) => (
|
||||
<EmotionCard
|
||||
key={emotion.id}
|
||||
emotion={emotion}
|
||||
onToggle={() => toggleEmotion(emotion)}
|
||||
onDelete={() => setDeleteTarget(emotion)}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -187,6 +221,29 @@ export function EmotionsSection({ embedded = false }: EmotionsSectionProps) {
|
||||
saving={saving}
|
||||
onSubmit={handleCreate}
|
||||
/>
|
||||
|
||||
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('emotions.delete.title', 'Emotion löschen?')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-sm text-slate-600">
|
||||
{t('emotions.delete.confirm', { defaultValue: 'Soll "{{name}}" wirklich gelöscht werden?' , name: deleteTarget?.name ?? '' })}
|
||||
</p>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setDeleteTarget(null)}>
|
||||
{t('actions.cancel', 'Abbrechen')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => deleteTarget && void handleDeleteEmotion(deleteTarget)}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : t('actions.delete', 'Löschen')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -203,10 +260,12 @@ export default function EmotionsPage() {
|
||||
function EmotionCard({
|
||||
emotion,
|
||||
onToggle,
|
||||
onDelete,
|
||||
locale,
|
||||
}: {
|
||||
emotion: TenantEmotion;
|
||||
onToggle: () => void;
|
||||
onDelete: () => void;
|
||||
locale: Locale;
|
||||
}) {
|
||||
const { t } = useTranslation('management');
|
||||
@@ -252,7 +311,13 @@ function EmotionCard({
|
||||
<Power className="mr-1 h-4 w-4" />
|
||||
{emotion.is_active ? t('emotions.actions.disable') : t('emotions.actions.enable')}
|
||||
</Button>
|
||||
<div className="h-8 w-8 rounded-full border border-slate-200" style={{ backgroundColor: emotion.color ?? DEFAULT_COLOR }} />
|
||||
{!emotion.is_global ? (
|
||||
<Button variant="ghost" size="sm" className="text-rose-600 hover:bg-rose-50" onClick={onDelete}>
|
||||
{t('actions.delete', 'Löschen')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="h-8 w-8 rounded-full border border-slate-200" style={{ backgroundColor: emotion.color ?? DEFAULT_COLOR }} />
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user