22
33import "@/styles/components/card.css" ;
44import { StatsCollection , SiteFooter , SiteHeader } from "@/components/SiteFormat" ;
5- import { useState , useEffect , Suspense } from "react" ;
5+ import { useState , useEffect , memo , Suspense } from "react" ;
66import { Button , HeroUIProvider } from "@heroui/react" ;
77import { ThemeProvider } from "next-themes" ;
88import { useSearchParams } from "next/navigation" ;
9- import * as PublicHolidayData from "./data/public_holidays.json" ;
10- import * as tollData from "./data/tolls.json" ;
119
1210interface NumberRange {
1311 range : number [ ] ;
@@ -34,18 +32,16 @@ interface HKTunnel {
3432}
3533
3634interface VehicleType {
37- name : VehicleTypeIdentifier | string ;
35+ name : VehicleTypeIdentifier ;
3836 hasTimeVaryingToll : boolean ;
3937 fixedTolls ?: Record < HKTunnelIdentifier , number | undefined > ;
4038 multiplier ?: number ;
4139 description ?: string ;
4240}
4341
44- type DateTimeRange = [ string , { value : string } ] ;
45-
4642interface PublicHoliday {
47- dtstart : DateTimeRange ;
48- dtend : DateTimeRange ;
43+ dtstart : [ string , { value : string } ] ;
44+ dtend : [ string , { value : string } ] ;
4945 summary : string ;
5046 uid : string ;
5147}
@@ -69,6 +65,7 @@ interface TollCardProps {
6965 tunnel : HKTunnel ;
7066 vehicle : VehicleTypeIdentifier ;
7167 priceAlert ?: string ;
68+ tollData : TollData | null ;
7269 currentDate : Date ;
7370 isPublicHoliday : boolean ;
7471}
@@ -105,16 +102,19 @@ function timeToMinutes(time: string): number {
105102
106103// Function to get current toll for a specific tunnel
107104function getCurrentTollForTunnel (
105+ tollData : TollData | null ,
108106 selectedVehicle : VehicleTypeIdentifier ,
109107 tunnelKey : HKTunnelIdentifier ,
110108 currentTime : Date ,
111109 isPublicHoliday : boolean
112110) : string {
111+ if ( ! tollData ) return "載入中..." ;
112+
113113 const vehicle = tollData . vehicleTypes [ selectedVehicle ] ;
114114 const tunnel = tollData . tunnels [ tunnelKey ] ;
115115
116116 // Fixed toll vehicles
117- if ( "fixedTolls" in vehicle && tunnelKey in vehicle . fixedTolls ) {
117+ if ( vehicle . fixedTolls && tunnelKey in vehicle . fixedTolls ) {
118118 return `$${ vehicle . fixedTolls [ tunnelKey ] } ` ;
119119 }
120120
@@ -140,17 +140,18 @@ function getCurrentTollForTunnel(
140140 if ( typeof tollForTunnel === "object" && "range" in tollForTunnel ) {
141141 // Transition period - show range
142142 const [ min , max ] = tollForTunnel . range ;
143- if ( "multiplier" in vehicle ) {
143+ if ( vehicle . multiplier ) {
144144 const minMoto = Math . round ( min * vehicle . multiplier * 10 ) / 10 ;
145145 const maxMoto = Math . round ( max * vehicle . multiplier * 10 ) / 10 ;
146146 return `$${ minMoto } - $${ maxMoto } ` ;
147147 }
148148 return `$${ min } - $${ max } ` ;
149- } // Apply multiplier for motorcycles
150- else if ( "multiplier" in vehicle ) {
151- const motorcycleToll = Math . round ( tollForTunnel * vehicle . multiplier * 10 ) / 10 ;
152- return `$${ motorcycleToll } ` ;
153149 } else {
150+ // Apply multiplier for motorcycles
151+ if ( vehicle . multiplier ) {
152+ const motorcycleToll = Math . round ( tollForTunnel * vehicle . multiplier * 10 ) / 10 ;
153+ return `$${ motorcycleToll } ` ;
154+ }
154155 return `$${ tollForTunnel } ` ;
155156 }
156157 }
@@ -160,15 +161,15 @@ function getCurrentTollForTunnel(
160161}
161162
162163function HKTollCard ( props : TollCardProps ) : JSX . Element {
163- const { tunnelKey, tunnel, priceAlert, vehicle, currentDate, isPublicHoliday } = props ;
164+ const { tunnelKey, tunnel, priceAlert, vehicle, tollData , currentDate, isPublicHoliday } = props ;
164165
165166 return (
166167 < div key = { tunnelKey } className = "border-b border-black dark:border-white pb-1 last:border-b-0" >
167168 < div className = "flex flex-col sm:flex-row sm:justify-between sm:items-start sm:gap-2" >
168169 < span className = "text-3xl md:text-2xl font-medium text-center sm:text-left" > { tunnel . name } </ span >
169170 < div className = "text-center sm:text-right" >
170171 < p className = "text-5xl py-1 md:py-2 font-bold text-green-600" >
171- { getCurrentTollForTunnel ( vehicle , tunnelKey , currentDate , isPublicHoliday ) }
172+ { getCurrentTollForTunnel ( tollData , vehicle , tunnelKey , currentDate , isPublicHoliday ) }
172173 </ p >
173174 { priceAlert && (
174175 < span className = "text-[1.45rem] md:text-lg bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200 px-3 rounded-md font-medium inline-block text-center" >
@@ -184,18 +185,28 @@ function HKTollCard(props: TollCardProps): JSX.Element {
184185function HKTunnelsTollsApp ( ) : JSX . Element {
185186 const [ selectedVehicle , setSelectedVehicle ] = useState < VehicleTypeIdentifier > ( "privateCar" ) ;
186187 const [ currentTime , setCurrentTime ] = useState < Date > ( new Date ( ) ) ;
188+ const [ tollData , setTollData ] = useState < TollData | null > ( null ) ;
189+ const [ publicHolidays , setPublicHolidays ] = useState < Set < string > > ( new Set ( ) ) ;
187190 const [ isPublicHoliday , setIsPublicHoliday ] = useState < boolean > ( false ) ;
188191 const searchParams = useSearchParams ( ) ;
189192
190193 // Load public holidays data
191- const publicHolidays = new Set < string > ( ) ;
192- if ( PublicHolidayData . vcalendar && PublicHolidayData . vcalendar [ 0 ] && PublicHolidayData . vcalendar [ 0 ] . vevent ) {
193- PublicHolidayData . vcalendar [ 0 ] . vevent . forEach ( ( event ) => {
194- // Extract date from dtstart format "20240101"
195- const dateStr = event . dtstart [ 0 ] as string ;
196- publicHolidays . add ( dateStr ) ;
197- } ) ;
198- }
194+ useEffect ( ( ) => {
195+ fetch ( "/api/hk-tunnels-tolls/public_holidays.json" )
196+ . then ( ( response ) => response . json ( ) )
197+ . then ( ( data : PublicHolidayData ) => {
198+ const holidays = new Set < string > ( ) ;
199+ if ( data . vcalendar && data . vcalendar [ 0 ] && data . vcalendar [ 0 ] . vevent ) {
200+ data . vcalendar [ 0 ] . vevent . forEach ( ( event ) => {
201+ // Extract date from dtstart format "20240101"
202+ const dateStr = event . dtstart [ 0 ] ;
203+ holidays . add ( dateStr ) ;
204+ } ) ;
205+ }
206+ setPublicHolidays ( holidays ) ;
207+ } )
208+ . catch ( ( error ) => console . error ( "載入公眾假期資料失敗:" , error ) ) ;
209+ } , [ ] ) ;
199210
200211 // Check if current date is a public holiday
201212 useEffect ( ( ) => {
@@ -204,6 +215,14 @@ function HKTunnelsTollsApp(): JSX.Element {
204215 setIsPublicHoliday ( publicHolidays . has ( dateStr ) ) ;
205216 } , [ currentTime , publicHolidays ] ) ;
206217
218+ // Load toll data
219+ useEffect ( ( ) => {
220+ fetch ( "/api/hk-tunnels-tolls/tolls.json" )
221+ . then ( ( response ) => response . json ( ) )
222+ . then ( ( data ) => setTollData ( data ) )
223+ . catch ( ( error ) => console . error ( "載入收費資料失敗:" , error ) ) ;
224+ } , [ ] ) ;
225+
207226 // Update current time every minute
208227 useEffect ( ( ) => {
209228 const timer = setInterval ( ( ) => {
@@ -276,18 +295,20 @@ function HKTunnelsTollsApp(): JSX.Element {
276295
277296 if ( typeof nextToll === "object" && "range" in nextToll ) {
278297 const [ min , max ] = nextToll . range ;
279- if ( "multiplier" in vehicle ) {
298+ if ( vehicle . multiplier ) {
280299 const minMoto = Math . round ( min * vehicle . multiplier * 10 ) / 10 ;
281300 const maxMoto = Math . round ( max * vehicle . multiplier * 10 ) / 10 ;
282301 nextTollDisplay = `$${ minMoto } - $${ maxMoto } ` ;
283302 } else {
284303 nextTollDisplay = `$${ min } - $${ max } ` ;
285304 }
286- } else if ( "multiplier" in vehicle ) {
287- const motorcycleToll = Math . round ( nextToll * vehicle . multiplier * 10 ) / 10 ;
288- nextTollDisplay = `$${ motorcycleToll } ` ;
289305 } else {
290- nextTollDisplay = `$${ nextToll } ` ;
306+ if ( vehicle . multiplier ) {
307+ const motorcycleToll = Math . round ( nextToll * vehicle . multiplier * 10 ) / 10 ;
308+ nextTollDisplay = `$${ motorcycleToll } ` ;
309+ } else {
310+ nextTollDisplay = `$${ nextToll } ` ;
311+ }
291312 }
292313
293314 return `${ nextPeriod . timeRange . split ( " - " ) [ 0 ] } 變為 ${ nextTollDisplay } ` ;
@@ -311,7 +332,7 @@ function HKTunnelsTollsApp(): JSX.Element {
311332 { /* Current Toll Display */ }
312333 < div className = "card-base-min mb-4" >
313334 < h3 className = "text-xl md:text-lg font-semibold" >
314- 目前收費 - { vehicleType . name } { "description" in vehicleType ? `(${ vehicleType . description } )` : "" }
335+ 目前收費 - { vehicleType . name } { vehicleType . description ? `(${ vehicleType . description } )` : "" }
315336 </ h3 >
316337 < div >
317338 < div className = "space-y-2" >
@@ -328,9 +349,10 @@ function HKTunnelsTollsApp(): JSX.Element {
328349 < HKTollCard
329350 key = { key }
330351 tunnelKey = { key }
331- tunnel = { tunnel as HKTunnel }
352+ tunnel = { tunnel }
332353 priceAlert = { priceAlert }
333354 vehicle = { selectedVehicle }
355+ tollData = { tollData }
334356 currentDate = { hkTime }
335357 isPublicHoliday = { isPublicHoliday }
336358 />
@@ -362,9 +384,7 @@ function HKTunnelsTollsApp(): JSX.Element {
362384 { /* Cross-Harbour Tunnels Time Periods Table */ }
363385 < div className = "card-base-min mb-4" >
364386 < h3 className = "text-2xl md:text-lg font-semibold border-b" >
365- 過海隧道收費時段表 (
366- < span suppressHydrationWarning > { hkTime . getDay ( ) === 0 || isPublicHoliday ? "星期日及公眾假期" : "平日" } </ span >
367- )
387+ 過海隧道收費時段表 ({ hkTime . getDay ( ) === 0 || isPublicHoliday ? "星期日及公眾假期" : "平日" } )
368388 </ h3 >
369389 < div className = "overflow-x-auto" >
370390 < table className = "w-full" >
@@ -437,12 +457,12 @@ function HKTunnelsTollsApp(): JSX.Element {
437457 < td className = "px-6 py-4 whitespace-nowrap" > { period . timeRange } </ td >
438458 < td className = "px-6 py-4 whitespace-nowrap" >
439459 { vehicle . hasTimeVaryingToll
440- ? formatToll ( "western" , period , "multiplier" in vehicle ? vehicle . multiplier : undefined )
460+ ? formatToll ( "western" , period , vehicle . multiplier )
441461 : getFixedToll ( "western" , vehicle ) }
442462 </ td >
443463 < td className = "px-6 py-4 whitespace-nowrap" >
444464 { vehicle . hasTimeVaryingToll
445- ? formatToll ( "cross_eastern" , period , "multiplier" in vehicle ? vehicle . multiplier : undefined )
465+ ? formatToll ( "cross_eastern" , period , vehicle . multiplier )
446466 : getFixedToll ( "cross_eastern" , vehicle ) }
447467 </ td >
448468 </ tr >
@@ -455,9 +475,7 @@ function HKTunnelsTollsApp(): JSX.Element {
455475 { /* Tai Lam Tunnel Time Periods Table */ }
456476 < div className = "card-base-min mb-8" >
457477 < h3 className = "text-2xl md:text-lg font-semibold border-b" >
458- 大欖隧道收費時段表 (
459- < span suppressHydrationWarning > { hkTime . getDay ( ) === 0 || isPublicHoliday ? "星期日及公眾假期" : "平日" } </ span >
460- )
478+ 大欖隧道收費時段表 ({ hkTime . getDay ( ) === 0 || isPublicHoliday ? "星期日及公眾假期" : "平日" } )
461479 </ h3 >
462480 < div className = "overflow-x-auto" >
463481 < table className = "w-full" >
@@ -520,9 +538,7 @@ function HKTunnelsTollsApp(): JSX.Element {
520538 < td className = "px-6 py-4 whitespace-nowrap font-medium" > { period . name } </ td >
521539 < td className = "px-6 py-4 whitespace-nowrap" > { period . timeRange } </ td >
522540 < td className = "px-6 py-4 whitespace-nowrap" >
523- { vehicle . hasTimeVaryingToll
524- ? formatToll ( period , "multiplier" in vehicle ? vehicle . multiplier : undefined )
525- : getFixedToll ( vehicle ) }
541+ { vehicle . hasTimeVaryingToll ? formatToll ( period , vehicle . multiplier ) : getFixedToll ( vehicle ) }
526542 </ td >
527543 </ tr >
528544 ) ) ;
@@ -559,17 +575,15 @@ function HKTunnelsTollsApp(): JSX.Element {
559575 </ p >
560576 < p >
561577 最後更新:
562- < span suppressHydrationWarning >
563- { hkTime . toLocaleString ( "zh-HK" , {
564- year : "numeric" ,
565- month : "2-digit" ,
566- day : "2-digit" ,
567- hour : "2-digit" ,
568- minute : "2-digit" ,
569- second : "2-digit" ,
570- hour12 : false ,
571- } ) }
572- </ span >
578+ { hkTime . toLocaleString ( "zh-HK" , {
579+ year : "numeric" ,
580+ month : "2-digit" ,
581+ day : "2-digit" ,
582+ hour : "2-digit" ,
583+ minute : "2-digit" ,
584+ second : "2-digit" ,
585+ hour12 : false ,
586+ } ) }
573587 </ p >
574588 </ div >
575589 </ div >
0 commit comments