Initial commit
This commit is contained in:
192
resources/js/components/admin/news/AdminNewsForm.tsx
Normal file
192
resources/js/components/admin/news/AdminNewsForm.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Input, Switch, Textarea } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type NewsItem, type NewsPayload, type FormMode, type NewsTranslation } from "./adminNewsTypes";
|
||||
|
||||
const resolveLocale = (field: NewsTranslation | null | undefined, lang: string) => {
|
||||
if (!field) return "";
|
||||
if (typeof field === "string") return field;
|
||||
return field[lang] ?? field["en"] ?? Object.values(field)[0] ?? "";
|
||||
};
|
||||
|
||||
const formatForDateTimeLocal = (value: string | null | undefined) => {
|
||||
if (!value) return "";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
|
||||
const pad = (num: number) => num.toString().padStart(2, "0");
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = pad(date.getMonth() + 1);
|
||||
const day = pad(date.getDate());
|
||||
const hours = pad(date.getHours());
|
||||
const minutes = pad(date.getMinutes());
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
type AdminNewsFormProps = {
|
||||
mode: Exclude<FormMode, "none">;
|
||||
editing: NewsItem | null;
|
||||
onSubmit: (payload: NewsPayload) => Promise<void> | void;
|
||||
onCancel: () => void;
|
||||
submitting: boolean;
|
||||
serverError: string | null;
|
||||
};
|
||||
|
||||
export default function AdminNewsForm({
|
||||
mode,
|
||||
editing,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitting,
|
||||
serverError,
|
||||
}: AdminNewsFormProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const [titleCs, setTitleCs] = useState("");
|
||||
const [titleEn, setTitleEn] = useState("");
|
||||
const [contentCs, setContentCs] = useState("");
|
||||
const [contentEn, setContentEn] = useState("");
|
||||
const [excerptCs, setExcerptCs] = useState("");
|
||||
const [excerptEn, setExcerptEn] = useState("");
|
||||
const [publishedAt, setPublishedAt] = useState("");
|
||||
const [isPublished, setIsPublished] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "edit" && editing) {
|
||||
setTitleCs(resolveLocale(editing.title, "cs"));
|
||||
setTitleEn(resolveLocale(editing.title, "en"));
|
||||
setExcerptCs(resolveLocale(editing.excerpt, "cs"));
|
||||
setExcerptEn(resolveLocale(editing.excerpt, "en"));
|
||||
setContentCs(resolveLocale(editing.content, "cs"));
|
||||
setContentEn(resolveLocale(editing.content, "en"));
|
||||
setPublishedAt(formatForDateTimeLocal(editing.published_at));
|
||||
setIsPublished(!!editing.is_published);
|
||||
setLocalError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setTitleCs("");
|
||||
setTitleEn("");
|
||||
setContentCs("");
|
||||
setContentEn("");
|
||||
setExcerptCs("");
|
||||
setExcerptEn("");
|
||||
setPublishedAt("");
|
||||
setIsPublished(false);
|
||||
setLocalError(null);
|
||||
}, [mode, editing]);
|
||||
|
||||
const submitLabel = useMemo(() => {
|
||||
if (mode === "edit") return t("admin_news_form_save") ?? "Uložit změny";
|
||||
return t("admin_news_form_create") ?? "Vytvořit novinku";
|
||||
}, [mode, t]);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setLocalError(null);
|
||||
|
||||
const titlePayload: Record<string, string> = {};
|
||||
if (titleCs.trim()) titlePayload.cs = titleCs.trim();
|
||||
if (titleEn.trim()) titlePayload.en = titleEn.trim();
|
||||
if (Object.keys(titlePayload).length === 0) {
|
||||
setLocalError(t("admin_news_title_required") ?? "Vyplň nadpis alespoň v jednom jazyce.");
|
||||
return;
|
||||
}
|
||||
|
||||
const contentPayload: Record<string, string> = {};
|
||||
if (contentCs.trim()) contentPayload.cs = contentCs.trim();
|
||||
if (contentEn.trim()) contentPayload.en = contentEn.trim();
|
||||
if (Object.keys(contentPayload).length === 0) {
|
||||
setLocalError(t("admin_news_content_required") ?? "Vyplň obsah alespoň v jednom jazyce.");
|
||||
return;
|
||||
}
|
||||
|
||||
const excerptPayload: Record<string, string> = {};
|
||||
if (excerptCs.trim()) excerptPayload.cs = excerptCs.trim();
|
||||
if (excerptEn.trim()) excerptPayload.en = excerptEn.trim();
|
||||
|
||||
const payload: NewsPayload = {
|
||||
title: titlePayload,
|
||||
content: contentPayload,
|
||||
is_published: isPublished,
|
||||
};
|
||||
|
||||
if (Object.keys(excerptPayload).length > 0) payload.excerpt = excerptPayload;
|
||||
if (publishedAt.trim()) payload.published_at = publishedAt.trim();
|
||||
|
||||
await onSubmit(payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Input
|
||||
label={t("admin_news_form_title_cs") ?? "Nadpis (cs)"}
|
||||
value={titleCs}
|
||||
onChange={(e) => setTitleCs(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label={t("admin_news_form_title_en") ?? "Title (en)"}
|
||||
value={titleEn}
|
||||
onChange={(e) => setTitleEn(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Textarea
|
||||
label={t("admin_news_form_excerpt_cs") ?? "Perex (cs)"}
|
||||
value={excerptCs}
|
||||
onChange={(e) => setExcerptCs(e.target.value)}
|
||||
minRows={2}
|
||||
/>
|
||||
<Textarea
|
||||
label={t("admin_news_form_excerpt_en") ?? "Excerpt (en)"}
|
||||
value={excerptEn}
|
||||
onChange={(e) => setExcerptEn(e.target.value)}
|
||||
minRows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Textarea
|
||||
label={t("admin_news_form_content_cs") ?? "Obsah (cs)"}
|
||||
value={contentCs}
|
||||
onChange={(e) => setContentCs(e.target.value)}
|
||||
minRows={4}
|
||||
/>
|
||||
<Textarea
|
||||
label={t("admin_news_form_content_en") ?? "Content (en)"}
|
||||
value={contentEn}
|
||||
onChange={(e) => setContentEn(e.target.value)}
|
||||
minRows={4}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2 items-center">
|
||||
<Input
|
||||
type="datetime-local"
|
||||
label={t("admin_news_form_published_from") ?? "Publikováno od"}
|
||||
value={publishedAt}
|
||||
onChange={(e) => setPublishedAt(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch isSelected={isPublished} onValueChange={setIsPublished}>
|
||||
{t("admin_news_form_publish") ?? "Publikovat"}
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(localError || serverError) && (
|
||||
<div className="text-sm text-red-600">{localError ?? serverError}</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button type="submit" color="primary" isLoading={submitting}>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
<Button type="button" variant="light" onPress={onCancel}>
|
||||
{t("admin_form_close") ?? "Zavřít formulář"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user