diff --git a/src/app/legal/privacy/page.test.tsx b/src/app/legal/privacy/page.test.tsx new file mode 100644 index 0000000..f383365 --- /dev/null +++ b/src/app/legal/privacy/page.test.tsx @@ -0,0 +1,55 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; + +import PrivacyPage from "@/app/legal/privacy/page"; + +afterEach(cleanup); + +describe("Privacy policy", () => { + it("names every service that sees visitor or account data", () => { + render(); + + for (const service of [ + "Supabase", + "Google", + "Vercel Analytics", + "Umami", + "MapTiler", + ]) { + expect(screen.getAllByText(service).length, service).toBeGreaterThan(0); + } + }); + + it("covers the per-account data the app stores", () => { + const { container } = render(); + const text = container.textContent ?? ""; + + for (const phrase of [ + "Saved places", + "home location", + "Calendar events", + "Competitions you save", + "graduation year", + ]) { + expect(text.toLowerCase(), phrase).toContain(phrase.toLowerCase()); + } + }); + + it("says Near me stays in the browser", () => { + const { container } = render(); + expect(container.textContent).toMatch(/Near me[\s\S]*not sent to StudyMap/); + }); + + it("gives a contact address for deletion requests and a current date", () => { + render(); + + const link = screen.getByRole("link", { name: "studentsuite3@gmail.com" }); + expect(link.getAttribute("href")).toBe("mailto:studentsuite3@gmail.com"); + expect(screen.getByText(/Last updated: November 2026/)).toBeTruthy(); + }); + + it("does not use em dashes", () => { + const { container } = render(); + expect(container.textContent).not.toContain("—"); + }); +}); diff --git a/src/app/legal/privacy/page.tsx b/src/app/legal/privacy/page.tsx index 416ba84..f742f9b 100644 --- a/src/app/legal/privacy/page.tsx +++ b/src/app/legal/privacy/page.tsx @@ -4,38 +4,119 @@ export const metadata = { title: "Privacy Policy", }; +const CONTACT = "studentsuite3@gmail.com"; + export default function PrivacyPage() { return ( - - -

StudyMap collects minimal data. We track:

+ + +

+ You do not need an account to use the map, the city pages or the competitions + list. Without one, StudyMap does not keep a profile of you. Page views are + counted by the analytics described below. +

+
+ + +

+ Signing in (with an email and password, or with Google) creates an account. + Everything below is stored against that account and is private to it: +

    -
  • Places you add to the map (name, location, category)
  • -
  • Your GitHub username (if you contribute)
  • -
  • Anonymous, aggregate usage analytics (page views, no personal profile)
  • +
  • + Your sign-in details: your email address and the account record that holds + them. +
  • +
  • + Saved places you add for yourself: name, type, city, coordinates, address and + a note. +
  • +
  • A home location, if you set one: coordinates and a label.
  • +
  • Calendar events you add: title, date, category and notes.
  • +
  • + Competitions you save, and a private calendar feed link (a random token) for + them. +
  • +
  • + The answers to the first-run questions, if you give them: graduation year, + school board, field of study, country, and how you heard about StudyMap. +
+

+ These tables are protected so that an account can read, change and delete only + its own rows. The one number that is public is how many accounts saved each + competition; it carries no names. +

- + +

+ A place you contribute to the public dataset (through a GitHub issue or pull + request) is public by design: its name, location and category, and your GitHub + username as the contributor. That is separate from the private saved places + above. +

+
+ + +

+ “Near me” asks your browser for your location when you press it. The + position is used in your browser to sort places by distance and is not sent to + StudyMap. If you deny the permission, the rest of the map still works. +

+
+ + +
    +
  • + Supabase hosts accounts and the + stored rows above. +
  • +
  • + Google handles sign-in if you + choose “Continue with Google”, and receives the request to sign + you in. +
  • +
  • + Vercel Analytics and{" "} + Umami count page views. Umami is + loaded only on the live site and only when it is configured. Neither is used + to build a personal profile. +
  • +
  • + MapTiler serves the map tiles. + Your browser requests them directly, so MapTiler sees your IP address and + which part of the map you are looking at. Map data is from OpenStreetMap + contributors. +
  • +

- Contributed places power the public map. Your GitHub username is shown as the - contributor on entries you add. Usage analytics help us see which features - students actually use, so we can improve them. + Signing in sets session cookies so you stay signed in. We do not sell your data + and do not run advertising trackers.

- +

- We do not sell your data, run third-party ad trackers, or require an account to - browse the map. The dataset is open, so anything you contribute is public by - design. + To limit repeated reset emails, we keep a hash of the email address and the time + of each reset request, never the address itself. This record is not linked to an + account.

- +

- You can request to see, edit, or delete your data anytime. Email us and we will - help. + You can edit or delete your saved places, home location, events and saved + competitions yourself in the app. To delete your account, email{" "} + + {CONTACT} + + . Deleting an account removes its saved places, home location, events, saved + competitions and profile answers. You can also write to the same address to ask + what we hold about you.