From 8b7ec88a357b63f90005f159fbd5aa2449d73ac3 Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Wed, 18 Mar 2026 12:51:52 +0000 Subject: [PATCH 01/10] feat: add HideProvider with sensitive info toggle Adds a global Eye/EyeOff toggle button (fixed top-right) that masks all email fields across the admin dashboard when activated. - New HideProvider context + useHide hook - New maskEmail utility (e.g. "jo***@ex***.com") - HideToggle component wired into global Providers - privyLoginsColumns, sandboxesColumns, AccountReposList all respect isHidden Co-Authored-By: Claude Sonnet 4.6 --- components/HideToggle.tsx | 29 ++++++++++++ components/PrivyLogins/privyLoginsColumns.tsx | 16 +++++-- components/SandboxOrgs/AccountReposList.tsx | 14 +++++- components/Sandboxes/sandboxesColumns.tsx | 47 ++++++++++++++----- lib/maskEmail.ts | 22 +++++++++ providers/HideProvider.tsx | 29 ++++++++++++ providers/Providers.tsx | 9 +++- 7 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 components/HideToggle.tsx create mode 100644 lib/maskEmail.ts create mode 100644 providers/HideProvider.tsx diff --git a/components/HideToggle.tsx b/components/HideToggle.tsx new file mode 100644 index 0000000..182ca78 --- /dev/null +++ b/components/HideToggle.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Eye, EyeOff } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useHide } from "@/providers/HideProvider"; + +export function HideToggle() { + const { isHidden, toggle } = useHide(); + + return ( +
+ +
+ ); +} diff --git a/components/PrivyLogins/privyLoginsColumns.tsx b/components/PrivyLogins/privyLoginsColumns.tsx index b9dc1a4..53fb51a 100644 --- a/components/PrivyLogins/privyLoginsColumns.tsx +++ b/components/PrivyLogins/privyLoginsColumns.tsx @@ -2,17 +2,25 @@ import { type ColumnDef } from "@tanstack/react-table"; import { SortableHeader } from "@/components/SandboxOrgs/SortableHeader"; import { getEmail } from "@/lib/privy/getEmail"; import { getLastSeen } from "@/lib/privy/getLastSeen"; +import { useHide } from "@/providers/HideProvider"; +import { maskEmail } from "@/lib/maskEmail"; import type { PrivyUser } from "@/types/privy"; +function EmailCell({ getValue }: { getValue: () => string | null }) { + const { isHidden } = useHide(); + const email = getValue(); + if (!email) return No email; + return {isHidden ? maskEmail(email) : email}; +} + export const privyLoginsColumns: ColumnDef[] = [ { id: "email", accessorFn: (row) => getEmail(row), header: "Email", - cell: ({ getValue }) => { - const email = getValue(); - return email ?? No email; - }, + cell: ({ getValue }) => ( + getValue()} /> + ), }, { id: "created_at", diff --git a/components/SandboxOrgs/AccountReposList.tsx b/components/SandboxOrgs/AccountReposList.tsx index be4b64c..f846ecf 100644 --- a/components/SandboxOrgs/AccountReposList.tsx +++ b/components/SandboxOrgs/AccountReposList.tsx @@ -1,4 +1,8 @@ +"use client"; + import Link from "next/link"; +import { useHide } from "@/providers/HideProvider"; +import { maskEmail } from "@/lib/maskEmail"; import type { AccountRepo } from "@/types/sandbox"; interface AccountReposListProps { @@ -6,19 +10,25 @@ interface AccountReposListProps { } export function AccountReposList({ repos }: AccountReposListProps) { + const { isHidden } = useHide(); + if (repos.length === 0) { return ; } return (
    {repos.map(({ account_id, email }) => { - const displayLabel = email ?? account_id; + const displayLabel = email + ? isHidden + ? maskEmail(email) + : email + : account_id; return (
  • {displayLabel} diff --git a/components/Sandboxes/sandboxesColumns.tsx b/components/Sandboxes/sandboxesColumns.tsx index b72ba38..287388c 100644 --- a/components/Sandboxes/sandboxesColumns.tsx +++ b/components/Sandboxes/sandboxesColumns.tsx @@ -2,25 +2,46 @@ import { type ColumnDef } from "@tanstack/react-table"; import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; +import { useHide } from "@/providers/HideProvider"; +import { maskEmail } from "@/lib/maskEmail"; import type { AccountSandboxRow } from "@/types/sandbox"; +function AccountEmailCell({ + email, + accountId, +}: { + email: string | null; + accountId: string; +}) { + const { isHidden } = useHide(); + const displayEmail = email + ? isHidden + ? maskEmail(email) + : email + : null; + return ( + + {displayEmail ?? ( + {accountId} + )} + + ); +} + export const sandboxesColumns: ColumnDef[] = [ { accessorKey: "account_email", header: "Account Email", - cell: ({ row }) => { - const email = row.getValue("account_email"); - const accountId = row.original.account_id; - return ( - - {email ?? {accountId}} - - ); - }, + cell: ({ row }) => ( + ("account_email")} + accountId={row.original.account_id} + /> + ), }, { accessorKey: "total_sandboxes", diff --git a/lib/maskEmail.ts b/lib/maskEmail.ts new file mode 100644 index 0000000..afac0e2 --- /dev/null +++ b/lib/maskEmail.ts @@ -0,0 +1,22 @@ +/** + * Masks an email address so sensitive info is not exposed. + * e.g. "john.doe@example.com" → "jo***@ex***.com" + */ +export function maskEmail(email: string): string { + const atIndex = email.indexOf("@"); + if (atIndex === -1) return "***"; + + const local = email.slice(0, atIndex); + const domain = email.slice(atIndex + 1); + + const maskedLocal = local.slice(0, 2).padEnd(2, "*") + "***"; + + const dotIndex = domain.lastIndexOf("."); + if (dotIndex === -1) return `${maskedLocal}@***`; + + const domainName = domain.slice(0, dotIndex); + const tld = domain.slice(dotIndex); + const maskedDomain = domainName.slice(0, 2).padEnd(2, "*") + "***"; + + return `${maskedLocal}@${maskedDomain}${tld}`; +} diff --git a/providers/HideProvider.tsx b/providers/HideProvider.tsx new file mode 100644 index 0000000..9d30d5d --- /dev/null +++ b/providers/HideProvider.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { createContext, useContext, useState } from "react"; + +interface HideContextValue { + isHidden: boolean; + toggle: () => void; +} + +const HideContext = createContext({ + isHidden: false, + toggle: () => {}, +}); + +export function HideProvider({ children }: { children: React.ReactNode }) { + const [isHidden, setIsHidden] = useState(false); + + return ( + setIsHidden((h) => !h) }} + > + {children} + + ); +} + +export function useHide() { + return useContext(HideContext); +} diff --git a/providers/Providers.tsx b/providers/Providers.tsx index 0a5dce3..8bbadd0 100644 --- a/providers/Providers.tsx +++ b/providers/Providers.tsx @@ -2,13 +2,20 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import PrivyProvider from "./PrivyProvider"; +import { HideProvider } from "./HideProvider"; +import { HideToggle } from "@/components/HideToggle"; const queryClient = new QueryClient(); export default function Providers({ children }: { children: React.ReactNode }) { return ( - {children} + + + + {children} + + ); } From 14d7afd06c480adf0cf5e25ba24d5a29df6cc544 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 18 Mar 2026 08:48:15 -0500 Subject: [PATCH 02/10] fix: hide toggle only when authenticated Prevents the toggle from overlapping the sign-in button on the login page. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/HideToggle.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/HideToggle.tsx b/components/HideToggle.tsx index 182ca78..cc8b6b2 100644 --- a/components/HideToggle.tsx +++ b/components/HideToggle.tsx @@ -3,9 +3,13 @@ import { Eye, EyeOff } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useHide } from "@/providers/HideProvider"; +import { usePrivy } from "@privy-io/react-auth"; export function HideToggle() { const { isHidden, toggle } = useHide(); + const { ready, authenticated } = usePrivy(); + + if (!ready || !authenticated) return null; return (
    From f141e992ea22f1c3d0fb65e5672b6513a2dbd217 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 18 Mar 2026 10:41:24 -0500 Subject: [PATCH 03/10] refactor: move hide toggle into header, simplify to icon only - HideToggle is now icon-only (no button background, no label) - Moved from fixed position in Providers to LoginButton header (next to email and sign out) - Extracted EmailCell to its own component file (SRP) Co-Authored-By: Claude Opus 4.6 (1M context) --- components/HideToggle.tsx | 29 +++++-------------- components/Login/LoginButton.tsx | 2 ++ components/PrivyLogins/EmailCell.tsx | 15 ++++++++++ components/PrivyLogins/privyLoginsColumns.tsx | 10 +------ providers/Providers.tsx | 3 -- 5 files changed, 25 insertions(+), 34 deletions(-) create mode 100644 components/PrivyLogins/EmailCell.tsx diff --git a/components/HideToggle.tsx b/components/HideToggle.tsx index cc8b6b2..a168eaf 100644 --- a/components/HideToggle.tsx +++ b/components/HideToggle.tsx @@ -1,33 +1,18 @@ "use client"; import { Eye, EyeOff } from "lucide-react"; -import { Button } from "@/components/ui/button"; import { useHide } from "@/providers/HideProvider"; -import { usePrivy } from "@privy-io/react-auth"; export function HideToggle() { const { isHidden, toggle } = useHide(); - const { ready, authenticated } = usePrivy(); - - if (!ready || !authenticated) return null; return ( -
    - -
    + ); } diff --git a/components/Login/LoginButton.tsx b/components/Login/LoginButton.tsx index 0da9404..67c768e 100644 --- a/components/Login/LoginButton.tsx +++ b/components/Login/LoginButton.tsx @@ -2,6 +2,7 @@ import { usePrivy } from "@privy-io/react-auth"; import { Button } from "@/components/ui/button"; +import { HideToggle } from "@/components/HideToggle"; import LoginButtonSkeleton from "./LoginButtonSkeleton"; export default function LoginButton() { @@ -17,6 +18,7 @@ export default function LoginButton() { {user?.email?.address ?? "Signed in"} + diff --git a/components/PrivyLogins/EmailCell.tsx b/components/PrivyLogins/EmailCell.tsx new file mode 100644 index 0000000..e0b6904 --- /dev/null +++ b/components/PrivyLogins/EmailCell.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { useHide } from "@/providers/HideProvider"; +import { maskEmail } from "@/lib/maskEmail"; + +interface EmailCellProps { + getValue: () => string | null; +} + +export default function EmailCell({ getValue }: EmailCellProps) { + const { isHidden } = useHide(); + const email = getValue(); + if (!email) return No email; + return {isHidden ? maskEmail(email) : email}; +} diff --git a/components/PrivyLogins/privyLoginsColumns.tsx b/components/PrivyLogins/privyLoginsColumns.tsx index 53fb51a..e36933d 100644 --- a/components/PrivyLogins/privyLoginsColumns.tsx +++ b/components/PrivyLogins/privyLoginsColumns.tsx @@ -2,17 +2,9 @@ import { type ColumnDef } from "@tanstack/react-table"; import { SortableHeader } from "@/components/SandboxOrgs/SortableHeader"; import { getEmail } from "@/lib/privy/getEmail"; import { getLastSeen } from "@/lib/privy/getLastSeen"; -import { useHide } from "@/providers/HideProvider"; -import { maskEmail } from "@/lib/maskEmail"; +import EmailCell from "@/components/PrivyLogins/EmailCell"; import type { PrivyUser } from "@/types/privy"; -function EmailCell({ getValue }: { getValue: () => string | null }) { - const { isHidden } = useHide(); - const email = getValue(); - if (!email) return No email; - return {isHidden ? maskEmail(email) : email}; -} - export const privyLoginsColumns: ColumnDef[] = [ { id: "email", diff --git a/providers/Providers.tsx b/providers/Providers.tsx index 8bbadd0..19447ee 100644 --- a/providers/Providers.tsx +++ b/providers/Providers.tsx @@ -3,8 +3,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import PrivyProvider from "./PrivyProvider"; import { HideProvider } from "./HideProvider"; -import { HideToggle } from "@/components/HideToggle"; - const queryClient = new QueryClient(); export default function Providers({ children }: { children: React.ReactNode }) { @@ -12,7 +10,6 @@ export default function Providers({ children }: { children: React.ReactNode }) { - {children} From e1bc6d937f9f3fb8878666b8ff91bd2ee5d97c3e Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 18 Mar 2026 10:48:33 -0500 Subject: [PATCH 04/10] refactor: shared root layout, fix PR review comments - AppHeader with LoginButton + HideToggle in root layout (all pages) - Removed duplicate header from HomePage - Fixed title tooltips to use masked email when hidden - Extracted AccountEmailCell from sandboxesColumns (SRP) - Extracted LastSeenCell from privyLoginsColumns (SRP) - Moved maskEmail to lib/hide/maskEmail.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- app/layout.tsx | 10 ++++++- components/AppHeader.tsx | 10 +++++++ components/Home/HomePage.tsx | 11 ++----- components/PrivyLogins/EmailCell.tsx | 2 +- components/PrivyLogins/LastSeenCell.tsx | 9 ++++++ components/PrivyLogins/privyLoginsColumns.tsx | 8 ++--- components/SandboxOrgs/AccountReposList.tsx | 4 +-- components/Sandboxes/AccountEmailCell.tsx | 30 +++++++++++++++++++ components/Sandboxes/sandboxesColumns.tsx | 30 +------------------ lib/{ => hide}/maskEmail.ts | 0 10 files changed, 68 insertions(+), 46 deletions(-) create mode 100644 components/AppHeader.tsx create mode 100644 components/PrivyLogins/LastSeenCell.tsx create mode 100644 components/Sandboxes/AccountEmailCell.tsx rename lib/{ => hide}/maskEmail.ts (100%) diff --git a/app/layout.tsx b/app/layout.tsx index 7958675..21add56 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import Providers from "@/providers/Providers"; +import AppHeader from "@/components/AppHeader"; import "./globals.css"; const geistSans = Geist({ @@ -32,7 +33,14 @@ export default function RootLayout({ - {children} + +
    + +
    + {children} +
    +
    +
    ); diff --git a/components/AppHeader.tsx b/components/AppHeader.tsx new file mode 100644 index 0000000..d225d76 --- /dev/null +++ b/components/AppHeader.tsx @@ -0,0 +1,10 @@ +import LoginButton from "@/components/Login/LoginButton"; + +export default function AppHeader() { + return ( +
    +

    Recoup Admin

    + +
    + ); +} diff --git a/components/Home/HomePage.tsx b/components/Home/HomePage.tsx index b62c45d..0b36227 100644 --- a/components/Home/HomePage.tsx +++ b/components/Home/HomePage.tsx @@ -1,16 +1,9 @@ -import LoginButton from "@/components/Login/LoginButton"; import HomeContent from "@/components/Home/HomeContent"; export default function HomePage() { return ( -
    -
    -

    Recoup Admin

    - -
    -
    - -
    +
    +
    ); } diff --git a/components/PrivyLogins/EmailCell.tsx b/components/PrivyLogins/EmailCell.tsx index e0b6904..4211cb2 100644 --- a/components/PrivyLogins/EmailCell.tsx +++ b/components/PrivyLogins/EmailCell.tsx @@ -1,7 +1,7 @@ "use client"; import { useHide } from "@/providers/HideProvider"; -import { maskEmail } from "@/lib/maskEmail"; +import { maskEmail } from "@/lib/hide/maskEmail"; interface EmailCellProps { getValue: () => string | null; diff --git a/components/PrivyLogins/LastSeenCell.tsx b/components/PrivyLogins/LastSeenCell.tsx new file mode 100644 index 0000000..40cc170 --- /dev/null +++ b/components/PrivyLogins/LastSeenCell.tsx @@ -0,0 +1,9 @@ +interface LastSeenCellProps { + getValue: () => number | null; +} + +export default function LastSeenCell({ getValue }: LastSeenCellProps) { + const ts = getValue(); + if (!ts) return Never; + return {new Date(ts * 1000).toLocaleString()}; +} diff --git a/components/PrivyLogins/privyLoginsColumns.tsx b/components/PrivyLogins/privyLoginsColumns.tsx index e36933d..d9d59e8 100644 --- a/components/PrivyLogins/privyLoginsColumns.tsx +++ b/components/PrivyLogins/privyLoginsColumns.tsx @@ -3,6 +3,7 @@ import { SortableHeader } from "@/components/SandboxOrgs/SortableHeader"; import { getEmail } from "@/lib/privy/getEmail"; import { getLastSeen } from "@/lib/privy/getLastSeen"; import EmailCell from "@/components/PrivyLogins/EmailCell"; +import LastSeenCell from "@/components/PrivyLogins/LastSeenCell"; import type { PrivyUser } from "@/types/privy"; export const privyLoginsColumns: ColumnDef[] = [ @@ -25,10 +26,9 @@ export const privyLoginsColumns: ColumnDef[] = [ id: "last_seen", accessorFn: (row) => getLastSeen(row), header: ({ column }) => , - cell: ({ getValue }) => { - const ts = getValue(); - return ts ? new Date(ts * 1000).toLocaleString() : Never; - }, + cell: ({ getValue }) => ( + getValue()} /> + ), sortingFn: "basic", }, ]; diff --git a/components/SandboxOrgs/AccountReposList.tsx b/components/SandboxOrgs/AccountReposList.tsx index f846ecf..3f01715 100644 --- a/components/SandboxOrgs/AccountReposList.tsx +++ b/components/SandboxOrgs/AccountReposList.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useHide } from "@/providers/HideProvider"; -import { maskEmail } from "@/lib/maskEmail"; +import { maskEmail } from "@/lib/hide/maskEmail"; import type { AccountRepo } from "@/types/sandbox"; interface AccountReposListProps { @@ -28,7 +28,7 @@ export function AccountReposList({ repos }: AccountReposListProps) { {displayLabel} diff --git a/components/Sandboxes/AccountEmailCell.tsx b/components/Sandboxes/AccountEmailCell.tsx new file mode 100644 index 0000000..2fafa35 --- /dev/null +++ b/components/Sandboxes/AccountEmailCell.tsx @@ -0,0 +1,30 @@ +"use client"; + +import Link from "next/link"; +import { useHide } from "@/providers/HideProvider"; +import { maskEmail } from "@/lib/hide/maskEmail"; + +interface AccountEmailCellProps { + email: string | null; + accountId: string; +} + +export default function AccountEmailCell({ email, accountId }: AccountEmailCellProps) { + const { isHidden } = useHide(); + const displayEmail = email + ? isHidden + ? maskEmail(email) + : email + : null; + return ( + + {displayEmail ?? ( + {accountId} + )} + + ); +} diff --git a/components/Sandboxes/sandboxesColumns.tsx b/components/Sandboxes/sandboxesColumns.tsx index 287388c..b6302fa 100644 --- a/components/Sandboxes/sandboxesColumns.tsx +++ b/components/Sandboxes/sandboxesColumns.tsx @@ -1,37 +1,9 @@ import { type ColumnDef } from "@tanstack/react-table"; import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react"; -import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { useHide } from "@/providers/HideProvider"; -import { maskEmail } from "@/lib/maskEmail"; +import AccountEmailCell from "@/components/Sandboxes/AccountEmailCell"; import type { AccountSandboxRow } from "@/types/sandbox"; -function AccountEmailCell({ - email, - accountId, -}: { - email: string | null; - accountId: string; -}) { - const { isHidden } = useHide(); - const displayEmail = email - ? isHidden - ? maskEmail(email) - : email - : null; - return ( - - {displayEmail ?? ( - {accountId} - )} - - ); -} - export const sandboxesColumns: ColumnDef[] = [ { accessorKey: "account_email", diff --git a/lib/maskEmail.ts b/lib/hide/maskEmail.ts similarity index 100% rename from lib/maskEmail.ts rename to lib/hide/maskEmail.ts From a9dd3bec0209f04aa6ea7cceb53dcabb346bddb0 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 18 Mar 2026 11:07:50 -0500 Subject: [PATCH 05/10] fix: address PR review comments - HideToggle: added aria-label, aria-pressed, type=button - LoginButton: mask header email when hidden - DRY: created useDisplayEmail hook, used by EmailCell, AccountEmailCell, AccountReposList, LoginButton - LastSeenCell: use == null instead of falsy check for timestamp 0 Co-Authored-By: Claude Opus 4.6 (1M context) --- components/HideToggle.tsx | 3 ++ components/Login/LoginButton.tsx | 4 +- components/PrivyLogins/EmailCell.tsx | 10 ++--- components/PrivyLogins/LastSeenCell.tsx | 2 +- components/SandboxOrgs/AccountReposList.tsx | 42 ++++++++++----------- components/Sandboxes/AccountEmailCell.tsx | 10 +---- lib/hide/useDisplayEmail.ts | 13 +++++++ 7 files changed, 46 insertions(+), 38 deletions(-) create mode 100644 lib/hide/useDisplayEmail.ts diff --git a/components/HideToggle.tsx b/components/HideToggle.tsx index a168eaf..0652d97 100644 --- a/components/HideToggle.tsx +++ b/components/HideToggle.tsx @@ -8,7 +8,10 @@ export function HideToggle() { return ( From c625d59fb043e17dae0cf82d429b61976d517601 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 18 Mar 2026 11:15:50 -0500 Subject: [PATCH 07/10] refactor: move components to subdirectories, extract AccountRepoLink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppHeader → components/Header/AppHeader.tsx - HideToggle → components/Header/HideToggle.tsx - ApiDocsLink → components/ApiDocs/ApiDocsLink.tsx - Extracted AccountRepoLink from AccountReposList (SRP) - Updated all imports Co-Authored-By: Claude Opus 4.6 (1M context) --- app/layout.tsx | 2 +- components/{ => ApiDocs}/ApiDocsLink.tsx | 0 components/{ => Header}/AppHeader.tsx | 2 +- components/{ => Header}/HideToggle.tsx | 0 components/Home/AdminDashboard.tsx | 2 +- components/PrivyLogins/PrivyLoginsPage.tsx | 2 +- components/SandboxOrgs/AccountRepoLink.tsx | 21 +++++++++++++++++++++ components/SandboxOrgs/AccountReposList.tsx | 19 +------------------ components/SandboxOrgs/SandboxOrgsPage.tsx | 2 +- components/Sandboxes/SandboxesPage.tsx | 2 +- 10 files changed, 28 insertions(+), 24 deletions(-) rename components/{ => ApiDocs}/ApiDocsLink.tsx (100%) rename components/{ => Header}/AppHeader.tsx (86%) rename components/{ => Header}/HideToggle.tsx (100%) create mode 100644 components/SandboxOrgs/AccountRepoLink.tsx diff --git a/app/layout.tsx b/app/layout.tsx index 21add56..3570e1c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import Providers from "@/providers/Providers"; -import AppHeader from "@/components/AppHeader"; +import AppHeader from "@/components/Header/AppHeader"; import "./globals.css"; const geistSans = Geist({ diff --git a/components/ApiDocsLink.tsx b/components/ApiDocs/ApiDocsLink.tsx similarity index 100% rename from components/ApiDocsLink.tsx rename to components/ApiDocs/ApiDocsLink.tsx diff --git a/components/AppHeader.tsx b/components/Header/AppHeader.tsx similarity index 86% rename from components/AppHeader.tsx rename to components/Header/AppHeader.tsx index 32372f1..4c3e591 100644 --- a/components/AppHeader.tsx +++ b/components/Header/AppHeader.tsx @@ -1,5 +1,5 @@ import LoginButton from "@/components/Login/LoginButton"; -import { HideToggle } from "@/components/HideToggle"; +import { HideToggle } from "@/components/Header/HideToggle"; export default function AppHeader() { return ( diff --git a/components/HideToggle.tsx b/components/Header/HideToggle.tsx similarity index 100% rename from components/HideToggle.tsx rename to components/Header/HideToggle.tsx diff --git a/components/Home/AdminDashboard.tsx b/components/Home/AdminDashboard.tsx index 32ac9e8..2fbc5ed 100644 --- a/components/Home/AdminDashboard.tsx +++ b/components/Home/AdminDashboard.tsx @@ -1,5 +1,5 @@ import NavButton from "@/components/Home/NavButton"; -import ApiDocsLink from "@/components/ApiDocsLink"; +import ApiDocsLink from "@/components/ApiDocs/ApiDocsLink"; export default function AdminDashboard() { return ( diff --git a/components/PrivyLogins/PrivyLoginsPage.tsx b/components/PrivyLogins/PrivyLoginsPage.tsx index e96faec..e1ee633 100644 --- a/components/PrivyLogins/PrivyLoginsPage.tsx +++ b/components/PrivyLogins/PrivyLoginsPage.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import PageBreadcrumb from "@/components/Sandboxes/PageBreadcrumb"; -import ApiDocsLink from "@/components/ApiDocsLink"; +import ApiDocsLink from "@/components/ApiDocs/ApiDocsLink"; import { usePrivyLogins } from "@/hooks/usePrivyLogins"; import PrivyLoginsTable from "@/components/PrivyLogins/PrivyLoginsTable"; import PrivyPeriodSelector from "@/components/PrivyLogins/PrivyPeriodSelector"; diff --git a/components/SandboxOrgs/AccountRepoLink.tsx b/components/SandboxOrgs/AccountRepoLink.tsx new file mode 100644 index 0000000..03ce3ad --- /dev/null +++ b/components/SandboxOrgs/AccountRepoLink.tsx @@ -0,0 +1,21 @@ +"use client"; + +import Link from "next/link"; +import { useDisplayEmail } from "@/lib/hide/useDisplayEmail"; +import type { AccountRepo } from "@/types/sandbox"; + +export default function AccountRepoLink({ account_id, email }: AccountRepo) { + const displayEmail = useDisplayEmail(email ?? null); + const displayLabel = displayEmail ?? account_id; + return ( +
  • + + {displayLabel} + +
  • + ); +} diff --git a/components/SandboxOrgs/AccountReposList.tsx b/components/SandboxOrgs/AccountReposList.tsx index e326c2d..c06dbb0 100644 --- a/components/SandboxOrgs/AccountReposList.tsx +++ b/components/SandboxOrgs/AccountReposList.tsx @@ -1,29 +1,12 @@ "use client"; -import Link from "next/link"; -import { useDisplayEmail } from "@/lib/hide/useDisplayEmail"; +import AccountRepoLink from "@/components/SandboxOrgs/AccountRepoLink"; import type { AccountRepo } from "@/types/sandbox"; interface AccountReposListProps { repos: AccountRepo[]; } -function AccountRepoLink({ account_id, email }: AccountRepo) { - const displayEmail = useDisplayEmail(email ?? null); - const displayLabel = displayEmail ?? account_id; - return ( -
  • - - {displayLabel} - -
  • - ); -} - export function AccountReposList({ repos }: AccountReposListProps) { if (repos.length === 0) { return ; diff --git a/components/SandboxOrgs/SandboxOrgsPage.tsx b/components/SandboxOrgs/SandboxOrgsPage.tsx index 02ad03d..a606b6b 100644 --- a/components/SandboxOrgs/SandboxOrgsPage.tsx +++ b/components/SandboxOrgs/SandboxOrgsPage.tsx @@ -1,5 +1,5 @@ import SandboxOrgsTableContainer from "@/components/SandboxOrgs/SandboxOrgsTableContainer"; -import ApiDocsLink from "@/components/ApiDocsLink"; +import ApiDocsLink from "@/components/ApiDocs/ApiDocsLink"; import PageBreadcrumb from "@/components/Sandboxes/PageBreadcrumb"; export default function SandboxOrgsPage() { diff --git a/components/Sandboxes/SandboxesPage.tsx b/components/Sandboxes/SandboxesPage.tsx index 757f17c..19b4dcd 100644 --- a/components/Sandboxes/SandboxesPage.tsx +++ b/components/Sandboxes/SandboxesPage.tsx @@ -1,5 +1,5 @@ import SandboxesTableContainer from "@/components/Sandboxes/SandboxesTableContainer"; -import ApiDocsLink from "@/components/ApiDocsLink"; +import ApiDocsLink from "@/components/ApiDocs/ApiDocsLink"; import PageBreadcrumb from "@/components/Sandboxes/PageBreadcrumb"; export default function SandboxesPage() { From 2d5f743978dcaa7bda5100030dd279b4208a1a84 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 18 Mar 2026 11:18:35 -0500 Subject: [PATCH 08/10] feat: add blink animation to hide toggle On click: eye closes (scaleY 0), icon swaps, eye opens (scaleY 1). 150ms transition for a natural blink effect. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/Header/HideToggle.tsx | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/components/Header/HideToggle.tsx b/components/Header/HideToggle.tsx index 0652d97..fe7c800 100644 --- a/components/Header/HideToggle.tsx +++ b/components/Header/HideToggle.tsx @@ -1,21 +1,41 @@ "use client"; +import { useState, useCallback } from "react"; import { Eye, EyeOff } from "lucide-react"; import { useHide } from "@/providers/HideProvider"; +const BLINK_MS = 150; + export function HideToggle() { const { isHidden, toggle } = useHide(); + const [isBlinking, setIsBlinking] = useState(false); + + const handleClick = useCallback(() => { + setIsBlinking(true); + setTimeout(() => { + toggle(); + setIsBlinking(false); + }, BLINK_MS); + }, [toggle]); + + const Icon = isHidden ? EyeOff : Eye; return ( ); } From a1b28becd23c9e22aa60ac37db25e0062982887f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 18 Mar 2026 11:19:26 -0500 Subject: [PATCH 09/10] fix: center HomeContent vertically on the page Made
    a flex column container so child flex-1 items-center can properly fill the remaining height. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/layout.tsx b/app/layout.tsx index 3570e1c..9c7bc45 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -36,7 +36,7 @@ export default function RootLayout({
    -
    +
    {children}
    From 9ae9eb360070f2d86e97d829404fd5cd82444fdf Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 18 Mar 2026 11:21:00 -0500 Subject: [PATCH 10/10] fix: smooth blink open animation Keep scaleY(0) when icon swaps, then animate to scaleY(1) on the next frame so the opening is a smooth transition instead of a flash. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/Header/HideToggle.tsx | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/components/Header/HideToggle.tsx b/components/Header/HideToggle.tsx index fe7c800..1e842c0 100644 --- a/components/Header/HideToggle.tsx +++ b/components/Header/HideToggle.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useRef } from "react"; import { Eye, EyeOff } from "lucide-react"; import { useHide } from "@/providers/HideProvider"; @@ -8,18 +8,33 @@ const BLINK_MS = 150; export function HideToggle() { const { isHidden, toggle } = useHide(); - const [isBlinking, setIsBlinking] = useState(false); + const [phase, setPhase] = useState<"idle" | "closing" | "opening">("idle"); + const iconRef = useRef(null); const handleClick = useCallback(() => { - setIsBlinking(true); + if (phase !== "idle") return; + + // Phase 1: close the eye + setPhase("closing"); + setTimeout(() => { + // Phase 2: swap icon while still closed, then open toggle(); - setIsBlinking(false); + setPhase("opening"); + + // Wait for next frame so the browser registers scaleY(0) before animating to scaleY(1) + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setPhase("idle"); + }); + }); }, BLINK_MS); - }, [toggle]); + }, [toggle, phase]); const Icon = isHidden ? EyeOff : Eye; + const scaleY = phase === "closing" || phase === "opening" ? 0 : 1; + return (