diff --git a/src/app/page.tsx b/src/app/page.tsx index 4ba154a..605d153 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import Image from 'next/image'; import Link from 'next/link'; -import { getChallenge, verifySignature, getMe, type AuthResponse } from '@/lib/api'; +import { getChallenge, verifySignature, getMe, getUserCount, type AuthResponse } from '@/lib/api'; type Asset = 'btc' | 'ltc' | 'xmr' | 'wow' | 'grin'; @@ -24,6 +24,7 @@ export default function Home() { const [user, setUser] = useState(null); const [hasExtension, setHasExtension] = useState(null); const [checkingSession, setCheckingSession] = useState(true); + const [userCount, setUserCount] = useState(null); // Check for extension on mount useEffect(() => { @@ -34,6 +35,13 @@ export default function Home() { return () => clearTimeout(timeout); }, []); + // Fetch user count on mount + useEffect(() => { + getUserCount() + .then((data) => setUserCount(data.count)) + .catch(() => {}); // Silently ignore errors + }, []); + const handleConnect = async () => { if (!window.smirk) { setError('Smirk extension not found'); @@ -143,6 +151,12 @@ export default function Home() { />

smirk.ca$h

+ {userCount !== null && userCount > 0 && ( +

+ {userCount.toLocaleString()}{' '} + {userCount === 1 ? 'smirker' : 'smirkers'} and counting +

+ )} {/* Initial state - Connect button */} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index cb18d05..a267255 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -8,6 +8,8 @@ import { getLinkedSocials, registerSocial, unlinkSocial, + getMyUsername, + setUsername, type MeResponse, type LinkedSocial, } from '@/lib/api'; @@ -20,6 +22,13 @@ export default function SettingsPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + // Username state + const [smirkUsername, setSmirkUsername] = useState(''); + const [editingUsername, setEditingUsername] = useState(false); + const [usernameInput, setUsernameInput] = useState(''); + const [usernameError, setUsernameError] = useState(null); + const [savingUsername, setSavingUsername] = useState(false); + // Telegram linking state const [linkStep, setLinkStep] = useState('idle'); const [telegramUsername, setTelegramUsername] = useState(''); @@ -35,15 +44,43 @@ export default function SettingsPage() { return; } - Promise.all([getMe(token), getLinkedSocials(token)]) - .then(([userData, socialsData]) => { + Promise.all([getMe(token), getLinkedSocials(token), getMyUsername(token)]) + .then(([userData, socialsData, username]) => { setUser(userData); setSocials(socialsData.socials); + setSmirkUsername(username || ''); + setUsernameInput(username || ''); }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); }, [token]); + const handleSaveUsername = async () => { + if (!token) return; + const trimmed = usernameInput.trim().toLowerCase(); + + if (trimmed.length < 3 || trimmed.length > 32) { + setUsernameError('Username must be 3-32 characters'); + return; + } + if (!/^[a-z0-9_]+$/.test(trimmed)) { + setUsernameError('Only lowercase letters, numbers, and underscores allowed'); + return; + } + + setSavingUsername(true); + setUsernameError(null); + try { + const result = await setUsername(token, trimmed); + setSmirkUsername(result.username); + setEditingUsername(false); + } catch (err) { + setUsernameError(err instanceof Error ? err.message : 'Failed to save username'); + } finally { + setSavingUsername(false); + } + }; + const handleStartLink = () => { setLinkStep('entering'); setLinkError(null); @@ -154,6 +191,70 @@ export default function SettingsPage() { + {/* Smirk Username */} +
+

Smirk Username

+
+ {!editingUsername ? ( +
+
+ {smirkUsername ? ( + <> +

Your username

+

@{smirkUsername}

+ + ) : ( +

No username set

+ )} +
+ +
+ ) : ( +
+ +
+ setUsernameInput(e.target.value.toLowerCase())} + placeholder="my_username" + maxLength={32} + className="flex-1 px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:border-[#fbeb0a]" + /> + + +
+ {usernameError &&

{usernameError}

} +

+ Others can tip you directly using your Smirk username +

+
+ )} +
+
+ {/* Linked Accounts */}

Linked Accounts

diff --git a/src/lib/api.ts b/src/lib/api.ts index efb8072..3aab09c 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -256,3 +256,73 @@ export async function getClaimableTips(token: string): Promise<{ tips: Claimable return res.json(); } + +// ============================================================================= +// Username Management +// ============================================================================= + +export async function getMyUsername(token: string): Promise { + const res = await fetch(`${API_URL}/api/v1/users/me/username`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!res.ok) { + throw new Error('Failed to get username'); + } + + return res.json(); +} + +export async function setUsername(token: string, username: string): Promise<{ username: string }> { + const res = await fetch(`${API_URL}/api/v1/users/me/username`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ username }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || 'Failed to set username'); + } + + return res.json(); +} + +export interface UserLookupResponse { + user_id: string; + username: string; +} + +export async function lookupByUsername(username: string): Promise { + const res = await fetch(`${API_URL}/api/v1/users/by-username/${encodeURIComponent(username)}`); + + if (!res.ok) { + if (res.status === 404) { + throw new Error('User not found'); + } + throw new Error('Failed to lookup user'); + } + + return res.json(); +} + +// ============================================================================= +// User Count (Public) +// ============================================================================= + +export interface UserCountResponse { + count: number; +} + +export async function getUserCount(): Promise { + const res = await fetch(`${API_URL}/api/v1/users/count`); + + if (!res.ok) { + throw new Error('Failed to get user count'); + } + + return res.json(); +}