Skip to content

Commit b42a4b5

Browse files
implement singapore airlines kuala lumpur page
1 parent 6cce52f commit b42a4b5

11 files changed

Lines changed: 427 additions & 12 deletions

File tree

app/topics/fixed-deposit-rates/deposit-rates-chart.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,11 @@ export function DepositRatesChart() {
232232
</Select>
233233
</div>
234234
</CardHeader>
235-
<CardContent className="pb-4 pl-2 pr-4">
235+
<CardContent className="px-4 pb-4">
236236
{isLoading ? (
237-
<Skeleton className="h-[350px] w-full" />
237+
<Skeleton className="h-[450px] w-full" />
238238
) : visibleBankKeys.length === 0 ? (
239-
<div className="flex h-[350px] items-center justify-center rounded-md border border-dashed text-sm text-muted-foreground">
239+
<div className="flex h-[450px] items-center justify-center rounded-md border border-dashed text-sm text-muted-foreground">
240240
Select at least one bank to display rate trends.
241241
</div>
242242
) : fetchStatus === FetchStatus.Failure || !chartData ? null : (
@@ -247,7 +247,7 @@ export function DepositRatesChart() {
247247
>
248248
<LineChart
249249
data={chartData.chartData}
250-
margin={{ top: 5, right: 10, left: 0, bottom: 5 }}
250+
margin={{ top: 5, right: 10, left: -20, bottom: 5 }}
251251
>
252252
<CartesianGrid strokeDasharray="3 3" vertical={false} />
253253
<XAxis
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Metadata } from "next";
2+
import React from "react";
3+
import { Container } from "@/components/ui/container";
4+
import Heading from "@/components/ui/heading";
5+
import Subheading from "@/components/ui/subheading";
6+
import Paragraph from "@/components/ui/paragraph";
7+
import { FlightPriceChart } from "@/components/flight-price-chart";
8+
import { FlightShareUrlAnchor } from "@/components/flight-share-url-anchor";
9+
import TelegramLinkButton from "@/components/telegram-link-button";
10+
import AdUnit from "@/components/ad-unit";
11+
import { META_OPEN_GRAPH, META_TWITTER } from "@/app/shared-metadata";
12+
import {
13+
FlightAirline,
14+
Routes,
15+
TelegramChannel,
16+
TopicTitle,
17+
} from "@/lib/enums";
18+
19+
const title = "Singapore Airlines Flights to Kuala Lumpur";
20+
const description =
21+
"Analyse Singapore Airlines fare trends from Singapore to Kuala Lumpur for the past 6 months, 1 year and 2 years.";
22+
const url = Routes.SingaporeAirlinesFlightsKualaLumpur;
23+
24+
export const metadata: Metadata = {
25+
title,
26+
description,
27+
alternates: {
28+
canonical: url,
29+
},
30+
openGraph: {
31+
...META_OPEN_GRAPH,
32+
title,
33+
description,
34+
url,
35+
},
36+
twitter: {
37+
...META_TWITTER,
38+
title,
39+
description,
40+
},
41+
};
42+
43+
export default function SingaporeAirlinesFlightsKualaLumpur() {
44+
return (
45+
<Container>
46+
<div className="space-y-2">
47+
<Heading>{title}</Heading>
48+
<Subheading>{description}</Subheading>
49+
</div>
50+
<FlightPriceChart
51+
airline={FlightAirline.SINGAPORE_AIRLINES}
52+
destinationCityCode="KUL"
53+
chartTitle="Kuala Lumpur Price Trend"
54+
/>
55+
<Paragraph>
56+
The above chart shows the fare trends for Singapore Airlines flights
57+
from Singapore to Kuala Lumpur (KUL) for the past 6 months, 1 year and 2
58+
years. You can use this information to decide when to{" "}
59+
<FlightShareUrlAnchor
60+
airline={FlightAirline.SINGAPORE_AIRLINES}
61+
destinationCityCode="KUL"
62+
linkText="book your flight"
63+
/>{" "}
64+
for the best price.
65+
</Paragraph>
66+
<AdUnit />
67+
<Paragraph>
68+
<span className="font-medium">SG Alerts</span> is a free notification
69+
service that monitors the Singapore Airlines flight fare deals from
70+
Singapore to Kuala Lumpur. It sends you a Telegram notification when the
71+
prices go down so that you can take advantage of the lower prices.
72+
</Paragraph>
73+
<Paragraph>
74+
To get started, simply click the button below to join the Telegram
75+
channel and start receiving notifications.
76+
</Paragraph>
77+
<div className="sticky bottom-6 z-50 mt-8 text-center">
78+
<TelegramLinkButton
79+
channel={TelegramChannel.SingaporeAirlinesFlights}
80+
linkText="Subscribe Now"
81+
topicTitle={TopicTitle.SingaporeAirlinesFlights}
82+
/>
83+
</div>
84+
</Container>
85+
);
86+
}

app/topics/singapore-airlines-flights/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
TelegramChannel,
1515
TopicTitle,
1616
} from "@/lib/enums";
17+
import { SINGAPORE_AIRLINES_DESTINATION_LINKS } from "@/lib/constants";
1718
import { META_OPEN_GRAPH, META_TWITTER } from "@/app/shared-metadata";
1819

1920
const title = "Singapore Airlines Flights";
@@ -49,6 +50,7 @@ export default function SingaporeAirlinesFlights() {
4950
</div>
5051
<FlightsTable
5152
airline={FlightAirline.SINGAPORE_AIRLINES}
53+
destinationLinks={SINGAPORE_AIRLINES_DESTINATION_LINKS}
5254
withTravelBy={false}
5355
/>
5456
<Paragraph>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use client";
2+
3+
import { formatDepositRatesChartTickDate } from "@/lib/date";
4+
import { Currency } from "@/lib/enums";
5+
import { formatMoney, formatMoneyPeso } from "@/lib/number";
6+
import type {
7+
DepositRatesChartRange,
8+
DepositRatesChartTooltipPayloadItem,
9+
} from "@/lib/types";
10+
11+
type Props = {
12+
active?: boolean;
13+
payload?: Array<DepositRatesChartTooltipPayloadItem>;
14+
label?: string;
15+
range: DepositRatesChartRange;
16+
currency: string;
17+
};
18+
19+
export function FlightPriceChartTooltip({
20+
active,
21+
payload,
22+
label,
23+
range,
24+
currency,
25+
}: Props) {
26+
if (!active || !payload?.length) {
27+
return null;
28+
}
29+
30+
const value = payload[0]?.value;
31+
const price = typeof value === "number" ? value : Number(value ?? 0);
32+
33+
return (
34+
<div className="grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl">
35+
<div className="font-medium">
36+
{typeof label === "string"
37+
? formatDepositRatesChartTickDate(label, range)
38+
: ""}
39+
</div>
40+
<div className="flex items-center justify-between gap-4">
41+
<span className="text-muted-foreground">Price</span>
42+
<span className="font-mono font-medium tabular-nums text-foreground">
43+
{currency === Currency.SGD
44+
? formatMoney(price)
45+
: formatMoneyPeso(price)}
46+
</span>
47+
</div>
48+
</div>
49+
);
50+
}

components/flight-price-chart.tsx

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"use client";
2+
3+
import { useEffect, useMemo, useState } from "react";
4+
import {
5+
CartesianGrid,
6+
Line,
7+
LineChart,
8+
Tooltip,
9+
XAxis,
10+
YAxis,
11+
} from "recharts";
12+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
13+
import { ChartContainer, type ChartConfig } from "@/components/ui/chart";
14+
import {
15+
Select,
16+
SelectContent,
17+
SelectItem,
18+
SelectTrigger,
19+
SelectValue,
20+
} from "@/components/ui/select";
21+
import { Skeleton } from "@/components/ui/skeleton";
22+
import { useGetFlightSnapshotsChartData } from "@/lib/api-hooks";
23+
import { DEPOSIT_RATES_CHART_RANGE_LABELS } from "@/lib/constants";
24+
import { formatDepositRatesChartTickDate } from "@/lib/date";
25+
import { Currency, FetchStatus, FlightAirline } from "@/lib/enums";
26+
import { formatMoney, formatMoneyPeso } from "@/lib/number";
27+
import type { DepositRatesChartRange } from "@/lib/types";
28+
import { FlightPriceChartTooltip } from "@/components/flight-price-chart-tooltip";
29+
30+
type Props = {
31+
airline: FlightAirline;
32+
destinationCityCode: string;
33+
chartTitle?: string;
34+
};
35+
36+
export function FlightPriceChart({
37+
airline,
38+
destinationCityCode,
39+
chartTitle,
40+
}: Props) {
41+
const [range, setRange] = useState<DepositRatesChartRange>("6m");
42+
const [fetchStatus, chartData, getFlightSnapshotsChartData] =
43+
useGetFlightSnapshotsChartData(airline, destinationCityCode);
44+
45+
useEffect(() => {
46+
getFlightSnapshotsChartData(range);
47+
48+
// eslint-disable-next-line react-hooks/exhaustive-deps
49+
}, [range]);
50+
51+
const isLoading =
52+
fetchStatus === FetchStatus.Idle || fetchStatus === FetchStatus.Loading;
53+
54+
const chartContainerConfig: ChartConfig = useMemo(
55+
() => ({
56+
price: {
57+
label: chartData?.chartConfig.price?.label ?? "Price",
58+
},
59+
}),
60+
[chartData],
61+
);
62+
63+
const allPrices = (chartData?.chartData ?? []).map((point) => point.price);
64+
const minPrice = allPrices.length ? Math.min(...allPrices) : 0;
65+
const maxPrice = allPrices.length ? Math.max(...allPrices) : 1;
66+
const yAxisMin = Math.max(0, minPrice - 10);
67+
const yAxisMax = maxPrice + 10;
68+
69+
return (
70+
<Card className="my-6">
71+
<CardHeader className="space-y-2 pb-4">
72+
<div className="flex flex-wrap items-center justify-between gap-2">
73+
<h3 className="text-base font-semibold">
74+
{chartTitle ??
75+
`${chartData?.destinationCityName ?? destinationCityCode} Price Trend`}
76+
</h3>
77+
<Select
78+
value={range}
79+
onValueChange={(v) => setRange(v as DepositRatesChartRange)}
80+
>
81+
<SelectTrigger className="w-[160px]">
82+
<SelectValue />
83+
</SelectTrigger>
84+
<SelectContent>
85+
{(
86+
Object.entries(DEPOSIT_RATES_CHART_RANGE_LABELS) as Array<
87+
[DepositRatesChartRange, string]
88+
>
89+
).map(([value, label]) => (
90+
<SelectItem key={value} value={value}>
91+
{label}
92+
</SelectItem>
93+
))}
94+
</SelectContent>
95+
</Select>
96+
</div>
97+
</CardHeader>
98+
<CardContent className="px-4 pb-4">
99+
{isLoading ? (
100+
<Skeleton className="h-[350px] w-full" />
101+
) : fetchStatus === FetchStatus.Failure || !chartData ? null : (
102+
<ChartContainer
103+
config={chartContainerConfig}
104+
className="h-[350px] w-full"
105+
>
106+
<LineChart
107+
data={chartData.chartData}
108+
margin={{ top: 5, right: 10, left: -20, bottom: 5 }}
109+
>
110+
<CartesianGrid strokeDasharray="3 3" vertical={false} />
111+
<XAxis
112+
dataKey="date"
113+
tick={{ fontSize: 11 }}
114+
tickFormatter={(value) =>
115+
formatDepositRatesChartTickDate(String(value), range)
116+
}
117+
tickLine={false}
118+
axisLine={false}
119+
/>
120+
<YAxis
121+
domain={[yAxisMin, yAxisMax]}
122+
tickFormatter={(value) =>
123+
chartData.currency === Currency.SGD
124+
? formatMoney(Number(value))
125+
: formatMoneyPeso(Number(value))
126+
}
127+
tick={{ fontSize: 11 }}
128+
tickLine={false}
129+
axisLine={false}
130+
width={60}
131+
/>
132+
<Tooltip
133+
content={
134+
<FlightPriceChartTooltip
135+
range={range}
136+
currency={chartData.currency}
137+
/>
138+
}
139+
cursor={{ stroke: "hsl(var(--border))", strokeWidth: 1 }}
140+
/>
141+
<Line
142+
type="monotone"
143+
dataKey="price"
144+
name={chartData.chartConfig.price?.label ?? "Price"}
145+
stroke="hsl(var(--primary))"
146+
strokeWidth={2}
147+
dot={false}
148+
connectNulls
149+
isAnimationActive={false}
150+
/>
151+
</LineChart>
152+
</ChartContainer>
153+
)}
154+
</CardContent>
155+
</Card>
156+
);
157+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client";
2+
3+
import { useEffect, useMemo } from "react";
4+
import { Anchor } from "@/components/ui/anchor";
5+
import { useGetFlightsInfo } from "@/lib/api-hooks";
6+
import { FetchStatus, FlightAirline } from "@/lib/enums";
7+
8+
type Props = {
9+
airline: FlightAirline;
10+
destinationCityCode: string;
11+
linkText: string;
12+
};
13+
14+
export function FlightShareUrlAnchor({
15+
airline,
16+
destinationCityCode,
17+
linkText,
18+
}: Props) {
19+
const [fetchStatus, flightsInfo, getFlightsInfo] = useGetFlightsInfo(airline);
20+
21+
useEffect(() => {
22+
getFlightsInfo();
23+
24+
// eslint-disable-next-line react-hooks/exhaustive-deps
25+
}, []);
26+
27+
const shareUrl = useMemo(() => {
28+
const destinationFlights = flightsInfo.items
29+
.filter((flight) => flight.destinationCityCode === destinationCityCode)
30+
.sort((left, right) => left.price - right.price);
31+
32+
return destinationFlights[0]?.shareUrl;
33+
}, [flightsInfo.items, destinationCityCode]);
34+
35+
if (fetchStatus !== FetchStatus.Success || !shareUrl) {
36+
return <span>{linkText}</span>;
37+
}
38+
39+
return (
40+
<Anchor href={shareUrl} isExternal>
41+
{linkText}
42+
</Anchor>
43+
);
44+
}

0 commit comments

Comments
 (0)