Skip to content

Commit 2a84f03

Browse files
committed
refactor: group master versions by common attributes for improved display and import Plus icon.
1 parent 5322175 commit 2a84f03

File tree

1 file changed

+117
-76
lines changed

1 file changed

+117
-76
lines changed

frontend/src/pages/MasterPage.tsx

Lines changed: 117 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import React, { useEffect, useState, useMemo, useCallback } from 'react';
3-
import { ArrowLeft, ChevronDown } from 'lucide-react';
3+
import { ArrowLeft, ChevronDown, Plus } from 'lucide-react';
44
import { useParams, useNavigate } from 'react-router';
55
import axios from 'axios';
66
import { useTranslation } from 'react-i18next';
@@ -116,6 +116,27 @@ const MasterPage: React.FC = () => {
116116
return filteredVersions.slice(0, visibleCount);
117117
}, [filteredVersions, visibleCount]);
118118

119+
// Group visible versions by attributes for rendering
120+
const groupedVisibleVersions = useMemo(() => {
121+
const groups: { [key: string]: { header: { released: string, majorFormat: string, label: string, country: string }, versions: MasterVersion[] } } = {};
122+
visibleVersions.forEach(version => {
123+
const key = `${version.released || 'N/A'}|${version.majorFormat}|${version.label}|${version.country || ''}`;
124+
if (!groups[key]) {
125+
groups[key] = {
126+
header: {
127+
released: version.released,
128+
majorFormat: version.majorFormat,
129+
label: version.label,
130+
country: version.country
131+
},
132+
versions: []
133+
};
134+
}
135+
groups[key].versions.push(version);
136+
});
137+
return Object.values(groups);
138+
}, [visibleVersions]);
139+
119140
// Auto-fetch release details for visible versions
120141
const fetchReleaseDetails = useCallback(async (releaseId: number) => {
121142
if (releaseDetailsCache.has(releaseId) || loadingReleaseIds.has(releaseId)) return;
@@ -323,84 +344,104 @@ const MasterPage: React.FC = () => {
323344
</div>
324345
)
325346
) : (
326-
<>
327-
<div className="space-y-4">
328-
{visibleVersions.map((version) => {
329-
const details = releaseDetailsCache.get(version.id);
330-
const isLoadingDetails = loadingReleaseIds.has(version.id);
331-
const formats = details?.availableFormats || [];
332-
333-
return (
334-
<div key={version.id} className="card bg-base-200 shadow-sm hover:shadow-md transition-shadow">
335-
<div className="card-body p-4">
336-
{/* Version header row */}
337-
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
338-
<div className="flex items-center gap-3 flex-1 min-w-0">
339-
<span className="font-bold text-lg flex-shrink-0">{version.released || 'N/A'}</span>
340-
<span className="font-medium text-sm truncate" title={version.majorFormat}>{version.majorFormat}</span>
341-
<span className="text-xs text-gray-500 truncate" title={version.label}>
342-
{version.label}
343-
</span>
347+
<div className="space-y-4">
348+
{groupedVisibleVersions.map((group, groupIdx) => (
349+
<div key={groupIdx} className="bg-base-200/30 rounded-lg p-4 border border-base-200 hover:border-base-300 transition-colors">
350+
{/* Group Header Info */}
351+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 mb-3">
352+
<span className="font-bold text-lg whitespace-nowrap">{group.header.released || 'N/A'}</span>
353+
<span className="text-base-content/40 hidden sm:inline"></span>
354+
<span className="font-medium text-base-content/90 truncate max-w-[200px] sm:max-w-[250px]" title={group.header.majorFormat}>{group.header.majorFormat}</span>
355+
<span className="text-base-content/40 hidden sm:inline"></span>
356+
<span className="text-base-content/70 truncate max-w-[200px] sm:max-w-[250px]" title={group.header.label}>{group.header.label}</span>
357+
<span className="text-base-content/40 hidden sm:inline"></span>
358+
<span className="text-sm bg-base-200 px-2 py-0.5 rounded font-medium whitespace-nowrap">{group.header.country || t('versions.unknown')}</span>
359+
</div>
360+
361+
{/* Formats list for this group */}
362+
<div className="flex flex-col gap-2 pl-2 border-l-2 border-base-300/50">
363+
{(() => {
364+
let isGroupLoading = false;
365+
const uniqueFormatsMap = new Map();
366+
367+
group.versions.forEach(version => {
368+
if (loadingReleaseIds.has(version.id)) {
369+
isGroupLoading = true;
370+
}
371+
const details = releaseDetailsCache.get(version.id);
372+
if (details && details.availableFormats) {
373+
details.availableFormats.forEach(format => {
374+
const key = `${format.text || ''}|${(format.descriptions || []).join(',')}`;
375+
if (!uniqueFormatsMap.has(key)) {
376+
uniqueFormatsMap.set(key, { details, format, versionId: version.id });
377+
}
378+
});
379+
}
380+
});
381+
382+
if (isGroupLoading) {
383+
return (
384+
<div className="text-sm text-base-content/40 flex items-center py-1">
385+
<span className="loading loading-spinner loading-xs mr-2"></span>
386+
{t('versions.loadingFormats')}
344387
</div>
345-
<div className="text-xs bg-base-100 rounded px-2 py-1.5 flex items-center gap-1.5 flex-shrink-0" title={version.country}>
346-
<span className="opacity-70">🌍</span>
347-
<span className="font-medium">{version.country || t('versions.unknown')}</span>
348-
</div>
349-
</div>
350-
351-
{/* Format variants section */}
352-
<div className="mt-3">
353-
{isLoadingDetails ? (
354-
<div className="flex items-center gap-2 text-sm text-gray-400">
355-
<span className="loading loading-spinner loading-xs"></span>
356-
{t('versions.loadingFormats')}
357-
</div>
358-
) : formats.length > 0 ? (
359-
<div className="flex flex-wrap gap-2">
360-
{formats.map((format, index) => (
361-
<button
362-
key={index}
363-
className="btn btn-sm btn-outline h-auto py-1.5 normal-case max-w-full"
364-
onClick={() => details && handleFormatClick(details, format)}
365-
disabled={isSubmitting}
366-
title={t('versions.clickToAdd')}
367-
style={getFormatButtonStyle(format.text, format.descriptions)}
368-
>
369-
<div className="text-left w-full break-words whitespace-normal overflow-hidden">
370-
<span className="font-semibold">{format.name}</span>
371-
{format.text && <span className="ml-1 break-words">{format.text}</span>}
372-
{format.descriptions?.length > 0 && (
373-
<span className="block text-xs opacity-70 break-words">
374-
{format.descriptions.join(', ')}
375-
</span>
376-
)}
377-
</div>
378-
</button>
379-
))}
388+
);
389+
}
390+
391+
const uniqueFormats = Array.from(uniqueFormatsMap.values());
392+
393+
if (uniqueFormats.length === 0) {
394+
return null;
395+
}
396+
397+
return uniqueFormats.map((item, index) => {
398+
const { details, format, versionId } = item;
399+
400+
const descStr = format.descriptions?.join(', ') || '';
401+
const displayTitle = format.text || descStr || format.name;
402+
const displaySubtitle = format.text ? descStr : '';
403+
404+
return (
405+
<button
406+
key={`${versionId}-fmt-${index}`}
407+
className="btn btn-sm btn-outline border-base-300 hover:border-primary/50 normal-case justify-start text-left group/btn transition-all w-full h-auto py-2 px-3"
408+
onClick={() => details && handleFormatClick(details, format)}
409+
disabled={isSubmitting}
410+
title={t('versions.clickToAdd')}
411+
style={getFormatButtonStyle(format.text, format.descriptions)}
412+
>
413+
<div className="flex flex-col items-start min-w-0 flex-1 w-full gap-0.5">
414+
<div className="flex items-center w-full">
415+
<span className="font-bold whitespace-normal break-words overflow-hidden text-sm mr-1.5">{displayTitle}</span>
416+
<Plus size={16} className="opacity-0 group-hover/btn:opacity-100 transition-opacity ml-auto flex-shrink-0" />
417+
</div>
418+
{displaySubtitle && (
419+
<span className="text-xs opacity-80 break-words whitespace-normal leading-tight mt-0.5 text-base-content/80 font-medium">
420+
{displaySubtitle}
421+
</span>
422+
)}
380423
</div>
381-
) : !isLoadingDetails && (
382-
<p className="text-xs text-gray-500">{t('versions.noFormats')}</p>
383-
)}
384-
</div>
385-
</div>
386-
</div>
387-
);
388-
})}
389-
</div>
390-
391-
{/* See more button */}
392-
{hasMore && (
393-
<div className="flex justify-center mt-6">
394-
<button
395-
className="btn btn-ghost gap-2"
396-
onClick={handleShowMore}
397-
>
398-
<ChevronDown size={18} />
399-
{t('versions.seeMore', { remaining: remaining > VERSIONS_PER_PAGE ? VERSIONS_PER_PAGE : remaining, total: filteredVersions.length })}
400-
</button>
424+
</button>
425+
);
426+
});
427+
})()}
428+
</div>
401429
</div>
402-
)}
403-
</>
430+
))}
431+
</div>
432+
)}
433+
434+
{/* See more button */}
435+
{hasMore && (
436+
<div className="flex justify-center mt-6">
437+
<button
438+
className="btn btn-ghost gap-2"
439+
onClick={handleShowMore}
440+
>
441+
<ChevronDown size={18} />
442+
{t('versions.seeMore', { remaining: remaining > VERSIONS_PER_PAGE ? VERSIONS_PER_PAGE : remaining, total: filteredVersions.length })}
443+
</button>
444+
</div>
404445
)}
405446
</div>
406447
</div>

0 commit comments

Comments
 (0)