diff --git a/frontend/website/package-lock.json b/frontend/website/package-lock.json
index ffae0b8..9083b2c 100644
--- a/frontend/website/package-lock.json
+++ b/frontend/website/package-lock.json
@@ -8,6 +8,7 @@
"name": "website",
"version": "0.0.0",
"dependencies": {
+ "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-slot": "^1.1.2",
@@ -15,6 +16,7 @@
"@types/node": "^22.13.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "embla-carousel-react": "^8.5.2",
"lucide-react": "^0.476.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -1036,6 +1038,32 @@
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
"license": "MIT"
},
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.3.tgz",
+ "integrity": "sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.2",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collection": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
@@ -2568,6 +2596,34 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/embla-carousel": {
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.2.tgz",
+ "integrity": "sha512-xQ9oVLrun/eCG/7ru3R+I5bJ7shsD8fFwLEY7yPe27/+fDHCNj0OT5EoG5ZbFyOxOcG6yTwW8oTz/dWyFnyGpg==",
+ "license": "MIT"
+ },
+ "node_modules/embla-carousel-react": {
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.5.2.tgz",
+ "integrity": "sha512-Tmx+uY3MqseIGdwp0ScyUuxpBgx5jX1f7od4Cm5mDwg/dptEiTKf9xp6tw0lZN2VA9JbnVMl/aikmbc53c6QFA==",
+ "license": "MIT",
+ "dependencies": {
+ "embla-carousel": "8.5.2",
+ "embla-carousel-reactive-utils": "8.5.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/embla-carousel-reactive-utils": {
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.5.2.tgz",
+ "integrity": "sha512-QC8/hYSK/pEmqEdU1IO5O+XNc/Ptmmq7uCB44vKplgLKhB/l0+yvYx0+Cv0sF6Ena8Srld5vUErZkT+yTahtDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "embla-carousel": "8.5.2"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
diff --git a/frontend/website/package.json b/frontend/website/package.json
index 98519ee..9e42121 100644
--- a/frontend/website/package.json
+++ b/frontend/website/package.json
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-slot": "^1.1.2",
@@ -17,6 +18,7 @@
"@types/node": "^22.13.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "embla-carousel-react": "^8.5.2",
"lucide-react": "^0.476.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/frontend/website/src/App.tsx b/frontend/website/src/App.tsx
index acedca7..80c50b2 100644
--- a/frontend/website/src/App.tsx
+++ b/frontend/website/src/App.tsx
@@ -5,7 +5,7 @@ import About from './components/About';
import Projects from './components/Projects';
import Contact from './components/Contact';
import Navbar04Page from './components/navbar-04/navbar-04';
-
+import Testimonial06 from './components/testimonial-06/testimonial-06';
function App() {
return (
@@ -13,6 +13,7 @@ function App() {
+
);
diff --git a/frontend/website/src/components/Hero.tsx b/frontend/website/src/components/Hero.tsx
index 95d695a..52732ec 100644
--- a/frontend/website/src/components/Hero.tsx
+++ b/frontend/website/src/components/Hero.tsx
@@ -7,9 +7,8 @@ const Hero = () => {
Craig Macfadyen
- Data Scientist with experience in both research
- and consulting roles. Expertise in Computer Vision and Large Language Models. Check out what
- I can do and get in touch if you have any questions or you'd like to work together!
+ Data Scientist with experience in consulting and research. Expertise in Computer Vision and Large Language Models. Take a look at what
+ I can do for your business and get in touch if you have any questions or you'd like to work together!
diff --git a/frontend/website/src/components/testimonial-06/testimonial-06.tsx b/frontend/website/src/components/testimonial-06/testimonial-06.tsx
new file mode 100644
index 0000000..fcd3e26
--- /dev/null
+++ b/frontend/website/src/components/testimonial-06/testimonial-06.tsx
@@ -0,0 +1,119 @@
+"use client";
+
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import {
+ Carousel,
+ CarouselApi,
+ CarouselContent,
+ CarouselItem,
+ CarouselNext,
+ CarouselPrevious,
+} from "@/components/ui/carousel";
+import { cn } from "@/lib/utils";
+import { useEffect, useState } from "react";
+
+const testimonials = [
+ {
+ id: 1,
+ name: "Steven Adair",
+ designation: "Director",
+ company: "Managing Utilities Ltd",
+ testimonial:
+ "What set Craig apart was his ability to understand our business challenges and deliver a solution that worked for us. He didn't just build a model, he delivered a system our team could actually use."
+ },
+];
+const Testimonial06 = () => {
+ const [api, setApi] = useState();
+ const [current, setCurrent] = useState(0);
+ const [count, setCount] = useState(0);
+
+ useEffect(() => {
+ if (!api) {
+ return;
+ }
+
+ setCount(api.scrollSnapList().length);
+ setCurrent(api.selectedScrollSnap() + 1);
+
+ api.on("select", () => {
+ setCurrent(api.selectedScrollSnap() + 1);
+ });
+ }, [api]);
+
+ return (
+
+
+
+ Testimonials
+
+
+
+
+ {testimonials.map((testimonial) => (
+
+
+
+ ))}
+
+
+
+
+
+ {Array.from({ length: count }).map((_, index) => (
+ api?.scrollTo(index)}
+ className={cn("h-3.5 w-3.5 rounded-full border-2", {
+ "bg-primary border-primary": current === index + 1,
+ })}
+ />
+ ))}
+
+
+
+
+ );
+};
+
+const TestimonialCard = ({
+ testimonial,
+}: {
+ testimonial: (typeof testimonials)[number];
+}) => (
+
+
+
+
+
+
+
+ {testimonial.name.charAt(0)}
+
+
+
+
{testimonial.name}
+
{testimonial.designation}
+
+
+
+
+ "{testimonial.testimonial}"
+
+
+
+
+ {testimonial.name.charAt(0)}
+
+
+
+
{testimonial.name}
+
{testimonial.designation}
+
{testimonial.company}
+
+
+
+
+
+);
+
+export default Testimonial06;
diff --git a/frontend/website/src/components/ui/avatar.tsx b/frontend/website/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..b7224f0
--- /dev/null
+++ b/frontend/website/src/components/ui/avatar.tsx
@@ -0,0 +1,51 @@
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/frontend/website/src/components/ui/carousel.tsx b/frontend/website/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..0e05a77
--- /dev/null
+++ b/frontend/website/src/components/ui/carousel.tsx
@@ -0,0 +1,241 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+function Carousel({
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & CarouselProps) {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) return
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) return
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) return
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+}
+
+function CarouselPrevious({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+
+ Previous slide
+
+ )
+}
+
+function CarouselNext({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+
+ Next slide
+
+ )
+}
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}