diff --git a/.forge/features/blade-responsive-navigation/evidence/README.md b/.forge/features/blade-responsive-navigation/evidence/README.md new file mode 100644 index 000000000..ee1abfdfa --- /dev/null +++ b/.forge/features/blade-responsive-navigation/evidence/README.md @@ -0,0 +1,26 @@ +# Blade navigation review media + +Screenshots and videos are hosted as attachments on [PR #537](https://github.com/KnightHacks/forge/pull/537), not stored in the repository. + +Captured from a local development server in Chromium. The full behavioral suite passed in headed mode; the final navigation videos use headless mode to capture the complete viewport. Navigation recordings use an isolated local database containing only the synthetic fixture from `apps/blade/src/tests/e2e/responsive-navigation.spec.ts`. + +The tests deliberately hold route responses to verify feedback before completion. These clips demonstrate behavior, not production response times. Recording uses Playwright's 100 ms action pacing to make the steps readable; videos preserve the recorded timing. + +| Evidence | What to look for | +| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| [Desktop navigation screenshot](https://github.com/user-attachments/assets/8e3d60f2-78b5-4c99-9012-4fc7a96c9b5b) | The Members destination highlights and the top progress bar appears while the current Forms page remains usable. | +| [Desktop navigation video](https://github.com/user-attachments/assets/a84c2db5-d4fd-4c5d-82da-5d2b8971b468) | Immediate feedback, completed navigation, browser back, and a repeated navigation without stuck loading. | +| [Mobile navigation screenshot](https://github.com/user-attachments/assets/680d6b59-6730-4a79-a93b-a274df958362) | The 320px drawer closes immediately, with a static progress indicator in reduced-motion mode. | +| [Mobile navigation video](https://github.com/user-attachments/assets/2c5fe6c4-1715-4531-82c6-c952653b516c) | Mobile menu selection, immediate dismissal, and the completed destination. The clip begins after the test's desktop sign-in setup. | +| [Desktop skeleton](https://github.com/user-attachments/assets/ff1f66c5-3079-4b7a-83c9-e257b12f2fa3) | Blade's root fallback at 1440px, using the existing logo, tokens, and inset rows. | +| [Mobile skeleton](https://github.com/user-attachments/assets/08498076-fa25-4b76-aaa7-c16bc5d67051) | The same fallback at 320px without horizontal overflow. | + +The root skeleton was captured by rendering the real `apps/blade/src/app/loading.tsx` through a temporary preview route, with reduced motion enabled for a stable frame. That preview route is not included in the change. The Next.js development indicator is visible in the captures. + +To repeat the behavioral checks against a local test database: + +```bash +pnpm --filter=@forge/blade run e2e responsive-navigation.spec.ts --headed +``` + +The standard Playwright configuration retains video only on failure. For these passing-run recordings, a temporary configuration enabled video recording; no permanent test configuration changed. diff --git a/.forge/features/blade-responsive-navigation/spec.md b/.forge/features/blade-responsive-navigation/spec.md new file mode 100644 index 000000000..ee13a7df4 --- /dev/null +++ b/.forge/features/blade-responsive-navigation/spec.md @@ -0,0 +1,21 @@ +# Blade responsive navigation + +Status: Implemented and validated. + +## Purpose and scope + +Members, officers and judges should see a response as soon as they navigate or select a view. Reuse Blade's violet accents, raised panels and existing page skeletons. Cover Blade routes and button-driven navigation; other websites are excluded. + +## Acceptance criteria + +- Links start navigation immediately, without a decorative delay. +- Pending navigation shows a subtle progress indicator and the intended rail destination immediately. +- Layout and page waits have responsive skeletons; existing content stays usable during in-page updates. +- Tabs and selectors acknowledge the chosen view immediately where the data is already available or can be safely represented as pending. +- Feedback clears after completion, interruption or error. Rapid navigation remains usable. +- Keyboard, modified clicks, downloads, scroll/history options, unsaved-settings guards and reduced motion retain their behavior. +- Saves keep their existing pending, success and error semantics. Do not invent successful results before confirmation. + +## Decisions and open questions + +The user prioritized navigation across Blade and authorized implementation and browser testing. No blocking product questions: use existing design and access contracts. Database, authentication, permissions, payments, uploads, email delivery and other websites are outside this change. diff --git a/.forge/features/blade-responsive-navigation/srd.md b/.forge/features/blade-responsive-navigation/srd.md new file mode 100644 index 000000000..4e75075c4 --- /dev/null +++ b/.forge/features/blade-responsive-navigation/srd.md @@ -0,0 +1,21 @@ +# Blade responsive navigation SRD + +## Implementation + +Follow `docs/agentic-development/forge-engineering-principles.md` and `apps/blade/DESIGN_SYSTEM.md`. Keep all implementation within `apps/blade`. + +Replace the existing timer/event-based route link with Next Link's `onNavigate` and a root React transition provider. Route imperative push/replace/refresh calls through the same Blade-local hook. React owns pending lifetime and optimistic destination rollback; avoid timers, event patching, new dependencies and duplicate routing state. Preserve Next prefetch, normal link behavior, navigation cancellation, history and scroll options. + +Use a narrow violet indeterminate progress bar, a polite status announcement and optimistic rail highlighting. Keep existing route skeletons and add a root fallback for missing leaf fallbacks and asynchronous layouts. Stop pulse/progress animation for reduced motion. + +GET search/filter forms use the same transition router and retain their native no-JavaScript fallback. Issue filter dialogs close when a search starts. + +In-page selected views may update optimistically while navigation is pending. Preserve server data ownership, URL state, permissions and current mutations. + +## Access and data compatibility + +Public routes remain public, member routes retain session gates, officer routes retain permission gates and judging retains its existing access gates. No APIs, validation rules, schemas, persisted data, Discord operations or environment configuration change. No migration. Rollback removes Blade UI changes. No annual configuration is introduced. + +## Verification + +Targeted Vitest regressions, Blade typecheck/lint, React analysis, repository format/lint/typecheck, and delayed-navigation Playwright checks with desktop and 320px screenshots. Record exact failures and limitations in status.md. diff --git a/.forge/features/blade-responsive-navigation/status.md b/.forge/features/blade-responsive-navigation/status.md new file mode 100644 index 000000000..41d2aaec2 --- /dev/null +++ b/.forge/features/blade-responsive-navigation/status.md @@ -0,0 +1,42 @@ +# Blade responsive navigation status + +Current phase: Complete + +## Decisions + +- Scope: Blade navigation/loading and feature notes only. Other websites and shared source are unchanged. +- Branch: `blade/responsive-navigation`, following the repository's app/slug convention. Pulled `origin/main` initially at `ec5e26ec`, then updated to `bd97fccb` when PR #533 landed. Incorporated `20878e0c` (PR #539) during the navigation review follow-up. +- Resolved import conflicts in form builder and responses dashboard while preserving upstream callback delivery and action feedback. +- Removed the 80 ms link delay and the never-reset exit flag. React transitions now own navigation feedback and optimistic destination rollback. +- Reused page skeletons and added a root fallback for layout waits and uncovered routes. Tabs and form section choices update immediately. +- Search/filter forms navigate without a full document reload; applying or clearing issue filters closes their dialog immediately. +- Addressed CodeRabbit's two navigation findings: the settings back arrow uses `data-pending`, and both hackathon check-in selectors show optimistic values. Check-in actions wait for the selected destination to commit; changing hackathons clears the event and disables the previous hackathon's event options while pending. + +## Validation + +- Blade suite: 799 tests passed across 142 files. +- After the final search/filter adjustment: 220 targeted tests passed across 34 files. +- Seven headed Playwright tests passed together in a clean run: delayed navigation/repeat/back, 320px drawer/reduced motion, immediate section selection, interrupted navigation, search without reload, unsaved-settings cancellation/discard, and issue-filter dialog completion. +- Inspected desktop and 320px navigation screenshots and root skeleton screenshots. No document overflow. The temporary skeleton preview route was removed after inspection. +- `pnpm format`: passed. +- `pnpm lint`: passed with existing repository warnings. +- `pnpm typecheck`: passed. +- `pnpm analyze:react:changed`: passed. +- `pnpm --filter=@forge/blade build`: passed with temporary local-only values for `JUDGING_ACCESS_SECRET` and `NEXT_PUBLIC_BLADE_URL`. The default invocation compiled but stopped at environment validation because those values are missing locally. No `.env` or deployment settings changed. +- Refreshed generated validator declarations and Next route types after stale generated files initially blocked checks. No shared source fixes were needed. +- `git diff --check`: passed. Test database fixtures were cleaned up and the temporary E2E server stopped. + +### CodeRabbit navigation follow-up + +- Reproduced the delayed hackathon/event values with three failing regression tests before the fix. Verified immediate values, cancellation/history restoration, and that scans wait for the committed station. +- Reproduced the settings arrow remaining at its hover position while navigation was held. The browser regression now verifies its pending translation and loading completion. +- After incorporating PR #539, 32 focused tests passed across five files, including admin access; all eight headed navigation browser tests passed again. Inspected the settings pending screenshot, kept outside the repository. +- Root format, lint, typecheck, React analysis, and the Blade production build passed. Lint reports existing repository warnings. Browser checks used an isolated local database containing synthetic fixtures. + +## Remaining scope and links + +No open implementation tasks. No deployment or real-service mutations were tested. + +- Issue: [#536 — Make Blade navigation respond immediately](https://github.com/KnightHacks/forge/issues/536). +- Review media: [screenshots and recordings](evidence/README.md), hosted as GitHub PR attachments. Removed the six media binaries from the branch; the PR retains embedded screenshots and video players. +- PR: [#537 — Add immediate loading feedback across Blade navigation](https://github.com/KnightHacks/forge/pull/537), authored by and assigned to `DGoel1602`. diff --git a/.forge/features/blade-responsive-navigation/test-cases.md b/.forge/features/blade-responsive-navigation/test-cases.md new file mode 100644 index 000000000..a3204d260 --- /dev/null +++ b/.forge/features/blade-responsive-navigation/test-cases.md @@ -0,0 +1,24 @@ +# Blade responsive navigation test cases + +## Navigation + +1. Click a member or admin link with a delayed route response: the request starts immediately, progress is visible and the navigation target highlights before the response completes. Existing shell remains usable when Next can retain it; a page/layout skeleton covers streamed waits. +2. Complete the response, navigate again, then use back/forward: correct URL and current navigation return, with no stuck dimming or loading state. +3. Navigate rapidly to a second destination: newest destination wins and feedback clears when navigation settles. +4. Close a navigation drawer while navigating: progress survives the drawer unmount. +5. Cancel navigation using the unsaved-settings guard or a link callback: no request or progress. Modified clicks, downloads and same-page anchors retain native behavior. +6. Navigate via a button, URL filter or refresh: immediate shared feedback, preserved replace/push and scroll behavior. +7. A failed route resolves to its existing error UI and clears navigation feedback. + +## Views and appearance + +8. Change a URL-backed tab/selector: chosen value responds immediately, data remains correctly associated with its view, and back/forward restores URL state. +9. Open a route while its layout waits: Blade-branded skeleton fits desktop and 320px without horizontal overflow. +10. Submit a form search or issue filter: retain the application shell, show progress before the server responds, preserve search parameters and close the filter dialog immediately. +11. Reduced-motion mode keeps a visible static progress indicator and skeletons without pulsing. Status is accessible without relying on motion. +12. On hackathon check-in, changing either selector shows the chosen value before the URL commits. Changing hackathon clears and disables the old event selection while pending; scans cannot submit against the previous station. Cancellation and history restore URL-selected values. +13. The profile settings Dashboard link moves its back arrow immediately while the route response is held, then clears loading after arrival. + +## Placement + +Regression tests in Blade's loading test directory; high-value delayed route tests in Blade's existing Playwright suite. Existing mutation and unsaved-settings tests protect behavior outside navigation feedback. diff --git a/apps/blade/src/app/_components/admin/alumni/alumni-bulletin-admin.tsx b/apps/blade/src/app/_components/admin/alumni/alumni-bulletin-admin.tsx index 40d1af33f..1d7313a87 100644 --- a/apps/blade/src/app/_components/admin/alumni/alumni-bulletin-admin.tsx +++ b/apps/blade/src/app/_components/admin/alumni/alumni-bulletin-admin.tsx @@ -1,7 +1,6 @@ "use client"; import { startTransition } from "react"; -import { useRouter } from "next/navigation"; import type { RouterOutputs } from "@forge/api"; import { toast } from "@forge/ui/toast"; @@ -10,6 +9,7 @@ import type { AlumniBulletinWorkspacePost, BulletinSaveHandler, } from "./alumni-bulletin-workspace"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; import { AlumniBulletinWorkspace } from "./alumni-bulletin-workspace"; diff --git a/apps/blade/src/app/_components/admin/analytics/analytics-dashboard.tsx b/apps/blade/src/app/_components/admin/analytics/analytics-dashboard.tsx index 444b558ed..56d7d35d5 100644 --- a/apps/blade/src/app/_components/admin/analytics/analytics-dashboard.tsx +++ b/apps/blade/src/app/_components/admin/analytics/analytics-dashboard.tsx @@ -2,8 +2,6 @@ import type { ReactNode } from "react"; import { useCallback, useMemo, useState, useTransition } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { ArrowDownRight, ArrowUpRight, @@ -80,6 +78,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { api } from "~/trpc/react"; import { diff --git a/apps/blade/src/app/_components/admin/analytics/hackathon-analytics-dashboard.tsx b/apps/blade/src/app/_components/admin/analytics/hackathon-analytics-dashboard.tsx index 04f98ce27..3dfd348df 100644 --- a/apps/blade/src/app/_components/admin/analytics/hackathon-analytics-dashboard.tsx +++ b/apps/blade/src/app/_components/admin/analytics/hackathon-analytics-dashboard.tsx @@ -2,8 +2,6 @@ import type { ReactNode } from "react"; import { useMemo, useState, useTransition } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { Activity, ArrowRight, @@ -73,6 +71,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; import { AnalyticsMetricCard as MetricCard, diff --git a/apps/blade/src/app/_components/admin/analytics/team-performance-analytics-dashboard.tsx b/apps/blade/src/app/_components/admin/analytics/team-performance-analytics-dashboard.tsx index 04fd77275..7a28e20ed 100644 --- a/apps/blade/src/app/_components/admin/analytics/team-performance-analytics-dashboard.tsx +++ b/apps/blade/src/app/_components/admin/analytics/team-performance-analytics-dashboard.tsx @@ -1,8 +1,6 @@ "use client"; import { useMemo, useState, useTransition } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { CalendarCheck2, Flame, @@ -39,6 +37,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { formatClubDateTime } from "~/lib/dates"; import { diff --git a/apps/blade/src/app/_components/admin/companies/company-admin-dashboard.tsx b/apps/blade/src/app/_components/admin/companies/company-admin-dashboard.tsx index f32d5e959..ac97f6b88 100644 --- a/apps/blade/src/app/_components/admin/companies/company-admin-dashboard.tsx +++ b/apps/blade/src/app/_components/admin/companies/company-admin-dashboard.tsx @@ -1,7 +1,6 @@ "use client"; import { useMemo, useState } from "react"; -import Link from "next/link"; import { ArrowUpRight, Building2, CheckCircle2, Search } from "lucide-react"; import type { RouterOutputs } from "@forge/api"; @@ -16,6 +15,7 @@ import { AdminPageHeader, adminPageStackClassName, } from "~/app/_components/shared/admin-page"; +import { RouteTransitionLink as Link } from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { CompanyAdminMark } from "./company-admin-mark"; diff --git a/apps/blade/src/app/_components/admin/companies/company-admin-detail.tsx b/apps/blade/src/app/_components/admin/companies/company-admin-detail.tsx index 14a75db30..5a60212dc 100644 --- a/apps/blade/src/app/_components/admin/companies/company-admin-detail.tsx +++ b/apps/blade/src/app/_components/admin/companies/company-admin-detail.tsx @@ -1,8 +1,6 @@ "use client"; import { useMemo, useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { ArrowLeft, ArrowUpRight, @@ -50,6 +48,10 @@ import { } from "@forge/validators"; import { adminPageClassName } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { formatUtcShortMonth } from "~/lib/dates"; import { getGuildCompanyUrl } from "~/lib/guild-urls"; diff --git a/apps/blade/src/app/_components/admin/email/email-portal-admin.tsx b/apps/blade/src/app/_components/admin/email/email-portal-admin.tsx index bc95bfe41..898bda09b 100644 --- a/apps/blade/src/app/_components/admin/email/email-portal-admin.tsx +++ b/apps/blade/src/app/_components/admin/email/email-portal-admin.tsx @@ -1,7 +1,6 @@ "use client"; import { startTransition, useCallback, useState } from "react"; -import { useRouter } from "next/navigation"; import type { RouterOutputs } from "@forge/api"; import { toast } from "@forge/ui/toast"; @@ -12,6 +11,7 @@ import type { EmailPortalTab, TemplateEditorSeed, } from "./email-portal-workspace"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; import { EmailPortalWorkspace } from "./email-portal-workspace"; diff --git a/apps/blade/src/app/_components/admin/email/email-portal-workspace.tsx b/apps/blade/src/app/_components/admin/email/email-portal-workspace.tsx index c91b271d1..0983c3fb6 100644 --- a/apps/blade/src/app/_components/admin/email/email-portal-workspace.tsx +++ b/apps/blade/src/app/_components/admin/email/email-portal-workspace.tsx @@ -3,7 +3,6 @@ import type { LucideIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import dynamic from "next/dynamic"; -import { useRouter } from "next/navigation"; import { Archive, CalendarClock, @@ -61,6 +60,7 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { formatClubDateTime, formatClubTime } from "~/lib/dates"; import { diff --git a/apps/blade/src/app/_components/admin/events/event-admin-dashboard.tsx b/apps/blade/src/app/_components/admin/events/event-admin-dashboard.tsx index a09c35aa7..674d49cfb 100644 --- a/apps/blade/src/app/_components/admin/events/event-admin-dashboard.tsx +++ b/apps/blade/src/app/_components/admin/events/event-admin-dashboard.tsx @@ -1,8 +1,6 @@ "use client"; import { useEffect, useRef, useState, useTransition } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { ArrowDownAZ, ArrowDownUp, @@ -36,6 +34,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { clubDateTimeInput, diff --git a/apps/blade/src/app/_components/admin/events/event-feedback-template-editor.tsx b/apps/blade/src/app/_components/admin/events/event-feedback-template-editor.tsx index 7a4d687a2..ac9e99489 100644 --- a/apps/blade/src/app/_components/admin/events/event-feedback-template-editor.tsx +++ b/apps/blade/src/app/_components/admin/events/event-feedback-template-editor.tsx @@ -1,7 +1,6 @@ "use client"; import { useEffect, useState } from "react"; -import Link from "next/link"; import { ArrowLeft, LockKeyhole, @@ -21,6 +20,7 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { RouteTransitionLink as Link } from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { api } from "~/trpc/react"; diff --git a/apps/blade/src/app/_components/admin/events/event-workspace-sections.tsx b/apps/blade/src/app/_components/admin/events/event-workspace-sections.tsx index 4bafeb358..02869a4da 100644 --- a/apps/blade/src/app/_components/admin/events/event-workspace-sections.tsx +++ b/apps/blade/src/app/_components/admin/events/event-workspace-sections.tsx @@ -1,8 +1,9 @@ import type { LucideIcon } from "lucide-react"; -import Link from "next/link"; import { cn } from "@forge/ui"; +import { RouteTransitionLink as Link } from "~/app/_components/shared/route-transition-link"; + export interface EventWorkspaceSection { href: string; icon: LucideIcon; diff --git a/apps/blade/src/app/_components/admin/forms/admin-form-builder.tsx b/apps/blade/src/app/_components/admin/forms/admin-form-builder.tsx index 04b7f7106..3717f28ee 100644 --- a/apps/blade/src/app/_components/admin/forms/admin-form-builder.tsx +++ b/apps/blade/src/app/_components/admin/forms/admin-form-builder.tsx @@ -1,8 +1,7 @@ "use client"; import { useEffect, useReducer, useState, useTransition } from "react"; -import Link from "next/link"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useSearchParams } from "next/navigation"; import { KeyboardSensor, PointerSensor, @@ -36,6 +35,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { api } from "~/trpc/react"; import { FormAvailabilityDialog } from "./form-availability-dialog"; diff --git a/apps/blade/src/app/_components/admin/forms/admin-forms-dashboard.tsx b/apps/blade/src/app/_components/admin/forms/admin-forms-dashboard.tsx index bf2d6ae29..9e5e933f1 100644 --- a/apps/blade/src/app/_components/admin/forms/admin-forms-dashboard.tsx +++ b/apps/blade/src/app/_components/admin/forms/admin-forms-dashboard.tsx @@ -1,4 +1,3 @@ -import Link from "next/link"; import { Archive, FileText, @@ -17,6 +16,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + RouteSearchForm, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { AdminFormsSectionSelect } from "./admin-forms-section-select"; @@ -191,9 +194,8 @@ export function AdminFormsDashboard({ className="overflow-hidden rounded-lg border border-white/10 bg-card/95 shadow-2xl shadow-black/25" >
-
{input.view === "archive" && ( @@ -221,7 +223,7 @@ export function AdminFormsDashboard({ > Search -
+
{!access.canRead && !access.isOfficer ? ( diff --git a/apps/blade/src/app/_components/admin/forms/admin-forms-section-select.tsx b/apps/blade/src/app/_components/admin/forms/admin-forms-section-select.tsx index 1894ec509..fe654226d 100644 --- a/apps/blade/src/app/_components/admin/forms/admin-forms-section-select.tsx +++ b/apps/blade/src/app/_components/admin/forms/admin-forms-section-select.tsx @@ -1,6 +1,9 @@ "use client"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useOptimistic, useTransition } from "react"; +import { usePathname, useSearchParams } from "next/navigation"; + +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; export function sectionSelectionHref( pathname: string, @@ -25,6 +28,8 @@ export function AdminFormsSectionSelect({ const pathname = usePathname(); const router = useRouter(); const searchParams = useSearchParams(); + const [sectionId, setSectionId] = useOptimistic(selectedSectionId ?? ""); + const [pending, startTransition] = useTransition(); return (
@@ -37,16 +42,21 @@ export function AdminFormsSectionSelect({ data-auto-swap="query-param" id="form-section" name="section" - onChange={(event) => - router.replace( - sectionSelectionHref( - pathname, - searchParams.toString(), - event.target.value, - ), - ) - } - value={selectedSectionId ?? ""} + aria-busy={pending} + onChange={(event) => { + const nextSectionId = event.target.value; + startTransition(() => { + setSectionId(nextSectionId); + router.replace( + sectionSelectionHref( + pathname, + searchParams.toString(), + nextSectionId, + ), + ); + }); + }} + value={sectionId} > {sections.map((section) => ( diff --git a/apps/blade/src/app/_components/admin/forms/form-responses-dashboard.tsx b/apps/blade/src/app/_components/admin/forms/form-responses-dashboard.tsx index 06eae92ea..437997813 100644 --- a/apps/blade/src/app/_components/admin/forms/form-responses-dashboard.tsx +++ b/apps/blade/src/app/_components/admin/forms/form-responses-dashboard.tsx @@ -1,8 +1,14 @@ "use client"; -import { useMemo, useState, useSyncExternalStore, useTransition } from "react"; -import Link from "next/link"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { + startTransition, + useMemo, + useOptimistic, + useState, + useSyncExternalStore, + useTransition, +} from "react"; +import { usePathname, useSearchParams } from "next/navigation"; import { ArrowLeft, BarChart3, @@ -60,6 +66,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { formatClubDateTime } from "~/lib/dates"; import { api } from "~/trpc/react"; @@ -916,10 +926,11 @@ export function FormResponsesDashboard({ const [retryingId, setRetryingId] = useState(null); const searchParams = useSearchParams(); const requestedView = searchParams.get("view"); - const activeView = + const resolvedView = requestedView === "responses" || requestedView === "delivery" ? requestedView : "analytics"; + const [activeView, setActiveView] = useOptimistic(resolvedView); const exportQuery = api.forms.exportResponses.useQuery( { formId }, { enabled: false }, @@ -996,11 +1007,14 @@ export function FormResponsesDashboard({

) : ( - router.replace(workspaceHref(pathname, searchParams, view), { - scroll: false, - }) - } + onValueChange={(view) => { + startTransition(() => { + setActiveView(view); + router.replace(workspaceHref(pathname, searchParams, view), { + scroll: false, + }); + }); + }} value={activeView} > diff --git a/apps/blade/src/app/_components/admin/forms/form-sections-manager.tsx b/apps/blade/src/app/_components/admin/forms/form-sections-manager.tsx index 622e04d12..1fb7bf3cf 100644 --- a/apps/blade/src/app/_components/admin/forms/form-sections-manager.tsx +++ b/apps/blade/src/app/_components/admin/forms/form-sections-manager.tsx @@ -1,8 +1,6 @@ "use client"; import { startTransition, useMemo, useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { ArrowLeft, Plus, Save, Search, Settings2 } from "lucide-react"; import type { RouterOutputs } from "@forge/api"; @@ -25,6 +23,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { api } from "~/trpc/react"; diff --git a/apps/blade/src/app/_components/admin/hackathon-events/hackathon-check-in-workspace.tsx b/apps/blade/src/app/_components/admin/hackathon-events/hackathon-check-in-workspace.tsx index 9d7683de2..fceaeea62 100644 --- a/apps/blade/src/app/_components/admin/hackathon-events/hackathon-check-in-workspace.tsx +++ b/apps/blade/src/app/_components/admin/hackathon-events/hackathon-check-in-workspace.tsx @@ -1,8 +1,16 @@ "use client"; import type { SetStateAction } from "react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { + useCallback, + useEffect, + useMemo, + useOptimistic, + useRef, + useState, + useTransition, +} from "react"; +import { useSearchParams } from "next/navigation"; import { Scanner } from "@yudiel/react-qr-scanner"; import { AlertTriangle, @@ -39,6 +47,7 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { formatClubDateTime } from "~/lib/dates"; import { api } from "~/trpc/react"; @@ -306,6 +315,11 @@ export function HackathonCheckInWorkspace() { const requestedEventId = searchParams.get("event"); const selectedEvent = eventData.data?.events.find(({ id }) => id === requestedEventId) ?? null; + const [selection, setSelection] = useOptimistic({ + hackathonId: selectedHackathon?.id ?? "", + eventId: selectedEvent?.id ?? "", + }); + const [selectionPending, startSelectionTransition] = useTransition(); const history = api.hackathonEvent.listCheckInHistory.useInfiniteQuery( { hackathonId: selectedHackathon?.id ?? "", limit: 25 }, { @@ -357,6 +371,7 @@ export function HackathonCheckInWorkspace() { const primary = selectedEvent?.purpose === "primary_check_in"; const stationReady = + !selectionPending && selectedEvent !== null && selectedEvent.ready && (!primary || eventData.data?.configReady === true) && @@ -396,7 +411,10 @@ export function HackathonCheckInWorkspace() { params.set("hackathon", hackathonId); if (eventId) params.set("event", eventId); else params.delete("event"); - router.replace(`/admin/hackathon-check-in?${params.toString()}`); + startSelectionTransition(() => { + setSelection({ hackathonId, eventId: eventId ?? "" }); + router.replace(`/admin/hackathon-check-in?${params.toString()}`); + }); } function present(value: unknown) { @@ -503,11 +521,12 @@ export function HackathonCheckInWorkspace() {
{ - if (selectedHackathon) + if (selection.hackathonId) replaceSelection( - selectedHackathon.id, + selection.hackathonId, event.target.value, ); }} - value={selectedEvent?.id ?? ""} + value={selection.eventId} > {eventData.data?.events.map((event) => ( @@ -683,7 +706,8 @@ export function HackathonCheckInWorkspace() { ) } onScan={(codes) => { - if (resultOpen || checkIn.isPending) return; + if (!stationReady || resultOpen || checkIn.isPending) + return; const payload = claimCheckInQrPayload( scanning, handledQrPayloads.current, diff --git a/apps/blade/src/app/_components/admin/hackathon-events/hackathon-events-workspace.tsx b/apps/blade/src/app/_components/admin/hackathon-events/hackathon-events-workspace.tsx index 69ab7e435..841fad87c 100644 --- a/apps/blade/src/app/_components/admin/hackathon-events/hackathon-events-workspace.tsx +++ b/apps/blade/src/app/_components/admin/hackathon-events/hackathon-events-workspace.tsx @@ -1,8 +1,7 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; -import Link from "next/link"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { AlertTriangle, ArrowDownAZ, @@ -61,6 +60,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { formatClubDateTime } from "~/lib/dates"; import { api } from "~/trpc/react"; diff --git a/apps/blade/src/app/_components/admin/hackathon/hackathon-detail.tsx b/apps/blade/src/app/_components/admin/hackathon/hackathon-detail.tsx index 81f8efde3..3e38bf9b5 100644 --- a/apps/blade/src/app/_components/admin/hackathon/hackathon-detail.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/hackathon-detail.tsx @@ -1,8 +1,6 @@ "use client"; import { useState, useTransition } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { ArrowLeft, CalendarRange, @@ -38,6 +36,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { api } from "~/trpc/react"; import { AgreementSection } from "./agreement-section"; diff --git a/apps/blade/src/app/_components/admin/hackathon/hackathon-list.tsx b/apps/blade/src/app/_components/admin/hackathon/hackathon-list.tsx index fa9f90ba2..43723ecc8 100644 --- a/apps/blade/src/app/_components/admin/hackathon/hackathon-list.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/hackathon-list.tsx @@ -1,8 +1,6 @@ "use client"; import { useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { CalendarRange, Plus, Swords, TriangleAlert } from "lucide-react"; import type { RouterOutputs } from "@forge/api"; @@ -20,6 +18,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { HackathonFormDialog } from "./hackathon-form-dialog"; import { formatHackathonDate } from "./hackathon-formatting"; diff --git a/apps/blade/src/app/_components/admin/hackathon/hackers/hacker-roster.tsx b/apps/blade/src/app/_components/admin/hackathon/hackers/hacker-roster.tsx index f39ca159d..18f5af767 100644 --- a/apps/blade/src/app/_components/admin/hackathon/hackers/hacker-roster.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/hackers/hacker-roster.tsx @@ -1,7 +1,6 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import Link from "next/link"; import { CalendarOff, Loader2, @@ -24,6 +23,7 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { RouteTransitionLink as Link } from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { api } from "~/trpc/react"; import { BulkConfirmDialog } from "./bulk-confirm-dialog"; diff --git a/apps/blade/src/app/_components/admin/hackathon/hackers/use-roster-url-state.ts b/apps/blade/src/app/_components/admin/hackathon/hackers/use-roster-url-state.ts index a2804e089..9f07488c2 100644 --- a/apps/blade/src/app/_components/admin/hackathon/hackers/use-roster-url-state.ts +++ b/apps/blade/src/app/_components/admin/hackathon/hackers/use-roster-url-state.ts @@ -1,11 +1,13 @@ "use client"; import { useCallback, useEffect, useMemo, useRef, useTransition } from "react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useSearchParams } from "next/navigation"; import type { HackerRosterFilter } from "@forge/validators"; import { hackerRosterFilterSchema } from "@forge/validators"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; + /** * Filter state, the selected hackathon, and show-all live in the URL, like the * rest of the admin surface. diff --git a/apps/blade/src/app/_components/admin/issues/issue-create-dialog.tsx b/apps/blade/src/app/_components/admin/issues/issue-create-dialog.tsx index 4092c1a43..ca4faf3de 100644 --- a/apps/blade/src/app/_components/admin/issues/issue-create-dialog.tsx +++ b/apps/blade/src/app/_components/admin/issues/issue-create-dialog.tsx @@ -1,7 +1,6 @@ "use client"; import { useMemo, useState } from "react"; -import { useRouter } from "next/navigation"; import { LayoutTemplate, Loader2, Sparkles } from "lucide-react"; import { Button } from "@forge/ui/button"; @@ -27,6 +26,7 @@ import type { IssueWorkspaceData, IssueWorkspaceItem, } from "./types"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { localNewYorkDateTime } from "~/lib/dates"; import { api } from "~/trpc/react"; import { EventFormDialog } from "../events/event-form-dialog"; diff --git a/apps/blade/src/app/_components/admin/issues/issue-detail.tsx b/apps/blade/src/app/_components/admin/issues/issue-detail.tsx index 7d80ec0f3..2de547888 100644 --- a/apps/blade/src/app/_components/admin/issues/issue-detail.tsx +++ b/apps/blade/src/app/_components/admin/issues/issue-detail.tsx @@ -1,8 +1,6 @@ "use client"; import { useEffect, useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { Archive, ArrowLeft, @@ -42,6 +40,10 @@ import { AdminPageHeader, adminPageStackClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { clubWallClock, diff --git a/apps/blade/src/app/_components/admin/issues/issue-filters.tsx b/apps/blade/src/app/_components/admin/issues/issue-filters.tsx index ec652fd85..9f0233177 100644 --- a/apps/blade/src/app/_components/admin/issues/issue-filters.tsx +++ b/apps/blade/src/app/_components/admin/issues/issue-filters.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import Link from "next/link"; import { Search } from "lucide-react"; import { ISSUE } from "@forge/consts"; @@ -13,13 +12,19 @@ import { Label } from "@forge/ui/label"; import type { IssueSearchInput } from "./params"; import type { IssueWorkspaceData } from "./types"; +import { + RouteTransitionLink as Link, + RouteSearchForm, +} from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; export function IssueFilters({ input, teams, + onApply, }: { input: IssueSearchInput; + onApply: () => void; teams: IssueWorkspaceData["teams"]; }) { const [selectedTeam, setSelectedTeam] = useState(input.teamIds[0] ?? ""); @@ -28,9 +33,9 @@ export function IssueFilters({ { enabled: Boolean(selectedTeam) }, ); return ( -
{input.calendarMode !== "month" && ( @@ -164,12 +169,14 @@ export function IssueFilters({ -
+ ); } diff --git a/apps/blade/src/app/_components/admin/issues/issue-views.tsx b/apps/blade/src/app/_components/admin/issues/issue-views.tsx index b985aed35..0cf1c871c 100644 --- a/apps/blade/src/app/_components/admin/issues/issue-views.tsx +++ b/apps/blade/src/app/_components/admin/issues/issue-views.tsx @@ -1,8 +1,6 @@ "use client"; import { useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { CalendarClock, Check, @@ -24,6 +22,10 @@ import { import { toast } from "@forge/ui/toast"; import type { IssueWorkspaceItem } from "./types"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { clubDateKey, formatClubDayTime, formatUtcFullDate } from "~/lib/dates"; import { api } from "~/trpc/react"; diff --git a/apps/blade/src/app/_components/admin/issues/issue-workspace-dock.tsx b/apps/blade/src/app/_components/admin/issues/issue-workspace-dock.tsx index 32d4f6c95..9ce69327b 100644 --- a/apps/blade/src/app/_components/admin/issues/issue-workspace-dock.tsx +++ b/apps/blade/src/app/_components/admin/issues/issue-workspace-dock.tsx @@ -1,7 +1,6 @@ "use client"; import type { useRouter } from "next/navigation"; -import Link from "next/link"; import { Archive, ArrowLeft, @@ -25,6 +24,7 @@ import type { IssueWorkspaceView, } from "./issue-workspace"; import type { IssueSearchInput } from "./params"; +import { RouteTransitionLink as Link } from "~/app/_components/shared/route-transition-link"; import { issueSearchHref, issueViewHref } from "./issue-view-href"; import { parseIssueSearchParams } from "./params"; diff --git a/apps/blade/src/app/_components/admin/issues/issue-workspace.tsx b/apps/blade/src/app/_components/admin/issues/issue-workspace.tsx index 14d9c376e..b3705f0e7 100644 --- a/apps/blade/src/app/_components/admin/issues/issue-workspace.tsx +++ b/apps/blade/src/app/_components/admin/issues/issue-workspace.tsx @@ -1,8 +1,6 @@ "use client"; import { useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { Archive, ListTodo } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@forge/ui/alert"; @@ -22,6 +20,10 @@ import { AdminPageHeader, adminPageStackClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { formatIssueCalendarPeriod, @@ -231,7 +233,11 @@ export function IssueWorkspace({ Narrow the current workspace without moving the work surface. - + setOverlay("none")} + /> diff --git a/apps/blade/src/app/_components/admin/issues/template-catalog-dialog.tsx b/apps/blade/src/app/_components/admin/issues/template-catalog-dialog.tsx index b89de0016..bfc2c9d4c 100644 --- a/apps/blade/src/app/_components/admin/issues/template-catalog-dialog.tsx +++ b/apps/blade/src/app/_components/admin/issues/template-catalog-dialog.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import { useRouter } from "next/navigation"; import { AlertTriangle, Archive, @@ -32,6 +31,7 @@ import { toast } from "@forge/ui/toast"; import { issueTemplateCreateSchema } from "@forge/validators"; import type { IssueTeamChoice, IssueTemplateChoice } from "./types"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; interface TemplateNode { diff --git a/apps/blade/src/app/_components/admin/logs/admin-logs-dashboard.tsx b/apps/blade/src/app/_components/admin/logs/admin-logs-dashboard.tsx index 02ed05a9d..d8151edc4 100644 --- a/apps/blade/src/app/_components/admin/logs/admin-logs-dashboard.tsx +++ b/apps/blade/src/app/_components/admin/logs/admin-logs-dashboard.tsx @@ -2,7 +2,7 @@ import type { CSSProperties } from "react"; import { useDeferredValue, useEffect, useMemo, useRef, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { AlertTriangle, ChevronLeft, @@ -54,6 +54,7 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { formatClubDateTime, localNewYorkDateTime } from "~/lib/dates"; import { api } from "~/trpc/react"; diff --git a/apps/blade/src/app/_components/admin/members/member-admin-dashboard.tsx b/apps/blade/src/app/_components/admin/members/member-admin-dashboard.tsx index 3df8d419e..a2dab7ede 100644 --- a/apps/blade/src/app/_components/admin/members/member-admin-dashboard.tsx +++ b/apps/blade/src/app/_components/admin/members/member-admin-dashboard.tsx @@ -7,7 +7,6 @@ import { useState, useTransition, } from "react"; -import { useRouter } from "next/navigation"; import { ArrowDownAZ, ArrowDownUp, @@ -50,6 +49,7 @@ import { AdminPageHeader, adminPageStackClassName, } from "~/app/_components/shared/admin-page"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { formatUtcDate } from "~/lib/dates"; import { api } from "~/trpc/react"; diff --git a/apps/blade/src/app/_components/admin/roles/admin-config-console.tsx b/apps/blade/src/app/_components/admin/roles/admin-config-console.tsx index cd454334d..2ebc83d8a 100644 --- a/apps/blade/src/app/_components/admin/roles/admin-config-console.tsx +++ b/apps/blade/src/app/_components/admin/roles/admin-config-console.tsx @@ -1,8 +1,6 @@ "use client"; import { startTransition } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { ArrowLeft, SlidersHorizontal } from "lucide-react"; import type { RouterOutputs } from "@forge/api"; @@ -12,6 +10,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { ClubClassificationSection } from "./club-classification-section"; import { DiscordConfigSection } from "./discord-config-section"; diff --git a/apps/blade/src/app/_components/admin/roles/role-detail-dialog.tsx b/apps/blade/src/app/_components/admin/roles/role-detail-dialog.tsx index 9abbf849e..0092b3fe9 100644 --- a/apps/blade/src/app/_components/admin/roles/role-detail-dialog.tsx +++ b/apps/blade/src/app/_components/admin/roles/role-detail-dialog.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import Link from "next/link"; import { AlertTriangle, BellRing, @@ -34,6 +33,7 @@ import { Switch } from "@forge/ui/switch"; import { toast } from "@forge/ui/toast"; import { ROLE_UNLINK_CONFIRMATION } from "@forge/validators"; +import { RouteTransitionLink as Link } from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; import { RolePermissionEditor } from "./role-permission-editor"; diff --git a/apps/blade/src/app/_components/admin/roles/role-management-dashboard.tsx b/apps/blade/src/app/_components/admin/roles/role-management-dashboard.tsx index 19daf9df7..beb85ba88 100644 --- a/apps/blade/src/app/_components/admin/roles/role-management-dashboard.tsx +++ b/apps/blade/src/app/_components/admin/roles/role-management-dashboard.tsx @@ -1,8 +1,6 @@ "use client"; import { useMemo, useState, useTransition } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; import { AlertTriangle, Check, @@ -73,6 +71,10 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { + RouteTransitionLink as Link, + useNavigationRouter as useRouter, +} from "~/app/_components/shared/route-transition-link"; import { ADMIN_PAGE_EYEBROWS } from "~/consts/admin-page-eyebrows"; import { api } from "~/trpc/react"; import { CreateRoleDialog } from "./create-role-dialog"; diff --git a/apps/blade/src/app/_components/auth/sign-out-button.tsx b/apps/blade/src/app/_components/auth/sign-out-button.tsx index 28a5fbd75..490212737 100644 --- a/apps/blade/src/app/_components/auth/sign-out-button.tsx +++ b/apps/blade/src/app/_components/auth/sign-out-button.tsx @@ -1,11 +1,11 @@ "use client"; import { useState } from "react"; -import { useRouter } from "next/navigation"; import { LogOut } from "lucide-react"; import { Button } from "@forge/ui/button"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { signOutFromBlade } from "./sign-out-flow"; export function SignOutButton() { diff --git a/apps/blade/src/app/_components/forms/generic-form-respondent.tsx b/apps/blade/src/app/_components/forms/generic-form-respondent.tsx index e09996c67..dfb310342 100644 --- a/apps/blade/src/app/_components/forms/generic-form-respondent.tsx +++ b/apps/blade/src/app/_components/forms/generic-form-respondent.tsx @@ -1,5 +1,4 @@ import type { ReactNode } from "react"; -import Link from "next/link"; import { Archive, ArrowLeft, @@ -14,6 +13,7 @@ import { Button } from "@forge/ui/button"; import { Card, CardContent, CardHeader } from "@forge/ui/card"; import { Skeleton } from "@forge/ui/skeleton"; +import { RouteTransitionLink as Link } from "~/app/_components/shared/route-transition-link"; import { formatClubLongDate } from "~/lib/dates"; import { FormBanner } from "./form-banner"; import { FormResponseValue } from "./form-response-value"; diff --git a/apps/blade/src/app/_components/judging/evaluation-dialog.tsx b/apps/blade/src/app/_components/judging/evaluation-dialog.tsx index 4b138573b..bb442e455 100644 --- a/apps/blade/src/app/_components/judging/evaluation-dialog.tsx +++ b/apps/blade/src/app/_components/judging/evaluation-dialog.tsx @@ -1,7 +1,6 @@ "use client"; import { useMemo, useState } from "react"; -import { useRouter } from "next/navigation"; import { Eye, LockKeyhole, Save } from "lucide-react"; import type { RouterOutputs } from "@forge/api"; @@ -21,6 +20,7 @@ import { RadioGroup, RadioGroupItem } from "@forge/ui/radio-group"; import { Textarea } from "@forge/ui/textarea"; import { toast } from "@forge/ui/toast"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; type Workspace = RouterOutputs["judging"]["getWorkspace"]; diff --git a/apps/blade/src/app/_components/judging/guest-name-gate.tsx b/apps/blade/src/app/_components/judging/guest-name-gate.tsx index 35798c90b..8edb4521b 100644 --- a/apps/blade/src/app/_components/judging/guest-name-gate.tsx +++ b/apps/blade/src/app/_components/judging/guest-name-gate.tsx @@ -2,7 +2,6 @@ import type { FormEvent } from "react"; import { useState } from "react"; -import { useRouter } from "next/navigation"; import { BadgeCheck } from "lucide-react"; import { Button } from "@forge/ui/button"; @@ -18,6 +17,7 @@ import { Label } from "@forge/ui/label"; import { toast } from "@forge/ui/toast"; import { guestJudgeNameSchema } from "@forge/validators"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; export function GuestNameGate() { diff --git a/apps/blade/src/app/_components/judging/judge-deliberation.tsx b/apps/blade/src/app/_components/judging/judge-deliberation.tsx index 5a596f763..f7d2779b9 100644 --- a/apps/blade/src/app/_components/judging/judge-deliberation.tsx +++ b/apps/blade/src/app/_components/judging/judge-deliberation.tsx @@ -2,7 +2,6 @@ import type { DragEndEvent } from "@dnd-kit/core"; import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; import { closestCenter, DndContext, @@ -42,6 +41,7 @@ import { Input } from "@forge/ui/input"; import { ResponsiveComboBox } from "@forge/ui/responsive-combo-box"; import { toast } from "@forge/ui/toast"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; type Workspace = RouterOutputs["judging"]["getWorkspace"]; diff --git a/apps/blade/src/app/_components/judging/judging-configuration-panel.tsx b/apps/blade/src/app/_components/judging/judging-configuration-panel.tsx index c4d8de98e..9fad05fd3 100644 --- a/apps/blade/src/app/_components/judging/judging-configuration-panel.tsx +++ b/apps/blade/src/app/_components/judging/judging-configuration-panel.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import { useRouter } from "next/navigation"; import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react"; import type { RouterOutputs } from "@forge/api"; @@ -14,6 +13,7 @@ import { Switch } from "@forge/ui/switch"; import { Textarea } from "@forge/ui/textarea"; import { toast } from "@forge/ui/toast"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; type ControlData = RouterOutputs["judging"]["listAdmin"]; diff --git a/apps/blade/src/app/_components/judging/judging-control-panel.tsx b/apps/blade/src/app/_components/judging/judging-control-panel.tsx index 686701377..ff9b7ec2a 100644 --- a/apps/blade/src/app/_components/judging/judging-control-panel.tsx +++ b/apps/blade/src/app/_components/judging/judging-control-panel.tsx @@ -3,7 +3,7 @@ import type { FormEvent } from "react"; import { useState, useTransition } from "react"; import Image from "next/image"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useSearchParams } from "next/navigation"; import { Archive, ArrowDown, @@ -41,6 +41,7 @@ import { AdminPageHeader, adminPageLayoutClassName, } from "~/app/_components/shared/admin-page"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { api } from "~/trpc/react"; type ControlData = RouterOutputs["judging"]["listAdmin"]; diff --git a/apps/blade/src/app/_components/judging/project-command-center.tsx b/apps/blade/src/app/_components/judging/project-command-center.tsx index d29ddb3d6..015ca7e07 100644 --- a/apps/blade/src/app/_components/judging/project-command-center.tsx +++ b/apps/blade/src/app/_components/judging/project-command-center.tsx @@ -1,13 +1,14 @@ "use client"; -import { useTransition } from "react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useOptimistic, useTransition } from "react"; +import { usePathname, useSearchParams } from "next/navigation"; import { ClipboardList } from "lucide-react"; import type { RouterOutputs } from "@forge/api"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@forge/ui/tabs"; import type { ProjectDirectoryInput } from "../projects/project-directory"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { AdminProjectWorkspace } from "../projects/admin-project-workspace"; import { AdminPageHeader, @@ -45,11 +46,16 @@ export function ProjectCommandCenter({ const searchParams = useSearchParams(); const [pending, startTransition] = useTransition(); + const [activeTab, setActiveTab] = useOptimistic(selectedTab); + function selectTab(tab: string) { const next = new URLSearchParams(searchParams.toString()); if (tab === "setup") next.delete("tab"); else next.set("tab", tab); - startTransition(() => router.replace(`${pathname}?${next.toString()}`)); + startTransition(() => { + setActiveTab(tab); + router.replace(`${pathname}?${next.toString()}`, { scroll: false }); + }); } function selectHackathon(hackathonId: string) { @@ -87,7 +93,7 @@ export function ProjectCommandCenter({ icon={ClipboardList} title="Project command center" /> - + Setup Projects diff --git a/apps/blade/src/app/_components/member/alumni-dashboard.tsx b/apps/blade/src/app/_components/member/alumni-dashboard.tsx index 45071239d..ecabfd7e4 100644 --- a/apps/blade/src/app/_components/member/alumni-dashboard.tsx +++ b/apps/blade/src/app/_components/member/alumni-dashboard.tsx @@ -1,7 +1,6 @@ "use client"; import Image from "next/image"; -import Link from "next/link"; import { ArrowUpRight, BriefcaseBusiness, @@ -25,6 +24,7 @@ import { Button } from "@forge/ui/button"; import { Card, CardContent, CardHeader } from "@forge/ui/card"; import { MarkdownContent } from "@forge/ui/markdown-content"; +import { RouteTransitionLink as Link } from "~/app/_components/shared/route-transition-link"; import { MemberQRCodeDialog } from "./member-qr-code-dialog"; const alumniDiscordUrl = diff --git a/apps/blade/src/app/_components/member/dashboard-client.tsx b/apps/blade/src/app/_components/member/dashboard-client.tsx index c6aa83f4e..c1d7297f5 100644 --- a/apps/blade/src/app/_components/member/dashboard-client.tsx +++ b/apps/blade/src/app/_components/member/dashboard-client.tsx @@ -1,7 +1,6 @@ "use client"; import { startTransition, useState } from "react"; -import { useRouter } from "next/navigation"; import type { RouterOutputs } from "@forge/api"; import { cn } from "@forge/ui"; @@ -20,6 +19,7 @@ import { dashboardPanelClass, MemberDashboard, } from "~/app/_components/member/member-dashboard"; +import { useNavigationRouter as useRouter } from "~/app/_components/shared/route-transition-link"; import { useDebugLatency } from "~/hooks/use-debug-latency"; import { api } from "~/trpc/react"; diff --git a/apps/blade/src/app/_components/member/member-dashboard.tsx b/apps/blade/src/app/_components/member/member-dashboard.tsx index efa47a9c8..a9a583d1d 100644 --- a/apps/blade/src/app/_components/member/member-dashboard.tsx +++ b/apps/blade/src/app/_components/member/member-dashboard.tsx @@ -397,7 +397,7 @@ function GuildProfileCard({ member }: { member: CurrentMember }) { aria-label="Edit profile" >