diff --git a/src/components/ControllerBadge.tsx b/src/components/ControllerBadge.tsx new file mode 100644 index 0000000..155e88c --- /dev/null +++ b/src/components/ControllerBadge.tsx @@ -0,0 +1,34 @@ +import { Crown, ShieldCheck, BadgeCheck } from 'lucide-react'; +import { useTranslation } from '@/contexts/language'; +import { cn } from '@/lib/utils'; + +type Role = 'owner' | 'admin' | 'organiser' | 'verifiedOwner'; + +const CONFIG: Record = { + owner: { key: 'owner', cls: 'border-[#ccff00]/40 bg-[#ccff00]/10 text-[#ccff00]', Icon: Crown }, + admin: { key: 'admin', cls: 'border-purple-400/40 bg-purple-400/10 text-purple-300', Icon: ShieldCheck }, + organiser: { key: 'organiser', cls: 'border-blue-400/40 bg-blue-400/10 text-blue-300', Icon: ShieldCheck }, + verifiedOwner: { key: 'verifiedOwner', cls: 'border-[#ccff00]/40 bg-[#ccff00]/10 text-[#ccff00]', Icon: BadgeCheck }, +}; + +/** + * A small badge showing the viewer's control over the entity on screen + * (`owner`/`admin`/`organiser`) or a verified-ownership signal (`verifiedOwner`). + * Deliberately only rendered when it applies — no empty chrome. + */ +export function ControllerBadge({ role, className }: { role: Role; className?: string }) { + const { t } = useTranslation(); + const { key, cls, Icon } = CONFIG[role]; + return ( + + + {t(`role.${key}`)} + + ); +} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index d5e4455..b74a913 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -6469,6 +6469,12 @@ walletUi: { verifyNote: 'Scan the QR on the card to verify it.', }, }, + role: { + owner: 'Owner', + admin: 'Admin', + organiser: 'Organiser', + verifiedOwner: 'Verified owner', + }, }; export default en; diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 79cdf19..404c2de 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -6469,6 +6469,12 @@ walletUi: { verifyNote: 'Escanea el QR del carné para verificarlo.', }, }, + role: { + owner: 'Propietario', + admin: 'Admin', + organiser: 'Organizador', + verifiedOwner: 'Propietario verificado', + }, }; export default es; diff --git a/src/pages/CrewDetailPage.tsx b/src/pages/CrewDetailPage.tsx index edf6fcf..7b784ca 100644 --- a/src/pages/CrewDetailPage.tsx +++ b/src/pages/CrewDetailPage.tsx @@ -13,6 +13,7 @@ import { ZapCrewDialog } from '@/components/ZapCrewDialog'; import { ClinkPayButton } from '@/components/ClinkPayButton'; import { CollectiveMembers } from '@/components/CollectiveMembers'; import { CrewHero } from '@/components/crew/CrewHero'; +import { ControllerBadge } from '@/components/ControllerBadge'; import { CrewLeadership } from '@/components/crew/CrewLeadership'; import { CrewSponsorBanner } from '@/components/crew/CrewSponsorBanner'; import { CrewCoordinatorCard } from '@/components/crew/CrewCoordinatorCard'; @@ -190,6 +191,8 @@ export default function CrewDetailPage() { actions={ (canManage || isFounder || (isAdmin && !isFounder) || role === 'member' || role === 'none') ? ( <> + {isFounder && } + {isAdmin && !isFounder && } {canManage && } {isFounder && ( <> diff --git a/src/pages/EventDetailPage.tsx b/src/pages/EventDetailPage.tsx index adf462e..ca13774 100644 --- a/src/pages/EventDetailPage.tsx +++ b/src/pages/EventDetailPage.tsx @@ -30,6 +30,7 @@ import { Users } from 'lucide-react'; import NotFound from './NotFound'; import { Calendar, Clock, MapPin, ArrowLeft, Share2, Navigation, CalendarPlus, Pencil, Music, Megaphone, XCircle, CalendarClock } from 'lucide-react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { ControllerBadge } from '@/components/ControllerBadge'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from '@/contexts/language'; @@ -411,6 +412,7 @@ export default function EventDetailPage() { {event.title}
+ {user?.pubkey === event.pubkey && } {crewHost && ( ev.id === eventId) ?? myEvents[0]; const eventRsvp = useEventRsvps(cardType === 'event' && activeEvent ? eventCoord(activeEvent) : undefined); + + // Which card entities does this account actually control? + const crewRoleOnCard: 'owner' | 'admin' | undefined = + memberships.some((m) => m.crew.pubkey === user?.pubkey) ? 'owner' + : memberships.some((m) => user?.pubkey && m.crew.admins.includes(user.pubkey)) ? 'admin' + : undefined; + const organisesEvent = myEvents.some((e) => e.pubkey === user?.pubkey); const myVenues = useMemo( () => (allSpots ?? []).filter((s) => s.pubkey === user?.pubkey), [allSpots, user?.pubkey], @@ -719,11 +726,11 @@ export default function RiderCardPage() {

{t('labUi.rider.cardType')}

{([ - { id: 'rider' as const, label: t('labUi.rider.licenseLabel'), icon: User }, - ...(myVenues.length > 0 ? [{ id: 'park' as const, label: t('labUi.rider.parkLabel'), icon: Building2 }] : []), - ...(memberships.length > 0 ? [{ id: 'crew' as const, label: t('cardUi.crew.crewCard'), icon: Users }] : []), - ...(myEvents.length > 0 ? [{ id: 'event' as const, label: t('cardUi.event.pass'), icon: Ticket }] : []), - ]).map(({ id, label, icon: Icon }) => ( + { id: 'rider' as const, label: t('labUi.rider.licenseLabel'), icon: User, role: undefined }, + ...(myVenues.length > 0 ? [{ id: 'park' as const, label: t('labUi.rider.parkLabel'), icon: Building2, role: 'owner' as const }] : []), + ...(memberships.length > 0 ? [{ id: 'crew' as const, label: t('cardUi.crew.crewCard'), icon: Users, role: crewRoleOnCard }] : []), + ...(myEvents.length > 0 ? [{ id: 'event' as const, label: t('cardUi.event.pass'), icon: Ticket, role: organisesEvent ? ('organiser' as const) : undefined }] : []), + ]).map(({ id, label, icon: Icon, role }) => ( ))}
diff --git a/src/pages/ShopDetailPage.tsx b/src/pages/ShopDetailPage.tsx index b69453b..e3d82a1 100644 --- a/src/pages/ShopDetailPage.tsx +++ b/src/pages/ShopDetailPage.tsx @@ -10,7 +10,6 @@ import { SpotMap } from '@/components/SpotMap'; import { RouteDialog } from '@/components/RouteDialog'; import type { SkateSpot } from '@/lib/types'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; @@ -19,6 +18,7 @@ import { ClinkPayButton } from '@/components/ClinkPayButton'; import { CommentsSection } from '@/components/comments/CommentsSection'; import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl'; import { OfficialBadge } from '@/components/OfficialBadge'; +import { ControllerBadge } from '@/components/ControllerBadge'; import { useShop } from '@/hooks/useShops'; import { useAuthor } from '@/hooks/useAuthor'; import { useDeleteEntity } from '@/hooks/useDeleteEntity'; @@ -42,7 +42,6 @@ import { Globe, Instagram, Clock, - CheckCircle2, } from 'lucide-react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useTranslation } from '@/contexts/language'; @@ -276,12 +275,7 @@ export default function ShopDetailPage() {

{shop.name}

- {isVerifiedOwner && ( - - - {t('common.verified')} - - )} + {isVerifiedOwner && }
{shop.location && ( diff --git a/src/pages/SpotDetailPage.tsx b/src/pages/SpotDetailPage.tsx index c9823f3..5c7b511 100644 --- a/src/pages/SpotDetailPage.tsx +++ b/src/pages/SpotDetailPage.tsx @@ -6,6 +6,7 @@ import ngeohash from 'ngeohash'; import { ImageWithFallback } from '@/components/ImageWithFallback'; import { ImageLightbox, type LightboxImage } from '@/components/ImageLightbox'; import { PageError } from '@/components/ErrorState'; +import { ControllerBadge } from '@/components/ControllerBadge'; import { SpotMap } from '@/components/SpotMap'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -373,7 +374,10 @@ export default function SpotDetailPage() {
-

{spot.title}

+
+

{spot.title}

+ {isOwner && } +
{spot.location && (