diff --git a/apps/journal/app/components/AlphaBanner.tsx b/apps/journal/app/components/AlphaBanner.tsx new file mode 100644 index 0000000..16ca223 --- /dev/null +++ b/apps/journal/app/components/AlphaBanner.tsx @@ -0,0 +1,10 @@ +import { useTranslation } from "react-i18next"; + +export function AlphaBanner() { + const { t } = useTranslation("journal"); + return ( +
+

{t("alpha.message")}

+
+ ); +} diff --git a/apps/journal/app/components/Footer.tsx b/apps/journal/app/components/Footer.tsx new file mode 100644 index 0000000..1542c1c --- /dev/null +++ b/apps/journal/app/components/Footer.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from "react-i18next"; + +export function Footer() { + const { t } = useTranslation("journal"); + return ( + + ); +} diff --git a/apps/journal/app/entry.client.tsx b/apps/journal/app/entry.client.tsx index 8e3f019..d5b4d57 100644 --- a/apps/journal/app/entry.client.tsx +++ b/apps/journal/app/entry.client.tsx @@ -1,30 +1,8 @@ -import * as Sentry from "@sentry/react"; -import { useEffect } from "react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; import { HydratedRouter } from "react-router/dom"; -import { useLocation, useNavigationType, createRoutesFromChildren, matchRoutes } from "react-router"; import { initI18nClient } from "@trails-cool/i18n"; -const sentryEnvironment = import.meta.env.VITE_SENTRY_ENVIRONMENT ?? - (import.meta.env.PROD ? "production" : "development"); - -Sentry.init({ - dsn: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728", - integrations: [ - Sentry.reactRouterV7BrowserTracingIntegration({ - useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ], - environment: sentryEnvironment, - tracesSampleRate: sentryEnvironment === "ci" ? 0 : 1.0, - enabled: import.meta.env.PROD && sentryEnvironment !== "ci", -}); - initI18nClient(); startTransition(() => { diff --git a/apps/journal/app/entry.server.tsx b/apps/journal/app/entry.server.tsx index 0bdfe8f..ff195e8 100644 --- a/apps/journal/app/entry.server.tsx +++ b/apps/journal/app/entry.server.tsx @@ -16,6 +16,7 @@ Sentry.init({ environment: sentryEnvironment, tracesSampleRate: 1.0, enabled: process.env.NODE_ENV === "production" && !process.env.CI, + sendDefaultPii: false, beforeSend(event) { // Drop 404s — they're expected (scanners, typos), not bugs const serialized = event.extra?.__serialized__ as Record | undefined; diff --git a/apps/journal/app/lib/auth.server.ts b/apps/journal/app/lib/auth.server.ts index d44e4cd..204aa64 100644 --- a/apps/journal/app/lib/auth.server.ts +++ b/apps/journal/app/lib/auth.server.ts @@ -76,6 +76,7 @@ export async function finishRegistration( email, username, domain, + termsAcceptedAt: new Date(), }); await db.insert(credentials).values({ @@ -158,7 +159,7 @@ export async function registerWithMagicLink(email: string, username: string): Pr const userId = randomUUID(); const domain = process.env.DOMAIN ?? "localhost"; - await db.insert(users).values({ id: userId, email, username, domain }); + await db.insert(users).values({ id: userId, email, username, domain, termsAcceptedAt: new Date() }); // Create magic token for verification const token = randomBytes(32).toString("base64url"); diff --git a/apps/journal/app/lib/operator.ts b/apps/journal/app/lib/operator.ts new file mode 100644 index 0000000..4ea49af --- /dev/null +++ b/apps/journal/app/lib/operator.ts @@ -0,0 +1,20 @@ +/** + * Operator details for the Impressum (§5 TMG / §18 MStV). + * + * Required by German law. Address must be a reachable physical address. + * Email must be a direct contact (no forms-only policy). + */ +export const operator = { + name: "Ullrich Schäfer", + address: { + street: "Mehringdamm 87", + postalCode: "10965", + city: "Berlin", + country: "Germany", + }, + email: "legal@trails.cool", + // Responsible for content per §18 Abs. 2 MStV + responsiblePerson: "Ullrich Schäfer", +} as const; + +export type Operator = typeof operator; diff --git a/apps/journal/app/lib/sentry.client.ts b/apps/journal/app/lib/sentry.client.ts new file mode 100644 index 0000000..d2e6dac --- /dev/null +++ b/apps/journal/app/lib/sentry.client.ts @@ -0,0 +1,30 @@ +import * as Sentry from "@sentry/react"; +import { useEffect } from "react"; +import { useLocation, useNavigationType, createRoutesFromChildren, matchRoutes } from "react-router"; + +let initialized = false; + +export function initSentryClient() { + if (initialized) return; + initialized = true; + + const sentryEnvironment = import.meta.env.VITE_SENTRY_ENVIRONMENT ?? + (import.meta.env.PROD ? "production" : "development"); + + Sentry.init({ + dsn: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728", + integrations: [ + Sentry.reactRouterV7BrowserTracingIntegration({ + useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ], + environment: sentryEnvironment, + tracesSampleRate: sentryEnvironment === "ci" ? 0 : 1.0, + enabled: import.meta.env.PROD && sentryEnvironment !== "ci", + sendDefaultPii: false, + }); +} diff --git a/apps/journal/app/root.tsx b/apps/journal/app/root.tsx index be4a5f4..d8967b0 100644 --- a/apps/journal/app/root.tsx +++ b/apps/journal/app/root.tsx @@ -7,6 +7,9 @@ import { useTranslation } from "react-i18next"; import { detectLocale } from "@trails-cool/i18n"; import { getSessionUser } from "~/lib/auth.server"; import { LocaleProvider } from "~/components/LocaleContext"; +import { AlphaBanner } from "~/components/AlphaBanner"; +import { Footer } from "~/components/Footer"; +import { initSentryClient } from "~/lib/sentry.client"; import stylesheet from "@trails-cool/ui/styles.css?url"; export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }]; @@ -116,7 +119,8 @@ export default function App({ loaderData }: Route.ComponentProps) { const locale = loaderData?.locale ?? "en"; useEffect(() => { if (user) { - Sentry.setUser({ id: user.id, username: user.username }); + initSentryClient(); + Sentry.setUser({ id: user.id }); } else { Sentry.setUser(null); } @@ -124,8 +128,10 @@ export default function App({ loaderData }: Route.ComponentProps) { return ( + +