Skip to content

Commit 30f36d2

Browse files
Made improvements in contact appointments and resolve existing issues.
1 parent ee81ba5 commit 30f36d2

5 files changed

Lines changed: 830 additions & 145 deletions

File tree

Lines changed: 324 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,335 @@
11
"use client";
22

3+
import { useState } from "react";
34
import { 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

Comments
 (0)