Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | 1x 1x 1x 1x 1x 1x 9x 9x 9x 9x 9x 9x 9x 5x 5x 5x 5x 5x 4x 4x 4x 1x 1x 5x 9x 4x 9x 9x 9x 9x 9x 9x 9x 9x | import { useState, useCallback, useEffect } from 'react';
import { PublicProfile, BadgeItem } from '@/models/profile';
import {
getPublicProfile,
getUserBadges,
getBadgeCatalog
} from '@/services/profileService';
import { ApiError } from '@/services/api';
import { useAuth } from '@/contexts/AuthContext';
import i18n from '@/i18n';
export interface PublicProfileViewModel {
profile: PublicProfile | null;
isLoading: boolean;
error: string | null;
primaryName: string;
secondaryName: string | null;
avatarInitial: string;
overallRatingLabel: string;
hostRatingLabel: string;
participantRatingLabel: string;
badges: BadgeItem[];
catalogVisible: boolean;
setCatalogVisible: (visible: boolean) => void;
refresh: () => Promise<void>;
}
export function usePublicProfileViewModel(userId: string): PublicProfileViewModel {
const { token } = useAuth();
const [profile, setProfile] = useState<PublicProfile | null>(null);
const [badges, setBadges] = useState<BadgeItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [catalogVisible, setCatalogVisible] = useState(false);
const fetchData = useCallback(async () => {
Iif (!token) {
setError(i18n.t('publicProfile.errors.loginRequired'));
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
try {
const [profileResult, catalogResult, earnedBadgesResult] = await Promise.all([
getPublicProfile(userId, token),
getBadgeCatalog(token),
getUserBadges(userId, token).catch(() => ({ items: [] })) // Fallback if user badges fail
]);
setProfile(profileResult);
// Merge earned status
const mergedBadges = (catalogResult.items || []).map((b: BadgeItem) => {
const earned = (earnedBadgesResult.items || []).find((eb: BadgeItem) => eb.slug === b.slug);
return {
...b,
earned: !!earned,
earned_at: earned?.earned_at || null,
};
});
// In the public profile, only show badges that the user has actually earned
setBadges(mergedBadges.filter(b => b.earned));
} catch (err) {
if (err instanceof ApiError) {
setError(err.message);
} else E{
setError(i18n.t('publicProfile.errors.loadFailed'));
}
} finally {
setIsLoading(false);
}
}, [userId, token]);
useEffect(() => {
void fetchData();
}, [fetchData]);
const primaryName = profile?.display_name ?? profile?.username ?? '';
const secondaryName = profile?.display_name ? profile.username : null;
const avatarInitial = primaryName.trim().charAt(0).toUpperCase() || '?';
const totalCount = (profile?.host_rating_count || 0) + (profile?.participant_rating_count || 0);
const overallRatingLabel = profile?.final_score != null && totalCount > 0
? i18n.t('publicProfile.ratingWithCount', {
rating: profile.final_score.toFixed(1),
count: totalCount,
})
: i18n.t('profile.new');
const hostRatingLabel = profile?.final_score != null && profile.host_rating_count > 0
? i18n.t('publicProfile.ratingWithCount', {
rating: profile.final_score.toFixed(1),
count: profile.host_rating_count,
})
: i18n.t('profile.new');
const participantRatingLabel = profile?.final_score != null && profile.participant_rating_count > 0
? i18n.t('publicProfile.ratingWithCount', {
rating: profile.final_score.toFixed(1),
count: profile.participant_rating_count,
})
: i18n.t('profile.new');
return {
profile,
isLoading,
error,
primaryName,
secondaryName,
avatarInitial,
overallRatingLabel,
hostRatingLabel,
participantRatingLabel,
badges,
catalogVisible,
setCatalogVisible,
refresh: fetchData,
};
}
|