11"use client" ;
22
3+ import { useState } from "react" ;
34import { Button } from "@/components/ui/button" ;
5+ import { FormData } from "../page" ;
6+
7+ const meetingTypes = [
8+ { value : "zoom" , label : "Zoom" , sub : "Via Zoom meeting link" } ,
9+ { value : "gmeet" , label : "Google Meet" , sub : "Via Google Meet link" } ,
10+ { value : "teams" , label : "Microsoft Teams" , sub : "Via Teams link" } ,
11+ { value : "other" , label : "Other" , sub : "Specify your preferred tool" } ,
12+ ] ;
13+
14+ const timeSlots = [
15+ "9:00 AM" , "10:00 AM" , "11:00 AM" ,
16+ "1:00 PM" , "2:00 PM" , "3:00 PM" ,
17+ "4:00 PM" , "5:00 PM" ,
18+ ] ;
19+
20+ const unavailableSlots : string [ ] = [ ] ;
21+
22+ const DAYS = [ "Su" , "Mo" , "Tu" , "We" , "Th" , "Fr" , "Sa" ] ;
23+ const MONTHS = [
24+ "January" , "February" , "March" , "April" , "May" , "June" ,
25+ "July" , "August" , "September" , "October" , "November" , "December" ,
26+ ] ;
27+
28+ const PHT_OFFSET = 8 * 60 ;
29+
30+ const getPHTToday = ( ) => {
31+ const now = new Date ( ) ;
32+ const utcMs = now . getTime ( ) + now . getTimezoneOffset ( ) * 60000 ;
33+ const pht = new Date ( utcMs + PHT_OFFSET * 60000 ) ;
34+ pht . setHours ( 0 , 0 , 0 , 0 ) ;
35+ return pht ;
36+ } ;
37+
38+ const toDateString = ( y : number , m : number , d : number ) =>
39+ `${ y } -${ String ( m + 1 ) . padStart ( 2 , "0" ) } -${ String ( d ) . padStart ( 2 , "0" ) } ` ;
40+
41+ const formatDisplay = ( dateStr : string ) => {
42+ const parts = dateStr . split ( "-" ) . map ( Number ) ;
43+ const y = parts [ 0 ] ?? 0 ;
44+ const m = parts [ 1 ] ?? 1 ;
45+ const d = parts [ 2 ] ?? 1 ;
46+ return new Date ( y , m - 1 , d ) . toLocaleDateString ( "en-PH" , {
47+ weekday : "long" , month : "long" , day : "numeric" , year : "numeric" ,
48+ timeZone : "Asia/Manila" ,
49+ } ) ;
50+ } ;
51+
52+ interface AppointmentProps {
53+ formData : FormData ;
54+ onBack : ( ) => void ;
55+ }
56+
57+ export default function Appointment ( { formData, onBack } : AppointmentProps ) {
58+ const today = getPHTToday ( ) ;
59+
60+ const [ viewYear , setViewYear ] = useState ( today . getFullYear ( ) ) ;
61+ const [ viewMonth , setViewMonth ] = useState ( today . getMonth ( ) ) ;
62+ const [ selectedDate , setSelectedDate ] = useState ( "" ) ;
63+ const [ selectedSlot , setSelectedSlot ] = useState ( "" ) ;
64+ const [ meetingType , setMeetingType ] = useState ( "" ) ;
65+ const [ otherTool , setOtherTool ] = useState ( "" ) ;
66+ const [ submitted , setSubmitted ] = useState ( false ) ;
67+ const [ loading , setLoading ] = useState ( false ) ;
68+ const [ error , setError ] = useState ( "" ) ;
69+
70+ const canSubmit = selectedDate && selectedSlot && meetingType &&
71+ ( meetingType !== "other" || otherTool . trim ( ) ) ;
72+
73+ const firstDayOfMonth = new Date ( viewYear , viewMonth , 1 ) . getDay ( ) ;
74+ const daysInMonth = new Date ( viewYear , viewMonth + 1 , 0 ) . getDate ( ) ;
75+
76+ const prevMonth = ( ) => {
77+ if ( viewMonth === 0 ) { setViewMonth ( 11 ) ; setViewYear ( y => y - 1 ) ; }
78+ else setViewMonth ( m => m - 1 ) ;
79+ } ;
80+ const nextMonth = ( ) => {
81+ if ( viewMonth === 11 ) { setViewMonth ( 0 ) ; setViewYear ( y => y + 1 ) ; }
82+ else setViewMonth ( m => m + 1 ) ;
83+ } ;
84+
85+ const canGoPrev =
86+ viewYear > today . getFullYear ( ) ||
87+ ( viewYear === today . getFullYear ( ) && viewMonth > today . getMonth ( ) ) ;
88+
89+ const isDisabled = ( day : number ) => {
90+ const d = new Date ( viewYear , viewMonth , day ) ;
91+ return d < today || d . getDay ( ) === 0 || d . getDay ( ) === 6 ;
92+ } ;
93+ const isToday = ( day : number ) =>
94+ day === today . getDate ( ) && viewMonth === today . getMonth ( ) && viewYear === today . getFullYear ( ) ;
95+ const isSelected = ( day : number ) =>
96+ selectedDate === toDateString ( viewYear , viewMonth , day ) ;
97+
98+ const handleSelectDate = ( day : number ) => {
99+ if ( isDisabled ( day ) ) return ;
100+ setSelectedDate ( toDateString ( viewYear , viewMonth , day ) ) ;
101+ setSelectedSlot ( "" ) ;
102+ } ;
103+
104+ const handleSubmit = async ( ) => {
105+ if ( ! canSubmit ) return ;
106+ setLoading ( true ) ;
107+ setError ( "" ) ;
108+
109+ try {
110+ const res = await fetch ( "/api/appointments" , {
111+ method : "POST" ,
112+ headers : { "Content-Type" : "application/json" } ,
113+ body : JSON . stringify ( {
114+ // Step 1
115+ firstName : formData . firstName ,
116+ lastName : formData . lastName ,
117+ email : formData . email ,
118+ companyName : formData . companyName ,
119+ phoneNumber : formData . phoneNumber ,
120+ industry : formData . industry ,
121+ // Step 2
122+ serviceInterest : formData . serviceInterest ,
123+ projectType : formData . projectType ,
124+ featuresNeeded : formData . featuresNeeded ,
125+ referralSource : formData . referralSource ,
126+ interestLevel : formData . interestLevel ,
127+ otherRequirements : formData . otherRequirements ,
128+ // Step 3
129+ appointmentDate : selectedDate ,
130+ appointmentTime : selectedSlot ,
131+ meetingType,
132+ meetingToolOther : meetingType === "other" ? otherTool : null ,
133+ } ) ,
134+ } ) ;
135+
136+ if ( ! res . ok ) {
137+ const data = ( await res . json ( ) ) as { error ?: string } ;
138+ throw new Error ( data . error || "Something went wrong." ) ;
139+ }
140+
141+ setSubmitted ( true ) ;
142+ } catch ( err : unknown ) {
143+ setError ( err instanceof Error ? err . message : "Failed to submit. Please try again." ) ;
144+ } finally {
145+ setLoading ( false ) ;
146+ }
147+ } ;
148+
149+ const meetingLabel =
150+ meetingType === "zoom" ? "Zoom" :
151+ meetingType === "gmeet" ? "Google Meet" :
152+ meetingType === "teams" ? "Microsoft Teams" :
153+ otherTool || "Other" ;
154+
155+ if ( submitted ) {
156+ return (
157+ < div className = "relative flex w-full flex-col items-center gap-6 text-pretty rounded-lg border border-white/5 bg-white/5 px-5 py-16 text-white" >
158+ < div className = "flex h-16 w-16 items-center justify-center rounded-full border border-violet-500 bg-violet-500/10" >
159+ < svg className = "h-7 w-7 text-violet-400" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
160+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2.5 } d = "M5 13l4 4L19 7" />
161+ </ svg >
162+ </ div >
163+ < div className = "flex flex-col items-center gap-2 text-center" >
164+ < p className = "text-xl font-semibold" > You're all set!</ p >
165+ < p className = "max-w-xs text-sm text-white/40" >
166+ We've received your appointment request. Please kindly wait for us to contact you via email at{ " " }
167+ < span className = "text-violet-300" > { formData . email } </ span > .
168+ </ p >
169+ </ div >
170+ < div className = "flex flex-col items-center gap-1 rounded-lg border border-white/5 bg-white/5 px-6 py-4 text-center text-sm" >
171+ < p className = "text-white/30" > Scheduled for</ p >
172+ < p className = "font-medium text-violet-300" > { formatDisplay ( selectedDate ) } · { selectedSlot } PHT</ p >
173+ < p className = "text-white/30" > { meetingLabel } meeting</ p >
174+ </ div >
175+ </ div >
176+ ) ;
177+ }
4178
5- export default function Appointment ( ) {
6179 return (
7- < div className = "relative mx-auto flex w-screen max-w-xl flex-col gap-10 text-pretty rounded-lg border border-white/5 bg-white/5 px-5 py-10 text-white lg:py-14 xl:px-20 " >
180+ < div className = "relative flex w-full flex-col gap-8 text-pretty rounded-lg border border-white/5 bg-white/5 px-5 py-10 text-white lg:py-14 xl:px-10 " >
8181 < div className = "flex flex-col gap-1" >
9- < p className = "text-xl" > Appointment</ p >
10- < p className = "text-grey-100" >
11- Key Details to Get Started on Your Website Project
12- </ p >
182+ < p className = "text-xl font-semibold" > Set an Appointment</ p >
183+ < p className = "text-sm text-white/40" > Choose a date and time that works for you</ p >
184+ </ div >
185+
186+ < div className = "flex flex-col gap-6" >
187+
188+ { /* Calendar */ }
189+ < div className = "flex flex-col gap-3" >
190+ < p className = "text-sm font-medium text-white/60" > Preferred date</ p >
191+ < div className = "rounded-lg border border-white/5 bg-white/5 p-4" >
192+
193+ { /* Month nav */ }
194+ < div className = "mb-4 flex items-center justify-between" >
195+ < button type = "button" onClick = { prevMonth } disabled = { ! canGoPrev }
196+ className = { `flex h-7 w-7 items-center justify-center rounded-md border transition-all ${
197+ canGoPrev
198+ ? "border-white/10 text-white/50 hover:border-white/20 hover:text-white/80"
199+ : "cursor-not-allowed border-white/5 text-white/10"
200+ } `} >
201+ < svg className = "h-3.5 w-3.5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
202+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M15 19l-7-7 7-7" />
203+ </ svg >
204+ </ button >
205+ < p className = "text-sm font-semibold" > { MONTHS [ viewMonth ] } { viewYear } </ p >
206+ < button type = "button" onClick = { nextMonth }
207+ className = "flex h-7 w-7 items-center justify-center rounded-md border border-white/10 text-white/50 transition-all hover:border-white/20 hover:text-white/80" >
208+ < svg className = "h-3.5 w-3.5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
209+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M9 5l7 7-7 7" />
210+ </ svg >
211+ </ button >
212+ </ div >
213+
214+ { /* Day headers */ }
215+ < div className = "mb-2 grid grid-cols-7 gap-1" >
216+ { DAYS . map ( ( d ) => (
217+ < div key = { d } className = "text-center text-[10px] font-medium text-white/20" > { d } </ div >
218+ ) ) }
219+ </ div >
220+
221+ { /* Day cells */ }
222+ < div className = "grid grid-cols-7 gap-1" >
223+ { Array . from ( { length : firstDayOfMonth } ) . map ( ( _ , i ) => < div key = { `e-${ i } ` } /> ) }
224+ { Array . from ( { length : daysInMonth } ) . map ( ( _ , i ) => {
225+ const day = i + 1 ;
226+ const disabled = isDisabled ( day ) ;
227+ const selected = isSelected ( day ) ;
228+ const todayCell = isToday ( day ) ;
229+ return (
230+ < button key = { day } type = "button" disabled = { disabled } onClick = { ( ) => handleSelectDate ( day ) }
231+ className = { `relative flex h-8 w-full items-center justify-center rounded-md text-xs font-medium transition-all duration-150 ${
232+ selected ? "bg-violet-600 text-white" :
233+ disabled ? "cursor-not-allowed text-white/10" :
234+ todayCell ? "border border-violet-500/40 text-violet-300 hover:bg-violet-500/10" :
235+ "text-white/60 hover:bg-white/5 hover:text-white"
236+ } `} >
237+ { day }
238+ { todayCell && ! selected && (
239+ < span className = "absolute bottom-1 left-1/2 h-0.5 w-0.5 -translate-x-1/2 rounded-full bg-violet-400" />
240+ ) }
241+ </ button >
242+ ) ;
243+ } ) }
244+ </ div >
245+
246+ { selectedDate && (
247+ < p className = "mt-3 border-t border-white/5 pt-3 text-center text-xs text-violet-300" >
248+ { formatDisplay ( selectedDate ) }
249+ </ p >
250+ ) }
251+ </ div >
252+ </ div >
253+
254+ { /* Time slots */ }
255+ < div className = "flex flex-col gap-3" >
256+ < p className = "text-sm font-medium text-white/60" > Available slots</ p >
257+ { ! selectedDate ? (
258+ < p className = "text-xs text-white/20" > Select a date above to see available times</ p >
259+ ) : (
260+ < div className = "grid grid-cols-4 gap-2" >
261+ { timeSlots . map ( ( slot ) => {
262+ const isUnavailable = unavailableSlots . includes ( slot ) ;
263+ return (
264+ < button key = { slot } type = "button" disabled = { isUnavailable }
265+ onClick = { ( ) => setSelectedSlot ( slot ) }
266+ className = { `rounded-lg border py-2 text-xs font-medium transition-all duration-200 ${
267+ isUnavailable ? "cursor-not-allowed border-white/5 text-white/15 line-through" :
268+ selectedSlot === slot ? "border-violet-500 bg-violet-500/10 text-violet-300" :
269+ "border-white/5 text-white/40 hover:border-white/10 hover:text-white/60"
270+ } `} >
271+ { slot }
272+ </ button >
273+ ) ;
274+ } ) }
275+ </ div >
276+ ) }
277+ </ div >
278+
279+ { /* Meeting type */ }
280+ < div className = "flex flex-col gap-3" >
281+ < p className = "text-sm font-medium text-white/60" > Meeting type</ p >
282+ < div className = "flex flex-col gap-2" >
283+ { meetingTypes . map ( ( type ) => (
284+ < button key = { type . value } type = "button" onClick = { ( ) => setMeetingType ( type . value ) }
285+ className = { `flex items-center gap-3 rounded-lg border px-4 py-3 text-left transition-all duration-200 ${
286+ meetingType === type . value
287+ ? "border-violet-500 bg-violet-500/10"
288+ : "border-white/5 bg-white/5 hover:border-white/10"
289+ } `} >
290+ < span className = { `flex h-4 w-4 shrink-0 items-center justify-center rounded-full border transition-all ${
291+ meetingType === type . value ? "border-violet-500 bg-violet-500" : "border-white/20"
292+ } `} >
293+ { meetingType === type . value && < span className = "h-1.5 w-1.5 rounded-full bg-white" /> }
294+ </ span >
295+ < div >
296+ < p className = { `text-sm font-medium ${ meetingType === type . value ? "text-violet-300" : "text-white/50" } ` } >
297+ { type . label }
298+ </ p >
299+ < p className = "text-xs text-white/20" > { type . sub } </ p >
300+ </ div >
301+ </ button >
302+ ) ) }
303+ </ div >
304+
305+ { meetingType === "other" && (
306+ < input type = "text" value = { otherTool } onChange = { ( e ) => setOtherTool ( e . target . value ) }
307+ placeholder = "e.g. Whereby, Skype, Discord..."
308+ className = "w-full rounded border border-white/10 bg-white/5 px-3 py-2 text-sm text-white placeholder:text-white/20 outline-none focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 transition-all duration-200" />
309+ ) }
310+ </ div >
13311 </ div >
14- < div className = "flex flex-col gap-3" >
15- < Button variant = "purple" rounded = "md" >
16- Set Appointment
312+
313+ { /* Error message */ }
314+ { error && (
315+ < p className = "rounded-lg border border-red-500/20 bg-red-500/10 px-4 py-3 text-xs text-red-400" >
316+ { error }
317+ </ p >
318+ ) }
319+
320+ { /* Navigation */ }
321+ < div className = "flex gap-3" >
322+ < Button variant = "outline" rounded = "md" onClick = { onBack }
323+ className = "flex-1 border-white/10 text-white/50 hover:border-white/20 hover:text-white/70" >
324+ ← Back
325+ </ Button >
326+ < Button variant = "purple" rounded = "md"
327+ onClick = { handleSubmit }
328+ disabled = { ! canSubmit || loading }
329+ className = "flex-[2]" >
330+ { loading ? "Submitting..." : "Confirm Appointment ✓" }
17331 </ Button >
18332 </ div >
19333 </ div >
20334 ) ;
21- }
335+ }
0 commit comments