Add tour countdown badges + fix/standardize NIP-05 provider prompts
Countdown badges (upcoming tours): - New getCountdown() in TourCard: "In 3 days" / "Tomorrow" / "In 2 weeks" / "Happening now" (pulsing) based on start/end dates, category-tinted - Shown on both compact and full TourCard, and in the TourDetailPage schedule card NIP-05 provider fix (removes obsolete nip05.social): - New single source of truth src/lib/nip05Providers.ts with working, curated providers: nostrcheck.me + nostr.directory (free), Nostr Plebs + nostr.band (paid) - New reusable Nip05Required component (full-page "gate" + inline "banner" variants) listing those providers and deep-linking to /settings?section=nip05 - Wired into CreateTourPage (gate), CreateListingPage (banner) and all four Become* pages (Coach/Filmer/Photographer/Builder) for a consistent prompt - SettingsPage NIP-05 screen now reads from the shared provider list - Updated internal planning docs to match Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
1c295e96e2
commit
5d96f8dfa8
13 changed files with 344 additions and 155 deletions
|
|
@ -40,8 +40,8 @@
|
||||||
|
|
||||||
### ✅ Phase 5: NIP-05 Providers Guide ($1.50)
|
### ✅ Phase 5: NIP-05 Providers Guide ($1.50)
|
||||||
- Added NIP-05 section to Settings profile tab
|
- Added NIP-05 section to Settings profile tab
|
||||||
- **Free providers**: nostr.directory, nip05.social
|
- **Free providers**: nostrcheck.me, nostr.directory
|
||||||
- **Paid providers**: nostrplebs.com ($3/mo), nostr.band (one-time)
|
- **Paid providers**: Nostr Plebs, nostr.band
|
||||||
- All providers clickable with descriptions
|
- All providers clickable with descriptions
|
||||||
- Includes helpful tip about adding NIP-05 to profile
|
- Includes helpful tip about adding NIP-05 to profile
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -180,18 +180,18 @@
|
||||||
**Steps**:
|
**Steps**:
|
||||||
1. Create NIP-05 section in SettingsPage
|
1. Create NIP-05 section in SettingsPage
|
||||||
2. List 4 providers:
|
2. List 4 providers:
|
||||||
- **Free**: nostr.directory, nip05.social
|
- **Free**: nostrcheck.me, nostr.directory
|
||||||
- **Paid**: nostrplebs.com, nostr.band
|
- **Paid**: Nostr Plebs, nostr.band
|
||||||
3. Add links and brief descriptions
|
3. Add links and brief descriptions
|
||||||
4. Show current NIP-05 if set
|
4. Show current NIP-05 if set
|
||||||
5. Add "Test your NIP-05" button
|
5. Add "Test your NIP-05" button
|
||||||
6. Style with neon theme
|
6. Style with neon theme
|
||||||
|
|
||||||
**Providers to Include**:
|
**Providers to Include** (single source of truth: `src/lib/nip05Providers.ts`):
|
||||||
|
- nostrcheck.me (Free)
|
||||||
- nostr.directory (Free)
|
- nostr.directory (Free)
|
||||||
- nip05.social (Free)
|
- Nostr Plebs (Paid)
|
||||||
- nostrplebs.com (Paid - $3/month)
|
- nostr.band (Paid)
|
||||||
- nostr.band (Paid - one-time)
|
|
||||||
|
|
||||||
**Files**: `src/pages/SettingsPage.tsx`
|
**Files**: `src/pages/SettingsPage.tsx`
|
||||||
|
|
||||||
|
|
|
||||||
138
src/components/Nip05Required.tsx
Normal file
138
src/components/Nip05Required.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { ShieldCheck, ExternalLink, AlertTriangle } from 'lucide-react';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { NIP05_PROVIDERS } from '@/lib/nip05Providers';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface Nip05RequiredProps {
|
||||||
|
/** What the user is trying to do, e.g. "tour organizers", "sellers". */
|
||||||
|
role?: string;
|
||||||
|
/** Extra sentence explaining why it matters for this feature. */
|
||||||
|
reason?: string;
|
||||||
|
/** "gate" = full centered card (blocks the page). "banner" = inline notice. */
|
||||||
|
variant?: 'gate' | 'banner';
|
||||||
|
/** Accent hex color. Defaults to the app red used for required gates. */
|
||||||
|
color?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consistent "NIP-05 verification required" prompt used across create/become
|
||||||
|
* flows. Lists the curated, working providers (see lib/nip05Providers) and
|
||||||
|
* links to Settings. Single source of truth so obsolete providers never linger.
|
||||||
|
*/
|
||||||
|
export function Nip05Required({
|
||||||
|
role = 'creators',
|
||||||
|
reason,
|
||||||
|
variant = 'gate',
|
||||||
|
color = '#ef4444',
|
||||||
|
className,
|
||||||
|
}: Nip05RequiredProps) {
|
||||||
|
const providerList = (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-[11px] font-black uppercase tracking-widest text-gray-400">
|
||||||
|
Get a free / cheap NIP-05
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
{NIP05_PROVIDERS.map((p) => (
|
||||||
|
<a
|
||||||
|
key={p.name}
|
||||||
|
href={p.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group flex items-center justify-between gap-2 rounded-lg border border-white/10 bg-black/40 p-3 text-left transition-all hover:border-white/30 hover:bg-white/5"
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-1.5 text-sm font-bold text-white">
|
||||||
|
<span className="truncate">{p.name}</span>
|
||||||
|
<ExternalLink className="h-3 w-3 flex-shrink-0 text-gray-500 group-hover:text-gray-300" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 truncate">{p.desc}</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'flex-shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-bold',
|
||||||
|
p.free
|
||||||
|
? 'border-green-500/30 bg-green-500/15 text-green-400'
|
||||||
|
: 'border-yellow-500/30 bg-yellow-500/15 text-yellow-400'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{p.free ? 'Free' : 'Paid'}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variant === 'banner') {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={cn('border-2', className)}
|
||||||
|
style={{ borderColor: `${color}55`, backgroundColor: `${color}12` }}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4 space-y-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertTriangle className="h-5 w-5 flex-shrink-0 mt-0.5" style={{ color }} />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold mb-1" style={{ color }}>
|
||||||
|
NIP-05 Verification Required
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-300">
|
||||||
|
{reason ??
|
||||||
|
`To prevent spam and build trust, ${role} need a verified Nostr address (NIP-05).`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{providerList}
|
||||||
|
<Link to="/settings?section=nip05">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="font-bold"
|
||||||
|
style={{ borderColor: `${color}55`, color }}
|
||||||
|
>
|
||||||
|
Add NIP-05 in Settings
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={cn('max-w-xl mx-auto border-2', className)}
|
||||||
|
style={{ borderColor: `${color}55`, backgroundColor: `${color}10` }}
|
||||||
|
>
|
||||||
|
<CardContent className="py-10 px-6 text-center">
|
||||||
|
<div
|
||||||
|
className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||||
|
style={{ backgroundColor: `${color}22` }}
|
||||||
|
>
|
||||||
|
<ShieldCheck className="h-8 w-8" style={{ color }} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-bold mb-2" style={{ color }}>
|
||||||
|
NIP-05 Verification Required
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-300 mb-2">
|
||||||
|
{reason ??
|
||||||
|
`To prevent spam and build trust, ${role} must have NIP-05 verification.`}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 text-sm mb-6">
|
||||||
|
NIP-05 proves your identity by linking your Nostr profile to a domain — like{' '}
|
||||||
|
<span className="text-gray-300">you@example.com</span>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="max-w-md mx-auto mb-6">{providerList}</div>
|
||||||
|
|
||||||
|
<Link to="/settings?section=nip05">
|
||||||
|
<Button className="bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold">
|
||||||
|
Update Your Profile
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -84,6 +84,49 @@ function formatDateRange(startDate?: number, endDate?: number, withTime = false)
|
||||||
return `${startStr} - ${endStr}`;
|
return `${startStr} - ${endStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A short, human countdown for an upcoming (or live) tour based on its start
|
||||||
|
* date. Returns null when there's no date or the tour is well in the past.
|
||||||
|
*/
|
||||||
|
function getCountdown(
|
||||||
|
tour: SkateTour
|
||||||
|
): { label: string; color: string; live?: boolean } | null {
|
||||||
|
if (!tour.startDate) return null;
|
||||||
|
|
||||||
|
const nowSecs = Date.now() / 1000;
|
||||||
|
const start = tour.startDate;
|
||||||
|
const end = tour.endDate;
|
||||||
|
const accent = TOUR_CATEGORIES[tour.category].color;
|
||||||
|
|
||||||
|
// Currently running (has an end date and we're inside the window).
|
||||||
|
if (start <= nowSecs && end && nowSecs <= end) {
|
||||||
|
return { label: 'Happening now', color: '#ff0040', live: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already started but single-session / no end — treat same-day as live, else past.
|
||||||
|
if (start <= nowSecs) {
|
||||||
|
if (nowSecs - start < 12 * 60 * 60) {
|
||||||
|
return { label: 'Happening now', color: '#ff0040', live: true };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffSecs = start - nowSecs;
|
||||||
|
const days = Math.floor(diffSecs / 86400);
|
||||||
|
const hours = Math.floor(diffSecs / 3600);
|
||||||
|
|
||||||
|
if (hours < 1) return { label: 'Starting soon', color: accent, live: true };
|
||||||
|
if (hours < 24) return { label: `In ${hours}h`, color: accent };
|
||||||
|
if (days === 1) return { label: 'Tomorrow', color: accent };
|
||||||
|
if (days < 7) return { label: `In ${days} days`, color: accent };
|
||||||
|
if (days < 30) {
|
||||||
|
const w = Math.round(days / 7);
|
||||||
|
return { label: `In ${w} week${w > 1 ? 's' : ''}`, color: accent };
|
||||||
|
}
|
||||||
|
const m = Math.round(days / 30);
|
||||||
|
return { label: `In ${m} month${m > 1 ? 's' : ''}`, color: accent };
|
||||||
|
}
|
||||||
|
|
||||||
function getTourStatus(tour: SkateTour): { label: string; color: string } | null {
|
function getTourStatus(tour: SkateTour): { label: string; color: string } | null {
|
||||||
if (tour.category !== 'destruction') return null;
|
if (tour.category !== 'destruction') return null;
|
||||||
|
|
||||||
|
|
@ -110,6 +153,7 @@ export function TourCard({ tour, compact = false }: TourCardProps) {
|
||||||
const categoryInfo = TOUR_CATEGORIES[tour.category];
|
const categoryInfo = TOUR_CATEGORIES[tour.category];
|
||||||
const skillInfo = tour.skillLevel ? TOUR_SKILL_LEVELS[tour.skillLevel as keyof typeof TOUR_SKILL_LEVELS] : null;
|
const skillInfo = tour.skillLevel ? TOUR_SKILL_LEVELS[tour.skillLevel as keyof typeof TOUR_SKILL_LEVELS] : null;
|
||||||
const tourStatus = getTourStatus(tour);
|
const tourStatus = getTourStatus(tour);
|
||||||
|
const countdown = getCountdown(tour);
|
||||||
const isOwner = user?.pubkey === tour.pubkey;
|
const isOwner = user?.pubkey === tour.pubkey;
|
||||||
|
|
||||||
// Get location text based on category
|
// Get location text based on category
|
||||||
|
|
@ -156,6 +200,17 @@ export function TourCard({ tour, compact = false }: TourCardProps) {
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
<ProBadge featuredUntil={tour.featuredUntil} compact />
|
<ProBadge featuredUntil={tour.featuredUntil} compact />
|
||||||
|
{countdown && (
|
||||||
|
<Badge
|
||||||
|
className={cn(
|
||||||
|
'text-[10px] border-0 font-bold uppercase',
|
||||||
|
countdown.live && 'animate-pulse'
|
||||||
|
)}
|
||||||
|
style={{ backgroundColor: countdown.color, color: countdown.color === '#ccff00' ? 'black' : 'white' }}
|
||||||
|
>
|
||||||
|
{countdown.label}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
{tourStatus && (
|
{tourStatus && (
|
||||||
<Badge
|
<Badge
|
||||||
className="text-xs border-0 font-bold uppercase"
|
className="text-xs border-0 font-bold uppercase"
|
||||||
|
|
@ -250,6 +305,21 @@ export function TourCard({ tour, compact = false }: TourCardProps) {
|
||||||
{/* Status/Price overlay */}
|
{/* Status/Price overlay */}
|
||||||
<div className="absolute top-3 right-3 flex flex-col gap-1 items-end">
|
<div className="absolute top-3 right-3 flex flex-col gap-1 items-end">
|
||||||
<ProBadge featuredUntil={tour.featuredUntil} />
|
<ProBadge featuredUntil={tour.featuredUntil} />
|
||||||
|
{countdown && (
|
||||||
|
<Badge
|
||||||
|
className={cn(
|
||||||
|
'backdrop-blur-sm shadow-lg font-bold text-xs uppercase',
|
||||||
|
countdown.live && 'animate-pulse'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${countdown.color}ee`,
|
||||||
|
color: countdown.color === '#ccff00' ? 'black' : 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Calendar className="h-3 w-3 mr-1" />
|
||||||
|
{countdown.label}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
{tourStatus && (
|
{tourStatus && (
|
||||||
<Badge
|
<Badge
|
||||||
className="backdrop-blur-sm shadow-lg font-bold text-xs uppercase"
|
className="backdrop-blur-sm shadow-lg font-bold text-xs uppercase"
|
||||||
|
|
|
||||||
44
src/lib/nip05Providers.ts
Normal file
44
src/lib/nip05Providers.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* Curated list of NIP-05 verification providers.
|
||||||
|
*
|
||||||
|
* Kept in one place so every "NIP-05 required" prompt across the app points at
|
||||||
|
* the same up-to-date, working services. Ordered best-first.
|
||||||
|
*
|
||||||
|
* Note: nip05.social was removed — it is obsolete / no longer reliable.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Nip05Provider {
|
||||||
|
name: string;
|
||||||
|
/** Short human description shown under the name. */
|
||||||
|
desc: string;
|
||||||
|
url: string;
|
||||||
|
/** true = has a free tier, false = paid only. */
|
||||||
|
free: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NIP05_PROVIDERS: Nip05Provider[] = [
|
||||||
|
{
|
||||||
|
name: 'nostrcheck.me',
|
||||||
|
desc: 'Free NIP-05 + media hosting',
|
||||||
|
url: 'https://nostrcheck.me',
|
||||||
|
free: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nostr.directory',
|
||||||
|
desc: 'Free NIP-05 via a tweet',
|
||||||
|
url: 'https://nostr.directory',
|
||||||
|
free: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Nostr Plebs',
|
||||||
|
desc: '~$5/yr · custom & shared domains',
|
||||||
|
url: 'https://nostrplebs.com',
|
||||||
|
free: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nostr.band',
|
||||||
|
desc: 'Paid · analytics included',
|
||||||
|
url: 'https://nostr.band',
|
||||||
|
free: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -18,6 +18,7 @@ import { useToast } from '@/hooks/useToast';
|
||||||
import { useWallet } from '@/hooks/useWallet';
|
import { useWallet } from '@/hooks/useWallet';
|
||||||
import { WalletModal } from '@/components/WalletModal';
|
import { WalletModal } from '@/components/WalletModal';
|
||||||
import { LoginArea } from '@/components/auth/LoginArea';
|
import { LoginArea } from '@/components/auth/LoginArea';
|
||||||
|
import { Nip05Required } from '@/components/Nip05Required';
|
||||||
import { SpotPicker } from '@/components/SpotPicker';
|
import { SpotPicker } from '@/components/SpotPicker';
|
||||||
import { NofferField } from '@/components/NofferField';
|
import { NofferField } from '@/components/NofferField';
|
||||||
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
||||||
|
|
@ -275,24 +276,13 @@ export default function BecomeBuilderPage() {
|
||||||
<p className="text-gray-400 mt-1">Pour it, weld it, build it — list your ramp & park building</p>
|
<p className="text-gray-400 mt-1">Pour it, weld it, build it — list your ramp & park building</p>
|
||||||
|
|
||||||
{!hasNip05 && (
|
{!hasNip05 && (
|
||||||
<Card className="mt-4 bg-yellow-950/30 border-2 border-yellow-500/50">
|
<Nip05Required
|
||||||
<CardContent className="p-4">
|
variant="banner"
|
||||||
<div className="flex items-start gap-3">
|
color="#eab308"
|
||||||
<div className="text-yellow-500">⚠️</div>
|
role="builders"
|
||||||
<div>
|
reason="Add a NIP-05 identifier to verify your identity and build trust with clients."
|
||||||
<h3 className="font-bold text-yellow-400 mb-1">NIP-05 Verification Required</h3>
|
className="mt-4"
|
||||||
<p className="text-sm text-gray-300 mb-2">
|
/>
|
||||||
Add a NIP-05 identifier to your profile to verify your identity and build trust with clients.
|
|
||||||
</p>
|
|
||||||
<Link to="/settings">
|
|
||||||
<Button variant="outline" size="sm" className="border-yellow-500/50 hover:bg-yellow-500/10 text-yellow-400 font-bold">
|
|
||||||
Add NIP-05 in Settings
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Wallet Requirement */}
|
{/* Wallet Requirement */}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { useToast } from '@/hooks/useToast';
|
||||||
import { useWallet } from '@/hooks/useWallet';
|
import { useWallet } from '@/hooks/useWallet';
|
||||||
import { WalletModal } from '@/components/WalletModal';
|
import { WalletModal } from '@/components/WalletModal';
|
||||||
import { LoginArea } from '@/components/auth/LoginArea';
|
import { LoginArea } from '@/components/auth/LoginArea';
|
||||||
|
import { Nip05Required } from '@/components/Nip05Required';
|
||||||
import { SpotPicker } from '@/components/SpotPicker';
|
import { SpotPicker } from '@/components/SpotPicker';
|
||||||
import { NofferField } from '@/components/NofferField';
|
import { NofferField } from '@/components/NofferField';
|
||||||
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
||||||
|
|
@ -226,24 +227,13 @@ export default function BecomeCoachPage() {
|
||||||
<p className="text-gray-400 mt-1">Share your skateboarding knowledge</p>
|
<p className="text-gray-400 mt-1">Share your skateboarding knowledge</p>
|
||||||
|
|
||||||
{!hasNip05 && (
|
{!hasNip05 && (
|
||||||
<Card className="mt-4 bg-yellow-950/30 border-2 border-yellow-500/50">
|
<Nip05Required
|
||||||
<CardContent className="p-4">
|
variant="banner"
|
||||||
<div className="flex items-start gap-3">
|
color="#eab308"
|
||||||
<div className="text-yellow-500">⚠️</div>
|
role="coaches"
|
||||||
<div>
|
reason="Add a NIP-05 identifier to verify your identity and build trust with students — it shows them you're a legitimate coach."
|
||||||
<h3 className="font-bold text-yellow-400 mb-1">NIP-05 Verification Required</h3>
|
className="mt-4"
|
||||||
<p className="text-sm text-gray-300 mb-2">
|
/>
|
||||||
Add a NIP-05 identifier to your profile to verify your identity and build trust with students. This helps students know you're a legitimate coach.
|
|
||||||
</p>
|
|
||||||
<Link to="/settings">
|
|
||||||
<Button variant="outline" size="sm" className="border-yellow-500/50 hover:bg-yellow-500/10 text-yellow-400 font-bold">
|
|
||||||
Add NIP-05 in Settings
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Wallet Requirement */}
|
{/* Wallet Requirement */}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { useToast } from '@/hooks/useToast';
|
||||||
import { useWallet } from '@/hooks/useWallet';
|
import { useWallet } from '@/hooks/useWallet';
|
||||||
import { WalletModal } from '@/components/WalletModal';
|
import { WalletModal } from '@/components/WalletModal';
|
||||||
import { LoginArea } from '@/components/auth/LoginArea';
|
import { LoginArea } from '@/components/auth/LoginArea';
|
||||||
|
import { Nip05Required } from '@/components/Nip05Required';
|
||||||
import { SpotPicker } from '@/components/SpotPicker';
|
import { SpotPicker } from '@/components/SpotPicker';
|
||||||
import { NofferField } from '@/components/NofferField';
|
import { NofferField } from '@/components/NofferField';
|
||||||
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
||||||
|
|
@ -242,24 +243,13 @@ export default function BecomeFilmerPage() {
|
||||||
<p className="text-gray-400 mt-1">Offer your skate video filming — clips, lines, full parts & contest coverage</p>
|
<p className="text-gray-400 mt-1">Offer your skate video filming — clips, lines, full parts & contest coverage</p>
|
||||||
|
|
||||||
{!hasNip05 && (
|
{!hasNip05 && (
|
||||||
<Card className="mt-4 bg-yellow-950/30 border-2 border-yellow-500/50">
|
<Nip05Required
|
||||||
<CardContent className="p-4">
|
variant="banner"
|
||||||
<div className="flex items-start gap-3">
|
color="#eab308"
|
||||||
<div className="text-yellow-500">⚠️</div>
|
role="filmers"
|
||||||
<div>
|
reason="Add a NIP-05 identifier to verify your identity and build trust with clients — it shows them you're a legitimate filmer."
|
||||||
<h3 className="font-bold text-yellow-400 mb-1">NIP-05 Verification Required</h3>
|
className="mt-4"
|
||||||
<p className="text-sm text-gray-300 mb-2">
|
/>
|
||||||
Add a NIP-05 identifier to your profile to verify your identity and build trust with clients. This helps clients know you're a legitimate filmer.
|
|
||||||
</p>
|
|
||||||
<Link to="/settings">
|
|
||||||
<Button variant="outline" size="sm" className="border-yellow-500/50 hover:bg-yellow-500/10 text-yellow-400 font-bold">
|
|
||||||
Add NIP-05 in Settings
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Wallet Requirement */}
|
{/* Wallet Requirement */}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { useToast } from '@/hooks/useToast';
|
||||||
import { useWallet } from '@/hooks/useWallet';
|
import { useWallet } from '@/hooks/useWallet';
|
||||||
import { WalletModal } from '@/components/WalletModal';
|
import { WalletModal } from '@/components/WalletModal';
|
||||||
import { LoginArea } from '@/components/auth/LoginArea';
|
import { LoginArea } from '@/components/auth/LoginArea';
|
||||||
|
import { Nip05Required } from '@/components/Nip05Required';
|
||||||
import { SpotPicker } from '@/components/SpotPicker';
|
import { SpotPicker } from '@/components/SpotPicker';
|
||||||
import { NofferField } from '@/components/NofferField';
|
import { NofferField } from '@/components/NofferField';
|
||||||
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
||||||
|
|
@ -292,24 +293,13 @@ export default function BecomePhotographerPage() {
|
||||||
<p className="text-gray-400 mt-1">Shoot the sessions — offer your skate photography</p>
|
<p className="text-gray-400 mt-1">Shoot the sessions — offer your skate photography</p>
|
||||||
|
|
||||||
{!hasNip05 && (
|
{!hasNip05 && (
|
||||||
<Card className="mt-4 bg-yellow-950/30 border-2 border-yellow-500/50">
|
<Nip05Required
|
||||||
<CardContent className="p-4">
|
variant="banner"
|
||||||
<div className="flex items-start gap-3">
|
color="#eab308"
|
||||||
<div className="text-yellow-500">⚠️</div>
|
role="photographers"
|
||||||
<div>
|
reason="Add a NIP-05 identifier to verify your identity and build trust with clients."
|
||||||
<h3 className="font-bold text-yellow-400 mb-1">NIP-05 Verification Required</h3>
|
className="mt-4"
|
||||||
<p className="text-sm text-gray-300 mb-2">
|
/>
|
||||||
Add a NIP-05 identifier to your profile to verify your identity and build trust with clients.
|
|
||||||
</p>
|
|
||||||
<Link to="/settings">
|
|
||||||
<Button variant="outline" size="sm" className="border-yellow-500/50 hover:bg-yellow-500/10 text-yellow-400 font-bold">
|
|
||||||
Add NIP-05 in Settings
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Wallet Requirement */}
|
{/* Wallet Requirement */}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||||
import { useSkateSpot, useSkateSpots } from '@/hooks/useSkateSpots';
|
import { useSkateSpot, useSkateSpots } from '@/hooks/useSkateSpots';
|
||||||
import { useListingRestrictions, LISTING_FEE_SATS, recordListingCreation } from '@/hooks/useListingRestrictions';
|
import { useListingRestrictions, LISTING_FEE_SATS, recordListingCreation } from '@/hooks/useListingRestrictions';
|
||||||
import { LoginArea } from '@/components/auth/LoginArea';
|
import { LoginArea } from '@/components/auth/LoginArea';
|
||||||
|
import { Nip05Required } from '@/components/Nip05Required';
|
||||||
import { SpamFeePayment } from '@/components/SpamFeePayment';
|
import { SpamFeePayment } from '@/components/SpamFeePayment';
|
||||||
import {
|
import {
|
||||||
MARKETPLACE_ITEM_KIND,
|
MARKETPLACE_ITEM_KIND,
|
||||||
|
|
@ -430,28 +431,12 @@ export default function CreateListingPage() {
|
||||||
|
|
||||||
{/* NIP-05 Requirement Notice - never shown to platform owners */}
|
{/* NIP-05 Requirement Notice - never shown to platform owners */}
|
||||||
{!hasNip05 && !restrictions.isPlatformOwner && (
|
{!hasNip05 && !restrictions.isPlatformOwner && (
|
||||||
<Card className="mb-6 bg-red-950/30 border-2 border-red-500/50">
|
<Nip05Required
|
||||||
<CardContent className="p-4">
|
variant="banner"
|
||||||
<div className="flex items-start gap-3">
|
role="sellers"
|
||||||
<AlertTriangle className="h-5 w-5 text-red-400 flex-shrink-0 mt-0.5" />
|
reason="To prevent spam and protect buyers, you need a verified Nostr address (NIP-05) to list gear."
|
||||||
<div>
|
className="mb-6"
|
||||||
<h3 className="font-bold text-red-400 mb-1">NIP-05 Verification Required</h3>
|
/>
|
||||||
<p className="text-sm text-gray-300 mb-3">
|
|
||||||
To prevent spam and protect buyers, you need a verified Nostr address (NIP-05) to list gear.
|
|
||||||
</p>
|
|
||||||
<Link to="/settings">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="border-red-500/50 hover:bg-red-500/10 text-red-400"
|
|
||||||
>
|
|
||||||
Add NIP-05 in Settings
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasNip05 && (
|
{hasNip05 && (
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { useShops } from '@/hooks/useShops';
|
||||||
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { LoginArea } from '@/components/auth/LoginArea';
|
import { LoginArea } from '@/components/auth/LoginArea';
|
||||||
|
import { Nip05Required } from '@/components/Nip05Required';
|
||||||
import { FeaturedUpgrade } from '@/components/FeaturedEventUpgrade';
|
import { FeaturedUpgrade } from '@/components/FeaturedEventUpgrade';
|
||||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||||
import { SKATE_TOUR_KIND, TOUR_SKILL_LEVELS, TOUR_SPOT_TYPES, TOUR_CATEGORIES, type TourCategory, type DestructionChallenge, type SkateTour } from '@/lib/types';
|
import { SKATE_TOUR_KIND, TOUR_SKILL_LEVELS, TOUR_SPOT_TYPES, TOUR_CATEGORIES, type TourCategory, type DestructionChallenge, type SkateTour } from '@/lib/types';
|
||||||
|
|
@ -924,50 +925,7 @@ export default function CreateTourPage({ existing }: { existing?: SkateTour } =
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="container px-4 py-8">
|
<div className="container px-4 py-8">
|
||||||
<Card className="max-w-xl mx-auto border-2 border-red-500/50 bg-red-950/20">
|
<Nip05Required role="tour organizers" />
|
||||||
<CardContent className="py-12 text-center">
|
|
||||||
<div className="w-16 h-16 rounded-full bg-red-500/20 flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Route className="h-8 w-8 text-red-400" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-xl font-bold mb-2 text-red-400">NIP-05 Verification Required</h2>
|
|
||||||
<p className="text-gray-300 mb-2">
|
|
||||||
To prevent spam and build trust, tour organizers must have NIP-05 verification.
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-500 text-sm mb-6">
|
|
||||||
NIP-05 proves your identity by linking your Nostr profile to a domain you control.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-3 max-w-sm mx-auto mb-6">
|
|
||||||
<p className="text-xs text-gray-400 font-bold uppercase">Get Free NIP-05:</p>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<a
|
|
||||||
href="https://nip05.social"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="block p-3 rounded-lg bg-black/50 border border-green-500/30 hover:border-green-500/60 transition-all text-left"
|
|
||||||
>
|
|
||||||
<div className="font-bold text-white text-sm">nip05.social</div>
|
|
||||||
<div className="text-xs text-gray-400">Free NIP-05</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://nostr.directory"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="block p-3 rounded-lg bg-black/50 border border-purple-500/30 hover:border-purple-500/60 transition-all text-left"
|
|
||||||
>
|
|
||||||
<div className="font-bold text-white text-sm">nostr.directory</div>
|
|
||||||
<div className="text-xs text-gray-400">Free NIP-05</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link to="/settings">
|
|
||||||
<Button className="bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold">
|
|
||||||
Update Your Profile
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { useAppContext } from '@/hooks/useAppContext';
|
||||||
import { useCitrine } from '@/hooks/useCitrine';
|
import { useCitrine } from '@/hooks/useCitrine';
|
||||||
import { toast } from '@/hooks/useToast';
|
import { toast } from '@/hooks/useToast';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { NIP05_PROVIDERS } from '@/lib/nip05Providers';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
|
|
@ -219,12 +220,7 @@ function ProfileScreen({ onBack }: { onBack: () => void }) {
|
||||||
// ─── NIP-05 sub-screen ────────────────────────────────────────────────────────
|
// ─── NIP-05 sub-screen ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function Nip05Screen({ onBack }: { onBack: () => void }) {
|
function Nip05Screen({ onBack }: { onBack: () => void }) {
|
||||||
const providers = [
|
const providers = NIP05_PROVIDERS;
|
||||||
{ name: 'nostr.directory', desc: 'Free NIP-05 addresses', url: 'https://nostr.directory', free: true },
|
|
||||||
{ name: 'nip05.social', desc: 'Free verification service', url: 'https://nip05.social', free: true },
|
|
||||||
{ name: 'nostrplebs.com', desc: '~$3/month · Custom domains', url: 'https://nostrplebs.com', free: false },
|
|
||||||
{ name: 'nostr.band', desc: 'One-time fee · Analytics included', url: 'https://nostr.band', free: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,28 @@ export default function TourDetailPage() {
|
||||||
return `${tour.duration} hour${tour.duration > 1 ? 's' : ''}`;
|
return `${tour.duration} hour${tour.duration > 1 ? 's' : ''}`;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const countdownLabel = (() => {
|
||||||
|
if (!tour.startDate) return null;
|
||||||
|
const nowSecs = Date.now() / 1000;
|
||||||
|
if (tour.startDate <= nowSecs) {
|
||||||
|
if (tour.endDate && nowSecs <= tour.endDate) return { text: 'Happening now', live: true };
|
||||||
|
if (nowSecs - tour.startDate < 12 * 60 * 60) return { text: 'Happening now', live: true };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const days = Math.floor((tour.startDate - nowSecs) / 86400);
|
||||||
|
const hours = Math.floor((tour.startDate - nowSecs) / 3600);
|
||||||
|
if (hours < 1) return { text: 'Starting soon', live: true };
|
||||||
|
if (hours < 24) return { text: `In ${hours}h`, live: false };
|
||||||
|
if (days === 1) return { text: 'Tomorrow', live: false };
|
||||||
|
if (days < 7) return { text: `In ${days} days`, live: false };
|
||||||
|
if (days < 30) {
|
||||||
|
const w = Math.round(days / 7);
|
||||||
|
return { text: `In ${w} week${w > 1 ? 's' : ''}`, live: false };
|
||||||
|
}
|
||||||
|
const m = Math.round(days / 30);
|
||||||
|
return { text: `In ${m} month${m > 1 ? 's' : ''}`, live: false };
|
||||||
|
})();
|
||||||
|
|
||||||
const calendarEvent = tour.startDate
|
const calendarEvent = tour.startDate
|
||||||
? {
|
? {
|
||||||
uid: `${SKATE_TOUR_KIND}:${tour.pubkey}:${tour.identifier}`,
|
uid: `${SKATE_TOUR_KIND}:${tour.pubkey}:${tour.identifier}`,
|
||||||
|
|
@ -304,9 +326,25 @@ export default function TourDetailPage() {
|
||||||
<Calendar className="h-7 w-7" style={{ color: accent }} />
|
<Calendar className="h-7 w-7" style={{ color: accent }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<p className="text-[11px] font-black uppercase tracking-widest" style={{ color: accent }}>
|
<p className="text-[11px] font-black uppercase tracking-widest" style={{ color: accent }}>
|
||||||
{isLocalSession ? 'Scheduled Session' : tour.category === 'destruction' ? 'Competition Window' : 'Trip Dates'}
|
{isLocalSession ? 'Scheduled Session' : tour.category === 'destruction' ? 'Competition Window' : 'Trip Dates'}
|
||||||
</p>
|
</p>
|
||||||
|
{countdownLabel && (
|
||||||
|
<Badge
|
||||||
|
className={cn(
|
||||||
|
'border-0 text-[10px] font-bold uppercase',
|
||||||
|
countdownLabel.live && 'animate-pulse'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: countdownLabel.live ? '#ff0040' : accent,
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{countdownLabel.text}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<p className="text-white font-bold text-lg leading-tight">{formatSchedule()}</p>
|
<p className="text-white font-bold text-lg leading-tight">{formatSchedule()}</p>
|
||||||
{durationLabel && (
|
{durationLabel && (
|
||||||
<p className="text-xs text-gray-400 mt-0.5">Runs about {durationLabel}</p>
|
<p className="text-xs text-gray-400 mt-0.5">Runs about {durationLabel}</p>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue