158 lines
6.7 KiB
TypeScript
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>
|
|
);
|
|
}
|