Add Pro/Featured display + optional featured events (Option B)
Lab: - useLabProfiles sorts featured (Pro) profiles first - ProBadge shown on Lab cards (LabPage) + Lab detail page; featured cards get a subtle amber highlight Events (same no-paywall model as Lab): - New FeaturedEventUpgrade: every event posts for free; optional fee adds a featured badge + top placement (reuses existing featuredEvent fee) - CreateEventPage: removed the paid-event fee gate; opt-in featured toggle, writes a `featured` tag when active - useSkateEvents sorts featured events first; EventCard shows ProBadge (compact in list view, overlay on the image in full view) Docs: - NIP.md documents the `featured` tag (unix-seconds expiry, self-attested) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
5c0d4715d5
commit
dcfa1fe1b2
8 changed files with 174 additions and 49 deletions
22
NIP.md
22
NIP.md
|
|
@ -1457,6 +1457,28 @@ They share a common base schema and are distinguished by kind and a self-tag.
|
||||||
below), `p` (collective member pubkeys — see Collective Membership below),
|
below), `p` (collective member pubkeys — see Collective Membership below),
|
||||||
`a` (spot references to kind 30818), `alt`.
|
`a` (spot references to kind 30818), `alt`.
|
||||||
|
|
||||||
|
### Pro / Featured Listings (`featured` tag)
|
||||||
|
|
||||||
|
Lab profiles and calendar events (kind 31923) support an optional **Pro /
|
||||||
|
featured** upgrade. Basic listings are always free; paying the optional
|
||||||
|
platform fee adds a `featured` tag whose value is the **unix-seconds expiry**
|
||||||
|
of the featured window:
|
||||||
|
|
||||||
|
```json
|
||||||
|
["featured", "1767225600"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Clients that see an unexpired `featured` timestamp render a gold **"Pro"** badge
|
||||||
|
and sort the listing ahead of non-featured ones. Once the timestamp passes, the
|
||||||
|
listing simply reverts to a normal free listing (no badge, no boost) — it is
|
||||||
|
never unpublished.
|
||||||
|
|
||||||
|
> **Trust note:** the `featured` tag is *self-attested* — the publishing client
|
||||||
|
> writes it after recording payment locally, rather than proving a Lightning
|
||||||
|
> payment cryptographically. This matches the platform-fee trust model used
|
||||||
|
> elsewhere in SkateSpots. A future revision could verify the tag against a
|
||||||
|
> NIP-57 zap receipt to the platform Lightning address.
|
||||||
|
|
||||||
### Entity Type (individual vs. collective)
|
### Entity Type (individual vs. collective)
|
||||||
|
|
||||||
Every Lab profile carries an optional `entity` tag declaring whether it
|
Every Lab profile carries an optional `entity` tag declaring whether it
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { genUserName } from '@/lib/genUserName';
|
||||||
import { ZapButton } from '@/components/ZapButton';
|
import { ZapButton } from '@/components/ZapButton';
|
||||||
import { TrustBadge } from '@/components/TrustBadge';
|
import { TrustBadge } from '@/components/TrustBadge';
|
||||||
import { OfficialBadge } from '@/components/OfficialBadge';
|
import { OfficialBadge } from '@/components/OfficialBadge';
|
||||||
|
import { ProBadge } from '@/components/ProBadge';
|
||||||
import { ReportDialog } from '@/components/ReportDialog';
|
import { ReportDialog } from '@/components/ReportDialog';
|
||||||
import type { SkateEvent } from '@/lib/types';
|
import type { SkateEvent } from '@/lib/types';
|
||||||
import { EVENT_TYPE_LABELS } from '@/lib/types';
|
import { EVENT_TYPE_LABELS } from '@/lib/types';
|
||||||
|
|
@ -97,12 +98,15 @@ export function EventCard({ event, compact = false }: EventCardProps) {
|
||||||
<h3 className="font-bold text-white group-hover:text-[#ccff00] transition-colors line-clamp-1 uppercase tracking-wide">
|
<h3 className="font-bold text-white group-hover:text-[#ccff00] transition-colors line-clamp-1 uppercase tracking-wide">
|
||||||
{event.title}
|
{event.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
|
<ProBadge featuredUntil={event.featuredUntil} compact />
|
||||||
{isUpcoming && (
|
{isUpcoming && (
|
||||||
<Badge className="flex-shrink-0 text-xs bg-[#ff0040] text-white border-0 font-bold uppercase">
|
<Badge className="text-xs bg-[#ff0040] text-white border-0 font-bold uppercase">
|
||||||
{relativeTime}
|
{relativeTime}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-400 flex-wrap">
|
<div className="flex items-center gap-2 text-xs text-gray-400 flex-wrap">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
|
|
@ -152,9 +156,9 @@ export function EventCard({ event, compact = false }: EventCardProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Type badges */}
|
{/* Type badges + Pro */}
|
||||||
{event.types.length > 0 && (
|
<div className="absolute top-3 right-3 flex flex-wrap gap-1 justify-end">
|
||||||
<div className="absolute top-3 right-3 flex flex-wrap gap-1">
|
<ProBadge featuredUntil={event.featuredUntil} />
|
||||||
{event.types.slice(0, 2).map((type) => (
|
{event.types.slice(0, 2).map((type) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={type}
|
key={type}
|
||||||
|
|
@ -164,7 +168,6 @@ export function EventCard({ event, compact = false }: EventCardProps) {
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
|
|
|
||||||
85
src/components/FeaturedEventUpgrade.tsx
Normal file
85
src/components/FeaturedEventUpgrade.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
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 FeaturedEventUpgradeProps {
|
||||||
|
enabled: boolean;
|
||||||
|
onEnabledChange: (enabled: boolean) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opt-in "Feature this event" upgrade.
|
||||||
|
*
|
||||||
|
* Every event is free to post. Toggling this on reveals the Lightning fee and,
|
||||||
|
* once paid (or for platform owners), the event is published with a `featured`
|
||||||
|
* tag that gives it a badge + top placement on the Events page.
|
||||||
|
*
|
||||||
|
* No hard paywall — even paid/ticketed events post for free.
|
||||||
|
*/
|
||||||
|
export function FeaturedEventUpgrade({
|
||||||
|
enabled,
|
||||||
|
onEnabledChange,
|
||||||
|
className,
|
||||||
|
}: FeaturedEventUpgradeProps) {
|
||||||
|
const { isActive, isPlatformOwner, amountSats, daysRemaining } = usePlatformFee('featuredEvent');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl border border-[#ccff00]/30 bg-gradient-to-br from-[#ccff00]/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-[#ccff00] to-lime-500 flex items-center justify-center flex-shrink-0 shadow-lg shadow-[#ccff00]/25">
|
||||||
|
<Sparkles className="h-4 w-4 text-black" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-black uppercase tracking-wide text-sm text-[#ccff00]">
|
||||||
|
Feature this event <span className="text-gray-500 font-normal normal-case">· optional</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">
|
||||||
|
Free to post. Featured events get a <strong className="text-[#ccff00]">badge</strong> and sit
|
||||||
|
{' '}<strong className="text-[#ccff00]">at the top</strong> of the events page.
|
||||||
|
</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-[#ccff00]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isActive && (
|
||||||
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
|
{isPlatformOwner
|
||||||
|
? 'Platform owner — featured is always on.'
|
||||||
|
: `Featured active · ${daysRemaining} day${daysRemaining !== 1 ? 's' : ''} remaining.`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enabled && !isActive && (
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
<PlatformFeePayment
|
||||||
|
type="featuredEvent"
|
||||||
|
onSuccess={() => { /* hook auto-updates isActive */ }}
|
||||||
|
context={`One-time ${amountSats.toLocaleString()} sat fee for 30 days of featured placement. Your event stays listed for free either way.`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { SKATE_COACH_KIND, SKATE_FILMER_KIND, SKATE_EDITOR_KIND, SKATE_PHOTOGRAPHER_KIND, SKATE_BUILDER_KIND, parseSkateCoach, parseSkateFilmer, parseSkateEditor, parseSkatePhotographer, parseSkateBuilder, type SkateCoach, type SkateFilmer, type SkateEditor, type SkatePhotographer, type SkateBuilder } from '@/lib/types';
|
import { SKATE_COACH_KIND, SKATE_FILMER_KIND, SKATE_EDITOR_KIND, SKATE_PHOTOGRAPHER_KIND, SKATE_BUILDER_KIND, parseSkateCoach, parseSkateFilmer, parseSkateEditor, parseSkatePhotographer, parseSkateBuilder, isFeaturedActive, type SkateCoach, type SkateFilmer, type SkateEditor, type SkatePhotographer, type SkateBuilder } from '@/lib/types';
|
||||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||||
|
|
||||||
export type LabProfile = (SkateCoach | SkateFilmer | SkateEditor | SkatePhotographer | SkateBuilder) & {
|
export type LabProfile = (SkateCoach | SkateFilmer | SkateEditor | SkatePhotographer | SkateBuilder) & {
|
||||||
|
|
@ -61,7 +61,13 @@ export function useLabProfiles(limit = 100) {
|
||||||
|
|
||||||
console.log(`[useLabProfiles] Parsed profiles: ${profiles.length} (coaches + filmers + editors + photographers)`);
|
console.log(`[useLabProfiles] Parsed profiles: ${profiles.length} (coaches + filmers + editors + photographers)`);
|
||||||
|
|
||||||
return profiles.sort((a, b) => b.createdAt - a.createdAt);
|
// Featured (Pro) profiles first, then newest.
|
||||||
|
return profiles.sort((a, b) => {
|
||||||
|
const aPro = isFeaturedActive(a.featuredUntil) ? 1 : 0;
|
||||||
|
const bPro = isFeaturedActive(b.featuredUntil) ? 1 : 0;
|
||||||
|
if (aPro !== bPro) return bPro - aPro;
|
||||||
|
return b.createdAt - a.createdAt;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
staleTime: 30000,
|
staleTime: 30000,
|
||||||
refetchOnMount: 'always',
|
refetchOnMount: 'always',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { CALENDAR_EVENT_KIND, parseSkateEvent, type SkateEvent } from '@/lib/types';
|
import { CALENDAR_EVENT_KIND, parseSkateEvent, isFeaturedActive, type SkateEvent } from '@/lib/types';
|
||||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||||
|
|
||||||
export function useSkateEvents(limit = 50) {
|
export function useSkateEvents(limit = 50) {
|
||||||
|
|
@ -27,8 +27,13 @@ export function useSkateEvents(limit = 50) {
|
||||||
if (skateEvent) skateEvents.push(skateEvent);
|
if (skateEvent) skateEvents.push(skateEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by start time
|
// Featured (paid) events first, then by soonest start time.
|
||||||
return skateEvents.sort((a, b) => a.start - b.start);
|
return skateEvents.sort((a, b) => {
|
||||||
|
const aF = isFeaturedActive(a.featuredUntil) ? 1 : 0;
|
||||||
|
const bF = isFeaturedActive(b.featuredUntil) ? 1 : 0;
|
||||||
|
if (aF !== bF) return bF - aF;
|
||||||
|
return a.start - b.start;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
staleTime: 60000,
|
staleTime: 60000,
|
||||||
});
|
});
|
||||||
|
|
@ -63,8 +68,15 @@ export function useUpcomingSkateEvents(limit = 20) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by start time ascending (soonest first)
|
// Featured (paid) events first, then soonest start time.
|
||||||
return skateEvents.sort((a, b) => a.start - b.start).slice(0, limit);
|
return skateEvents
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aF = isFeaturedActive(a.featuredUntil) ? 1 : 0;
|
||||||
|
const bF = isFeaturedActive(b.featuredUntil) ? 1 : 0;
|
||||||
|
if (aF !== bF) return bF - aF;
|
||||||
|
return a.start - b.start;
|
||||||
|
})
|
||||||
|
.slice(0, limit);
|
||||||
},
|
},
|
||||||
staleTime: 60000,
|
staleTime: 60000,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { genUserName } from '@/lib/genUserName';
|
||||||
import { COACH_SKILL_LABELS, COACH_SKILL_DESCRIPTIONS, FILMING_SERVICE_LABELS, FILMING_SERVICE_DESCRIPTIONS, FILMING_SKILL_LABELS, FILMING_SKILL_DESCRIPTIONS, EDITOR_SERVICE_LABELS, EDITOR_SERVICE_DESCRIPTIONS, EDITOR_SKILL_LABELS, EDITOR_SKILL_DESCRIPTIONS, PHOTO_SERVICE_LABELS, PHOTO_SERVICE_DESCRIPTIONS, PHOTO_CAMERA_LABELS, PHOTO_LENS_LABELS, PHOTO_LIGHTING_LABELS, PHOTO_SUPPORT_LABELS, PHOTO_STYLE_LABELS, BUILDER_SERVICE_LABELS, BUILDER_SERVICE_DESCRIPTIONS, BUILDER_SKILL_LABELS, BUILDER_SCALE_LABELS } from '@/lib/types';
|
import { COACH_SKILL_LABELS, COACH_SKILL_DESCRIPTIONS, FILMING_SERVICE_LABELS, FILMING_SERVICE_DESCRIPTIONS, FILMING_SKILL_LABELS, FILMING_SKILL_DESCRIPTIONS, EDITOR_SERVICE_LABELS, EDITOR_SERVICE_DESCRIPTIONS, EDITOR_SKILL_LABELS, EDITOR_SKILL_DESCRIPTIONS, PHOTO_SERVICE_LABELS, PHOTO_SERVICE_DESCRIPTIONS, PHOTO_CAMERA_LABELS, PHOTO_LENS_LABELS, PHOTO_LIGHTING_LABELS, PHOTO_SUPPORT_LABELS, PHOTO_STYLE_LABELS, BUILDER_SERVICE_LABELS, BUILDER_SERVICE_DESCRIPTIONS, BUILDER_SKILL_LABELS, BUILDER_SCALE_LABELS } from '@/lib/types';
|
||||||
import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl';
|
import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl';
|
||||||
import { OfficialBadge } from '@/components/OfficialBadge';
|
import { OfficialBadge } from '@/components/OfficialBadge';
|
||||||
|
import { ProBadge } from '@/components/ProBadge';
|
||||||
import NotFound from './NotFound';
|
import NotFound from './NotFound';
|
||||||
import {
|
import {
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
|
|
@ -193,6 +194,7 @@ export default function CoachDetailPage() {
|
||||||
)}
|
)}
|
||||||
<TrustBadge pubkey={profile.pubkey} size="md" />
|
<TrustBadge pubkey={profile.pubkey} size="md" />
|
||||||
<OfficialBadge pubkey={profile.pubkey} size="md" />
|
<OfficialBadge pubkey={profile.pubkey} size="md" />
|
||||||
|
<ProBadge featuredUntil={profile.featuredUntil} />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-400 flex items-center gap-2">
|
<p className="text-gray-400 flex items-center gap-2">
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import {
|
||||||
Map,
|
Map,
|
||||||
Zap,
|
Zap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
|
import { FeaturedEventUpgrade } from '@/components/FeaturedEventUpgrade';
|
||||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
@ -88,8 +88,9 @@ export default function CreateEventPage() {
|
||||||
// Paid event / entry fee
|
// Paid event / entry fee
|
||||||
const [entryFee, setEntryFee] = useState('');
|
const [entryFee, setEntryFee] = useState('');
|
||||||
const isPaidEvent = entryFee.trim().length > 0 && entryFee.trim() !== '0';
|
const isPaidEvent = entryFee.trim().length > 0 && entryFee.trim() !== '0';
|
||||||
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('featuredEvent');
|
// Optional "Feature this event" upgrade — every event posts for free.
|
||||||
const canSubmit = !isPaidEvent || feePaid || isPlatformOwner;
|
const { isActive: isFeatured, featuredUntilSeconds } = usePlatformFee('featuredEvent');
|
||||||
|
const [goFeatured, setGoFeatured] = useState(false);
|
||||||
|
|
||||||
// Get the selected tour (if any)
|
// Get the selected tour (if any)
|
||||||
const selectedTour = tours?.find((t) => `${t.pubkey}:${t.identifier}` === linkedTourId);
|
const selectedTour = tours?.find((t) => `${t.pubkey}:${t.identifier}` === linkedTourId);
|
||||||
|
|
@ -302,6 +303,11 @@ export default function CreateEventPage() {
|
||||||
tags.push(['t', 'paid']);
|
tags.push(['t', 'paid']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Featured listing (optional, paid)
|
||||||
|
if (isFeatured && featuredUntilSeconds) {
|
||||||
|
tags.push(['featured', String(featuredUntilSeconds)]);
|
||||||
|
}
|
||||||
|
|
||||||
// Add spot link if selected
|
// Add spot link if selected
|
||||||
if (selectedSpot && linkedSpotId !== 'none') {
|
if (selectedSpot && linkedSpotId !== 'none') {
|
||||||
// Use 'a' tag to reference the spot (addressable event reference)
|
// Use 'a' tag to reference the spot (addressable event reference)
|
||||||
|
|
@ -891,42 +897,26 @@ export default function CreateEventPage() {
|
||||||
onChange={(e) => setEntryFee(e.target.value)}
|
onChange={(e) => setEntryFee(e.target.value)}
|
||||||
className="bg-black/50 border-white/20"
|
className="bg-black/50 border-white/20"
|
||||||
/>
|
/>
|
||||||
{isPaidEvent && (
|
|
||||||
<p className="text-xs text-[#ff6600]/80">
|
|
||||||
Paid events require a 5,000 sat platform listing fee (30 days).
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Platform fee gate for paid events */}
|
{/* Optional featured upgrade — every event posts for free */}
|
||||||
{isPaidEvent && !feePaid && !isPlatformOwner && (
|
<FeaturedEventUpgrade enabled={goFeatured} onEnabledChange={setGoFeatured} />
|
||||||
<PlatformFeePayment
|
|
||||||
type="featuredEvent"
|
|
||||||
onSuccess={() => { /* fee hook auto-updates isActive */ }}
|
|
||||||
context={`You set an entry fee of "${entryFee}" — a 5,000 sat listing fee is required to publish paid events.`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
disabled={isPublishing || isUploading || !canSubmit}
|
disabled={isPublishing || isUploading}
|
||||||
>
|
>
|
||||||
{isPublishing ? (
|
{isPublishing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
{t('createEvent.publishing')}
|
{t('createEvent.publishing')}
|
||||||
</>
|
</>
|
||||||
) : !canSubmit ? (
|
|
||||||
<>
|
|
||||||
<Zap className="h-4 w-4 mr-2" />
|
|
||||||
Pay listing fee above to publish
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Calendar className="h-4 w-4 mr-2" />
|
<Calendar className="h-4 w-4 mr-2" />
|
||||||
{t('events.createEvent')}
|
{isFeatured ? 'Publish Featured Event' : t('events.createEvent')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ import {
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||||
|
import { ProBadge } from '@/components/ProBadge';
|
||||||
|
import { isFeaturedActive } from '@/lib/types';
|
||||||
|
|
||||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -685,7 +687,9 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
|
||||||
'group relative flex gap-3 p-4 rounded-2xl bg-white/3 border border-white/8',
|
'group relative flex gap-3 p-4 rounded-2xl bg-white/3 border border-white/8',
|
||||||
'hover:bg-white/5 hover:border-white/15 hover:shadow-xl',
|
'hover:bg-white/5 hover:border-white/15 hover:shadow-xl',
|
||||||
'transition-all duration-200 active:scale-[0.99]',
|
'transition-all duration-200 active:scale-[0.99]',
|
||||||
'overflow-hidden'
|
'overflow-hidden',
|
||||||
|
isFeaturedActive(profile.featuredUntil) &&
|
||||||
|
'border-amber-500/40 bg-gradient-to-br from-amber-500/[0.07] to-transparent',
|
||||||
)}>
|
)}>
|
||||||
{/* Left color stripe */}
|
{/* Left color stripe */}
|
||||||
<div className={cn('absolute left-0 top-0 bottom-0 w-[3px] rounded-l-2xl', accent.stripe)} />
|
<div className={cn('absolute left-0 top-0 bottom-0 w-[3px] rounded-l-2xl', accent.stripe)} />
|
||||||
|
|
@ -714,6 +718,7 @@ function ProfileCard({ profile }: { profile: LabProfile }) {
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className="font-black text-sm text-white truncate leading-tight flex items-center gap-1.5">
|
<h3 className="font-black text-sm text-white truncate leading-tight flex items-center gap-1.5">
|
||||||
<span className="truncate">{displayName}</span>
|
<span className="truncate">{displayName}</span>
|
||||||
|
<ProBadge featuredUntil={profile.featuredUntil} compact className="flex-shrink-0" />
|
||||||
{profile.entityType === 'collective' && (
|
{profile.entityType === 'collective' && (
|
||||||
<span
|
<span
|
||||||
className={cn('inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[9px] font-bold flex-shrink-0', accent.badgeSkill)}
|
className={cn('inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[9px] font-bold flex-shrink-0', accent.badgeSkill)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue