Skip to content
Merged
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
34 changes: 34 additions & 0 deletions src/app/api/help/credits/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextResponse } from 'next/server';

import { client } from '@/api/sanity/client';
import { logError } from '@/util/logger';

export type CreditsPack = {
quantity: string;
price: number;
discount: number;
pricePerCredit: number;
};

const queryForCreditsPacks = `*[_type == "credits"][] {
quantity,
price,
discount,
pricePerCredit
}`;

export async function GET() {
try {
const data = await client.fetch<CreditsPack[]>({
query: queryForCreditsPacks,
});

return NextResponse.json({ creditsPacks: data ?? [] });
} catch (error) {
logError('Failed to fetch credits packs from Sanity:', error);
return NextResponse.json(
{ error: 'Failed to fetch credits packs', creditsPacks: [] },
{ status: 500 }
);
}
}
4 changes: 3 additions & 1 deletion src/app/api/help/prices/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ export type SinglePrice = {
freePrice: number | null;
proPrice: number | null;
costUnit: string | null;
section: string | null;
};

const queryForSinglePrice = `*[_type == "singlePrice"][] {
itemName,
freePrice,
proPrice,
costUnit
costUnit,
section
}`;

export async function GET() {
Expand Down
41 changes: 41 additions & 0 deletions src/hooks/use-credits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { useEffect, useState } from 'react';

import type { CreditsPack } from '@/app/api/help/credits/route';

interface UseCreditsReturn {
creditsPacks: CreditsPack[];
loading: boolean;
error: string | null;
}

export function useCredits(): UseCreditsReturn {
const [creditsPacks, setCreditsPacks] = useState<CreditsPack[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
async function fetchCredits() {
try {
setLoading(true);
const response = await fetch('/api/help/credits');
if (!response.ok) {
throw new Error('Failed to fetch credits packs');
}
const data = await response.json();
setCreditsPacks(data.creditsPacks ?? []);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred');
setCreditsPacks([]);
} finally {
setLoading(false);
}
}

fetchCredits();
}, []);

return { creditsPacks, loading, error };
}
41 changes: 41 additions & 0 deletions src/hooks/use-prices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { useEffect, useState } from 'react';

import type { SinglePrice } from '@/app/api/help/prices/route';

interface UsePricesReturn {
prices: SinglePrice[];
loading: boolean;
error: string | null;
}

export function usePrices(): UsePricesReturn {
const [prices, setPrices] = useState<SinglePrice[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
async function fetchPrices() {
try {
setLoading(true);
const response = await fetch('/api/help/prices');
if (!response.ok) {
throw new Error('Failed to fetch prices');
}
const data = await response.json();
setPrices(data.prices ?? []);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred');
setPrices([]);
} finally {
setLoading(false);
}
}

fetchPrices();
}, []);

return { prices, loading, error };
}
38 changes: 8 additions & 30 deletions src/ui/segments/help/priceList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,21 @@
'use client';

import { useEffect, useState } from 'react';

import { useCredits } from '@/hooks/use-credits';
import { usePrices } from '@/hooks/use-prices';
import PriceTable from '@/ui/segments/help/priceList/price-table';

import type { SinglePrice } from '@/app/api/help/prices/route';

export default function PriceList() {
const [prices, setPrices] = useState<SinglePrice[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
async function fetchPrices() {
try {
setLoading(true);
const response = await fetch('/api/help/prices');
if (!response.ok) {
throw new Error('Failed to fetch prices');
}
const data = await response.json();
setPrices(data.prices ?? []);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'An unknown error occurred');
setPrices([]);
} finally {
setLoading(false);
}
}
const { prices, loading: pricesLoading, error: pricesError } = usePrices();
const { creditsPacks, loading: creditsLoading, error: creditsError } = useCredits();

fetchPrices();
}, []);
const loading = pricesLoading || creditsLoading;
const error = pricesError || creditsError;

return (
<div className="flex flex-col">
<div className="flex h-full flex-col overflow-auto">
{loading && <div>Loading prices...</div>}
{error && <div>Error: {error}</div>}
{!loading && !error && <PriceTable prices={prices} />}
{!loading && !error && <PriceTable prices={prices} creditsPacks={creditsPacks} />}
</div>
);
}
172 changes: 151 additions & 21 deletions src/ui/segments/help/priceList/price-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import type { ColumnsType } from 'antd/es/table';
import type { CSSProperties, ReactNode } from 'react';
import { useMemo } from 'react';

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

interface PriceTableProps {
type PriceTableProps = {
prices: SinglePrice[];
}
creditsPacks: CreditsPack[];
};

const costUnitDictionary: Record<string, string> = {
creditsSimulation: 'credits / simulation',
Expand Down Expand Up @@ -51,7 +53,54 @@ function CustomHeaderCell({
);
}

const columns: ColumnsType<SinglePrice> = [
const creditsPackColumns: ColumnsType<CreditsPack> = [
{
title: 'Credits',
dataIndex: 'quantity',
key: 'quantity',
render: (value: string) => (
<span style={{ fontWeight: 'bold', color: '#002766' }}>{value}</span>
),
},
{
title: 'Discount',
dataIndex: 'discount',
key: 'discount',
render: (value: number) => {
if (!value || value === 0) {
return <span style={{ color: '#002766' }}>—</span>;
}
return (
<span style={{ color: '#002766' }}>
<span style={{ fontWeight: 'normal' }}>Save </span>
<span style={{ fontWeight: 'bold' }}>{value}</span>%
</span>
);
},
},
{
title: 'Price (CHF)',
dataIndex: 'price',
key: 'price',
render: (value: number) => (
<span style={{ color: '#002766' }}>
<span style={{ fontWeight: 'bold' }}>{value}</span> CHF
</span>
),
},
{
title: 'Price/Credit (CHF)',
dataIndex: 'pricePerCredit',
key: 'pricePerCredit',
render: (value: number) => (
<span style={{ color: '#002766' }}>
<span style={{ fontWeight: 'bold' }}>{value}</span> CHF
</span>
),
},
];

const priceColumns: ColumnsType<SinglePrice> = [
{
title: 'Item Name',
dataIndex: 'itemName',
Expand Down Expand Up @@ -90,33 +139,114 @@ const columns: ColumnsType<SinglePrice> = [
},
];

export default function PriceTable({ prices }: PriceTableProps) {
export default function PriceTable({ prices, creditsPacks }: PriceTableProps) {
const sortedPrices = useMemo(() => {
return [...prices].sort((a, b) => {
// Sort by freePrice (smallest to greatest), handling null values
const priceA = a.freePrice ?? Infinity;
const priceB = b.freePrice ?? Infinity;
if (priceA !== priceB) {
return priceA - priceB;
}
// If prices are equal, sort by itemName alphabetically
const nameA = (a.itemName ?? '').toLowerCase();
const nameB = (b.itemName ?? '').toLowerCase();
return nameA.localeCompare(nameB);
});
}, [prices]);

return (
<div>
<Table
dataSource={sortedPrices}
columns={columns}
rowKey={(record) =>
`${record.itemName ?? ''}-${record.freePrice ?? ''}-${record.proPrice ?? ''}-${record.costUnit ?? ''}`
const pricesBySection = useMemo(() => {
const grouped: Record<string, SinglePrice[]> = {};
sortedPrices.forEach((price) => {
const section = price.section || 'Other';
if (!grouped[section]) {
grouped[section] = [];
}
grouped[section].push(price);
});
// Sort each section by freePrice (smallest to greatest)
Object.keys(grouped).forEach((section) => {
grouped[section].sort((a, b) => {
const priceA = a.freePrice ?? Infinity;
const priceB = b.freePrice ?? Infinity;
if (priceA !== priceB) {
return priceA - priceB;
}
pagination={false}
locale={{ emptyText: 'No prices available' }}
style={{ fontSize: '16px', color: '#002766', backgroundColor: 'transparent' }}
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"
components={{
header: {
cell: CustomHeaderCell,
},
}}
/>
// If prices are equal, sort by itemName alphabetically
const nameA = (a.itemName ?? '').toLowerCase();
const nameB = (b.itemName ?? '').toLowerCase();
return nameA.localeCompare(nameB);
});
});
return grouped;
}, [sortedPrices]);

const tableClassName =
'[&_.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';

const tableStyle = {
fontSize: '16px',
color: '#002766',
backgroundColor: 'transparent',
} as const;
const tableComponents = {
header: {
cell: CustomHeaderCell,
},
};

const sortedCreditsPacks = useMemo(() => {
return [...creditsPacks].sort((a, b) => {
// Sort by discount (starting with no discount/0, then ascending)
const discountA = a.discount ?? 0;
const discountB = b.discount ?? 0;
return discountA - discountB;
});
}, [creditsPacks]);

return (
<div className="flex flex-col gap-8 overflow-auto">
{/* Credits Packs Table */}
{creditsPacks.length > 0 && (
<div>
<h3 className="text-primary-8 mb-4 rounded-full bg-white/50 px-12 py-6 text-2xl font-bold">
Credits
</h3>
<Table
dataSource={sortedCreditsPacks}
columns={creditsPackColumns}
rowKey={(record, index) => `credits-${record.quantity}-${index}`}
pagination={false}
locale={{ emptyText: 'No credits packs available' }}
style={tableStyle}
className={tableClassName}
components={tableComponents}
/>
</div>
)}

{/* Prices Tables by Section */}
{Object.entries(pricesBySection).map(([section, sectionPrices]) => (
<div key={section}>
{section !== 'Other' && (
<h3 className="text-primary-8 mt-12 mb-4 rounded-full bg-white/50 px-12 py-6 text-2xl font-bold">
{section}
</h3>
)}
<Table
dataSource={sectionPrices}
columns={priceColumns}
rowKey={(record) =>
`${record.itemName ?? ''}-${record.freePrice ?? ''}-${record.proPrice ?? ''}-${record.costUnit ?? ''}`
}
pagination={false}
locale={{ emptyText: 'No prices available' }}
style={tableStyle}
className={tableClassName}
components={tableComponents}
/>
</div>
))}
</div>
);
}