Skip to content

Commit d8a88a4

Browse files
committed
Show remaining slots per sponsorship tier
1 parent 53c9313 commit d8a88a4

3 files changed

Lines changed: 96 additions & 15 deletions

File tree

src/pages/SponsorshipPage.tsx

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { useSponsors } from "@/hooks/useSponsors";
12
import { useTranslation } from "@/hooks/useTranslation";
3+
import { remainingSponsorships } from "@/utils/remainingSponsorships";
24
import { FaLinkedin, FaYoutube } from "react-icons/fa";
35
import { Link } from "react-router-dom";
46

57
const TIERS = [
68
{
79
name: { fr: "Or", en: "Gold" },
10+
level: "or",
811
color: "bg-yellow-500",
912
price: "3 000",
1013
available: 3,
@@ -13,6 +16,7 @@ const TIERS = [
1316
},
1417
{
1518
name: { fr: "Argent", en: "Silver" },
19+
level: "argent",
1620
color: "bg-gray-400",
1721
price: "2 000",
1822
available: 4,
@@ -21,6 +25,7 @@ const TIERS = [
2125
},
2226
{
2327
name: { fr: "Bronze", en: "Bronze" },
28+
level: "bronze",
2429
color: "bg-amber-700",
2530
price: "750",
2631
available: 5,
@@ -61,6 +66,15 @@ const PERKS = [
6166

6267
export default function SponsorshipPage() {
6368
const { t } = useTranslation();
69+
const { allSponsors } = useSponsors();
70+
71+
const tiers = TIERS.map((tier) => ({
72+
...tier,
73+
remaining: remainingSponsorships(
74+
tier.available,
75+
allSponsors.filter((sponsor) => sponsor.level === tier.level).length,
76+
),
77+
}));
6478

6579
return (
6680
<div className="container mx-auto px-4 py-8">
@@ -90,7 +104,7 @@ export default function SponsorshipPage() {
90104
<thead>
91105
<tr>
92106
<th className="p-4 text-left" />
93-
{TIERS.map((tier) => (
107+
{tiers.map((tier) => (
94108
<th key={t(tier.name)} className="p-4 text-center">
95109
<span
96110
className={`inline-block px-6 py-2 rounded-full text-white font-bold text-lg ${tier.color}`}
@@ -106,20 +120,23 @@ export default function SponsorshipPage() {
106120
<td className="p-4 font-medium text-gray-700">
107121
{t({ fr: "Disponibles", en: "Available" })}
108122
</td>
109-
{TIERS.map((tier) => (
123+
{tiers.map((tier) => (
110124
<td
111125
key={t(tier.name)}
112126
className="p-4 text-center text-gray-600"
113127
>
114-
{tier.available}
128+
<AvailabilityLabel
129+
total={tier.available}
130+
remaining={tier.remaining}
131+
/>
115132
</td>
116133
))}
117134
</tr>
118135
<tr className="border-t border-gray-200 bg-gray-50">
119136
<td className="p-4 font-medium text-gray-700">
120137
{t({ fr: "Prix ($ CAD)", en: "Price ($ CAD)" })}
121138
</td>
122-
{TIERS.map((tier) => (
139+
{tiers.map((tier) => (
123140
<td
124141
key={t(tier.name)}
125142
className="p-4 text-center font-bold text-xl text-primary"
@@ -135,7 +152,7 @@ export default function SponsorshipPage() {
135152
en: "Conference tickets included",
136153
})}
137154
</td>
138-
{TIERS.map((tier) => (
155+
{tiers.map((tier) => (
139156
<td
140157
key={t(tier.name)}
141158
className="p-4 text-center text-gray-600"
@@ -150,11 +167,8 @@ export default function SponsorshipPage() {
150167
className={`border-t border-gray-200 ${perkIndex % 2 === 0 ? "bg-gray-50" : ""}`}
151168
>
152169
<td className="p-4 font-medium text-gray-700">{t(perk)}</td>
153-
{TIERS.map((tier) => (
154-
<td
155-
key={t(tier.name)}
156-
className="p-4 text-center text-xl"
157-
>
170+
{tiers.map((tier) => (
171+
<td key={t(tier.name)} className="p-4 text-center text-xl">
158172
{tier.perks[perkIndex] ? (
159173
<span className="text-green-600">&#10003;</span>
160174
) : (
@@ -170,22 +184,25 @@ export default function SponsorshipPage() {
170184

171185
{/* Mobile cards */}
172186
<div className="md:hidden space-y-8">
173-
{TIERS.map((tier) => (
187+
{tiers.map((tier) => (
174188
<div
175189
key={t(tier.name)}
176190
className="border rounded-lg shadow-md overflow-hidden"
177191
>
178-
<div
179-
className={`${tier.color} text-white text-center py-4 px-6`}
180-
>
192+
<div className={`${tier.color} text-white text-center py-4 px-6`}>
181193
<h3 className="text-2xl font-bold">{t(tier.name)}</h3>
182194
<p className="text-3xl font-bold mt-1">${tier.price}</p>
183195
<p className="text-sm opacity-90">CAD</p>
184196
</div>
185197
<div className="p-6 space-y-3">
186198
<div className="flex justify-between text-gray-700 pb-3 border-b border-gray-200">
187199
<span>{t({ fr: "Disponibles", en: "Available" })}</span>
188-
<span className="font-medium">{tier.available}</span>
200+
<span className="font-medium">
201+
<AvailabilityLabel
202+
total={tier.available}
203+
remaining={tier.remaining}
204+
/>
205+
</span>
189206
</div>
190207
<div className="flex justify-between text-gray-700 pb-3 border-b border-gray-200">
191208
<span>
@@ -301,3 +318,44 @@ export default function SponsorshipPage() {
301318
</div>
302319
);
303320
}
321+
322+
function AvailabilityLabel({
323+
total,
324+
remaining,
325+
}: {
326+
total: number;
327+
remaining: number;
328+
}) {
329+
const { t } = useTranslation();
330+
331+
if (remaining <= 0) {
332+
return (
333+
<span className="text-gray-500">
334+
{t({ fr: "Complet", en: "Sold out" })}
335+
</span>
336+
);
337+
}
338+
339+
// No sponsor at this tier yet: a struck-through total would just be noise.
340+
if (remaining >= total) {
341+
return <span className="font-semibold text-gray-700">{remaining}</span>;
342+
}
343+
344+
return (
345+
<span
346+
className="inline-flex items-baseline gap-1.5"
347+
aria-label={t({
348+
fr: `${remaining} sur ${total} disponibles`,
349+
en: `${remaining} of ${total} available`,
350+
})}
351+
>
352+
<span aria-hidden className="relative inline-block text-gray-500">
353+
{total}
354+
<span className="absolute left-1/2 top-1/2 h-[1.5px] w-[130%] -translate-x-1/2 -translate-y-1/2 -rotate-12 bg-red-500" />
355+
</span>
356+
<span aria-hidden className="font-semibold text-gray-700">
357+
{remaining}
358+
</span>
359+
</span>
360+
);
361+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, it } from "vitest";
2+
import { remainingSponsorships } from "./remainingSponsorships";
3+
4+
describe("remainingSponsorships", () => {
5+
it("returns the full count when none are taken", () => {
6+
expect(remainingSponsorships(5, 0)).toBe(5);
7+
});
8+
9+
it("subtracts the taken slots", () => {
10+
expect(remainingSponsorships(3, 1)).toBe(2);
11+
});
12+
13+
it("returns zero when all slots are taken", () => {
14+
expect(remainingSponsorships(3, 3)).toBe(0);
15+
});
16+
17+
it("never goes negative when more sponsors are enabled than slots offered", () => {
18+
expect(remainingSponsorships(3, 4)).toBe(0);
19+
});
20+
});

src/utils/remainingSponsorships.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function remainingSponsorships(total: number, taken: number): number {
2+
return Math.max(0, total - taken);
3+
}

0 commit comments

Comments
 (0)