diff --git a/quests.json b/quests.json
index d5fef4a..8a8c964 100644
--- a/quests.json
+++ b/quests.json
@@ -1,8 +1,8 @@
-{
- "name": "New Project",
- "description": "",
- "icon": {
- "lucide": "square-dashed",
- "background": "conic-gradient(from 42deg at 50% 50%, #18181b, #27272a)"
- }
-}
+{
+ "description": "",
+ "icon": {
+ "background": "conic-gradient(from 42deg at 50% 50%, #ffd60a, #26cd41)",
+ "lucide": "calendar"
+ },
+ "name": "Beauty Shop Booking Platform"
+}
\ No newline at end of file
diff --git a/src/client/app.tsx b/src/client/app.tsx
index de11294..29abc67 100644
--- a/src/client/app.tsx
+++ b/src/client/app.tsx
@@ -1,14 +1,115 @@
-function App() {
- return (
-
- {/* Replace this placeholder content with your app components */}
-
-
- Building your new project...
-
-
-
- );
-}
-
-export default App;
+import { useState } from "react";
+import { BookingForm } from "@/client/components/booking-form";
+import { AdminTreatments } from "@/client/components/admin-treatments";
+import { AdminBookings } from "@/client/components/admin-bookings";
+import { InitialDataLoader } from "@/client/components/initial-data-loader";
+
+function App() {
+ const [activeTab, setActiveTab] = useState<"booking" | "admin-treatments" | "admin-bookings">("booking");
+
+ const tabs = [
+ { id: "booking", label: "Book Appointment", icon: "📅" },
+ { id: "admin-treatments", label: "Manage Treatments", icon: "💅" },
+ { id: "admin-bookings", label: "Manage Bookings", icon: "📋" },
+ ] as const;
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
💅
+
+
Bella Nails Studio
+
Professional Nail Design & Care
+
+
+
+
+
+
+ {/* Navigation */}
+
+
+ {/* Main Content */}
+
+ {activeTab === "booking" && (
+
+
+
+ Book Your Perfect Nail Treatment
+
+
+ Experience professional nail care with our expert technicians.
+ Choose from our wide range of treatments and book your appointment today.
+
+
+
+
+ )}
+
+ {activeTab === "admin-treatments" && (
+
+
+
+ Treatment Management
+
+
+ Add, edit, and manage your nail treatment services.
+
+
+
+
+ )}
+
+ {activeTab === "admin-bookings" && (
+
+
+
+ Booking Management
+
+
+ View and manage customer appointments and bookings.
+
+
+
+
+ )}
+
+
+ {/* Footer */}
+
+
+ );
+}
+
+export default App;
diff --git a/src/client/components/admin-bookings.tsx b/src/client/components/admin-bookings.tsx
new file mode 100644
index 0000000..7f39d1c
--- /dev/null
+++ b/src/client/components/admin-bookings.tsx
@@ -0,0 +1,213 @@
+import { useState } from "react";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { queryClient } from "@/client/rpc-client";
+
+export function AdminBookings() {
+ const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
+
+ const { data: bookings } = useQuery(
+ queryClient.bookings.live.list.experimental_liveOptions()
+ );
+
+ const { data: treatments } = useQuery(
+ queryClient.treatments.live.list.experimental_liveOptions()
+ );
+
+ const { mutate: updateBookingStatus } = useMutation(
+ queryClient.bookings.updateStatus.mutationOptions()
+ );
+
+ const getTreatmentName = (treatmentId: string) => {
+ return treatments?.find(t => t.id === treatmentId)?.name || "Unknown Treatment";
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case "pending": return "bg-yellow-100 text-yellow-800";
+ case "confirmed": return "bg-green-100 text-green-800";
+ case "cancelled": return "bg-red-100 text-red-800";
+ case "completed": return "bg-blue-100 text-blue-800";
+ default: return "bg-gray-100 text-gray-800";
+ }
+ };
+
+ const filteredBookings = bookings?.filter(booking =>
+ selectedDate ? booking.appointmentDate === selectedDate : true
+ ).sort((a, b) => {
+ if (a.appointmentDate === b.appointmentDate) {
+ return a.appointmentTime.localeCompare(b.appointmentTime);
+ }
+ return a.appointmentDate.localeCompare(b.appointmentDate);
+ });
+
+ const upcomingBookings = bookings?.filter(booking => {
+ const bookingDate = new Date(booking.appointmentDate);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ return bookingDate >= today && booking.status !== "cancelled";
+ }).sort((a, b) => {
+ if (a.appointmentDate === b.appointmentDate) {
+ return a.appointmentTime.localeCompare(b.appointmentTime);
+ }
+ return a.appointmentDate.localeCompare(b.appointmentDate);
+ });
+
+ return (
+
+
Manage Bookings
+
+ {/* Quick Stats */}
+
+
+
+ {bookings?.filter(b => b.status === "pending").length || 0}
+
+
Pending Approval
+
+
+
+ {bookings?.filter(b => b.status === "confirmed").length || 0}
+
+
Confirmed
+
+
+
+ {upcomingBookings?.length || 0}
+
+
Upcoming
+
+
+
+ {bookings?.length || 0}
+
+
Total Bookings
+
+
+
+ {/* Date Filter */}
+
+
+
+ setSelectedDate(e.target.value)}
+ className="p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
+ />
+
+
+
+
+ {/* Bookings Table */}
+
+
+
+
+
+ Customer
+ |
+
+ Treatment
+ |
+
+ Date & Time
+ |
+
+ Status
+ |
+
+ Actions
+ |
+
+
+
+ {filteredBookings?.map((booking) => (
+
+
+
+ {booking.customerName}
+ {booking.customerEmail}
+ {booking.customerPhone}
+
+ |
+
+ {getTreatmentName(booking.treatmentId)}
+ {booking.notes && (
+ Notes: {booking.notes}
+ )}
+ |
+
+ {new Date(booking.appointmentDate).toLocaleDateString()}
+ {booking.appointmentTime}
+ |
+
+
+ {booking.status}
+
+ |
+
+
+ {booking.status === "pending" && (
+ <>
+
+
+ >
+ )}
+ {booking.status === "confirmed" && (
+ <>
+
+
+ >
+ )}
+ {(booking.status === "cancelled" || booking.status === "completed") && (
+
+ )}
+
+ |
+
+ ))}
+
+
+
+ {!filteredBookings?.length && (
+
+ {selectedDate
+ ? `No bookings found for ${new Date(selectedDate).toLocaleDateString()}`
+ : "No bookings available."
+ }
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/client/components/admin-treatments.tsx b/src/client/components/admin-treatments.tsx
new file mode 100644
index 0000000..8c03b65
--- /dev/null
+++ b/src/client/components/admin-treatments.tsx
@@ -0,0 +1,270 @@
+import { useState } from "react";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { queryClient } from "@/client/rpc-client";
+
+export function AdminTreatments() {
+ const [showForm, setShowForm] = useState(false);
+ const [editingTreatment, setEditingTreatment] = useState(null);
+ const [formData, setFormData] = useState({
+ name: "",
+ description: "",
+ duration: 60,
+ price: 5000, // $50.00 in cents
+ category: "Manicure",
+ });
+
+ const { data: treatments } = useQuery(
+ queryClient.treatments.live.list.experimental_liveOptions()
+ );
+
+ const { mutate: createTreatment, isPending: isCreating } = useMutation(
+ queryClient.treatments.create.mutationOptions()
+ );
+
+ const { mutate: updateTreatment, isPending: isUpdating } = useMutation(
+ queryClient.treatments.update.mutationOptions()
+ );
+
+ const { mutate: deleteTreatment } = useMutation(
+ queryClient.treatments.remove.mutationOptions()
+ );
+
+ const categories = ["Manicure", "Pedicure", "Nail Art", "Extensions", "Other"];
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (editingTreatment) {
+ updateTreatment({
+ id: editingTreatment.id,
+ ...formData,
+ }, {
+ onSuccess: () => {
+ setEditingTreatment(null);
+ setShowForm(false);
+ resetForm();
+ }
+ });
+ } else {
+ createTreatment(formData, {
+ onSuccess: () => {
+ setShowForm(false);
+ resetForm();
+ }
+ });
+ }
+ };
+
+ const resetForm = () => {
+ setFormData({
+ name: "",
+ description: "",
+ duration: 60,
+ price: 5000,
+ category: "Manicure",
+ });
+ };
+
+ const handleEdit = (treatment: any) => {
+ setEditingTreatment(treatment);
+ setFormData({
+ name: treatment.name,
+ description: treatment.description,
+ duration: treatment.duration,
+ price: treatment.price,
+ category: treatment.category,
+ });
+ setShowForm(true);
+ };
+
+ const handleCancel = () => {
+ setShowForm(false);
+ setEditingTreatment(null);
+ resetForm();
+ };
+
+ return (
+
+
+
Manage Treatments
+
+
+
+ {showForm && (
+
+
+ {editingTreatment ? "Edit Treatment" : "Add New Treatment"}
+
+
+
+
+ )}
+
+
+
+
+
+
+ Treatment
+ |
+
+ Category
+ |
+
+ Duration
+ |
+
+ Price
+ |
+
+ Actions
+ |
+
+
+
+ {treatments?.map((treatment) => (
+
+
+
+ {treatment.name}
+ {treatment.description}
+
+ |
+
+ {treatment.category}
+ |
+
+ {treatment.duration} min
+ |
+
+ ${(treatment.price / 100).toFixed(2)}
+ |
+
+
+
+ |
+
+ ))}
+
+
+
+ {!treatments?.length && (
+
+ No treatments available. Add your first treatment to get started.
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/client/components/booking-form.tsx b/src/client/components/booking-form.tsx
new file mode 100644
index 0000000..5983c6b
--- /dev/null
+++ b/src/client/components/booking-form.tsx
@@ -0,0 +1,225 @@
+import { useState } from "react";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { queryClient } from "@/client/rpc-client";
+
+export function BookingForm() {
+ const [selectedTreatment, setSelectedTreatment] = useState("");
+ const [customerName, setCustomerName] = useState("");
+ const [customerEmail, setCustomerEmail] = useState("");
+ const [customerPhone, setCustomerPhone] = useState("");
+ const [appointmentDate, setAppointmentDate] = useState("");
+ const [appointmentTime, setAppointmentTime] = useState("");
+ const [notes, setNotes] = useState("");
+
+ const { data: treatments } = useQuery(
+ queryClient.treatments.live.list.experimental_liveOptions()
+ );
+
+ const { data: allBookings } = useQuery(
+ queryClient.bookings.live.list.experimental_liveOptions()
+ );
+
+ const selectedDateBookings = allBookings?.filter(booking =>
+ booking.appointmentDate === appointmentDate
+ ) || [];
+
+ const { mutate: createBooking, isPending } = useMutation(
+ queryClient.bookings.create.mutationOptions()
+ );
+
+ const selectedTreatmentData = treatments?.find(t => t.id === selectedTreatment);
+
+ // Generate available time slots (9 AM to 6 PM, 30-minute intervals)
+ const generateTimeSlots = () => {
+ const slots: string[] = [];
+ for (let hour = 9; hour < 18; hour++) {
+ for (let minute = 0; minute < 60; minute += 30) {
+ const time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
+ slots.push(time);
+ }
+ }
+ return slots;
+ };
+
+ const availableTimeSlots = generateTimeSlots().filter(time => {
+ if (!selectedDateBookings || !selectedTreatmentData) return true;
+
+ // Check if this time slot conflicts with existing bookings
+ const timeSlotStart = new Date(`${appointmentDate}T${time}:00`);
+ const timeSlotEnd = new Date(timeSlotStart.getTime() + selectedTreatmentData.duration * 60000);
+
+ return !selectedDateBookings.some(booking => {
+ if (booking.status === 'cancelled') return false;
+
+ const bookingStart = new Date(`${booking.appointmentDate}T${booking.appointmentTime}:00`);
+ const treatment = treatments?.find(t => t.id === booking.treatmentId);
+ if (!treatment) return false;
+
+ const bookingEnd = new Date(bookingStart.getTime() + treatment.duration * 60000);
+
+ return (timeSlotStart < bookingEnd && timeSlotEnd > bookingStart);
+ });
+ });
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !appointmentTime) {
+ alert("Please fill in all required fields");
+ return;
+ }
+
+ createBooking({
+ treatmentId: selectedTreatment,
+ customerName,
+ customerEmail,
+ customerPhone,
+ appointmentDate,
+ appointmentTime,
+ notes,
+ }, {
+ onSuccess: () => {
+ setSelectedTreatment("");
+ setCustomerName("");
+ setCustomerEmail("");
+ setCustomerPhone("");
+ setAppointmentDate("");
+ setAppointmentTime("");
+ setNotes("");
+ alert("Booking created successfully! We'll contact you to confirm your appointment.");
+ }
+ });
+ };
+
+ // Get minimum date (today)
+ const today = new Date().toISOString().split('T')[0];
+
+ return (
+
+
Book Your Nail Treatment
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/client/components/initial-data-loader.tsx b/src/client/components/initial-data-loader.tsx
new file mode 100644
index 0000000..a2da34b
--- /dev/null
+++ b/src/client/components/initial-data-loader.tsx
@@ -0,0 +1,69 @@
+import { useEffect } from "react";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { queryClient } from "@/client/rpc-client";
+
+export function InitialDataLoader() {
+ const { data: treatments } = useQuery(
+ queryClient.treatments.list.queryOptions()
+ );
+
+ const { mutate: createTreatment } = useMutation(
+ queryClient.treatments.create.mutationOptions()
+ );
+
+ useEffect(() => {
+ // Only initialize if no treatments exist
+ if (treatments && treatments.length === 0) {
+ const sampleTreatments = [
+ {
+ name: "Classic Manicure",
+ description: "Traditional nail care with cuticle treatment, shaping, and polish application",
+ duration: 45,
+ price: 3500, // $35.00
+ category: "Manicure"
+ },
+ {
+ name: "Gel Manicure",
+ description: "Long-lasting gel polish that cures under UV light for chip-free wear",
+ duration: 60,
+ price: 4500, // $45.00
+ category: "Manicure"
+ },
+ {
+ name: "Classic Pedicure",
+ description: "Foot soak, exfoliation, nail care, and polish application",
+ duration: 60,
+ price: 4000, // $40.00
+ category: "Pedicure"
+ },
+ {
+ name: "French Manicure",
+ description: "Elegant white tips with natural base for a timeless look",
+ duration: 50,
+ price: 4000, // $40.00
+ category: "Manicure"
+ },
+ {
+ name: "Nail Art Design",
+ description: "Custom nail art with intricate designs and decorations",
+ duration: 90,
+ price: 6500, // $65.00
+ category: "Nail Art"
+ },
+ {
+ name: "Acrylic Extensions",
+ description: "Full set of acrylic nail extensions with shape and length customization",
+ duration: 120,
+ price: 8000, // $80.00
+ category: "Extensions"
+ }
+ ];
+
+ sampleTreatments.forEach(treatment => {
+ createTreatment(treatment);
+ });
+ }
+ }, [treatments, createTreatment]);
+
+ return null; // This component doesn't render anything
+}
\ No newline at end of file
diff --git a/src/server/rpc/bookings.ts b/src/server/rpc/bookings.ts
new file mode 100644
index 0000000..d3e2843
--- /dev/null
+++ b/src/server/rpc/bookings.ts
@@ -0,0 +1,96 @@
+import { call, os } from "@orpc/server";
+import { z } from "zod";
+import { randomUUID } from "crypto";
+import { createKV } from "@/server/lib/create-kv";
+
+const BookingSchema = z.object({
+ id: z.string(),
+ treatmentId: z.string(),
+ customerName: z.string(),
+ customerEmail: z.string(),
+ customerPhone: z.string(),
+ appointmentDate: z.string(), // ISO date string
+ appointmentTime: z.string(), // HH:MM format
+ status: z.enum(["pending", "confirmed", "cancelled", "completed"]),
+ notes: z.string().optional(),
+ createdAt: z.string(),
+});
+
+type Booking = z.output;
+
+const kv = createKV("bookings");
+
+const create = os
+ .input(BookingSchema.omit({ id: true, createdAt: true, status: true }))
+ .handler(async ({ input }) => {
+ const id = randomUUID();
+ const booking = {
+ id,
+ ...input,
+ status: "pending" as const,
+ createdAt: new Date().toISOString()
+ };
+ await kv.setItem(id, booking);
+ return booking;
+ });
+
+const updateStatus = os
+ .input(z.object({
+ id: z.string(),
+ status: z.enum(["pending", "confirmed", "cancelled", "completed"])
+ }))
+ .handler(async ({ input }) => {
+ const booking = await kv.getItem(input.id);
+ if (!booking) throw new Error("Booking not found");
+
+ const updatedBooking = { ...booking, status: input.status };
+ await kv.setItem(input.id, updatedBooking);
+ return updatedBooking;
+ });
+
+const remove = os.input(z.string()).handler(async ({ input }) => {
+ await kv.removeItem(input);
+});
+
+const list = os.handler(async () => {
+ return kv.getAllItems();
+});
+
+const get = os.input(z.string()).handler(async ({ input }) => {
+ return kv.getItem(input);
+});
+
+const getByDate = os
+ .input(z.string()) // YYYY-MM-DD format
+ .handler(async ({ input }) => {
+ const allBookings = await kv.getAllItems();
+ return allBookings.filter(booking => booking.appointmentDate === input);
+ });
+
+const live = {
+ list: os.handler(async function* ({ signal }) {
+ yield call(list, {}, { signal });
+ for await (const _ of kv.subscribe()) {
+ yield call(list, {}, { signal });
+ }
+ }),
+
+ byDate: os
+ .input(z.string())
+ .handler(async function* ({ input, signal }) {
+ yield call(getByDate, input, { signal });
+ for await (const _ of kv.subscribe()) {
+ yield call(getByDate, input, { signal });
+ }
+ }),
+};
+
+export const router = {
+ create,
+ updateStatus,
+ remove,
+ list,
+ get,
+ getByDate,
+ live,
+};
\ No newline at end of file
diff --git a/src/server/rpc/index.ts b/src/server/rpc/index.ts
index d00ea76..39712a5 100644
--- a/src/server/rpc/index.ts
+++ b/src/server/rpc/index.ts
@@ -1,5 +1,9 @@
-import { demo } from "./demo";
-
-export const router = {
- demo,
-};
+import { demo } from "./demo";
+import { router as treatments } from "./treatments";
+import { router as bookings } from "./bookings";
+
+export const router = {
+ demo,
+ treatments,
+ bookings,
+};
diff --git a/src/server/rpc/treatments.ts b/src/server/rpc/treatments.ts
new file mode 100644
index 0000000..fb9eaa4
--- /dev/null
+++ b/src/server/rpc/treatments.ts
@@ -0,0 +1,63 @@
+import { call, os } from "@orpc/server";
+import { z } from "zod";
+import { randomUUID } from "crypto";
+import { createKV } from "@/server/lib/create-kv";
+
+const TreatmentSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ description: z.string(),
+ duration: z.number(), // duration in minutes
+ price: z.number(), // price in cents
+ category: z.string(),
+});
+
+type Treatment = z.output;
+
+const kv = createKV("treatments");
+
+const create = os
+ .input(TreatmentSchema.omit({ id: true }))
+ .handler(async ({ input }) => {
+ const id = randomUUID();
+ const treatment = { id, ...input };
+ await kv.setItem(id, treatment);
+ return treatment;
+ });
+
+const update = os
+ .input(TreatmentSchema)
+ .handler(async ({ input }) => {
+ await kv.setItem(input.id, input);
+ return input;
+ });
+
+const remove = os.input(z.string()).handler(async ({ input }) => {
+ await kv.removeItem(input);
+});
+
+const list = os.handler(async () => {
+ return kv.getAllItems();
+});
+
+const get = os.input(z.string()).handler(async ({ input }) => {
+ return kv.getItem(input);
+});
+
+const live = {
+ list: os.handler(async function* ({ signal }) {
+ yield call(list, {}, { signal });
+ for await (const _ of kv.subscribe()) {
+ yield call(list, {}, { signal });
+ }
+ }),
+};
+
+export const router = {
+ create,
+ update,
+ remove,
+ list,
+ get,
+ live,
+};
\ No newline at end of file