Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/routes/admin/settings/analytics.tsx
T

158 lines
6.7 KiB
TypeScript

import { ActionFunction, LoaderFunctionArgs, MetaFunction, useActionData } from "react-router";
import { Form, useLoaderData } from "react-router";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import InputCheckboxWithDescription from "~/components/ui/input/InputCheckboxWithDescription";
import { getTranslations } from "~/locale/i18next.server";
import { useAdminData } from "~/utils/data/useAdminData";
import { AppConfiguration, getAppConfiguration, getOrCreateAppConfiguration, updateAppConfiguration } from "~/utils/db/appConfiguration.db.server";
import { createAdminLog } from "~/utils/db/logs.db.server";
import { getUserHasPermission } from "~/utils/helpers/PermissionsHelper";
import { verifyUserHasPermission } from "~/utils/helpers/.server/PermissionsService";
import { parseAdminSettingsForm, redactSettingsAuditDetails } from "~/utils/services/adminSettingsValidation.server";
import { toast } from "sonner";
import BackButtonWithTitle from "~/components/ui/buttons/BackButtonWithTitle";
import EditPageLayout from "~/components/ui/layouts/EditPageLayout";
import SettingSection from "~/components/ui/sections/SettingSection";
import { Button } from "~/components/ui/button";
import { adminPath } from "~/utils/app/CanonicalUrlUtils";
import { validateCSRFToken } from "~/utils/session.server";
import { useRootData } from "~/utils/data/useRootData";
export const meta: MetaFunction<typeof loader> = ({ data }) => [{ title: data?.title }];
type LoaderData = {
title: string;
appConfiguration: AppConfiguration;
};
type ActionData = {
success?: string;
error?: string;
fieldErrors?: Record<string, string[]>;
};
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { t } = await getTranslations(request);
await verifyUserHasPermission(request, "admin.settings.analytics.view");
const appConfiguration = await getAppConfiguration({ request });
const data: LoaderData = {
title: `${t("settings.admin.analytics.title")} | ${process.env.APP_NAME}`,
appConfiguration,
};
return data;
};
export const action: ActionFunction = async ({ request }) => {
const { t } = await getTranslations(request);
await validateCSRFToken(request);
const form = await request.formData();
const parsed = parseAdminSettingsForm("analytics", form);
if (!parsed.ok) {
return Response.json({ error: t("shared.invalidForm"), fieldErrors: parsed.errors }, { status: parsed.status });
}
await verifyUserHasPermission(request, "admin.settings.analytics.update");
await getOrCreateAppConfiguration({ request });
await updateAppConfiguration(parsed.data);
await createAdminLog(request, "admin.settings.updated", redactSettingsAuditDetails("analytics", parsed.data));
return Response.json({ success: t("shared.updated") });
};
export default function AdminSettingsAnalytics() {
const { t } = useTranslation();
const data = useLoaderData<LoaderData>();
const adminData = useAdminData();
const actionData = useActionData<ActionData>();
const rootData = useRootData();
const [enabled, setEnabled] = useState(data.appConfiguration.analytics.enabled);
const [simpleAnalytics, setSimpleAnalytics] = useState(data.appConfiguration.analytics.simpleAnalytics);
const [plausibleAnalytics, setPlausibleAnalytics] = useState(data.appConfiguration.analytics.plausibleAnalytics);
const [canUpdate] = useState(getUserHasPermission(adminData, "admin.settings.analytics.update"));
const fieldErrors = actionData?.fieldErrors ?? {};
const fieldErrorEntries = Object.entries(fieldErrors);
useEffect(() => {
if (actionData?.success) {
toast.success(actionData.success);
} else if (actionData?.error) {
toast.error(actionData.error);
}
}, [actionData]);
return (
<EditPageLayout title={""}>
<SettingSection
title={<BackButtonWithTitle href={adminPath("settings", { mode: "host-aware" })}>{t("settings.admin.analytics.title")}</BackButtonWithTitle>}
description={t("analytics.configuration.description")}
>
<Form method="post" className="space-y-8">
{rootData.csrf && <input hidden readOnly name="csrf" value={rootData.csrf} />}
{(actionData?.error || fieldErrorEntries.length > 0) && (
<div role="alert" className="rounded-md border border-red-200 bg-red-50 p-4 text-sm text-red-800">
{actionData?.error && <p className="font-medium">{actionData.error}</p>}
{fieldErrorEntries.length > 0 && (
<ul className="mt-2 list-disc space-y-1 pl-5">
{fieldErrorEntries.flatMap(([field, messages]) =>
messages.map((message) => (
<li key={`${field}-${message}`}>
<span className="font-medium">{field}</span>: {message}
</li>
))
)}
</ul>
)}
</div>
)}
<input name="action" value="update" hidden readOnly />
<div className="grid grid-cols-1 gap-y-6 sm:grid-cols-6 sm:gap-x-6">
<div className="sm:col-span-6">
<InputCheckboxWithDescription
name="enabled"
value={enabled}
setValue={setEnabled}
title={t("shared.enabled")}
description={t("analytics.configuration.builtInDescription")}
disabled={!canUpdate}
/>
<InputCheckboxWithDescription
name="simpleAnalytics"
value={simpleAnalytics}
setValue={setSimpleAnalytics}
title={t("analytics.configuration.simpleAnalyticsEnabled")}
description={
<a href="https://www.simpleanalytics.com/?referral=hesef" target="_blank" rel="noreferrer">
{t("shared.clickHereToTryLearnMore")}
</a>
}
disabled={!canUpdate}
/>
<InputCheckboxWithDescription
name="plausibleAnalytics"
value={plausibleAnalytics}
setValue={setPlausibleAnalytics}
title={t("analytics.configuration.plausibleAnalyticsEnabled")}
description={
<a href="https://plausible.io/" target="_blank" rel="noreferrer">
{t("shared.clickHereToTryLearnMore")}
</a>
}
disabled={!canUpdate}
/>
</div>
</div>
<div className="flex justify-end">
<Button type="submit" variant="outline" disabled={!canUpdate}>
{t("shared.save")}
</Button>
</div>
</Form>
</SettingSection>
</EditPageLayout>
);
}