diff --git a/client/src/Hooks/useStatusPageForm.ts b/client/src/Hooks/useStatusPageForm.ts new file mode 100644 index 000000000..0ea128e0d --- /dev/null +++ b/client/src/Hooks/useStatusPageForm.ts @@ -0,0 +1,27 @@ +import { useMemo } from "react"; +import { statusPageSchema, type StatusPageFormData } from "@/Validation/statusPage"; +import type { StatusPage } from "@/Types/StatusPage"; + +interface UseStatusPageFormOptions { + data?: StatusPage | null; +} + +export const useStatusPageForm = ({ data = null }: UseStatusPageFormOptions = {}) => { + return useMemo(() => { + const defaults: StatusPageFormData = { + companyName: data?.companyName || "", + url: data?.url || "", + timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, + color: data?.color || "#4169E1", + monitors: data?.monitors || [], + isPublished: data?.isPublished ?? false, + showCharts: data?.showCharts ?? true, + showUptimePercentage: data?.showUptimePercentage ?? true, + showAdminLoginLink: data?.showAdminLoginLink ?? false, + customCSS: data?.customCSS || "", + logo: data?.logo || null, + }; + + return { schema: statusPageSchema, defaults }; + }, [data]); +}; diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index 25c4d03f8..7e4e94a9d 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -1,16 +1,68 @@ import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import { ImageUpload, SwitchComponent } from "@/Components/v2/inputs"; +import IconButton from "@mui/material/IconButton"; +import Divider from "@mui/material/Divider"; +import { Trash2 } from "lucide-react"; +import { + ImageUpload, + SwitchComponent, + Button, + TextField, + Autocomplete, +} from "@/Components/v2/inputs"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; +import { useEffect } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useStatusPageForm } from "@/Hooks/useStatusPageForm"; +import type { StatusPageFormData } from "@/Validation/statusPage"; +import { useGet } from "@/Hooks/UseApi"; +import type { Monitor } from "@/Types/Monitor"; +import timezones from "@/Utils/timezones.json"; + +interface TimezoneOption { + _id: string; + name: string; +} const CreateStatusPage = () => { const theme = useTheme(); const { t } = useTranslation(); + + const { schema, defaults } = useStatusPageForm(); + + const { data: monitorsResponse } = useGet( + "/monitors/team?type=http&type=ping&type=port&type=docker" + ); + const monitors = monitorsResponse ?? []; + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + }); + + const { control, reset, handleSubmit } = form; + + useEffect(() => { + reset(defaults); + }, [defaults, reset]); + + const onError = (errors: any) => { + console.log(errors); + }; + + const onSubmit = (data: StatusPageFormData) => { + console.log("Form Data Submitted: ", data); + }; + return ( - + { alignItems="center" spacing={theme.spacing(2)} > - field.onChange(e.target.checked)} + ( + field.onChange(e.target.checked)} + /> + )} /> {t("pages.statusPages.form.access.option.published.name")} @@ -30,15 +88,184 @@ const CreateStatusPage = () => { } /> + + ( + + )} + /> + ( + + )} + /> + + } + /> + { + const selectedMonitors = monitors.filter((m) => field.value.includes(m.id)); + return ( + + option.name} + value={selectedMonitors} + onChange={(_, newValue: Monitor[]) => { + field.onChange(newValue.map((m) => m.id)); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + fieldLabel={t( + "pages.statusPages.form.monitors.option.monitors.label" + )} + renderInput={(params) => ( + + )} + /> + {selectedMonitors.length > 0 && ( + + {selectedMonitors.map((monitor, index) => ( + + {monitor.name} + { + field.onChange( + field.value.filter((id: string) => id !== monitor.id) + ); + }} + aria-label="Remove monitor" + > + + + {index < selectedMonitors.length - 1 && } + + ))} + + )} + + ); + }} + /> + } + /> + ( + option.name} + value={ + timezones.find((tz: TimezoneOption) => tz._id === field.value) ?? null + } + onChange={(_, newValue: TimezoneOption | null) => { + field.onChange(newValue?._id ?? ""); + }} + fieldLabel={t("pages.statusPages.form.timezone.option.timezone.label")} + renderInput={(params) => ( + + )} + /> + )} + /> + } + /> - + ( + { + if (file) { + field.onChange({ + data: file.src, + contentType: file.file.type, + }); + } else { + field.onChange(null); + } + }} + /> + )} + /> } /> + + + ); }; diff --git a/client/src/Validation/statusPage.ts b/client/src/Validation/statusPage.ts new file mode 100644 index 000000000..cb148e633 --- /dev/null +++ b/client/src/Validation/statusPage.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +export const statusPageSchema = z.object({ + companyName: z + .string() + .min(1, "Company name is required") + .max(100, "Company name must be at most 100 characters"), + url: z + .string() + .min(1, "URL is required") + .max(50, "URL must be at most 50 characters") + .regex( + /^[a-z0-9-]+$/, + "URL can only contain lowercase letters, numbers, and hyphens" + ), + timezone: z.string().optional(), + color: z.string().min(1, "Color is required"), + monitors: z.array(z.string()), + isPublished: z.boolean(), + showCharts: z.boolean(), + showUptimePercentage: z.boolean(), + showAdminLoginLink: z.boolean(), + customCSS: z.string().optional(), + logo: z + .object({ + data: z.string(), + contentType: z.string(), + }) + .nullable() + .optional(), +}); + +export type StatusPageFormData = z.infer; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 2d1819696..2d3eb614c 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -508,6 +508,41 @@ "title": "Appearance", "description": "Define the default look and feel of your public status page", "option": {} + }, + "basicInfo": { + "title": "Basic information", + "description": "Name and address of your status page", + "option": { + "name": { + "label": "Name", + "placeholder": "My status page" + }, + "url": { + "label": "URL", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "Monitors", + "description": "Select which monitors to display on your status page", + "noMonitors": "No monitors available. Create uptime monitors first.", + "option": { + "monitors": { + "label": "Monitors", + "placeholder": "Select monitors..." + } + } + }, + "timezone": { + "title": "Timezone", + "description": "Select the timezone for your status page", + "option": { + "timezone": { + "label": "Timezone", + "placeholder": "Search timezones..." + } + } } }, "settings": {