diff --git a/package-lock.json b/package-lock.json
index 20f2b97a..c38ea667 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4193,16 +4193,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/node": {
- "version": "20.12.8",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "undici-types": "~5.26.4"
- }
- },
"node_modules/@types/prop-types": {
"version": "15.7.12",
"dev": true,
@@ -8648,13 +8638,6 @@
"node": ">=14.0"
}
},
- "node_modules/undici-types": {
- "version": "5.26.5",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
@@ -11939,15 +11922,6 @@
"integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==",
"dev": true
},
- "@types/node": {
- "version": "20.12.8",
- "dev": true,
- "optional": true,
- "peer": true,
- "requires": {
- "undici-types": "~5.26.4"
- }
- },
"@types/prop-types": {
"version": "15.7.12",
"dev": true
@@ -14798,12 +14772,6 @@
"@fastify/busboy": "^2.0.0"
}
},
- "undici-types": {
- "version": "5.26.5",
- "dev": true,
- "optional": true,
- "peer": true
- },
"unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
diff --git a/src/App.tsx b/src/App.tsx
index b7ca42c1..6827c58a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,24 +1,12 @@
import { Footer, Header } from "compositions";
import { AllProviders } from "data";
-import { Demo } from "./examples/Demo";
-import { FAQs } from "./examples/FAQs";
-import { PanelSections } from "./examples/PanelSections";
-import { PricingGrid } from "./examples/PricingGrid";
-import { ProductDetails } from "./examples/ProductDetails";
-import { ProductGrid } from "./examples/ProductGrid";
-import { WelcomeHero } from "./examples/WelcomeHero";
+import AppointmentBooking from "./examples/AppointmentBooking";
function App() {
return (
-
-
-
-
-
-
-
+
);
diff --git a/src/data/services/servicesService.ts b/src/data/services/servicesService.ts
new file mode 100644
index 00000000..6f89c717
--- /dev/null
+++ b/src/data/services/servicesService.ts
@@ -0,0 +1,39 @@
+import { Service } from "../types/service";
+
+/**
+ * Mock services service
+ */
+export const servicesService = {
+ /**
+ * Get all services
+ */
+ async getServices(): Promise {
+ // Simulate API delay
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ return [
+ {
+ id: "1",
+ name: "Haircut",
+ price: 30.0,
+ description: "A stylish haircut",
+ durationInMinutes: 30,
+ },
+ {
+ id: "2",
+ name: "Haircut w/ Foil Razor & Beard Trim",
+ price: 50.0,
+ description: "A stylish haircut with a foil razor and beard trim",
+ durationInMinutes: 45,
+ signatureCuts: ["tapered cut", "foil razor finish"],
+ },
+ {
+ id: "3",
+ name: "Beard Trim(no haircut)",
+ price: 25.0,
+ description: "A stylish beard trim without a haircut",
+ durationInMinutes: 30,
+ imageUrl: "https://images.unsplash.com/photo-1503951914875-452162b0f3f1?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
+ },
+ ];
+ }
+};
diff --git a/src/data/types/service.ts b/src/data/types/service.ts
new file mode 100644
index 00000000..a1df05a3
--- /dev/null
+++ b/src/data/types/service.ts
@@ -0,0 +1,12 @@
+/**
+ * Service types
+ */
+export type Service = {
+ id: string;
+ name: string;
+ description: string;
+ price: number;
+ durationInMinutes: number;
+ imageUrl?: string;
+ signatureCuts?: string[];
+};
diff --git a/src/examples/AppointmentBooking.tsx b/src/examples/AppointmentBooking.tsx
new file mode 100644
index 00000000..e9fcc2c1
--- /dev/null
+++ b/src/examples/AppointmentBooking.tsx
@@ -0,0 +1,81 @@
+import { ServiceItemCard, ServiceItemCardSkeleton } from "compositions";
+import { servicesService } from "data/services/servicesService";
+import { Service } from "data/types/service";
+import { useMediaQuery } from "hooks";
+import { Flex, Section } from "layout";
+import { TextContentHeading } from "primitives";
+import type { FC } from "react";
+import { useEffect, useState } from "react";
+import "./appointmentbooking.css";
+
+export const AppointmentBooking: FC = () => {
+ const [services, setServices] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const { isMobile } = useMediaQuery();
+ const sectionPadding = isMobile ? "800" : "1600";
+
+ useEffect(() => {
+ let mounted = true;
+ const load = async () => {
+ try {
+ const data = await servicesService.getServices();
+ if (!mounted) return;
+ setServices(data);
+ } catch (err) {
+ console.error("Failed to load services", err);
+ if (mounted) setServices([]);
+ } finally {
+ if (mounted) setLoading(false);
+ }
+ };
+
+ load();
+ return () => {
+ mounted = false;
+ };
+ }, []);
+
+ return (
+
+
+
+ {loading ? (
+ <>
+
+
+
+
+ >
+ ) : (
+ services?.map((service, i) => {
+ return (
+
+ );
+ })
+ )}
+
+
+ );
+};
+
+export default AppointmentBooking;
diff --git a/src/examples/appointmentbooking.css b/src/examples/appointmentbooking.css
new file mode 100644
index 00000000..317e0fdb
--- /dev/null
+++ b/src/examples/appointmentbooking.css
@@ -0,0 +1,33 @@
+.service-item-card-asset {
+ width: 160px;
+ height: 160px;
+ border-radius: 0px;
+ object-fit: cover;
+}
+
+.appointment-booking-section{
+ padding: var(--sds-size-space-1600);
+}
+
+@media screen and (max-width: 768px) {
+ .appointment-booking-section {
+ padding: var(--sds-size-space-800);
+ }
+}
+
+.appointment-booking-heading p{
+ color: var(--sds-color-text-default-secondary);
+}
+
+.appointment-booking-heading h3{
+ color: var(--sds-color-text-default-default);
+}
+
+.book-now-button {
+ padding: var(--sds-size-space-300);
+ font-size: var(--sds-font-size-200);
+ border-radius: var(--sds-size-radius-200);
+ border: var(--sds-size-stroke-border) solid
+ var(--sds-color-border-neutral-secondary);
+ background: var(--sds-color-background-neutral-tertiary);
+}
diff --git a/src/ui/compositions/Cards/Cards.tsx b/src/ui/compositions/Cards/Cards.tsx
index 7bb6ee02..94408a0c 100644
--- a/src/ui/compositions/Cards/Cards.tsx
+++ b/src/ui/compositions/Cards/Cards.tsx
@@ -1,7 +1,8 @@
import clsx from "clsx";
import { PricingPlan, Product } from "data";
import { useMediaQuery } from "hooks";
-import { IconStar } from "icons";
+import { IconInfo, IconStar } from "icons";
+import { placeholder } from "images";
import { Flex } from "layout";
import {
Avatar,
@@ -10,6 +11,8 @@ import {
Button,
ButtonGroup,
ButtonProps,
+ DialogTrigger,
+ IconButton,
Image,
Text,
TextHeading,
@@ -20,6 +23,7 @@ import {
TextSmall,
TextStrong,
TextSubheading,
+ Tooltip,
} from "primitives";
import { ComponentPropsWithoutRef, ReactNode } from "react";
import { AnchorOrButton, AnchorOrButtonProps } from "utils";
@@ -552,3 +556,137 @@ export function TestimonialCard({
);
}
+
+export type ServiceItemCardProps = Pick & {
+ /**
+ * The service id for booking
+ */
+ id: string;
+ /**
+ * The service name
+ */
+ heading: string;
+ /**
+ * The price excluding currency
+ */
+ price: number;
+ /**
+ * The duration of the service excluding units (e.g. "30 minutes")
+ */
+ duration: number;
+ /**
+ * The signature cuts offered
+ */
+ signatureCuts?: string;
+ /**
+ * The image url for the service
+ */
+ imageUrl?: string;
+};
+
+/**
+ * This is used to show a loading state for ServiceItemCard.
+ * It has no props, but accepts a size prop to determine the size of the card.
+ */
+
+export function ServiceItemCardSkeleton({}: {}) {
+ return (
+
+ }
+ >
+
+ Loading Products...
+
+
+
+
+ );
+}
+
+/**
+ * A card that demonstrates service item
+ */
+export function ServiceItemCard({
+ asset,
+ heading,
+ price,
+ signatureCuts,
+ id,
+ duration,
+ ...props
+}: ServiceItemCardProps) {
+ return (
+
+ }
+ >
+
+
+
+ {heading}
+ {signatureCuts && (
+
+
+
+
+
+ Our signature cut
+ {signatureCuts}
+
+
+ )}
+
+
+ ${price} – {duration} minutes
+
+
+
+
+
+
+
+
+
+ );
+}