From 5905cdadea7dbdc3b2af6628b0409aba4df5910d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 19 Apr 2026 09:11:39 +0200 Subject: [PATCH] Apply public-content-visibility: visibility flag + public profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the public-content-visibility OpenSpec change. Adds the smallest social surface that lets us demo the product to logged-out visitors without user signup. Schema: - `visibility text NOT NULL DEFAULT 'private'` on routes + activities. - Shared Visibility type exported from the schema module. Access: - New canView(content, viewer, { asDirectLink }) helper in auth.server.ts centralises the rule: public → anyone; unlisted → anyone on direct link; private → owner only. - routes.$id and activities.$id loaders return 404 (not 403) when canView rejects, so existence of private content isn't leaked. - Detail pages emit Open Graph + Twitter Card meta on public/unlisted content only. Editing: - Visibility +
+ + +
+ + + + )} + {isOwner && (
{ if (!confirm(t("activities.deleteConfirm"))) e.preventDefault(); }}> diff --git a/apps/journal/app/routes/legal.privacy.tsx b/apps/journal/app/routes/legal.privacy.tsx index 90a601c..bdc805d 100644 --- a/apps/journal/app/routes/legal.privacy.tsx +++ b/apps/journal/app/routes/legal.privacy.tsx @@ -375,6 +375,14 @@ export default function PrivacyPage() {
  • Routes: GPX, geometry, title, description
  • Activities: title, description, date, linked route
  • GPX / JSON export available per object and overall
  • +
  • + Routes and activities each carry a visibility setting + (private / unlisted / public) + that defaults to private. Content you explicitly mark + public is visible to anyone on the internet, + including on your public profile at /users/<you> + and on search engines that index those pages. +
  • diff --git a/apps/journal/app/routes/routes.$id.edit.tsx b/apps/journal/app/routes/routes.$id.edit.tsx index 9dce0ca..e1fe2e9 100644 --- a/apps/journal/app/routes/routes.$id.edit.tsx +++ b/apps/journal/app/routes/routes.$id.edit.tsx @@ -1,7 +1,11 @@ import { data, redirect } from "react-router"; +import { useTranslation } from "react-i18next"; import type { Route } from "./+types/routes.$id.edit"; import { getSessionUser } from "~/lib/auth.server"; import { getRoute, updateRoute } from "~/lib/routes.server"; +import type { Visibility } from "@trails-cool/db/schema/journal"; + +const VISIBILITY_VALUES = new Set(["private", "unlisted", "public"]); export async function loader({ params, request }: Route.LoaderArgs) { const user = await getSessionUser(request); @@ -12,7 +16,12 @@ export async function loader({ params, request }: Route.LoaderArgs) { if (route.ownerId !== user.id) throw data({ error: "Not authorized" }, { status: 403 }); return data({ - route: { id: route.id, name: route.name, description: route.description }, + route: { + id: route.id, + name: route.name, + description: route.description, + visibility: route.visibility, + }, }); } @@ -24,13 +33,17 @@ export async function action({ params, request }: Route.ActionArgs) { const name = formData.get("name") as string; const description = formData.get("description") as string; const gpxFile = formData.get("gpx") as File | null; + const visibilityRaw = formData.get("visibility") as string | null; - const input: { name?: string; description?: string; gpx?: string } = {}; + const input: { name?: string; description?: string; gpx?: string; visibility?: Visibility } = {}; if (name) input.name = name; if (description !== null) input.description = description; if (gpxFile && gpxFile.size > 0) { input.gpx = await gpxFile.text(); } + if (visibilityRaw && VISIBILITY_VALUES.has(visibilityRaw as Visibility)) { + input.visibility = visibilityRaw as Visibility; + } await updateRoute(params.id, user.id, input); return redirect(`/routes/${params.id}`); @@ -42,6 +55,7 @@ export function meta(_args: Route.MetaArgs) { export default function EditRoutePage({ loaderData }: Route.ComponentProps) { const { route } = loaderData; + const { t } = useTranslation("journal"); return (
    @@ -75,6 +89,27 @@ export default function EditRoutePage({ loaderData }: Route.ComponentProps) { />
    +
    + + +

    + {route.visibility === "private" && t("routes.visibility.privateHelp")} + {route.visibility === "unlisted" && t("routes.visibility.unlistedHelp")} + {route.visibility === "public" && t("routes.visibility.publicHelp")} +

    +
    +