ux: migrate more empty states + fill in missing page titles (U1/U2)

Empty states:
- LabPage and NotificationBell now use the shared <EmptyState> component.
- LabPage's logged-out "Log in to create a profile" prompt preserved via
  the hint slot; removed now-unused cn import.

Page titles (U2):
- Audited all page routes for document titles. Only 3 were missing:
  - EditTourPage: now sets "Edit <tour> - SkateSpots".
  - AmberCallbackPage: now sets "Signing in with Amber - SkateSpots".
  - NIP19Page ProfilePage: sets "<name> - SkateSpots" with about as
    description; other NIP-19 branches already delegate to NotFound /
    NewsletterIssueView which set their own titles.

ScrollToTop (U1): verified already correct — resets window scroll on every
pathname change, wired in AppRouter. No change needed.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-24 18:50:25 +07:00
parent 16e943d4ef
commit a23225cad4
5 changed files with 49 additions and 30 deletions

View file

@ -12,6 +12,7 @@ import { JoinRequestCard } from '@/components/JoinRequestCard';
import { CollectiveJoinRequestCard } from '@/components/CollectiveJoinRequestCard'; import { CollectiveJoinRequestCard } from '@/components/CollectiveJoinRequestCard';
import { ActivityNotificationCard } from '@/components/ActivityNotificationCard'; import { ActivityNotificationCard } from '@/components/ActivityNotificationCard';
import { ImageWithFallback } from '@/components/ImageWithFallback'; import { ImageWithFallback } from '@/components/ImageWithFallback';
import { EmptyState as SharedEmptyState } from '@/components/EmptyState';
import { useCrewNotifications, useIsPendingMember } from '@/hooks/useCrewNotifications'; import { useCrewNotifications, useIsPendingMember } from '@/hooks/useCrewNotifications';
import { useActivityNotifications } from '@/hooks/useActivityNotifications'; import { useActivityNotifications } from '@/hooks/useActivityNotifications';
import { useNotificationsRead } from '@/hooks/useNotificationsRead'; import { useNotificationsRead } from '@/hooks/useNotificationsRead';
@ -180,13 +181,12 @@ function NewsletterItem({
function EmptyState({ label }: { label: string }) { function EmptyState({ label }: { label: string }) {
return ( return (
<div className="flex flex-col items-center gap-2 py-10 text-center"> <SharedEmptyState
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-white/5"> icon={Bell}
<Bell className="h-5 w-5 text-gray-600" /> title="You're all caught up"
</div> hint={label}
<p className="text-sm font-bold text-gray-400">You're all caught up</p> className="py-10"
<p className="max-w-[220px] text-xs text-gray-600">{label}</p> />
</div>
); );
} }

View file

@ -11,6 +11,7 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useNavigate, Link } from 'react-router-dom'; import { useNavigate, Link } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
import { useLoginActions } from '@/hooks/useLoginActions'; import { useLoginActions } from '@/hooks/useLoginActions';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { import {
@ -34,6 +35,11 @@ export default function AmberCallbackPage() {
const [status, setStatus] = useState<Status>('processing'); const [status, setStatus] = useState<Status>('processing');
const [message, setMessage] = useState('Completing login with Amber...'); const [message, setMessage] = useState('Completing login with Amber...');
useSeoMeta({
title: 'Signing in with Amber - SkateSpots',
description: 'Completing your Amber (NIP-55) login.',
});
useEffect(() => { useEffect(() => {
if (hasProcessed.current) return; if (hasProcessed.current) return;
hasProcessed.current = true; hasProcessed.current = true;

View file

@ -1,4 +1,5 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
import { Layout } from '@/components/Layout'; import { Layout } from '@/components/Layout';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
@ -20,6 +21,11 @@ export default function EditTourPage() {
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const { data: tour, isLoading, error } = useTour(identifier || '', pubkey); const { data: tour, isLoading, error } = useTour(identifier || '', pubkey);
useSeoMeta({
title: tour?.title ? `Edit ${tour.title} - SkateSpots` : 'Edit Tour - SkateSpots',
description: 'Edit your skate tour — update stops, dates and details.',
});
if (isLoading) { if (isLoading) {
return ( return (
<Layout> <Layout>

View file

@ -71,7 +71,7 @@ import {
PersonStanding, PersonStanding,
ArrowRight, ArrowRight,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { EmptyState as SharedEmptyState } from '@/components/EmptyState';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { ProBadge } from '@/components/ProBadge'; import { ProBadge } from '@/components/ProBadge';
import { LabCrewsStrip } from '@/components/LabCrewsStrip'; import { LabCrewsStrip } from '@/components/LabCrewsStrip';
@ -1096,28 +1096,29 @@ function EmptyState({ serviceType, user }: { serviceType: ServiceType; user: boo
const Icon = c.icon; const Icon = c.icon;
return ( return (
<div className="flex flex-col items-center justify-center py-20 px-4 text-center"> <SharedEmptyState
<div className="w-16 h-16 rounded-2xl bg-white/5 flex items-center justify-center mb-4"> icon={Icon}
<Icon className="h-8 w-8 text-gray-600" /> title={c.title}
</div> hint={
<h3 className="text-lg font-black mb-2 text-white">{c.title}</h3> user ? (
<p className="text-gray-500 text-sm max-w-xs mb-4">{c.body}</p> c.body
{user ? ( ) : (
<Link to={c.to}> <>
<button className={cn('flex items-center gap-2 px-5 py-2.5 rounded-xl font-black text-sm uppercase tracking-wide text-white transition-all active:scale-95', c.color)}> {c.body}{' '}
<Plus className="h-4 w-4" /> <Link to="/settings" className="text-[#ccff00] underline underline-offset-2">
{c.cta} Log in
</button> </Link>{' '}
</Link> to create a profile.
) : ( </>
<p className="text-xs text-gray-600 mb-4"> )
<Link to="/settings" className="text-[#ccff00] underline underline-offset-2">Log in</Link> to create a profile }
</p> actions={
)} user
<Link to="/faq" className="mt-4 text-xs text-gray-600 hover:text-gray-400 underline underline-offset-2 transition-colors"> ? [{ label: c.cta, to: c.to, icon: Plus, className: c.color }]
Have questions? Check the FAQ → : []
</Link> }
</div> footer={{ label: 'Have questions? Check the FAQ →', to: '/faq' }}
/>
); );
} }

View file

@ -1,5 +1,6 @@
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
import { Layout } from '@/components/Layout'; import { Layout } from '@/components/Layout';
import { ImageWithFallback } from '@/components/ImageWithFallback'; import { ImageWithFallback } from '@/components/ImageWithFallback';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -164,6 +165,11 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
const npub = nip19.npubEncode(pubkey); const npub = nip19.npubEncode(pubkey);
const isOwnProfile = user?.pubkey === pubkey; const isOwnProfile = user?.pubkey === pubkey;
useSeoMeta({
title: `${displayName} - SkateSpots`,
description: about ? about.slice(0, 160) : `${displayName}'s profile on SkateSpots.`,
});
// At-a-glance skater stats for the summary strip. // At-a-glance skater stats for the summary strip.
const { data: riderStats } = useRiderStats(pubkey); const { data: riderStats } = useRiderStats(pubkey);
const memberSince = profileEventCreatedAt(author.data?.event?.created_at); const memberSince = profileEventCreatedAt(author.data?.event?.created_at);