Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f6de51e
chore: remove verification-service
craftsoldier Feb 15, 2026
7665d3f
draft: add AGENT.md documentation for AI coding agents
craftsoldier Feb 15, 2026
58963bf
refactor: colocate Zustand stores with UI features
craftsoldier Feb 15, 2026
dc294a1
chore: update next-env.d.ts (auto-generated)
craftsoldier Feb 15, 2026
1156933
refactor: remove pendingEdits system for ZVS verification
craftsoldier Feb 15, 2026
60dc835
refactor: colocate style utilities with related components
craftsoldier Feb 15, 2026
45cc1c3
chore: remove unused useValidation hook
craftsoldier Feb 15, 2026
95ce5a2
refactor: remove barrel exports, use direct imports
craftsoldier Feb 15, 2026
7f75d5d
refactor: remove duplicate CopyButton, use common version
craftsoldier Feb 15, 2026
0484e18
refactor: replace polling-based ZVS with deterministic HMAC-SHA256 OTP
craftsoldier Feb 15, 2026
8b19e9b
feat: add brute-force protection and improve verification UX
craftsoldier Feb 15, 2026
8762da9
refactor: move sessionId to edits store, regenerate on Generate QR click
craftsoldier Feb 15, 2026
ce1f99c
refactor: simplify social verification with provider-first OAuth flow
craftsoldier Feb 16, 2026
4566fd0
refactor: use absolute imports in API routes
craftsoldier Feb 16, 2026
84de60e
feat: add VerifyProfileModal with combined verification and OTP flow
craftsoldier Feb 16, 2026
c610fd3
fix: persist verification session before showing QR code
craftsoldier Feb 16, 2026
5c13d97
refactor: simplify verification to stateless single-session flow
craftsoldier Feb 16, 2026
66b0039
fix: add server-side address verification check to verifyLinkAction
craftsoldier Feb 16, 2026
81bf6ed
fix: verify memo address matches profile before marking verified
craftsoldier Feb 16, 2026
2c71873
refactor: move memo field above ZEC input in verification flow
craftsoldier Feb 16, 2026
230f22c
docs: add AGENT.md for social OAuth verification module
craftsoldier Feb 16, 2026
618d660
feat: save profile edits to DB after ZVS verification
craftsoldier Feb 16, 2026
611500e
refactor: extract shared validation logic in forms and swap modules
craftsoldier Feb 16, 2026
c930a3a
fix: prevent form edits from being reset before OTP submission
craftsoldier Feb 16, 2026
9331aee
fix: align OTP generation with ZVS backend and use service role key
craftsoldier Feb 16, 2026
c431af3
fix: resolve TypeScript type error for crypto.subtle.importKey
craftsoldier Feb 16, 2026
0fad432
fix: use SUPABASE_SERVICE_KEY env var to match Vercel config
craftsoldier Feb 16, 2026
82cae9e
fix: write nearest_city_name to DB and resolve it on fetch
craftsoldier Feb 18, 2026
b7ef88a
chore: remove debug console.log statements from OTP flow
craftsoldier Feb 18, 2026
dd71203
chore: remove dead profile code from orphaned OAuth and directory ref…
craftsoldier Feb 18, 2026
e07c44e
docs: remove stale references to deleted code from AGENT.md
craftsoldier Feb 18, 2026
3aa463c
refactor: extract ProfileCard into focused components (591 → 418 lines)
craftsoldier Feb 18, 2026
bd9494e
refactor: decouple social link verification from profile editor
craftsoldier Feb 19, 2026
c2bc932
refactor: replace worldcities table with city-timezones library
craftsoldier Feb 19, 2026
28e3e1d
chore: add migration to drop worldcities table and nearest_city_id co…
craftsoldier Feb 19, 2026
2974f39
fix: gate social link authentication behind address verification
craftsoldier Feb 19, 2026
eb2516a
refactor: replace || with ?? for nullish coalescing across codebase
craftsoldier Feb 19, 2026
d5498b9
fix: validate OAuth identity server-side before marking social links …
craftsoldier Feb 19, 2026
2c9a965
fix: move memo generation server-side and cap OTP attempts at 5
craftsoldier Feb 19, 2026
0e0bcda
refactor: clean up social verification, profile links, and remove unu…
craftsoldier Feb 19, 2026
3969239
refactor: move lib/social to ui/links for user-facing link management
craftsoldier Feb 19, 2026
eaed612
refactor: delete avatars.ts, inline avatar fetch via public APIs
craftsoldier Feb 19, 2026
f684cf3
refactor: simplify social lookup using platform column instead of URL…
craftsoldier Feb 19, 2026
6c85e9d
refactor: inline city-timezones lookup into server action, delete sea…
craftsoldier Feb 19, 2026
5a06860
refactor: delete dead lib/supabase/auth.ts wrapper
craftsoldier Feb 19, 2026
7098fa8
refactor: use stored platform column directly instead of re-deriving …
craftsoldier Feb 19, 2026
0deb1f8
refactor: delete dead searchProfiles, Dropdown example, and modal exa…
craftsoldier Feb 19, 2026
dba417a
reorg /ns
Rashmi-278 Feb 19, 2026
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
239 changes: 239 additions & 0 deletions AGENT.md

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions app/AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# /app - Next.js App Router

## Purpose
Contains all page routes and API endpoints for zcash.me. Uses Next.js 16 App Router with React 19.

## Key Routes

| Path | Description |
|------|-------------|
| `/` | Homepage with featured Zcash profiles |
| `/[slug]` | Dynamic profile pages (e.g., /alice) |
| `/ns` | Network School directory - filtered profile list |
| `/swap-app` | Cryptocurrency swap interface (Defuse OneClick) |
| `/leader-app` | Referral leaderboard dashboard |
| `/stats-app` | Network statistics |
| `/thread` | Discussion board (OTP-verified posting) |
| `/design-system` | Component showcase for manual testing |

## API Routes

### `/api/resolve/[username]` - GET
Profile lookup by username. Returns profile with links.

### `/api/directory` - GET
Search profiles with ranking. Supports `q`, `limit`, `cursor`, `verified_only` params.
Features space-insensitive, case-insensitive matching with relevance ranking.

### `/api/social` - GET
Social platform lookup (stub implementation).

## Zcash-Specific Patterns
- Profile pages display Zcash unified addresses (u1...) prominently
- QR codes encode `zcash:` URIs with memo for verification
- Swap routes handle ZEC as primary currency with cross-chain support

## Testing Harness
- No automated tests in /app
- Use `/design-system` route for manual component testing
- API routes use rate limiting via `/lib/api/guard.ts`

## Adding New Pages
1. Create folder under `/app/[route-name]`
2. Add `page.tsx` with default export
3. Use server components by default, `'use client'` only when needed
4. Import UI from `/ui/*`, logic from `/lib/*`

## Environment Variables
```
NEXT_PUBLIC_BASE_DOMAIN - zcash.me or localhost:3000
NEXT_PUBLIC_SUPABASE_URL - Database URL
ZVS_SECRET_SEED - HMAC secret for OTP generation
```
6 changes: 0 additions & 6 deletions app/[slug]/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { useEffect, useState, useCallback } from "react";
import type { Profile } from "@/lib/profile/types";
import type { Token, SwapContextQuoteData, SwapQuoteDisplay } from "@/lib/swap/types";

// Stores
import { useEditsStore } from "@/lib/stores/edits";

// Swap utilities
import { getTokenId } from "@/lib/swap/utils";
Expand Down Expand Up @@ -68,9 +66,6 @@ export default function ProfilePage({
const [forceShowQR, setForceShowQR] = useState(false);
const [isProfileEditing, setIsProfileEditing] = useState(false);

// Granular subscriptions to prevent unnecessary re-renders
const pendingEdits = useEditsStore(state => state.pendingEdits);

// Local state
const [mode, setMode] = useState<'donate' | 'swap' | 'verification'>('donate');
const [originTokenId, setOriginTokenId] = useState<string | null>(null);
Expand Down Expand Up @@ -249,7 +244,6 @@ export default function ProfilePage({
{mode === "verification" ? (
<ProfileVerification
profile={initialProfile}
pendingEdits={pendingEdits}
/>
) : mode === "swap" ? (
<div className="p-0">
Expand Down
8 changes: 5 additions & 3 deletions app/api/directory/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface ZcasherLink {
id: number;
label: string;
url: string;
platform?: string;
is_verified: boolean;
zcasher_id: number;
}
Expand All @@ -26,6 +27,7 @@ interface LinkOutput {
id: number;
label: string;
url: string;
platform?: string;
is_verified: boolean;
}

Expand Down Expand Up @@ -272,7 +274,7 @@ export async function GET(request: Request): Promise<Response> {
if (profileIds.length > 0) {
const { data: links, error: linksError } = await supabase
.from("zcasher_links")
.select("id,label,url,is_verified,zcasher_id")
.select("id,label,url,platform,is_verified,zcasher_id")
.in("zcasher_id", profileIds);

if (linksError) {
Expand All @@ -293,10 +295,10 @@ export async function GET(request: Request): Promise<Response> {
const profileLinks = linksMap.get(p.id) || [];
const authenticated_links: LinkOutput[] = profileLinks
.filter((l) => l.is_verified)
.map((l) => ({ id: l.id, label: l.label, url: l.url, is_verified: l.is_verified }));
.map((l) => ({ id: l.id, label: l.label, url: l.url, platform: l.platform, is_verified: l.is_verified }));
const unauthenticated_links: LinkOutput[] = profileLinks
.filter((l) => !l.is_verified)
.map((l) => ({ id: l.id, label: l.label, url: l.url, is_verified: l.is_verified }));
.map((l) => ({ id: l.id, label: l.label, url: l.url, platform: l.platform, is_verified: l.is_verified }));

return {
id: p.id,
Expand Down
7 changes: 4 additions & 3 deletions app/api/resolve/[username]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { createSupabaseServerClient } from "../../../../lib/supabase/supabase-server";
import { enforceApiGuard, withCacheHeaders } from "../../../../lib/api/guard";
import { createSupabaseServerClient } from "@/lib/supabase/supabase-server";
import { enforceApiGuard, withCacheHeaders } from "@/lib/api/guard";

interface ZcasherProfile {
id: number;
Expand All @@ -18,6 +18,7 @@ interface ZcasherLink {
id: number;
label: string;
url: string;
platform?: string;
is_verified: boolean;
}

Expand Down Expand Up @@ -73,7 +74,7 @@ export async function GET(

const { data: links, error: linksError } = await supabase
.from("zcasher_links")
.select("id,label,url,is_verified")
.select("id,label,url,platform,is_verified")
.eq("zcasher_id", typedProfile.id);

if (linksError) {
Expand Down
7 changes: 4 additions & 3 deletions app/api/resolve/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSupabaseServerClient } from "../../../lib/supabase/supabase-server";
import { enforceApiGuard, withCacheHeaders } from "../../../lib/api/guard";
import { createSupabaseServerClient } from "@/lib/supabase/supabase-server";
import { enforceApiGuard, withCacheHeaders } from "@/lib/api/guard";

const jsonResponse = (body: Record<string, unknown>, status: number = 200, cacheSeconds: number = 0): Response =>
new Response(JSON.stringify(body), {
Expand All @@ -23,6 +23,7 @@ interface ZcasherLink {
id: number;
label: string;
url: string;
platform?: string;
is_verified: boolean;
}

Expand Down Expand Up @@ -65,7 +66,7 @@ export async function GET(request: Request): Promise<Response> {

const { data: links, error: linksError } = await supabase
.from("zcasher_links")
.select("id,label,url,is_verified")
.select("id,label,url,platform,is_verified")
.eq("zcasher_id", typedProfile.id);

if (linksError) {
Expand Down
122 changes: 101 additions & 21 deletions app/api/social/route.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,111 @@
import { lookupSocialAddress } from "../../../lib/profile/social-lookup";
import { enforceApiGuard, withCacheHeaders } from "../../../lib/api/guard";
import { createSupabaseServerClient } from "@/lib/supabase/supabase-server";
import { enforceApiGuard, withCacheHeaders } from "@/lib/api/guard";
import { normalizeSocialUsername, type SocialPlatform } from "@/lib/profile/usernameNormalizer";

const PLATFORM_ALIASES: Record<string, SocialPlatform> = {
x: "X",
twitter: "X",
github: "GitHub",
instagram: "Instagram",
reddit: "Reddit",
linkedin: "LinkedIn",
discord: "Discord",
tiktok: "TikTok",
bluesky: "Bluesky",
mastodon: "Mastodon",
snapchat: "Snapchat",
telegram: "Telegram",
};

const json = (body: Record<string, unknown>, status: number, cacheSeconds = 0): Response =>
new Response(JSON.stringify(body), {
status,
headers: withCacheHeaders({ "Content-Type": "application/json" }, cacheSeconds),
});

export async function GET(request: Request): Promise<Response> {
const guard = await enforceApiGuard(request, { cacheSeconds: 300 });
if (guard instanceof Response) return guard;

const { searchParams } = new URL(request.url);
const platform = searchParams.get("platform") || "";
const handle = searchParams.get("handle") || "";

if (!platform || !handle) {
return new Response(
JSON.stringify({ error: "missing_parameters", platform: null, handle: null }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
const rawPlatform = (searchParams.get("platform") || "").trim().toLowerCase();
const rawHandle = searchParams.get("handle") || "";

const platform = PLATFORM_ALIASES[rawPlatform];
if (!platform) {
return json({ error: "unsupported_platform", address: null, handle: null }, 400);
}

const handle = normalizeSocialUsername(rawHandle, platform);
if (!handle) {
return json({ error: "invalid_handle", address: null, handle: null }, 400);
}

const result = await lookupSocialAddress(platform, handle);
const supabase = createSupabaseServerClient();
if (!supabase) {
return json({ error: "server_misconfigured", address: null, handle }, 500);
}

return new Response(JSON.stringify(result.body), {
status: result.status,
headers: withCacheHeaders(
{ "Content-Type": "application/json" },
guard.cacheSeconds
),
});
// Query by platform column + handle match on label or url tail
const { data: links, error: linksError } = await supabase
.from("zcasher_links")
.select("id,zcasher_id,label,url,is_verified")
.eq("platform", platform)
.eq("is_verified", true)
.or(`label.ilike.${handle},url.ilike.%/${handle}`)
.limit(25);

if (linksError) {
return json({ error: "lookup_failed", address: null, handle }, 500);
}

if (!links || !links.length) {
return json({ error: "not_found", address: null, handle }, 404);
}

// Fetch profiles for matched links
const ids = [...new Set(links.map((l) => l.zcasher_id))];
const { data: profiles, error: profileError } = await supabase
.from("zcasher")
.select("id,address,name,address_verified")
.in("id", ids);

if (profileError || !profiles?.length) {
return json({ error: "profile_lookup_failed", address: null, handle }, 500);
}

const profilesById = new Map(profiles.map((p) => [p.id, p]));

// Pick best: prefer verified link + verified address, then oldest profile
const best = links
.map((link) => ({ link, profile: profilesById.get(link.zcasher_id) }))
.filter((c): c is { link: typeof links[0]; profile: NonNullable<typeof profiles[0]> } =>
!!c.profile?.address
)
.sort((a, b) => {
const scoreA = (a.profile.address_verified ? 1 : 0);
const scoreB = (b.profile.address_verified ? 1 : 0);
if (scoreA !== scoreB) return scoreB - scoreA;
return a.profile.id - b.profile.id;
})[0];

if (!best) {
return json({ error: "address_missing", address: null, handle }, 404);
}

return json(
{
link: {
platform: platform.toLowerCase(),
handle,
url: best.link.url,
is_verified: true,
},
address: best.profile.address,
profile_name: best.profile.name,
address_verified: !!best.profile.address_verified,
},
200,
guard.cacheSeconds
);
}
58 changes: 28 additions & 30 deletions app/design-system/page.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
"use client";

import { useState } from "react";
import {
// Buttons
Button,
CopyButton,
IconButton,
// Layout
Card,
Section,
Divider,
// Feedback
Badge,
Spinner,
Alert,
// Forms
Input,
TextArea,
Select,
Checkbox,
FormField,
Dropdown,
// Modals
Modal,
ModalHeader,
ModalBody,
ModalFooter,
ConfirmDialog,
TutorialModal,
// Other
HelpIcon,
} from "@/ui/common";
// Buttons
import Button from "@/ui/common/buttons/Button";
import CopyButton from "@/ui/common/buttons/CopyButton";
import IconButton from "@/ui/common/buttons/IconButton";
// Layout
import Card from "@/ui/common/layout/Card";
import Section from "@/ui/common/layout/Section";
import Divider from "@/ui/common/layout/Divider";
// Feedback
import Badge from "@/ui/common/feedback/Badge";
import Spinner from "@/ui/common/feedback/Spinner";
import Alert from "@/ui/common/feedback/Alert";
// Forms
import Input from "@/ui/common/forms/Input";
import TextArea from "@/ui/common/forms/TextArea";
import Select from "@/ui/common/forms/Select";
import Checkbox from "@/ui/common/forms/Checkbox";
import FormField from "@/ui/common/forms/FormField";
import Dropdown from "@/ui/common/forms/Dropdown";
// Modals
import Modal from "@/ui/common/modals/Modal";
import ModalHeader from "@/ui/common/modals/ModalHeader";
import ModalBody from "@/ui/common/modals/ModalBody";
import ModalFooter from "@/ui/common/modals/ModalFooter";
import ConfirmDialog from "@/ui/common/modals/ConfirmDialog";
import TutorialModal from "@/ui/common/modals/TutorialModal";
// Other
import HelpIcon from "@/ui/common/HelpIcon";

export default function DesignSystemPage() {
const [modalOpen, setModalOpen] = useState(false);
Expand Down
Loading