From 203fd65331d88c0fb236453ca5387d0e482b7dab Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 02:07:49 +0000 Subject: [PATCH] Add account page, global session menu, and prefs-aware waypoint customization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lifts the session out of /explore-only territory and onto every page, adds a sign-in / account dropdown to the nav, and ships a first-class account settings page where users can personalize the waypoint catalog. * AtprotoSessionProvider moves to the root layout so the OAuth session is available on every route (homepage, universal links, account, explore, …). Explore layout drops its local wrapper. * SessionMenu — header dropdown: \"Sign in\" button when signed out, avatar pill with display name + menu (my repo / account settings / sign out) when signed in. Wired into all three Header modes (compact / simple / default). * /account — gated settings page. When signed out, shows a sign-in form that walks through OAuth. When signed in, renders the user's profile header, PDS sync status, and the WaypointsManager. * WaypointsManager — drag-and-drop reorder (via @dnd-kit), per-row show/hide eye toggle, and full CRUD for custom waypoints with URL templates supporting {handle} / {did} / {actor} / {collection} / {rkey} placeholders. * PreferencesProvider — hybrid storage: localStorage primary, optionally mirrored to the user's PDS as a to.aturi.actor.preferences/self record. On sign-in the two are reconciled by updatedAt (newer wins) and the loser is pushed back so cross-device sync settles automatically. PDS writes are debounced 1.5s so burst edits collapse into a single network call. * WaypointPicker now consumes prefs: hidden built-ins disappear, the user's order is applied within each category, and custom waypoints show up under a new \"My Waypoints\" group on every universal link page. Anonymous users still get the default catalog; they can also customize locally without signing in (changes live in localStorage and stay per-device until they sign in, at which point they migrate up to the PDS). --- package-lock.json | 70 ++++ package.json | 3 + src/app/account/layout.tsx | 19 + src/app/account/page.tsx | 13 + src/app/explore/layout.tsx | 42 +- src/app/globals.css | 16 + src/app/layout.tsx | 12 +- .../{explore => }/AtprotoSessionProvider.tsx | 0 src/components/Header.tsx | 46 ++- src/components/PreferencesProvider.tsx | 194 +++++++++ src/components/SessionMenu.tsx | 370 ++++++++++++++++++ src/components/WaypointPicker.tsx | 37 +- src/components/account/AccountPage.tsx | 272 +++++++++++++ src/components/account/CustomWaypointForm.tsx | 303 ++++++++++++++ .../account/SortableWaypointRow.tsx | 202 ++++++++++ src/components/account/WaypointsManager.tsx | 257 ++++++++++++ src/components/explore/RecordExplorer.tsx | 2 +- src/components/explore/SessionBadge.tsx | 2 +- src/components/explore/SignInPanel.tsx | 2 +- src/utils/atproto/preferencesPds.ts | 98 +++++ src/utils/personalizeWaypoints.tsx | 139 +++++++ src/utils/preferences.ts | 177 +++++++++ 22 files changed, 2209 insertions(+), 67 deletions(-) create mode 100644 src/app/account/layout.tsx create mode 100644 src/app/account/page.tsx rename src/components/{explore => }/AtprotoSessionProvider.tsx (100%) create mode 100644 src/components/PreferencesProvider.tsx create mode 100644 src/components/SessionMenu.tsx create mode 100644 src/components/account/AccountPage.tsx create mode 100644 src/components/account/CustomWaypointForm.tsx create mode 100644 src/components/account/SortableWaypointRow.tsx create mode 100644 src/components/account/WaypointsManager.tsx create mode 100644 src/utils/atproto/preferencesPds.ts create mode 100644 src/utils/personalizeWaypoints.tsx create mode 100644 src/utils/preferences.ts diff --git a/package-lock.json b/package-lock.json index 35d4fc2..9047e2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "dependencies": { "@atproto/api": "^0.20.5", "@atproto/oauth-client-browser": "^0.4.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", "@vercel/analytics": "^1.6.1", "@vercel/og": "^0.8.6", "framer-motion": "^12.23.26", @@ -689,6 +692,73 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", diff --git a/package.json b/package.json index 526e924..6171eeb 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "dependencies": { "@atproto/api": "^0.20.5", "@atproto/oauth-client-browser": "^0.4.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", "@vercel/analytics": "^1.6.1", "@vercel/og": "^0.8.6", "framer-motion": "^12.23.26", diff --git a/src/app/account/layout.tsx b/src/app/account/layout.tsx new file mode 100644 index 0000000..f47e1ae --- /dev/null +++ b/src/app/account/layout.tsx @@ -0,0 +1,19 @@ +import type { ReactNode } from 'react'; +import Header from '@/components/Header'; + +// AtprotoSessionProvider lives in the root layout, so we don't re-wrap. +// Just the page chrome. +export default function AccountLayout({ children }: { children: ReactNode }) { + return ( +
+
+ {children} +
+ ); +} diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx new file mode 100644 index 0000000..36744f4 --- /dev/null +++ b/src/app/account/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from 'next'; +import AccountPage from '@/components/account/AccountPage'; + +export const metadata: Metadata = { + title: 'Account · aturi.to', + description: 'Customize your Aturi waypoints and preferences.', + // Don't index the account page — it's gated by sign-in and personalized. + robots: { index: false, follow: false }, +}; + +export default function Account() { + return ; +} diff --git a/src/app/explore/layout.tsx b/src/app/explore/layout.tsx index 481c529..c9b7d89 100644 --- a/src/app/explore/layout.tsx +++ b/src/app/explore/layout.tsx @@ -1,45 +1,21 @@ -'use client'; - import type { ReactNode } from 'react'; -import { AtprotoSessionProvider } from '@/components/explore/AtprotoSessionProvider'; import Header from '@/components/Header'; -import SessionBadge from '@/components/explore/SessionBadge'; +// AtprotoSessionProvider now lives in the root layout, so the session and +// the Header's sign-in menu are available on every page. This layout just +// adds the explore-specific page chrome and pins min-height so route +// transitions don't shrink the viewport. export default function ExploreLayout({ children }: { children: ReactNode }) { - return ( - -
-
- - {children} -
-
- ); -} - -/** - * Thin one-line strip beneath the compact header. Only renders when the - * user is signed in (SessionBadge handles the empty case internally). - */ -function SessionStrip() { return (
- +
+ {children}
); } diff --git a/src/app/globals.css b/src/app/globals.css index 5630f5b..78d3809 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1181,6 +1181,22 @@ html { } } +/* Spinner used by the account-page PDS sync status indicator. */ +.explore-spin { + animation: explore-spin 0.9s linear infinite; +} + +@keyframes explore-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@media (prefers-reduced-motion: reduce) { + .explore-spin { + animation: none; + } +} + @media (prefers-reduced-motion: reduce) { .explore-jetstream-row[data-fresh] { animation: none; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 66ab48f..cb49041 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; import { Analytics } from "@vercel/analytics/next"; +import { AtprotoSessionProvider } from "@/components/AtprotoSessionProvider"; +import { PreferencesProvider } from "@/components/PreferencesProvider"; import Footer from "@/components/Footer"; import PageTransition from "@/components/PageTransition"; import ThemeSync from "@/components/ThemeSync"; @@ -45,9 +47,13 @@ export default function RootLayout({ - -
{children}
-
+ + + +
{children}
+
+
+