Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';

import { memo, useCallback, useMemo } from 'react';

import type { Community } from '#app/models/community';
import EmptyState from '#components/EmptyState';
import Loading from '#components/ui/Loading';
Expand Down Expand Up @@ -27,12 +29,23 @@ export function MPSubvComparison({
}: MPSubvComparisonProperties) {
const { year: selectedYear, setYear: setSelectedYear } = useComparisonYear();

// Memoize section title to prevent recalculation
const sectionTitle = useMemo(() => getSectionTitle(comparisonType), [comparisonType]);

// Stabilize year selection handler
const handleYearSelect = useCallback(
(year: number) => {
setSelectedYear(year);
},
[setSelectedYear],
);

return (
<>
<SectionSeparator
sectionTitle={getSectionTitle(comparisonType)}
sectionTitle={sectionTitle}
year={selectedYear}
onSelectYear={setSelectedYear}
onSelectYear={handleYearSelect}
/>
{/* Desktop layout */}
<div className='hidden md:block'>
Expand All @@ -53,7 +66,6 @@ export function MPSubvComparison({
bgColor='bg-primary-light'
/>
}
className='my-10'
/>
</div>

Expand All @@ -77,37 +89,37 @@ type ComparingMPSubvProperties = {
bgColor?: string;
};

function ComparingMPSubv({ siren, year, comparisonType, bgColor }: ComparingMPSubvProperties) {
const { data, isPending, isError } = useMPSubvComparison(siren, year, comparisonType);
const ComparingMPSubv = memo(
({ siren, year, comparisonType, bgColor }: ComparingMPSubvProperties) => {
const { data, isPending, isError } = useMPSubvComparison(siren, year, comparisonType);

// Show loading state
if (isPending) {
return <Loading />;
}
// Show EmptyState for actual errors or missing data
if (isError || (!isPending && (!data || data.top5 === undefined))) {
return (
<EmptyState
title={`Aucune donnée de ${getName(comparisonType)} disponible`}
description={`Il n'y a pas de données de ${getName(comparisonType)} disponibles pour cette période. Tu peux utiliser la plateforme pour interpeller directement les élus ou les services concernés.`}
siren={siren}
className='h-full'
/>
);
}

// Show EmptyState for actual errors or missing data
if (isError || !data || data.top5 === undefined) {
return (
<EmptyState
title={`Aucune donnée de ${getName(comparisonType)} disponible`}
description={`Il n'y a pas de données de ${getName(comparisonType)} disponibles pour cette période. Tu peux utiliser la plateforme pour interpeller directement les élus ou les services concernés.`}
siren={siren}
className='h-full'
<TableInfoBlock
totalAmount={data?.total_amount || 0}
totalNumber={data?.total_number || 0}
top5Items={data?.top5 || null}
comparisonName={getName(comparisonType)}
columnLabel={getColumnLabel(comparisonType)}
bgColor={bgColor}
isLoadingDetails={isPending}
/>
);
}
},
);

return (
<TableInfoBlock
totalAmount={data.total_amount}
totalNumber={data.total_number}
top5Items={data.top5}
comparisonName={getName(comparisonType)}
columnLabel={getColumnLabel(comparisonType)}
bgColor={bgColor}
/>
);
}
ComparingMPSubv.displayName = 'ComparingMPSubv';

function getSectionTitle(comparisonType: ComparisonType) {
switch (comparisonType) {
Expand Down Expand Up @@ -149,7 +161,7 @@ type MobileMPSubvCardProps = {
comparisonType: ComparisonType;
};

function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubvCardProps) {
const MobileMPSubvCard = memo(({ siren1, siren2, year, comparisonType }: MobileMPSubvCardProps) => {
const {
data: data1,
isPending: isPending1,
Expand All @@ -162,6 +174,26 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv
isError: isError2,
} = useMPSubvComparison(siren2, year, comparisonType);

// Memoize comparison name to prevent recalculation
const comparisonName = useMemo(() => getName(comparisonType), [comparisonType]);

const renderInfoBlock = useCallback(
(label: string, value1: string, value2: string) => (
<>
<h4 className='mb-3 text-sm font-semibold text-primary-900'>{label}</h4>
<div className='flex justify-between'>
<div className='rounded-full bg-brand-3 px-4 py-2'>
<span className='text-lg font-bold text-primary-900'>{value1}</span>
</div>
<div className='rounded-full bg-primary-light px-4 py-2'>
<span className='text-lg font-bold text-primary-900'>{value2}</span>
</div>
</div>
</>
),
[],
);

if (isPending1 || isPending2) {
return <Loading />;
}
Expand All @@ -176,28 +208,14 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv
) {
return (
<EmptyState
title={`Aucune donnée de ${getName(comparisonType)} disponible`}
description={`Il n'y a pas de données de ${getName(comparisonType)} disponibles pour cette période.`}
title={`Aucune donnée de ${comparisonName} disponible`}
description={`Il n'y a pas de données de ${comparisonName} disponibles pour cette période.`}
siren={siren1}
className='h-full'
/>
);
}

const renderInfoBlock = (label: string, value1: string, value2: string) => (
<>
<h4 className='mb-3 text-sm font-semibold text-primary-900'>{label}</h4>
<div className='flex justify-between'>
<div className='rounded-full bg-brand-3 px-4 py-2'>
<span className='text-lg font-bold text-primary-900'>{value1}</span>
</div>
<div className='rounded-full bg-primary-light px-4 py-2'>
<span className='text-lg font-bold text-primary-900'>{value2}</span>
</div>
</div>
</>
);

return (
<Card className='space-y-6 p-4'>
{/* Comparaison Montant Total */}
Expand All @@ -212,7 +230,7 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv
{/* Comparaison Nombre */}
<div className='border-b pb-4'>
{renderInfoBlock(
`Nombre de ${getName(comparisonType)}`,
`Nombre de ${comparisonName}`,
data1.total_number.toString(),
data2.total_number.toString(),
)}
Expand All @@ -224,7 +242,7 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv
totalAmount={data1.total_amount}
totalNumber={data1.total_number}
top5Items={data1.top5}
comparisonName={getName(comparisonType)}
comparisonName={comparisonName}
columnLabel={getColumnLabel(comparisonType)}
communityName={formatLocationName(data1?.community_name || 'N/A')}
bgColor='bg-brand-3'
Expand All @@ -234,12 +252,14 @@ function MobileMPSubvCard({ siren1, siren2, year, comparisonType }: MobileMPSubv
totalAmount={data2.total_amount}
totalNumber={data2.total_number}
top5Items={data2.top5}
comparisonName={getName(comparisonType)}
comparisonName={comparisonName}
columnLabel={getColumnLabel(comparisonType)}
communityName={formatLocationName(data2?.community_name || 'N/A')}
bgColor='bg-primary-light'
/>
</div>
</Card>
);
}
});

MobileMPSubvCard.displayName = 'MobileMPSubvCard';
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import { formatCompactPrice } from '#utils/utils';
type TableInfoBlockProps = {
totalAmount: number;
totalNumber: number;
top5Items: Array<{ label: string | null; value: number }>;
top5Items: Array<{ label: string | null; value: number }> | null;
comparisonName: string;
columnLabel: string;
communityName?: string;
bgColor?: string;
className?: string;
isLoadingDetails?: boolean;
};

export function TableInfoBlock({
Expand All @@ -31,6 +32,7 @@ export function TableInfoBlock({
communityName,
bgColor = 'bg-brand-3',
className = '',
isLoadingDetails = false,
}: TableInfoBlockProps) {
return (
<div className={className}>
Expand All @@ -45,38 +47,79 @@ export function TableInfoBlock({
<div className='mb-2 hidden h-11 flex-row gap-4 md:block'>
<h4 className='text-base text-primary'>
Montant total{' '}
<span className={`mx-4 rounded-full px-4 py-2 font-bold ${bgColor}`}>
{formatCompactPrice(totalAmount)}
<span
className={`mx-4 rounded-full px-4 py-2 font-bold ${bgColor} ${isLoadingDetails ? 'animate-pulse' : ''}`}
>
{isLoadingDetails ? '---' : formatCompactPrice(totalAmount)}
</span>
</h4>
</div>
<div className='mb-8 hidden h-11 flex-row gap-4 md:block'>
<h4 className='text-base text-primary'>
Nombre de {comparisonName}{' '}
<span className={`mx-4 rounded-full px-4 py-2 font-bold ${bgColor}`}>{totalNumber}</span>
Nombre de {comparisonName}
<span
className={`mx-4 rounded-full px-4 py-2 font-bold ${bgColor} ${isLoadingDetails ? 'animate-pulse' : ''}`}
>
{isLoadingDetails ? '---' : totalNumber}
</span>
</h4>
</div>

<div className='md:mx-5'>
<Table className='text-xs sm:text-sm'>
<TableCaption>Top 5 des {comparisonName}</TableCaption>
<TableHeader>
<TableRow>
<TableRow className='hover:bg-gray-100 data-[state=selected]:bg-gray-100'>
<TableHead className='text-left'>{columnLabel}</TableHead>
<TableHead className='w-[75px] text-right'>Montant</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{top5Items.map(({ label, value }) => (
<TableRow key={label || 'no-label'}>
<TableCell className='text-left text-base text-primary'>
{label !== null ? label.toLocaleUpperCase() : 'Non précisé'}
</TableCell>
<TableCell className='text-right text-base font-bold text-primary'>
{formatCompactPrice(value)}
</TableCell>
</TableRow>
))}
{isLoadingDetails || !top5Items
? Array.from({ length: 5 }, (_, index) => (
<TableRow
key={`skeleton-row-${index.toString()}`}
className='animate-fadeIn hover:bg-gray-100 data-[state=selected]:bg-gray-100'
>
<TableCell className='text-left text-base text-primary'>
<div className='animate-pulse'>
<div
className={`h-4 rounded bg-gray-300 transition-all duration-300 ${
index === 0
? 'w-32'
: index === 1
? 'w-28'
: index === 2
? 'w-36'
: index === 3
? 'w-24'
: 'w-30'
}`}
/>
</div>
</TableCell>
<TableCell className='text-right text-base font-bold text-primary'>
<div className='flex justify-end'>
<div className='h-4 w-16 animate-pulse rounded bg-gray-300 transition-all duration-300' />
</div>
</TableCell>
</TableRow>
))
: top5Items.map(({ label, value }, index) => (
<TableRow
key={`${label || 'no-label'}-${index}`}
className='animate-fadeIn transition-all duration-300 hover:bg-gray-100 data-[state=selected]:bg-gray-100'
>
<TableCell className='text-left text-base text-primary'>
{label !== null
? label.charAt(0).toUpperCase() + label.slice(1).toLowerCase()
: 'Non précisé'}
</TableCell>
<TableCell className='text-right text-base font-bold text-primary'>
{formatCompactPrice(value)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@ import { SideBySideComparison } from './shared/SideBySideComparison';
type TransparencyComparisonProperties = {
siren1: string;
siren2: string;
community1Name: string;
community2Name: string;
};

export function TransparencyComparison({ siren1, siren2 }: TransparencyComparisonProperties) {
export function TransparencyComparison({
siren1,
siren2,
community1Name,
community2Name,
}: TransparencyComparisonProperties) {
const { year: selectedYear, setYear: setSelectedYear } = useComparisonYear();

return (
Expand All @@ -34,13 +41,18 @@ export function TransparencyComparison({ siren1, siren2 }: TransparencyCompariso
<SideBySideComparison
leftChild={<ComparingScore siren={siren1} year={selectedYear as number} />}
rightChild={<ComparingScore siren={siren2} year={selectedYear as number} />}
className='my-10'
/>
</div>

{/* Mobile layout - unified card */}
<div className='my-6 md:hidden'>
<MobileComparisonCard siren1={siren1} siren2={siren2} year={selectedYear as number} />
<MobileComparisonCard
siren1={siren1}
siren2={siren2}
year={selectedYear as number}
community1Name={community1Name}
community2Name={community2Name}
/>
</div>
</>
);
Expand Down Expand Up @@ -120,9 +132,17 @@ type MobileComparisonCardProps = {
siren1: string;
siren2: string;
year: number;
community1Name: string;
community2Name: string;
};

function MobileComparisonCard({ siren1, siren2, year }: MobileComparisonCardProps) {
function MobileComparisonCard({
siren1,
siren2,
year,
community1Name,
community2Name,
}: MobileComparisonCardProps) {
const {
data: data1,
isPending: isPending1,
Expand Down Expand Up @@ -156,8 +176,8 @@ function MobileComparisonCard({ siren1, siren2, year }: MobileComparisonCardProp
<Card className='p-4'>
{/* Header avec les noms des villes */}
<div className='mb-4 flex items-center justify-between border-b pb-4'>
<span className='text-sm font-medium text-primary'>Ville de Paris</span>
<span className='text-sm font-medium text-primary'>Dijon Métropole</span>
<span className='text-sm font-medium text-primary'>{community1Name}</span>
<span className='text-sm font-medium text-primary'>{community2Name}</span>
</div>

{/* Section Marchés publics */}
Expand Down
Loading
Loading