Skip to content

Commit c002b9d

Browse files
Table for each price section (#1027)
1 parent 009e1bc commit c002b9d

File tree

6 files changed

+278
-52
lines changed

6 files changed

+278
-52
lines changed

src/app/api/help/credits/route.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { NextResponse } from 'next/server';
2+
3+
import { client } from '@/api/sanity/client';
4+
import { logError } from '@/util/logger';
5+
6+
export type CreditsPack = {
7+
quantity: string;
8+
price: number;
9+
discount: number;
10+
pricePerCredit: number;
11+
};
12+
13+
const queryForCreditsPacks = `*[_type == "credits"][] {
14+
quantity,
15+
price,
16+
discount,
17+
pricePerCredit
18+
}`;
19+
20+
export async function GET() {
21+
try {
22+
const data = await client.fetch<CreditsPack[]>({
23+
query: queryForCreditsPacks,
24+
});
25+
26+
return NextResponse.json({ creditsPacks: data ?? [] });
27+
} catch (error) {
28+
logError('Failed to fetch credits packs from Sanity:', error);
29+
return NextResponse.json(
30+
{ error: 'Failed to fetch credits packs', creditsPacks: [] },
31+
{ status: 500 }
32+
);
33+
}
34+
}

src/app/api/help/prices/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ export type SinglePrice = {
88
freePrice: number | null;
99
proPrice: number | null;
1010
costUnit: string | null;
11+
section: string | null;
1112
};
1213

1314
const queryForSinglePrice = `*[_type == "singlePrice"][] {
1415
itemName,
1516
freePrice,
1617
proPrice,
17-
costUnit
18+
costUnit,
19+
section
1820
}`;
1921

2022
export async function GET() {

src/hooks/use-credits.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
5+
import type { CreditsPack } from '@/app/api/help/credits/route';
6+
7+
interface UseCreditsReturn {
8+
creditsPacks: CreditsPack[];
9+
loading: boolean;
10+
error: string | null;
11+
}
12+
13+
export function useCredits(): UseCreditsReturn {
14+
const [creditsPacks, setCreditsPacks] = useState<CreditsPack[]>([]);
15+
const [loading, setLoading] = useState(true);
16+
const [error, setError] = useState<string | null>(null);
17+
18+
useEffect(() => {
19+
async function fetchCredits() {
20+
try {
21+
setLoading(true);
22+
const response = await fetch('/api/help/credits');
23+
if (!response.ok) {
24+
throw new Error('Failed to fetch credits packs');
25+
}
26+
const data = await response.json();
27+
setCreditsPacks(data.creditsPacks ?? []);
28+
setError(null);
29+
} catch (err) {
30+
setError(err instanceof Error ? err.message : 'An unknown error occurred');
31+
setCreditsPacks([]);
32+
} finally {
33+
setLoading(false);
34+
}
35+
}
36+
37+
fetchCredits();
38+
}, []);
39+
40+
return { creditsPacks, loading, error };
41+
}

src/hooks/use-prices.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
5+
import type { SinglePrice } from '@/app/api/help/prices/route';
6+
7+
interface UsePricesReturn {
8+
prices: SinglePrice[];
9+
loading: boolean;
10+
error: string | null;
11+
}
12+
13+
export function usePrices(): UsePricesReturn {
14+
const [prices, setPrices] = useState<SinglePrice[]>([]);
15+
const [loading, setLoading] = useState(true);
16+
const [error, setError] = useState<string | null>(null);
17+
18+
useEffect(() => {
19+
async function fetchPrices() {
20+
try {
21+
setLoading(true);
22+
const response = await fetch('/api/help/prices');
23+
if (!response.ok) {
24+
throw new Error('Failed to fetch prices');
25+
}
26+
const data = await response.json();
27+
setPrices(data.prices ?? []);
28+
setError(null);
29+
} catch (err) {
30+
setError(err instanceof Error ? err.message : 'An unknown error occurred');
31+
setPrices([]);
32+
} finally {
33+
setLoading(false);
34+
}
35+
}
36+
37+
fetchPrices();
38+
}, []);
39+
40+
return { prices, loading, error };
41+
}
Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,21 @@
11
'use client';
22

3-
import { useEffect, useState } from 'react';
4-
3+
import { useCredits } from '@/hooks/use-credits';
4+
import { usePrices } from '@/hooks/use-prices';
55
import PriceTable from '@/ui/segments/help/priceList/price-table';
66

7-
import type { SinglePrice } from '@/app/api/help/prices/route';
8-
97
export default function PriceList() {
10-
const [prices, setPrices] = useState<SinglePrice[]>([]);
11-
const [loading, setLoading] = useState(true);
12-
const [error, setError] = useState<string | null>(null);
13-
14-
useEffect(() => {
15-
async function fetchPrices() {
16-
try {
17-
setLoading(true);
18-
const response = await fetch('/api/help/prices');
19-
if (!response.ok) {
20-
throw new Error('Failed to fetch prices');
21-
}
22-
const data = await response.json();
23-
setPrices(data.prices ?? []);
24-
setError(null);
25-
} catch (err) {
26-
setError(err instanceof Error ? err.message : 'An unknown error occurred');
27-
setPrices([]);
28-
} finally {
29-
setLoading(false);
30-
}
31-
}
8+
const { prices, loading: pricesLoading, error: pricesError } = usePrices();
9+
const { creditsPacks, loading: creditsLoading, error: creditsError } = useCredits();
3210

33-
fetchPrices();
34-
}, []);
11+
const loading = pricesLoading || creditsLoading;
12+
const error = pricesError || creditsError;
3513

3614
return (
37-
<div className="flex flex-col">
15+
<div className="flex h-full flex-col overflow-auto">
3816
{loading && <div>Loading prices...</div>}
3917
{error && <div>Error: {error}</div>}
40-
{!loading && !error && <PriceTable prices={prices} />}
18+
{!loading && !error && <PriceTable prices={prices} creditsPacks={creditsPacks} />}
4119
</div>
4220
);
4321
}

src/ui/segments/help/priceList/price-table.tsx

Lines changed: 151 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import type { ColumnsType } from 'antd/es/table';
55
import type { CSSProperties, ReactNode } from 'react';
66
import { useMemo } from 'react';
77

8+
import type { CreditsPack } from '@/app/api/help/credits/route';
89
import type { SinglePrice } from '@/app/api/help/prices/route';
910

10-
interface PriceTableProps {
11+
type PriceTableProps = {
1112
prices: SinglePrice[];
12-
}
13+
creditsPacks: CreditsPack[];
14+
};
1315

1416
const costUnitDictionary: Record<string, string> = {
1517
creditsSimulation: 'credits / simulation',
@@ -51,7 +53,54 @@ function CustomHeaderCell({
5153
);
5254
}
5355

54-
const columns: ColumnsType<SinglePrice> = [
56+
const creditsPackColumns: ColumnsType<CreditsPack> = [
57+
{
58+
title: 'Credits',
59+
dataIndex: 'quantity',
60+
key: 'quantity',
61+
render: (value: string) => (
62+
<span style={{ fontWeight: 'bold', color: '#002766' }}>{value}</span>
63+
),
64+
},
65+
{
66+
title: 'Discount',
67+
dataIndex: 'discount',
68+
key: 'discount',
69+
render: (value: number) => {
70+
if (!value || value === 0) {
71+
return <span style={{ color: '#002766' }}></span>;
72+
}
73+
return (
74+
<span style={{ color: '#002766' }}>
75+
<span style={{ fontWeight: 'normal' }}>Save </span>
76+
<span style={{ fontWeight: 'bold' }}>{value}</span>%
77+
</span>
78+
);
79+
},
80+
},
81+
{
82+
title: 'Price (CHF)',
83+
dataIndex: 'price',
84+
key: 'price',
85+
render: (value: number) => (
86+
<span style={{ color: '#002766' }}>
87+
<span style={{ fontWeight: 'bold' }}>{value}</span> CHF
88+
</span>
89+
),
90+
},
91+
{
92+
title: 'Price/Credit (CHF)',
93+
dataIndex: 'pricePerCredit',
94+
key: 'pricePerCredit',
95+
render: (value: number) => (
96+
<span style={{ color: '#002766' }}>
97+
<span style={{ fontWeight: 'bold' }}>{value}</span> CHF
98+
</span>
99+
),
100+
},
101+
];
102+
103+
const priceColumns: ColumnsType<SinglePrice> = [
55104
{
56105
title: 'Item Name',
57106
dataIndex: 'itemName',
@@ -90,33 +139,114 @@ const columns: ColumnsType<SinglePrice> = [
90139
},
91140
];
92141

93-
export default function PriceTable({ prices }: PriceTableProps) {
142+
export default function PriceTable({ prices, creditsPacks }: PriceTableProps) {
94143
const sortedPrices = useMemo(() => {
95144
return [...prices].sort((a, b) => {
145+
// Sort by freePrice (smallest to greatest), handling null values
146+
const priceA = a.freePrice ?? Infinity;
147+
const priceB = b.freePrice ?? Infinity;
148+
if (priceA !== priceB) {
149+
return priceA - priceB;
150+
}
151+
// If prices are equal, sort by itemName alphabetically
96152
const nameA = (a.itemName ?? '').toLowerCase();
97153
const nameB = (b.itemName ?? '').toLowerCase();
98154
return nameA.localeCompare(nameB);
99155
});
100156
}, [prices]);
101157

102-
return (
103-
<div>
104-
<Table
105-
dataSource={sortedPrices}
106-
columns={columns}
107-
rowKey={(record) =>
108-
`${record.itemName ?? ''}-${record.freePrice ?? ''}-${record.proPrice ?? ''}-${record.costUnit ?? ''}`
158+
const pricesBySection = useMemo(() => {
159+
const grouped: Record<string, SinglePrice[]> = {};
160+
sortedPrices.forEach((price) => {
161+
const section = price.section || 'Other';
162+
if (!grouped[section]) {
163+
grouped[section] = [];
164+
}
165+
grouped[section].push(price);
166+
});
167+
// Sort each section by freePrice (smallest to greatest)
168+
Object.keys(grouped).forEach((section) => {
169+
grouped[section].sort((a, b) => {
170+
const priceA = a.freePrice ?? Infinity;
171+
const priceB = b.freePrice ?? Infinity;
172+
if (priceA !== priceB) {
173+
return priceA - priceB;
109174
}
110-
pagination={false}
111-
locale={{ emptyText: 'No prices available' }}
112-
style={{ fontSize: '16px', color: '#002766', backgroundColor: 'transparent' }}
113-
className="[&_.ant-table]:bg-transparent [&_.ant-table-cell]:bg-transparent [&_.ant-table-cell]:text-[18px] [&_.ant-table-cell]:text-[#002766] [&_.ant-table-tbody>tr]:bg-transparent [&_.ant-table-tbody>tr>td]:bg-transparent [&_.ant-table-tbody>tr>td]:text-[18px] [&_.ant-table-thead>tr]:bg-transparent [&_.ant-table-thead>tr>th]:bg-transparent [&_.ant-table-thead>tr>th]:text-[16px] [&_.ant-table-thead>tr>th]:font-normal [&_.ant-table-thead>tr>th]:tracking-[0.025em] [&_.ant-table-thead>tr>th]:text-[#A5A5A5] [&_.ant-table-thead>tr>th]:uppercase"
114-
components={{
115-
header: {
116-
cell: CustomHeaderCell,
117-
},
118-
}}
119-
/>
175+
// If prices are equal, sort by itemName alphabetically
176+
const nameA = (a.itemName ?? '').toLowerCase();
177+
const nameB = (b.itemName ?? '').toLowerCase();
178+
return nameA.localeCompare(nameB);
179+
});
180+
});
181+
return grouped;
182+
}, [sortedPrices]);
183+
184+
const tableClassName =
185+
'[&_.ant-table]:bg-transparent [&_.ant-table-cell]:bg-transparent [&_.ant-table-cell]:text-[18px] [&_.ant-table-cell]:text-[#002766] [&_.ant-table-tbody>tr]:bg-transparent [&_.ant-table-tbody>tr>td]:bg-transparent [&_.ant-table-tbody>tr>td]:text-[18px] [&_.ant-table-thead>tr]:bg-transparent [&_.ant-table-thead>tr>th]:bg-transparent [&_.ant-table-thead>tr>th]:text-[16px] [&_.ant-table-thead>tr>th]:font-normal [&_.ant-table-thead>tr>th]:tracking-[0.025em] [&_.ant-table-thead>tr>th]:text-[#A5A5A5] [&_.ant-table-thead>tr>th]:uppercase';
186+
187+
const tableStyle = {
188+
fontSize: '16px',
189+
color: '#002766',
190+
backgroundColor: 'transparent',
191+
} as const;
192+
const tableComponents = {
193+
header: {
194+
cell: CustomHeaderCell,
195+
},
196+
};
197+
198+
const sortedCreditsPacks = useMemo(() => {
199+
return [...creditsPacks].sort((a, b) => {
200+
// Sort by discount (starting with no discount/0, then ascending)
201+
const discountA = a.discount ?? 0;
202+
const discountB = b.discount ?? 0;
203+
return discountA - discountB;
204+
});
205+
}, [creditsPacks]);
206+
207+
return (
208+
<div className="flex flex-col gap-8 overflow-auto">
209+
{/* Credits Packs Table */}
210+
{creditsPacks.length > 0 && (
211+
<div>
212+
<h3 className="text-primary-8 mb-4 rounded-full bg-white/50 px-12 py-6 text-2xl font-bold">
213+
Credits
214+
</h3>
215+
<Table
216+
dataSource={sortedCreditsPacks}
217+
columns={creditsPackColumns}
218+
rowKey={(record, index) => `credits-${record.quantity}-${index}`}
219+
pagination={false}
220+
locale={{ emptyText: 'No credits packs available' }}
221+
style={tableStyle}
222+
className={tableClassName}
223+
components={tableComponents}
224+
/>
225+
</div>
226+
)}
227+
228+
{/* Prices Tables by Section */}
229+
{Object.entries(pricesBySection).map(([section, sectionPrices]) => (
230+
<div key={section}>
231+
{section !== 'Other' && (
232+
<h3 className="text-primary-8 mt-12 mb-4 rounded-full bg-white/50 px-12 py-6 text-2xl font-bold">
233+
{section}
234+
</h3>
235+
)}
236+
<Table
237+
dataSource={sectionPrices}
238+
columns={priceColumns}
239+
rowKey={(record) =>
240+
`${record.itemName ?? ''}-${record.freePrice ?? ''}-${record.proPrice ?? ''}-${record.costUnit ?? ''}`
241+
}
242+
pagination={false}
243+
locale={{ emptyText: 'No prices available' }}
244+
style={tableStyle}
245+
className={tableClassName}
246+
components={tableComponents}
247+
/>
248+
</div>
249+
))}
120250
</div>
121251
);
122252
}

0 commit comments

Comments
 (0)