harden media
This commit is contained in:
@@ -33,6 +33,7 @@ export default function DriverCard(props: DriverCardProps) {
|
||||
const driverViewModel = new DriverViewModel({
|
||||
id,
|
||||
name,
|
||||
avatarUrl: null,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
|
||||
export interface DriverIdentityProps {
|
||||
@@ -21,8 +22,8 @@ export default function DriverIdentity(props: DriverIdentityProps) {
|
||||
|
||||
const metaTextClasses = 'text-xs md:text-sm text-gray-400';
|
||||
|
||||
// Use provided avatar URL or fallback to default avatar path
|
||||
const avatarUrl = driver.avatarUrl || `/api/media/avatar/${driver.id}`;
|
||||
// Use provided avatar URL or show placeholder if null
|
||||
const avatarUrl = driver.avatarUrl;
|
||||
|
||||
const content = (
|
||||
<div className="flex items-center gap-3 md:gap-4 flex-1 min-w-0">
|
||||
@@ -30,13 +31,17 @@ export default function DriverIdentity(props: DriverIdentityProps) {
|
||||
className={`rounded-full bg-primary-blue/20 overflow-hidden flex items-center justify-center shrink-0`}
|
||||
style={{ width: avatarSize, height: avatarSize }}
|
||||
>
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt={driver.name}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{avatarUrl ? (
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt={driver.name}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={avatarSize} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { Users, Crown, Shield, ChevronRight } from 'lucide-react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
interface TeamLeaderboardPreviewProps {
|
||||
teams: TeamSummaryViewModel[];
|
||||
@@ -82,9 +84,15 @@ export default function TeamLeaderboardPreview({ teams, onTeamClick }: TeamLeade
|
||||
{position <= 3 ? <Crown className="w-3.5 h-3.5" /> : position}
|
||||
</div>
|
||||
|
||||
{/* Team Icon */}
|
||||
<div className={`flex h-9 w-9 items-center justify-center rounded-lg ${levelConfig?.bgColor} border ${levelConfig?.borderColor}`}>
|
||||
<LevelIcon className={`w-4 h-4 ${levelConfig?.color}`} />
|
||||
{/* Team Logo */}
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-charcoal-outline border border-charcoal-outline overflow-hidden">
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={36}
|
||||
height={36}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
||||
import { getLeagueCoverClasses } from '@/lib/leagueCovers';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
interface LeagueCardProps {
|
||||
league: LeagueSummaryViewModel;
|
||||
@@ -114,9 +115,8 @@ function isNewLeague(createdAt: string | Date): boolean {
|
||||
}
|
||||
|
||||
export default function LeagueCard({ league, onClick }: LeagueCardProps) {
|
||||
const { mediaService } = useServices();
|
||||
const coverUrl = mediaService.getLeagueCover(league.id);
|
||||
const logoUrl = mediaService.getLeagueLogo(league.id);
|
||||
const coverUrl = getMediaUrl('league-cover', league.id);
|
||||
const logoUrl = league.logoUrl;
|
||||
|
||||
const ChampionshipIcon = getChampionshipIcon(league.scoring?.primaryChampionshipType);
|
||||
const championshipLabel = getChampionshipLabel(league.scoring?.primaryChampionshipType);
|
||||
@@ -190,15 +190,19 @@ export default function LeagueCard({ league, onClick }: LeagueCardProps) {
|
||||
{/* Logo */}
|
||||
<div className="absolute left-4 -bottom-6 z-10">
|
||||
<div className="w-12 h-12 rounded-lg overflow-hidden border-2 border-iron-gray bg-deep-graphite shadow-xl">
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${league.name} logo`}
|
||||
width={48}
|
||||
height={48}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{logoUrl ? (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${league.name} logo`}
|
||||
width={48}
|
||||
height={48}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={48} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import MembershipStatus from '@/components/leagues/MembershipStatus';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
@@ -28,8 +28,7 @@ export default function LeagueHeader({
|
||||
ownerId,
|
||||
mainSponsor,
|
||||
}: LeagueHeaderProps) {
|
||||
const { mediaService } = useServices();
|
||||
const logoUrl = mediaService.getLeagueLogo(leagueId);
|
||||
const logoUrl = getMediaUrl('league-logo', leagueId);
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function LeagueMembers({
|
||||
|
||||
const byId: Record<string, DriverViewModel> = {};
|
||||
for (const dto of driverDtos) {
|
||||
byId[dto.id] = new DriverViewModel(dto);
|
||||
byId[dto.id] = new DriverViewModel({ ...dto, avatarUrl: (dto as any).avatarUrl ?? null });
|
||||
}
|
||||
setDriversById(byId);
|
||||
} else {
|
||||
|
||||
@@ -48,6 +48,7 @@ export default function LeagueOwnershipTransfer({
|
||||
driver={new DriverViewModel({
|
||||
id: ownerSummary.driver.id,
|
||||
name: ownerSummary.driver.name,
|
||||
avatarUrl: (ownerSummary.driver as any).avatarUrl ?? null,
|
||||
iracingId: ownerSummary.driver.iracingId,
|
||||
country: ownerSummary.driver.country,
|
||||
bio: ownerSummary.driver.bio,
|
||||
|
||||
@@ -8,7 +8,8 @@ import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
import { LeagueRoleDisplay } from '@/lib/display-objects/LeagueRoleDisplay';
|
||||
import CountryFlag from '@/components/ui/CountryFlag';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
|
||||
// Position background colors
|
||||
const getPositionBgColor = (position: number): string => {
|
||||
@@ -52,7 +53,6 @@ export default function StandingsTable({
|
||||
onRemoveMember,
|
||||
onUpdateRole
|
||||
}: StandingsTableProps) {
|
||||
const { mediaService } = useServices();
|
||||
const [hoveredRow, setHoveredRow] = useState<string | null>(null);
|
||||
const [activeMenu, setActiveMenu] = useState<{ driverId: string; type: 'member' | 'points' } | null>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -320,16 +320,20 @@ export default function StandingsTable({
|
||||
{/* Avatar */}
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-blue/20 overflow-hidden flex items-center justify-center shrink-0">
|
||||
{driver && (
|
||||
<Image
|
||||
src={mediaService.getDriverAvatar(driver.id)}
|
||||
alt={driver.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{driver && (
|
||||
driver.avatarUrl ? (
|
||||
<Image
|
||||
src={driver.avatarUrl}
|
||||
alt={driver.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={40} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{/* Nationality flag */}
|
||||
{driver && driver.country && (
|
||||
<div className="absolute -bottom-1 -right-1">
|
||||
|
||||
@@ -5,13 +5,13 @@ import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import DriverRating from '@/components/profile/DriverRatingPill';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
|
||||
export interface DriverSummaryPillProps {
|
||||
driver: DriverViewModel;
|
||||
rating: number | null;
|
||||
rank: number | null;
|
||||
avatarSrc?: string;
|
||||
avatarSrc?: string | null;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
}
|
||||
@@ -19,21 +19,22 @@ export interface DriverSummaryPillProps {
|
||||
export default function DriverSummaryPill(props: DriverSummaryPillProps) {
|
||||
const { driver, rating, rank, avatarSrc, onClick, href } = props;
|
||||
|
||||
const { mediaService } = useServices();
|
||||
|
||||
const resolvedAvatar =
|
||||
avatarSrc ?? mediaService.getDriverAvatar(driver.id);
|
||||
const resolvedAvatar = avatarSrc;
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden bg-charcoal-outline flex items-center justify-center border border-charcoal-outline/80">
|
||||
<Image
|
||||
src={resolvedAvatar}
|
||||
alt={driver.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{resolvedAvatar ? (
|
||||
<Image
|
||||
src={resolvedAvatar}
|
||||
alt={driver.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={32} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col leading-tight text-left">
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import Button from '../ui/Button';
|
||||
import DriverRatingPill from '@/components/profile/DriverRatingPill';
|
||||
import CountryFlag from '@/components/ui/CountryFlag';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
|
||||
interface ProfileHeaderProps {
|
||||
driver: DriverViewModel;
|
||||
@@ -26,19 +26,21 @@ export default function ProfileHeader({
|
||||
teamName,
|
||||
teamTag,
|
||||
}: ProfileHeaderProps) {
|
||||
const { mediaService } = useServices();
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-primary-blue to-purple-600 overflow-hidden flex items-center justify-center">
|
||||
<Image
|
||||
src={mediaService.getDriverAvatar(driver.id)}
|
||||
alt={driver.name}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{driver.avatarUrl ? (
|
||||
<Image
|
||||
src={driver.avatarUrl}
|
||||
alt={driver.name}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={80} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -19,9 +19,8 @@ vi.mock('@/hooks/useEffectiveDriverId', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock services hook to inject stub driverService/mediaService
|
||||
const mockFindById = vi.fn<[], Promise<DriverDTO | null>>();
|
||||
const mockGetDriverAvatar = vi.fn<(driverId: string) => string>();
|
||||
// Mock services hook to inject stub driverService
|
||||
const mockFindById = vi.fn();
|
||||
|
||||
vi.mock('@/lib/services/ServiceProvider', () => {
|
||||
return {
|
||||
@@ -30,7 +29,7 @@ vi.mock('@/lib/services/ServiceProvider', () => {
|
||||
findById: mockFindById,
|
||||
},
|
||||
mediaService: {
|
||||
getDriverAvatar: mockGetDriverAvatar,
|
||||
getDriverAvatar: vi.fn(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -66,7 +65,6 @@ describe('UserPill', () => {
|
||||
mockedAuthValue = { session: null };
|
||||
mockedDriverId = null;
|
||||
mockFindById.mockReset();
|
||||
mockGetDriverAvatar.mockReset();
|
||||
});
|
||||
|
||||
it('renders auth links when there is no session', () => {
|
||||
@@ -94,19 +92,19 @@ describe('UserPill', () => {
|
||||
expect(mockFindById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loads driver via driverService and uses mediaService avatar', async () => {
|
||||
it('loads driver via driverService and uses driver avatarUrl', async () => {
|
||||
const driver: DriverDTO = {
|
||||
id: 'driver-1',
|
||||
iracingId: 'ir-123',
|
||||
name: 'Test Driver',
|
||||
country: 'DE',
|
||||
avatarUrl: '/api/media/avatar/driver-1',
|
||||
};
|
||||
|
||||
mockedAuthValue = { session: { user: { id: 'user-1' } } };
|
||||
mockedDriverId = driver.id;
|
||||
|
||||
mockFindById.mockResolvedValue(driver);
|
||||
mockGetDriverAvatar.mockImplementation((driverId: string) => `/api/media/avatar/${driverId}`);
|
||||
|
||||
render(<UserPill />);
|
||||
|
||||
@@ -115,6 +113,5 @@ describe('UserPill', () => {
|
||||
});
|
||||
|
||||
expect(mockFindById).toHaveBeenCalledWith('driver-1');
|
||||
expect(mockGetDriverAvatar).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function UserPill() {
|
||||
|
||||
const dto = await driverService.findById(primaryDriverId);
|
||||
if (!cancelled) {
|
||||
setDriver(dto ? new DriverViewModelClass(dto) : null);
|
||||
setDriver(dto ? new DriverViewModelClass({ ...dto, avatarUrl: (dto as any).avatarUrl ?? null }) : null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function UserPill() {
|
||||
const rating: number | null = null;
|
||||
const rank: number | null = null;
|
||||
|
||||
const avatarSrc = mediaService.getDriverAvatar(primaryDriverId);
|
||||
const avatarSrc = driver.avatarUrl;
|
||||
|
||||
return {
|
||||
driver,
|
||||
@@ -135,7 +135,7 @@ export default function UserPill() {
|
||||
rating,
|
||||
rank,
|
||||
};
|
||||
}, [session, driver, primaryDriverId, mediaService]);
|
||||
}, [session, driver, primaryDriverId]);
|
||||
|
||||
// Close menu when clicking outside
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
interface Friend {
|
||||
id: string;
|
||||
@@ -23,8 +23,6 @@ function getCountryFlag(countryCode: string): string {
|
||||
}
|
||||
|
||||
export default function FriendPill({ friend }: FriendPillProps) {
|
||||
const { mediaService } = useServices();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/drivers/${friend.id}`}
|
||||
@@ -32,7 +30,7 @@ export default function FriendPill({ friend }: FriendPillProps) {
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
|
||||
<Image
|
||||
src={mediaService.getDriverAvatar(friend.id)}
|
||||
src={getMediaUrl('driver-avatar', friend.id)}
|
||||
alt={friend.name}
|
||||
width={32}
|
||||
height={32}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Image from 'next/image';
|
||||
import { UserPlus, Users, Trophy } from 'lucide-react';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
const SKILL_LEVELS: {
|
||||
id: string;
|
||||
@@ -77,8 +79,14 @@ export default function FeaturedRecruiting({ teams, onTeamClick }: FeaturedRecru
|
||||
className="p-4 rounded-xl bg-iron-gray/60 border border-charcoal-outline hover:border-performance-green/40 transition-all duration-200 text-left group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className={`flex h-8 w-8 items-center justify-center rounded-lg ${levelConfig?.bgColor} border ${levelConfig?.borderColor}`}>
|
||||
{/* LevelIcon would be here */}
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-charcoal-outline border border-charcoal-outline overflow-hidden">
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] bg-performance-green/10 text-performance-green border border-performance-green/20">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-performance-green animate-pulse" />
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Languages,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
|
||||
interface TeamCardProps {
|
||||
id: string;
|
||||
@@ -81,8 +81,7 @@ export default function TeamCard({
|
||||
category,
|
||||
onClick,
|
||||
}: TeamCardProps) {
|
||||
const { mediaService } = useServices();
|
||||
const logoUrl = logo || mediaService.getTeamLogo(id);
|
||||
const logoUrl = logo;
|
||||
const performanceBadge = getPerformanceBadge(performanceLevel);
|
||||
const specializationBadge = getSpecializationBadge(specialization);
|
||||
|
||||
@@ -98,13 +97,17 @@ export default function TeamCard({
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Logo */}
|
||||
<div className="w-14 h-14 rounded-xl bg-charcoal-outline flex items-center justify-center flex-shrink-0 overflow-hidden border border-charcoal-outline">
|
||||
<Image
|
||||
src={logoUrl}
|
||||
alt={name}
|
||||
width={56}
|
||||
height={56}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{logoUrl ? (
|
||||
<Image
|
||||
src={logoUrl}
|
||||
alt={name}
|
||||
width={56}
|
||||
height={56}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={56} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title & Badges */}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
export interface TeamLadderRowProps {
|
||||
rank: number;
|
||||
@@ -26,8 +26,7 @@ export default function TeamLadderRow({
|
||||
totalRaces,
|
||||
}: TeamLadderRowProps) {
|
||||
const router = useRouter();
|
||||
const { mediaService } = useServices();
|
||||
const logo = teamLogoUrl ?? mediaService.getTeamLogo(teamId);
|
||||
const logo = teamLogoUrl ?? getMediaUrl('team-logo', teamId);
|
||||
|
||||
const handleClick = () => {
|
||||
router.push(`/teams/${teamId}`);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { Award, ChevronRight, Crown, Trophy, Users } from 'lucide-react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
const SKILL_LEVELS: {
|
||||
id: string;
|
||||
@@ -133,8 +135,14 @@ export default function TeamLeaderboardPreview({
|
||||
</div>
|
||||
|
||||
{/* Team Info */}
|
||||
<div className={`flex h-9 w-9 items-center justify-center rounded-lg ${levelConfig?.bgColor} border ${levelConfig?.borderColor}`}>
|
||||
{/* LevelIcon */}
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-charcoal-outline border border-charcoal-outline overflow-hidden">
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={36}
|
||||
height={36}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white font-medium truncate group-hover:text-purple-400 transition-colors">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Image from 'next/image';
|
||||
import { Trophy, Crown, Users } from 'lucide-react';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
const SKILL_LEVELS: {
|
||||
id: string;
|
||||
@@ -128,11 +130,15 @@ export default function TopThreePodium({ teams, onClick }: TopThreePodiumProps)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Team icon */}
|
||||
<div
|
||||
className={`flex h-16 w-16 md:h-20 md:w-20 items-center justify-center rounded-xl ${levelConfig?.bgColor} border ${levelConfig?.borderColor} mb-3`}
|
||||
>
|
||||
{/* LevelIcon */}
|
||||
{/* Team logo */}
|
||||
<div className="flex h-16 w-16 md:h-20 md:w-20 items-center justify-center rounded-xl bg-charcoal-outline border border-charcoal-outline overflow-hidden mb-3">
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Team name */}
|
||||
|
||||
20
apps/website/components/ui/PlaceholderImage.tsx
Normal file
20
apps/website/components/ui/PlaceholderImage.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { User } from 'lucide-react';
|
||||
|
||||
export interface PlaceholderImageProps {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared placeholder image component for when no avatar/logo URL is available
|
||||
*/
|
||||
export default function PlaceholderImage({ size = 48, className = '' }: PlaceholderImageProps) {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-full bg-charcoal-outline flex items-center justify-center ${className}`}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<User className="w-6 h-6 text-gray-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user