~3,470 new keys (5,770 total, EN=ES), ~115 more files wired. - nav + chromeUi: header, More menu, burger menu, footer, map toolbar widgets (sort/trust/weather), spot-type label helper. - placesUi: events, shops, tours, food. socialUi: crews, DMs, notifications, collectives, crew chat/coordinator. settingsUi: settings, auth/signup, onboarding, profile edit, legal. walletUi: wallet/Cashu, stash, mints, app shop, newsletter, community stats, leaderboards, profile. - Fixed 738 usages where an agent dropped the socialUi. prefix (caught by check:i18n).
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
import { cn } from '@/lib/utils';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import { crewBadge, ACTIVITY_META, type CrewActivity } from '@/lib/crewHelpers';
|
|
import type { SkateCrew } from '@/lib/types';
|
|
|
|
/**
|
|
* A small "legitimacy" badge (OG / Verified / Established) derived from a crew's
|
|
* founding year, NIP-05 and member count. Social proof for the directory.
|
|
*/
|
|
export function CrewRepBadge({
|
|
crew,
|
|
confirmedMembers,
|
|
className,
|
|
}: {
|
|
crew: Pick<SkateCrew, 'founded' | 'nip05' | 'members'>;
|
|
confirmedMembers?: number;
|
|
className?: string;
|
|
}) {
|
|
const badge = crewBadge(crew, confirmedMembers);
|
|
if (badge.key === 'crew') return null;
|
|
|
|
const styles: Record<string, string> = {
|
|
og: 'bg-amber-500/15 text-amber-300 border-amber-500/40',
|
|
verified: 'bg-teal-500/15 text-teal-300 border-teal-500/40',
|
|
established: 'bg-fuchsia-500/15 text-fuchsia-300 border-fuchsia-500/40',
|
|
};
|
|
|
|
return (
|
|
<span
|
|
className={cn(
|
|
'inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide',
|
|
styles[badge.key],
|
|
className,
|
|
)}
|
|
title={badge.label}
|
|
>
|
|
<span className="leading-none">{badge.emoji}</span>
|
|
{badge.label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A subtle activity pulse ("🔥 Active this week"). Dormant crews render nothing
|
|
* unless `showDormant` is set, so the grid highlights life rather than death.
|
|
*/
|
|
export function CrewActivityPulse({
|
|
activity,
|
|
showDormant = false,
|
|
className,
|
|
}: {
|
|
activity: CrewActivity | undefined;
|
|
showDormant?: boolean;
|
|
className?: string;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
if (!activity) return null;
|
|
// In dense card grids we only surface positive signals — hide dormant crews.
|
|
if (activity.level === 'dormant' && !showDormant) return null;
|
|
|
|
const meta = ACTIVITY_META[activity.level];
|
|
const isHot = activity.level === 'hot';
|
|
const upcoming =
|
|
activity.upcomingEvents === 1
|
|
? t('socialUi.badge.upcomingOne', { count: activity.upcomingEvents })
|
|
: t('socialUi.badge.upcomingMany', { count: activity.upcomingEvents });
|
|
const coming =
|
|
activity.upcomingEvents === 1
|
|
? t('socialUi.badge.comingOne', { count: activity.upcomingEvents })
|
|
: t('socialUi.badge.comingMany', { count: activity.upcomingEvents });
|
|
|
|
return (
|
|
<span
|
|
className={cn('inline-flex items-center gap-1 text-[10px] font-bold', meta.text, className)}
|
|
title={
|
|
activity.upcomingEvents > 0
|
|
? upcoming
|
|
: meta.label
|
|
}
|
|
>
|
|
<span className="relative flex h-1.5 w-1.5">
|
|
{isHot && (
|
|
<span className={cn('absolute inline-flex h-full w-full animate-ping rounded-full opacity-75', meta.dot)} />
|
|
)}
|
|
<span className={cn('relative inline-flex h-1.5 w-1.5 rounded-full', meta.dot)} />
|
|
</span>
|
|
{activity.upcomingEvents > 0
|
|
? coming
|
|
: meta.label}
|
|
</span>
|
|
);
|
|
}
|