Rework Lab fee into optional "Pro" upgrade (Option B, no paywall)
Basic Lab profiles are now always free to publish. The 5,000 sat fee is opt-in and unlocks a Pro badge + featured placement instead of gating publication behind a rate. - types.ts: `featuredUntil` field on all 5 Lab types + SkateEvent, parseFeaturedUntil() + isFeaturedActive() helpers, wired into parsers - usePlatformFee: exposes featuredUntilSeconds for the `featured` tag (platform owners get a 1yr window) - New ProBadge + LabProUpgrade (opt-in toggle → reveals PlatformFeePayment) - All 5 Become pages: removed rate-based fee gate and wallet-required gate; publish is free; when Pro is active a `["featured", "<expiry>"]` tag is added Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
77dc4b562c
commit
5c0d4715d5
9 changed files with 250 additions and 98 deletions
100
src/components/LabProUpgrade.tsx
Normal file
100
src/components/LabProUpgrade.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { Sparkles, Check } from 'lucide-react';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
|
||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface LabProUpgradeProps {
|
||||
/** Whether the user opted in to Pro. */
|
||||
enabled: boolean;
|
||||
onEnabledChange: (enabled: boolean) => void;
|
||||
/** Accent color class for the entity (e.g. "text-orange-400"). */
|
||||
accentText?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opt-in "Go Pro" upgrade for Lab profiles.
|
||||
*
|
||||
* Free profiles publish without this. Toggling it on reveals the Lightning fee
|
||||
* payment; once paid (or for platform owners), the profile is published with a
|
||||
* `featured` tag that unlocks the Pro badge + featured placement in The Lab.
|
||||
*
|
||||
* There is NO hard paywall — a basic profile is always free.
|
||||
*/
|
||||
export function LabProUpgrade({
|
||||
enabled,
|
||||
onEnabledChange,
|
||||
accentText = 'text-amber-400',
|
||||
className,
|
||||
}: LabProUpgradeProps) {
|
||||
const { isActive, isPlatformOwner, amountSats, daysRemaining } = usePlatformFee('labProfile');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl border border-amber-500/30 bg-gradient-to-br from-amber-500/8 to-black/40 overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center flex-shrink-0 shadow-lg shadow-amber-500/25">
|
||||
<Sparkles className="h-4 w-4 text-black" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className={cn('font-black uppercase tracking-wide text-sm', accentText)}>
|
||||
Go Pro <span className="text-gray-500 font-normal normal-case">· optional</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
Free to list. Pro adds a <strong className="text-amber-400">Pro badge</strong> and
|
||||
{' '}<strong className="text-amber-400">featured placement</strong> at the top of The Lab.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{isActive ? (
|
||||
<span className="flex items-center gap-1 text-[11px] font-bold text-green-400 flex-shrink-0">
|
||||
<Check className="h-3.5 w-3.5" /> Active
|
||||
</span>
|
||||
) : (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={onEnabledChange}
|
||||
className="flex-shrink-0 data-[state=checked]:bg-amber-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Benefits list */}
|
||||
{!isActive && (
|
||||
<ul className="mt-3 grid gap-1.5 text-xs text-gray-400">
|
||||
<li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 flex-shrink-0" /> Gold "Pro" badge on your profile & cards</li>
|
||||
<li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 flex-shrink-0" /> Featured first when skaters browse The Lab</li>
|
||||
<li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 flex-shrink-0" /> Supports SkateSpots — no ads, no middlemen</li>
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* Already active */}
|
||||
{isActive && (
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
{isPlatformOwner
|
||||
? 'Platform owner — Pro is always on.'
|
||||
: `Pro active · ${daysRemaining} day${daysRemaining !== 1 ? 's' : ''} remaining. Your profile will be featured.`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Payment appears once opted in and not already active */}
|
||||
{enabled && !isActive && (
|
||||
<div className="px-4 pb-4">
|
||||
<PlatformFeePayment
|
||||
type="labProfile"
|
||||
onSuccess={() => { /* hook auto-updates isActive */ }}
|
||||
context={`One-time ${amountSats.toLocaleString()} sat fee for 30 days of Pro placement. Your profile stays live for free after it expires — you just lose the badge & boost.`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
src/components/ProBadge.tsx
Normal file
34
src/components/ProBadge.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Sparkles } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isFeaturedActive } from '@/lib/types';
|
||||
|
||||
interface ProBadgeProps {
|
||||
/** The `featuredUntil` unix-seconds timestamp from a parsed entity. */
|
||||
featuredUntil?: number;
|
||||
className?: string;
|
||||
/** Compact = icon only (for dense card corners). */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Pro" badge shown on featured Lab profiles and events.
|
||||
* Renders nothing if the entity isn't currently featured.
|
||||
*/
|
||||
export function ProBadge({ featuredUntil, className, compact = false }: ProBadgeProps) {
|
||||
if (!isFeaturedActive(featuredUntil)) return null;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
'gap-1 border-0 bg-gradient-to-r from-amber-400 to-orange-500 text-black font-black uppercase tracking-wide shadow-sm shadow-orange-500/30',
|
||||
compact ? 'px-1.5 py-0 text-[9px]' : 'text-[10px]',
|
||||
className,
|
||||
)}
|
||||
title="Pro listing — this profile supports SkateSpots and is featured."
|
||||
>
|
||||
<Sparkles className={compact ? 'h-2.5 w-2.5' : 'h-3 w-3'} />
|
||||
Pro
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
|
@ -77,6 +77,16 @@ export function usePlatformFee(type: FeeType) {
|
|||
? Math.ceil((record.expiresAt - Date.now()) / (24 * 60 * 60 * 1000))
|
||||
: 0;
|
||||
|
||||
// Expiry as unix SECONDS, for writing into a `featured` event tag.
|
||||
// Platform owners get a long-lived (1 year) featured window.
|
||||
const featuredUntilSeconds = (() => {
|
||||
if (isPlatformOwner) {
|
||||
return Math.floor((Date.now() + 365 * 24 * 60 * 60 * 1000) / 1000);
|
||||
}
|
||||
if (record) return Math.floor(record.expiresAt / 1000);
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
// Record a successful payment
|
||||
const recordPayment = () => {
|
||||
const now = Date.now();
|
||||
|
|
@ -98,6 +108,8 @@ export function usePlatformFee(type: FeeType) {
|
|||
isActive,
|
||||
/** Days remaining on active fee */
|
||||
daysRemaining,
|
||||
/** Expiry in unix seconds — write into a `featured` tag when publishing */
|
||||
featuredUntilSeconds,
|
||||
/** Platform owners bypass everything */
|
||||
isPlatformOwner,
|
||||
/** Call this after successful payment */
|
||||
|
|
|
|||
|
|
@ -88,6 +88,30 @@ export function parseMemberPubkeys(event: NostrEvent): string[] {
|
|||
/** All Lab provider kinds (The Lab service profiles). */
|
||||
export const LAB_KINDS = [35001, 35002, 35003, 35004, 35005] as const;
|
||||
|
||||
/**
|
||||
* "Pro" / featured status for Lab profiles and events.
|
||||
*
|
||||
* When a user pays the optional Pro/featured listing fee, the published event
|
||||
* carries a `["featured", "<unix-seconds-expiry>"]` tag. This is *self-attested*
|
||||
* (the app records payment and writes the tag) rather than cryptographically
|
||||
* verified against a real Lightning payment — the same trust level the rest of
|
||||
* the platform-fee flow already uses. A future enhancement could verify against
|
||||
* NIP-57 zap receipts to the platform Lightning address.
|
||||
*/
|
||||
export function parseFeaturedUntil(event: NostrEvent): number | undefined {
|
||||
const raw = event.tags.find(([name]) => name === 'featured')?.[1];
|
||||
if (!raw) return undefined;
|
||||
const ts = parseInt(raw, 10);
|
||||
if (isNaN(ts) || ts <= 0) return undefined;
|
||||
return ts; // unix seconds
|
||||
}
|
||||
|
||||
/** Is a `featuredUntil` timestamp (unix seconds) still active right now? */
|
||||
export function isFeaturedActive(featuredUntil: number | undefined): boolean {
|
||||
if (!featuredUntil) return false;
|
||||
return featuredUntil * 1000 > Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* A parsed addressable coordinate: `<kind>:<pubkey>:<d>`.
|
||||
*/
|
||||
|
|
@ -198,6 +222,7 @@ export interface SkateEvent {
|
|||
location?: string;
|
||||
image?: string;
|
||||
types: string[];
|
||||
featuredUntil?: number; // Featured event listing expiry (unix seconds), if paid
|
||||
createdAt: number;
|
||||
event: NostrEvent;
|
||||
}
|
||||
|
|
@ -389,6 +414,7 @@ export function parseSkateEvent(event: NostrEvent): SkateEvent | null {
|
|||
location,
|
||||
image,
|
||||
types,
|
||||
featuredUntil: parseFeaturedUntil(event),
|
||||
createdAt: event.created_at,
|
||||
event,
|
||||
};
|
||||
|
|
@ -847,6 +873,7 @@ export interface SkateBuilder {
|
|||
id: string;
|
||||
pubkey: string;
|
||||
identifier: string;
|
||||
featuredUntil?: number; // "Pro" listing expiry (unix seconds), if paid
|
||||
title: string;
|
||||
bio: string;
|
||||
geohash?: string;
|
||||
|
|
@ -876,6 +903,7 @@ export interface SkateCoach {
|
|||
id: string;
|
||||
pubkey: string;
|
||||
identifier: string;
|
||||
featuredUntil?: number; // "Pro" listing expiry (unix seconds), if paid
|
||||
title: string;
|
||||
bio: string;
|
||||
geohash?: string;
|
||||
|
|
@ -901,6 +929,7 @@ export interface SkateFilmer {
|
|||
id: string;
|
||||
pubkey: string;
|
||||
identifier: string;
|
||||
featuredUntil?: number; // "Pro" listing expiry (unix seconds), if paid
|
||||
title: string;
|
||||
bio: string;
|
||||
geohash?: string;
|
||||
|
|
@ -928,6 +957,7 @@ export interface SkateEditor {
|
|||
id: string;
|
||||
pubkey: string;
|
||||
identifier: string;
|
||||
featuredUntil?: number; // "Pro" listing expiry (unix seconds), if paid
|
||||
title: string;
|
||||
bio: string;
|
||||
geohash?: string;
|
||||
|
|
@ -955,6 +985,7 @@ export interface SkatePhotographer {
|
|||
id: string;
|
||||
pubkey: string;
|
||||
identifier: string;
|
||||
featuredUntil?: number; // "Pro" listing expiry (unix seconds), if paid
|
||||
title: string;
|
||||
bio: string;
|
||||
geohash?: string;
|
||||
|
|
@ -1089,6 +1120,7 @@ export function parseSkateCoach(event: NostrEvent): SkateCoach | null {
|
|||
members,
|
||||
spotRefs: spotRefs.length > 0 ? spotRefs : undefined,
|
||||
crewRef: parseCrewRef(event),
|
||||
featuredUntil: parseFeaturedUntil(event),
|
||||
createdAt: event.created_at,
|
||||
event,
|
||||
};
|
||||
|
|
@ -1147,6 +1179,7 @@ export function parseSkateFilmer(event: NostrEvent): SkateFilmer | null {
|
|||
members,
|
||||
spotRefs: spotRefs.length > 0 ? spotRefs : undefined,
|
||||
crewRef: parseCrewRef(event),
|
||||
featuredUntil: parseFeaturedUntil(event),
|
||||
createdAt: event.created_at,
|
||||
event,
|
||||
};
|
||||
|
|
@ -1202,6 +1235,7 @@ export function parseSkateEditor(event: NostrEvent): SkateEditor | null {
|
|||
entityType,
|
||||
members,
|
||||
crewRef: parseCrewRef(event),
|
||||
featuredUntil: parseFeaturedUntil(event),
|
||||
createdAt: event.created_at,
|
||||
event,
|
||||
};
|
||||
|
|
@ -1259,6 +1293,7 @@ export function parseSkatePhotographer(event: NostrEvent): SkatePhotographer | n
|
|||
members,
|
||||
spotRefs: spotRefs.length > 0 ? spotRefs : undefined,
|
||||
crewRef: parseCrewRef(event),
|
||||
featuredUntil: parseFeaturedUntil(event),
|
||||
createdAt: event.created_at,
|
||||
event,
|
||||
};
|
||||
|
|
@ -1320,6 +1355,7 @@ export function parseSkateBuilder(event: NostrEvent): SkateBuilder | null {
|
|||
members,
|
||||
spotRefs: spotRefs.length > 0 ? spotRefs : undefined,
|
||||
crewRef: parseCrewRef(event),
|
||||
featuredUntil: parseFeaturedUntil(event),
|
||||
createdAt: event.created_at,
|
||||
event,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ import {
|
|||
type EntityType,
|
||||
} from '@/lib/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { HardHat, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Zap, Hammer } from 'lucide-react';
|
||||
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
|
||||
import { HardHat, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Hammer } from 'lucide-react';
|
||||
import { LabProUpgrade } from '@/components/LabProUpgrade';
|
||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||
|
||||
const SERVICE_ENTRIES = Object.entries(BUILDER_SERVICE_LABELS);
|
||||
|
|
@ -86,7 +86,8 @@ export default function BecomeBuilderPage() {
|
|||
const { webln, activeNWC } = useWallet();
|
||||
|
||||
const hasWallet = !!webln || !!activeNWC;
|
||||
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('labProfile');
|
||||
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
|
||||
const [goPro, setGoPro] = useState(false);
|
||||
|
||||
const [bio, setBio] = useState('');
|
||||
const [location, setLocation] = useState('');
|
||||
|
|
@ -221,6 +222,11 @@ export default function BecomeBuilderPage() {
|
|||
tags.push(['a', `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`]);
|
||||
}
|
||||
|
||||
// Pro / featured listing (optional, paid)
|
||||
if (isPro && featuredUntilSeconds) {
|
||||
tags.push(['featured', String(featuredUntilSeconds)]);
|
||||
}
|
||||
|
||||
await createEvent({
|
||||
kind: SKATE_BUILDER_KIND,
|
||||
content: bio.trim(),
|
||||
|
|
@ -523,28 +529,18 @@ export default function BecomeBuilderPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Platform fee gate — only when a rate is set */}
|
||||
{rate.trim() && !feePaid && !isPlatformOwner && (
|
||||
<PlatformFeePayment
|
||||
type="labProfile"
|
||||
onSuccess={() => { /* hook auto-updates */ }}
|
||||
context={`You set a rate of "${rate}" — a 5,000 sat listing fee is required to publish monetized profiles.`}
|
||||
/>
|
||||
)}
|
||||
{/* Optional Pro upgrade — free to list without it */}
|
||||
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-amber-400" />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-amber-500 hover:bg-amber-600 text-black uppercase font-black"
|
||||
disabled={isPublishing || isUploading || !hasWallet || (!!rate.trim() && !feePaid && !isPlatformOwner)}
|
||||
disabled={isPublishing || isUploading}
|
||||
>
|
||||
{isPublishing ? (
|
||||
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />Publishing...</>
|
||||
) : !hasWallet ? (
|
||||
<><Wallet className="h-4 w-4 mr-2" />CONNECT WALLET TO CONTINUE</>
|
||||
) : rate.trim() && !feePaid && !isPlatformOwner ? (
|
||||
<><Zap className="h-4 w-4 mr-2" />PAY LISTING FEE ABOVE TO CONTINUE</>
|
||||
) : (
|
||||
<><HardHat className="h-4 w-4 mr-2" />CREATE BUILDER PROFILE</>
|
||||
<><HardHat className="h-4 w-4 mr-2" />{isPro ? 'CREATE PRO BUILDER PROFILE' : 'CREATE BUILDER PROFILE'}</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import { EntityTypeField } from '@/components/EntityTypeField';
|
|||
import { CrewLinkField } from '@/components/CrewLinkField';
|
||||
import { SKATE_COACH_KIND, SKATE_SPOT_KIND, COACH_SKILL_LABELS, COACH_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType } from '@/lib/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Zap } from 'lucide-react';
|
||||
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
|
||||
import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink } from 'lucide-react';
|
||||
import { LabProUpgrade } from '@/components/LabProUpgrade';
|
||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||
|
||||
const SKILL_ENTRIES = Object.entries(COACH_SKILL_LABELS);
|
||||
|
|
@ -46,8 +46,9 @@ export default function BecomeCoachPage() {
|
|||
// Check if user has any wallet connected
|
||||
const hasWallet = !!webln || !!activeNWC;
|
||||
|
||||
// Platform listing fee — required when a rate is set
|
||||
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('labProfile');
|
||||
// Optional "Pro" upgrade — free to list; Pro adds a badge + featured placement
|
||||
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
|
||||
const [goPro, setGoPro] = useState(false);
|
||||
|
||||
const [bio, setBio] = useState('');
|
||||
const [location, setLocation] = useState('');
|
||||
|
|
@ -172,6 +173,11 @@ export default function BecomeCoachPage() {
|
|||
tags.push(['a', `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`]);
|
||||
}
|
||||
|
||||
// Pro / featured listing (optional, paid)
|
||||
if (isPro && featuredUntilSeconds) {
|
||||
tags.push(['featured', String(featuredUntilSeconds)]);
|
||||
}
|
||||
|
||||
await createEvent({
|
||||
kind: SKATE_COACH_KIND,
|
||||
content: bio.trim(),
|
||||
|
|
@ -441,39 +447,23 @@ export default function BecomeCoachPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Platform fee gate — only when a rate is set */}
|
||||
{rate.trim() && !feePaid && !isPlatformOwner && (
|
||||
<PlatformFeePayment
|
||||
type="labProfile"
|
||||
onSuccess={() => { /* hook auto-updates */ }}
|
||||
context={`You set a rate of "${rate}" — a 5,000 sat listing fee is required to publish monetized profiles.`}
|
||||
/>
|
||||
)}
|
||||
{/* Optional Pro upgrade — free to list without it */}
|
||||
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-red-400" />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-red-500 hover:bg-red-600 uppercase font-black"
|
||||
disabled={isPublishing || isUploading || !hasWallet || (!!rate.trim() && !feePaid && !isPlatformOwner)}
|
||||
disabled={isPublishing || isUploading}
|
||||
>
|
||||
{isPublishing ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Publishing...
|
||||
</>
|
||||
) : !hasWallet ? (
|
||||
<>
|
||||
<Wallet className="h-4 w-4 mr-2" />
|
||||
CONNECT WALLET TO CONTINUE
|
||||
</>
|
||||
) : rate.trim() && !feePaid && !isPlatformOwner ? (
|
||||
<>
|
||||
<Zap className="h-4 w-4 mr-2" />
|
||||
PAY LISTING FEE ABOVE TO CONTINUE
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GraduationCap className="h-4 w-4 mr-2" />
|
||||
CREATE COACH PROFILE
|
||||
{isPro ? 'CREATE PRO COACH PROFILE' : 'CREATE COACH PROFILE'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ import {
|
|||
import { EntityTypeField } from '@/components/EntityTypeField';
|
||||
import { CrewLinkField } from '@/components/CrewLinkField';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Scissors, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Monitor, Zap } from 'lucide-react';
|
||||
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
|
||||
import { Scissors, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Monitor } from 'lucide-react';
|
||||
import { LabProUpgrade } from '@/components/LabProUpgrade';
|
||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||
|
||||
const SERVICE_ENTRIES = Object.entries(EDITOR_SERVICE_LABELS);
|
||||
|
|
@ -54,7 +54,8 @@ export default function BecomeEditorPage() {
|
|||
const hasWallet = !!webln || !!activeNWC;
|
||||
|
||||
// Platform listing fee — required when a rate is set
|
||||
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('labProfile');
|
||||
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
|
||||
const [goPro, setGoPro] = useState(false);
|
||||
|
||||
const [bio, setBio] = useState('');
|
||||
const [location, setLocation] = useState('');
|
||||
|
|
@ -171,6 +172,11 @@ export default function BecomeEditorPage() {
|
|||
if (hardware.trim()) tags.push(['hardware', hardware.trim()]);
|
||||
if (software.trim()) tags.push(['software', software.trim()]);
|
||||
|
||||
// Pro / featured listing (optional, paid)
|
||||
if (isPro && featuredUntilSeconds) {
|
||||
tags.push(['featured', String(featuredUntilSeconds)]);
|
||||
}
|
||||
|
||||
await createEvent({ kind: SKATE_EDITOR_KIND, content: bio.trim(), tags });
|
||||
|
||||
toast({ title: 'Editor profile created!', description: 'Your profile has been published to SkateSpots.' });
|
||||
|
|
@ -426,16 +432,10 @@ export default function BecomeEditorPage() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Platform fee gate — only when a rate is set */}
|
||||
{rate.trim() && !feePaid && !isPlatformOwner && (
|
||||
<div className="mt-4">
|
||||
<PlatformFeePayment
|
||||
type="labProfile"
|
||||
onSuccess={() => { /* hook auto-updates */ }}
|
||||
context={`You set a rate of "${rate}" — a 5,000 sat listing fee is required to publish monetized profiles.`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Optional Pro upgrade — free to list without it */}
|
||||
<div className="mt-4">
|
||||
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-purple-400" />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex gap-3">
|
||||
<Link to="/lab" className="flex-1">
|
||||
|
|
@ -443,15 +443,13 @@ export default function BecomeEditorPage() {
|
|||
</Link>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPublishing || selectedServices.length === 0 || (!!rate.trim() && !feePaid && !isPlatformOwner)}
|
||||
disabled={isPublishing || selectedServices.length === 0}
|
||||
className="flex-1 bg-purple-600 hover:bg-purple-700 font-black uppercase gap-2"
|
||||
>
|
||||
{isPublishing ? (
|
||||
<><Loader2 className="h-4 w-4 animate-spin" />Publishing…</>
|
||||
) : rate.trim() && !feePaid && !isPlatformOwner ? (
|
||||
<><Zap className="h-4 w-4" />Pay Fee Above</>
|
||||
) : (
|
||||
<><Scissors className="h-4 w-4" />Publish Editor Profile</>
|
||||
<><Scissors className="h-4 w-4" />{isPro ? 'Publish Pro Editor Profile' : 'Publish Editor Profile'}</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import { EntityTypeField } from '@/components/EntityTypeField';
|
|||
import { CrewLinkField } from '@/components/CrewLinkField';
|
||||
import { SKATE_FILMER_KIND, SKATE_SPOT_KIND, FILMING_SERVICE_LABELS, FILMING_SERVICE_DESCRIPTIONS, FILMING_SKILL_LABELS, FILMING_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType } from '@/lib/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Video, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Zap } from 'lucide-react';
|
||||
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
|
||||
import { Video, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink } from 'lucide-react';
|
||||
import { LabProUpgrade } from '@/components/LabProUpgrade';
|
||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||
|
||||
const SERVICE_ENTRIES = Object.entries(FILMING_SERVICE_LABELS);
|
||||
|
|
@ -48,7 +48,8 @@ export default function BecomeFilmerPage() {
|
|||
const hasWallet = !!webln || !!activeNWC;
|
||||
|
||||
// Platform listing fee — required when a rate is set
|
||||
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('labProfile');
|
||||
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
|
||||
const [goPro, setGoPro] = useState(false);
|
||||
|
||||
const [bio, setBio] = useState('');
|
||||
const [location, setLocation] = useState('');
|
||||
|
|
@ -188,6 +189,11 @@ export default function BecomeFilmerPage() {
|
|||
tags.push(['a', `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`]);
|
||||
}
|
||||
|
||||
// Pro / featured listing (optional, paid)
|
||||
if (isPro && featuredUntilSeconds) {
|
||||
tags.push(['featured', String(featuredUntilSeconds)]);
|
||||
}
|
||||
|
||||
await createEvent({
|
||||
kind: SKATE_FILMER_KIND,
|
||||
content: bio.trim(),
|
||||
|
|
@ -499,39 +505,23 @@ export default function BecomeFilmerPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Platform fee gate — only when a rate is set */}
|
||||
{rate.trim() && !feePaid && !isPlatformOwner && (
|
||||
<PlatformFeePayment
|
||||
type="labProfile"
|
||||
onSuccess={() => { /* hook auto-updates */ }}
|
||||
context={`You set a rate of "${rate}" — a 5,000 sat listing fee is required to publish monetized profiles.`}
|
||||
/>
|
||||
)}
|
||||
{/* Optional Pro upgrade — free to list without it */}
|
||||
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-red-400" />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-red-500 hover:bg-red-600 uppercase font-black"
|
||||
disabled={isPublishing || isUploading || !hasWallet || (!!rate.trim() && !feePaid && !isPlatformOwner)}
|
||||
disabled={isPublishing || isUploading}
|
||||
>
|
||||
{isPublishing ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Publishing...
|
||||
</>
|
||||
) : !hasWallet ? (
|
||||
<>
|
||||
<Wallet className="h-4 w-4 mr-2" />
|
||||
CONNECT WALLET TO CONTINUE
|
||||
</>
|
||||
) : rate.trim() && !feePaid && !isPlatformOwner ? (
|
||||
<>
|
||||
<Zap className="h-4 w-4 mr-2" />
|
||||
PAY LISTING FEE ABOVE TO CONTINUE
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Video className="h-4 w-4 mr-2" />
|
||||
CREATE FILMING PROFILE
|
||||
{isPro ? 'CREATE PRO FILMING PROFILE' : 'CREATE FILMING PROFILE'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ import {
|
|||
isValidNoffer,
|
||||
} from '@/lib/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Camera, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Zap, Aperture } from 'lucide-react';
|
||||
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
|
||||
import { Camera, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Aperture } from 'lucide-react';
|
||||
import { LabProUpgrade } from '@/components/LabProUpgrade';
|
||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||
|
||||
const SERVICE_ENTRIES = Object.entries(PHOTO_SERVICE_LABELS);
|
||||
|
|
@ -92,7 +92,8 @@ export default function BecomePhotographerPage() {
|
|||
const { webln, activeNWC } = useWallet();
|
||||
|
||||
const hasWallet = !!webln || !!activeNWC;
|
||||
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('labProfile');
|
||||
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
|
||||
const [goPro, setGoPro] = useState(false);
|
||||
|
||||
const [bio, setBio] = useState('');
|
||||
const [location, setLocation] = useState('');
|
||||
|
|
@ -238,6 +239,11 @@ export default function BecomePhotographerPage() {
|
|||
tags.push(['a', `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`]);
|
||||
}
|
||||
|
||||
// Pro / featured listing (optional, paid)
|
||||
if (isPro && featuredUntilSeconds) {
|
||||
tags.push(['featured', String(featuredUntilSeconds)]);
|
||||
}
|
||||
|
||||
await createEvent({
|
||||
kind: SKATE_PHOTOGRAPHER_KIND,
|
||||
content: bio.trim(),
|
||||
|
|
@ -545,28 +551,18 @@ export default function BecomePhotographerPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Platform fee gate — only when a rate is set */}
|
||||
{rate.trim() && !feePaid && !isPlatformOwner && (
|
||||
<PlatformFeePayment
|
||||
type="labProfile"
|
||||
onSuccess={() => { /* hook auto-updates */ }}
|
||||
context={`You set a rate of "${rate}" — a 5,000 sat listing fee is required to publish monetized profiles.`}
|
||||
/>
|
||||
)}
|
||||
{/* Optional Pro upgrade — free to list without it */}
|
||||
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-indigo-400" />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-indigo-600 hover:bg-indigo-700 uppercase font-black"
|
||||
disabled={isPublishing || isUploading || !hasWallet || (!!rate.trim() && !feePaid && !isPlatformOwner)}
|
||||
disabled={isPublishing || isUploading}
|
||||
>
|
||||
{isPublishing ? (
|
||||
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />Publishing...</>
|
||||
) : !hasWallet ? (
|
||||
<><Wallet className="h-4 w-4 mr-2" />CONNECT WALLET TO CONTINUE</>
|
||||
) : rate.trim() && !feePaid && !isPlatformOwner ? (
|
||||
<><Zap className="h-4 w-4 mr-2" />PAY LISTING FEE ABOVE TO CONTINUE</>
|
||||
) : (
|
||||
<><Camera className="h-4 w-4 mr-2" />CREATE PHOTOGRAPHY PROFILE</>
|
||||
<><Camera className="h-4 w-4 mr-2" />{isPro ? 'CREATE PRO PHOTOGRAPHY PROFILE' : 'CREATE PHOTOGRAPHY PROFILE'}</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
|
|
|||
Loading…
Reference in a new issue