Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
510 changes: 208 additions & 302 deletions src/app/globals.css

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/app/settings/my-data/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export default function MyDataPage() {
</div>

{/* Records list */}
<div className="dash-card mt-4">
<div className="dash-card">
{loading ? (
<div className="my-data__loading">
<LoadingSpinner size="md" />
Expand Down Expand Up @@ -150,7 +150,7 @@ export default function MyDataPage() {
</div>

{/* Raw data info */}
<div className="dash-card mt-4">
<div className="dash-card">
<h2 className="dash-card__title">About Your Data</h2>
<p className="dash-card__desc">
All your data is stored on your Personal Data Server (PDS) under the collection <code className="my-data__code">{COLLECTION}</code>. Because your data lives on the AT Protocol, it is portable — you can move it to any compatible server at any time.
Expand Down
4 changes: 2 additions & 2 deletions src/app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function SettingsPage() {
<PasswordSection email={email || ""} />

{/* App Passwords card */}
<div className="dash-card mt-4">
<div className="dash-card">
<h2 className="dash-card__title">App passwords</h2>
<p className="dash-card__desc">
Use app passwords to sign in to other apps without giving full access to your account or your password.
Expand All @@ -40,7 +40,7 @@ export default function SettingsPage() {
</div>

{/* 2FA card */}
<div className="dash-card mt-4">
<div className="dash-card">
<h2 className="dash-card__title">Two-factor authentication</h2>
<p className="dash-card__desc">
Add an extra layer of security to your account.
Expand Down
2 changes: 1 addition & 1 deletion src/app/settings/wallet/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function WalletPage() {
</div>

{/* Identity Link Card */}
<div className="dash-card mt-4">
<div className="dash-card">
<IdentityLinkCard did={did} />
</div>
</div>
Expand Down
11 changes: 2 additions & 9 deletions src/components/landing/home-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import Avatar from "@/components/ui/avatar";
import { getInitials } from "@/lib/utils/initials";
import Button from "@/components/ui/button";

// Dashboard components — only loaded for authenticated users
const SignInPreviewCard = dynamic(() => import("@/components/dashboard/sign-in-preview-card"));
// Landing sections — only loaded for unauthenticated users
const WhatYouGet = dynamic(() => import("@/components/landing/sections/what-you-get"));
const HowItWorks = dynamic(() => import("@/components/landing/sections/how-it-works"));
Expand Down Expand Up @@ -92,7 +90,7 @@ export default function HomeClient() {
</div>

{/* Account Details card */}
<div className="dash-card mt-4">
<div className="dash-card">
<div className="username-card__header">
<h2 className="dash-card__title" style={{ marginBottom: 0 }}>Account Details</h2>
<Link href="/settings/edit-profile">
Expand Down Expand Up @@ -130,11 +128,6 @@ export default function HomeClient() {
</div>

</div>

{/* Right sidebar */}
<div className="dashboard__aside">
<SignInPreviewCard />
</div>
</div>
</div>
);
Expand Down Expand Up @@ -167,7 +160,7 @@ export default function HomeClient() {
<span className="hero__title-accent">Any app.</span>
</h1>
<p className="hero__subtitle hero-reveal">
Your identity and data — everywhere you go. One account across many platforms, with no new logins and no lock-in.
Your identity and data — everywhere you go.
</p>
<div className="hero-reveal">
<div className="hero__actions">
Expand Down
63 changes: 0 additions & 63 deletions src/components/layout/app-shell.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,13 @@
"use client";

import { useState, useEffect, useRef } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { useAuth } from "@/lib/auth/auth-context";
import { Menu } from "lucide-react";
import Sidebar from "@/components/layout/sidebar";

export default function AppShell({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuth();
const pathname = usePathname();
const [sidebarOpen, setSidebarOpen] = useState(false);

const hamburgerRef = useRef<HTMLButtonElement>(null);

// Escape key closes the sidebar
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && sidebarOpen) {
setSidebarOpen(false);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [sidebarOpen]);

// Body scroll lock when sidebar is open
useEffect(() => {
document.body.style.overflow = sidebarOpen ? "hidden" : "";
return () => {
document.body.style.overflow = "";
};
}, [sidebarOpen]);

// Focus management: move focus into sidebar close button when open,
// return focus to hamburger button when closed
useEffect(() => {
if (sidebarOpen) {
// Query the close button rendered inside the Sidebar component
const closeBtn = document.querySelector<HTMLElement>(".sidebar__close");
closeBtn?.focus();
} else {
hamburgerRef.current?.focus();
}
}, [sidebarOpen]);

// App pages always show the shell (even while loading)
// so the user sees the sidebar + a spinner instead of the landing logo
const isAppPage = pathname.startsWith("/settings") || pathname.startsWith("/connected-apps");
const showShell = isAppPage || (!isLoading && isAuthenticated);

Expand All @@ -56,30 +17,6 @@ export default function AppShell({ children }: { children: React.ReactNode }) {

return (
<div className="app-shell">
{/* Mobile header — visible only on small screens */}
<div className="mobile-header">
<Link href="/" className="mobile-header__logo">
<img src="/assets/certified_wordmark_white_green.svg" alt="Certified" />
</Link>
<button
ref={hamburgerRef}
className="mobile-header__toggle"
onClick={() => setSidebarOpen(true)}
aria-label="Open menu"
>
<Menu size={22} />
</button>
</div>

{/* Overlay — decorative; Escape key and close button handle keyboard access */}
{sidebarOpen && (
<div
className="sidebar-overlay"
onClick={() => setSidebarOpen(false)}
/>
)}

<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="app-shell__content">
{children}
</div>
Expand Down
8 changes: 3 additions & 5 deletions src/components/layout/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ const Footer: React.FC = () => {
const pathname = usePathname();
const { isAuthenticated } = useAuth();

// Hide footer for authenticated users (sidebar layout replaces it)
if (isAuthenticated) return null;

// Hide footer on landing page (has its own footer)
if (pathname === "/") return null;
// Hide footer on landing page (has its own inline footer) — but show it
// when authenticated since "/" is the profile page in that case
if (pathname === "/" && !isAuthenticated) return null;

return (
<footer className="landing-footer">
Expand Down
102 changes: 80 additions & 22 deletions src/components/layout/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,29 @@

import React, { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useAuth } from "@/lib/auth/auth-context";
import { useNavbarVariant } from "@/lib/navbar-context";
import { useProfile } from "@/hooks/use-profile";
import { useSession } from "@/hooks/use-session";
import Avatar from "@/components/ui/avatar";
import { getInitials } from "@/lib/utils/initials";
import { Menu, X } from "lucide-react";

const NAV_LINKS = [
{ href: "/", label: "Profile" },
{ href: "/connected-apps", label: "Apps" },
{ href: "/settings", label: "Settings" },
];

const Navbar: React.FC = () => {
const { isLoading, isAuthenticated, did, openSignIn, signOut } = useAuth();
const { variant } = useNavbarVariant();
const { profile, avatarUrl } = useProfile();
const { handle } = useSession();
const pathname = usePathname();
const [scrolled, setScrolled] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false);

useEffect(() => {
const handleScroll = () => {
Expand All @@ -22,58 +34,104 @@ const Navbar: React.FC = () => {
return () => window.removeEventListener("scroll", handleScroll);
}, []);

// Close dropdown on navigation
useEffect(() => {
setDropdownOpen(false);
}, [pathname]);

const avatarInitials = getInitials(profile?.displayName, did);

// Don't render navbar while auth is loading — prevents white flash
if (isLoading) return null;

// Hide navbar for authenticated users (sidebar replaces it)
if (isAuthenticated) return null;

const isTransparent = variant === "transparent";
const isTransparent = !isAuthenticated && variant === "transparent";

const navClasses = [
"navbar",
isTransparent ? "navbar--transparent" : "navbar--default",
isAuthenticated ? "navbar--default" : (isTransparent ? "navbar--transparent" : "navbar--default"),
scrolled ? "navbar--scrolled" : "",
]
.filter(Boolean)
.join(" ");

const isActive = (href: string) => {
if (href === "/") return pathname === "/" || pathname === "/settings/edit-profile";
return pathname.startsWith(href);
};

return (
<nav className={navClasses}>
<div className="navbar__inner">
<Link href="/" className="navbar__logo">
<img src="/assets/certified_wordmark_black_green.png" alt="Certified" className="navbar__logo-img" />
</Link>

<div className="navbar__right">
{isLoading ? (
<div className="w-20" />
) : isAuthenticated ? (
<>
<Link
href="/"
className="flex items-center hover:opacity-80 transition-opacity duration-150"
>
{isAuthenticated ? (
<>
{/* Desktop: nav links */}
<div className="navbar__app-links">
{NAV_LINKS.map((link) => (
<Link
key={link.href}
href={link.href}
className={`navbar__app-link ${isActive(link.href) ? "navbar__app-link--active" : ""}`}
>
{link.label}
</Link>
))}
</div>

{/* Desktop: user + sign out */}
<div className="navbar__user">
<Link href="/">
<Avatar size="sm" src={avatarUrl || undefined} fallbackInitials={avatarInitials} />
</Link>
<button
onClick={signOut}
className="navbar__signout"
>
<button onClick={signOut} className="navbar__signout">
Sign out
</button>
</>
) : (
</div>

{/* Mobile: hamburger */}
<button
className="navbar__hamburger"
onClick={() => setDropdownOpen(!dropdownOpen)}
aria-label={dropdownOpen ? "Close menu" : "Open menu"}
>
{dropdownOpen ? <X size={22} /> : <Menu size={22} />}
</button>

{/* Mobile: dropdown */}
<div className={`navbar__dropdown ${dropdownOpen ? "navbar__dropdown--open" : ""}`}>
{NAV_LINKS.map((link) => (
<Link
key={link.href}
href={link.href}
className={`navbar__dropdown-link ${isActive(link.href) ? "navbar__dropdown-link--active" : ""}`}
>
{link.label}
</Link>
))}
<div className="navbar__dropdown-user">
<div className="navbar__dropdown-user-info">
<Avatar size="sm" src={avatarUrl || undefined} fallbackInitials={avatarInitials} />
<span className="navbar__dropdown-user-name">{profile?.displayName || `@${handle}`}</span>
</div>
<button onClick={signOut} className="navbar__signout">
Sign out
</button>
</div>
</div>
</>
) : (
<div className="navbar__right">
<button
onClick={openSignIn}
className="navbar__signin"
>
<img src="/assets/sign_in_black_small.svg" alt="Sign in" className="navbar__signin-img" />
</button>
)}
</div>
</div>
)}
</div>
</nav>
);
Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
ref
) => {
const baseStyles =
"rounded font-mono text-sm font-medium tracking-wider transition-all duration-150 focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 inline-flex items-center justify-center gap-2";
"rounded text-sm font-medium tracking-wider transition-all duration-150 focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 inline-flex items-center justify-center gap-2";

const variantStyles = {
primary:
"bg-accent/10 text-accent border border-accent/20 hover:bg-accent/15 hover:border-accent/35 hover:text-[#A3CDF2]",
"bg-[var(--color-navy)] text-white border-none hover:opacity-90",
secondary:
"bg-transparent text-navy border border-navy/20 hover:border-navy/40",
"bg-transparent text-[var(--color-navy)] border border-[var(--color-navy)]/15 hover:border-[var(--color-navy)]/40",
ghost: "bg-transparent text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700",
destructive: "bg-error/10 text-error border border-error/20 hover:bg-error/15 hover:border-error/35",
};
Expand Down