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:
parent
16e943d4ef
commit
a23225cad4
5 changed files with 49 additions and 30 deletions
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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' }}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue