diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
index d8c9c45..523ac36 100644
--- a/apps/web/app/globals.css
+++ b/apps/web/app/globals.css
@@ -78,58 +78,49 @@
}
:root {
- --background: hsl(0 0% 100%);
- --foreground: hsl(0 0% 3.9%);
- --card: hsl(0 0% 100%);
- --card-foreground: hsl(0 0% 3.9%);
- --popover: hsl(0 0% 100%);
- --popover-foreground: hsl(0 0% 3.9%);
- --primary: hsl(0 0% 9%);
- --primary-foreground: hsl(0 0% 98%);
- --secondary: hsl(0 0% 96.1%);
- --secondary-foreground: hsl(0 0% 9%);
- --muted: hsl(0 0% 96.1%);
- --muted-foreground: hsl(0 0% 45.1%);
- --accent: hsl(0 0% 96.1%);
- --accent-foreground: hsl(0 0% 9%);
- --destructive: hsl(0 84.2% 60.2%);
- --destructive-foreground: hsl(0 0% 98%);
- --border: hsl(0 0% 89.8%);
- --input: hsl(0 0% 89.8%);
- --ring: hsl(0 0% 3.9%);
- --chart-1: hsl(12 76% 61%);
- --chart-2: hsl(173 58% 39%);
- --chart-3: hsl(197 37% 24%);
- --chart-4: hsl(43 74% 66%);
- --chart-5: hsl(27 87% 67%);
+ --background: hsl(292 0% 100%);
+ --foreground: hsl(292 0% 10%);
+ --card: hsl(292 0% 100%);
+ --card-foreground: hsl(292 0% 15%);
+ --popover: hsl(292 0% 100%);
+ --popover-foreground: hsl(292 95% 10%);
+ --primary: hsl(292 72% 59%);
+ --primary-foreground: hsl(0 0% 100%);
+ --secondary: hsl(292 10% 90%);
+ --secondary-foreground: hsl(0 0% 0%);
+ --muted: hsl(254 10% 95%);
+ --muted-foreground: hsl(292 0% 40%);
+ --accent: hsl(0, 0%, 95%);
+ --accent-foreground: hsl(292 0% 15%);
+ --destructive: hsl(0 50% 50%);
+ --destructive-foreground: hsl(292 0% 100%);
+ --border: hsl(285, 6%, 87%);
+ --input: hsl(292 20% 50%);
+ --ring: hsl(292 72% 59%);
--radius: 0.6rem;
}
.dark {
- --background: hsl(0 0% 3.9%);
- --foreground: hsl(0 0% 98%);
- --card: hsl(0 0% 3.9%);
- --card-foreground: hsl(0 0% 98%);
- --popover: hsl(0 0% 3.9%);
- --popover-foreground: hsl(0 0% 98%);
- --primary: hsl(0 0% 98%);
- --primary-foreground: hsl(0 0% 9%);
- --secondary: hsl(0 0% 14.9%);
- --secondary-foreground: hsl(0 0% 98%);
- --muted: hsl(0 0% 14.9%);
- --muted-foreground: hsl(0 0% 63.9%);
- --accent: hsl(0 0% 14.9%);
- --accent-foreground: hsl(0 0% 98%);
- --destructive: hsl(0 62.8% 30.6%);
- --destructive-foreground: hsl(0 0% 98%);
- --border: hsl(0 0% 14.9%);
- --input: hsl(0 0% 14.9%);
- --ring: hsl(0 0% 83.1%);
- --chart-1: hsl(220 70% 50%);
- --chart-2: hsl(160 60% 45%);
- --chart-3: hsl(30 80% 55%);
- --chart-4: hsl(280 65% 60%);
- --chart-5: hsl(340 75% 55%);
+ --background: hsl(292 10% 7%);
+ --foreground: hsl(292 0% 90%);
+ --card: hsl(292 0% 7%);
+ --card-foreground: hsl(292 0% 90%);
+ --popover: hsl(292 10% 5%);
+ --popover-foreground: hsl(292 0% 90%);
+ --primary: hsl(292 72% 59%);
+ --primary-foreground: 0 0% 100%;
+ --secondary: hsl(292 10% 10%);
+ --secondary-foreground: 0 0% 100%;
+ --muted: hsl(254 10% 15%);
+ --muted-foreground: hsl(292 0% 60%);
+ --accent: hsl(254 10% 15%);
+ --accent-foreground: hsl(292 0% 90%);
+ --destructive: hsl(0 50% 30%);
+ --destructive-foreground: hsl(292 0% 90%);
+ --border: hsl(300, 6%, 13%);
+ --input: hsl(292 20% 18%);
+ --ring: hsl(292 72% 59%);
+ --radius: 0.6rem;
}
@theme inline {
@@ -172,6 +163,11 @@
}
}
+@theme {
+ --font-primary: "Manrope", sans-serif;
+ --font-secondary: "Instrument Serif", serif;
+}
+
@layer base {
:root {
--sidebar-background: 0 0% 98%;
@@ -195,3 +191,51 @@
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
+
+/* Animations */
+
+@keyframes rotate {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.rotate-circle {
+ animation: rotate 60s linear infinite;
+}
+
+.counter-rotate {
+ animation: spin 60s linear infinite reverse;
+}
+
+/* Accordion */
+
+@keyframes accordion-down {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(--radix-accordion-content-height);
+ }
+}
+
+@keyframes accordion-up {
+ from {
+ height: var(--radix-accordion-content-height);
+ }
+ to {
+ height: 0;
+ }
+}
+
+/* Animation classes you can apply to your elements */
+.animate-accordion-down {
+ animation: accordion-down 0.2s ease-out;
+}
+
+.animate-accordion-up {
+ animation: accordion-up 0.2s ease-out;
+}
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 6c35438..94f6135 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,18 +1,20 @@
import type { Metadata } from "next";
-import localFont from "next/font/local";
+import { Instrument_Serif, Manrope } from "next/font/google";
import "./globals.css";
import { Appbar } from "@/components/Appbar";
import { Providers } from "@/components/providers/Providers";
-import { Footer } from "@/components/Footer";
+import Footer from "@/components/landing/footer";
-const geistSans = localFont({
- src: "./fonts/GeistVF.woff",
- variable: "--font-geist-sans",
+const manrope = Manrope({
+ subsets: ["latin"],
+ variable: "--font-manrope",
+ weight: ["300", "400", "500", "600", "700"],
});
-const geistMono = localFont({
- src: "./fonts/GeistMonoVF.woff",
- variable: "--font-geist-mono",
+const instrumental_serif = Instrument_Serif({
+ subsets: ["latin"],
+ variable: "--font-instrumental-serif",
+ weight: ["400"],
});
export const metadata: Metadata = {
@@ -36,7 +38,7 @@ export default function RootLayout({
/>
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index fb0800a..b3c47c5 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -1,9 +1,14 @@
-import { Hero } from "@/components/home/Hero";
+import Features from "@/components/landing/features";
+import FAQ from "@/components/landing/faq";
+import Footer from "@/components/landing/footer";
+import Hero from "@/components/landing/hero";
export default function Home() {
return (
+
+
);
}
diff --git a/apps/web/components/Hero.tsx b/apps/web/components/Hero.tsx
new file mode 100644
index 0000000..72dcf2c
--- /dev/null
+++ b/apps/web/components/Hero.tsx
@@ -0,0 +1,394 @@
+"use client";
+import { SignInButton, SignedIn, SignedOut } from "@clerk/nextjs";
+import { Button } from "./ui/button";
+import { useRouter } from "next/navigation";
+import { motion } from "motion/react";
+import {
+ ArrowRight,
+ Sparkles,
+ Camera,
+ Wand2,
+ Users,
+ Star,
+ Clock,
+ CheckCircle2,
+ Zap,
+} from "lucide-react";
+import useEmblaCarousel from "embla-carousel-react";
+import Autoplay from "embla-carousel-autoplay";
+import { useEffect, useState } from "react";
+
+export function Hero() {
+ const router = useRouter();
+ const [emblaRef, emblaApi] = useEmblaCarousel(
+ {
+ loop: true,
+ align: "start",
+ skipSnaps: false,
+ dragFree: true,
+ },
+ [
+ Autoplay({
+ delay: 3000,
+ stopOnInteraction: false,
+ stopOnMouseEnter: true,
+ }),
+ ]
+ );
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const [scrollSnaps, setScrollSnaps] = useState
([]);
+
+ useEffect(() => {
+ if (emblaApi) {
+ setScrollSnaps(emblaApi.scrollSnapList());
+ emblaApi.on("select", () => {
+ setSelectedIndex(emblaApi.selectedScrollSnap());
+ });
+ }
+ }, [emblaApi]);
+
+ const features = [
+ {
+ icon: ,
+ title: "Professional Quality",
+ description: "Studio-grade portraits generated in seconds",
+ gradient: "from-blue-500 to-purple-500",
+ },
+ {
+ icon: ,
+ title: "Magic Editing",
+ description: "Advanced AI tools to perfect every detail",
+ gradient: "from-purple-500 to-pink-500",
+ },
+ {
+ icon: ,
+ title: "Family Collections",
+ description: "Create stunning portraits for the whole family",
+ gradient: "from-pink-500 to-red-500",
+ },
+ {
+ icon: ,
+ title: "Instant Delivery",
+ description: "Get your photos in minutes, not days",
+ gradient: "from-red-500 to-orange-500",
+ },
+ ];
+
+ const testimonials = [
+ {
+ text: "The quality of these AI portraits is absolutely incredible. They look better than my professional headshots!",
+ author: "Sarah J.",
+ role: "Marketing Director",
+ avatar:
+ "https://r2-us-west.photoai.com/1739277231-0b2465581e9551abecd467b163d0d48a-1.png",
+ },
+ {
+ text: "We used this for our family portraits and the results were stunning. So much easier than a traditional photoshoot.",
+ author: "Michael R.",
+ role: "Family of 5",
+ avatar:
+ "https://r2-us-west.photoai.com/1739273789-920e7410ef180855f9a5718d1e37eb3a-1.png",
+ },
+ {
+ text: "Game-changer for my professional brand. The variety of styles and quick delivery is unmatched.",
+ author: "David L.",
+ role: "Entrepreneur",
+ avatar:
+ "https://r2-us-west.photoai.com/1739273783-9effbeb7239423cba9629e7dd06f3565-1.png",
+ },
+ ];
+
+ return (
+
+ {/* Animated background elements */}
+
+
+
+ {/* Hero Header Section */}
+
+
+
+
+ Next-Gen AI Portrait Generation
+
+
+
+ Powered by Advanced AI
+
+
+
+ Transform Your Photos
+
+
+ Into Magic
+
+
+
+ Experience the next evolution in portrait photography. Create
+ stunning, professional-quality portraits that capture your essence
+ in seconds.
+
+
+
+
+
+ 100% AI-Powered
+
+
+
+ Instant Results
+
+
+
+ Professional Quality
+
+
+
+
+ {/* Main CTA and Carousel Section */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {[
+ "https://r2-us-west.photoai.com/1739277231-0b2465581e9551abecd467b163d0d48a-1.png",
+ "https://r2-us-west.photoai.com/1739273789-920e7410ef180855f9a5718d1e37eb3a-1.png",
+ "https://r2-us-west.photoai.com/1739273783-9effbeb7239423cba9629e7dd06f3565-1.png",
+ "https://r2-us-west.photoai.com/1738861046-1175c64ebe0ecfe10b857e205b3b4a1e-3.png",
+ "https://r2-us-west.photoai.com/1738859038-086cec35785b734c68f99cab1f23d5a2-3.png",
+ "https://r2-us-west.photoai.com/1738859049-0c3f5f8cbb13210cf7bb1e356fd5a30a-3.png",
+ ].map((src, index) => (
+
+
+
+
+
+
+
+ Portrait Style {index + 1}
+
+
+ Professional Grade Output
+
+
+
+
+
+
+ ))}
+
+
+
+
emblaApi?.scrollPrev()}
+ >
+
+
+
+ {scrollSnaps.map((_, i) => (
+ emblaApi?.scrollTo(i)}
+ />
+ ))}
+
+
emblaApi?.scrollNext()}
+ >
+
+
+
+
+
+
+
+ {/* Features Section */}
+
+ {features.map((feature, index) => (
+
+
+
+ {feature.title}
+
+ {feature.description}
+
+ ))}
+
+
+ {/* Testimonials Section */}
+
+
+ Loved by Creators
+
+
+ Join thousands of satisfied users who have transformed their
+ portraits
+
+
+ {testimonials.map((testimonial, index) => (
+
+
+
+

+
+
+
+ {testimonial.text}
+
+
+ {testimonial.author}
+
+
{testimonial.role}
+
+
+ ))}
+
+
+
+ {/* Final CTA Section */}
+
+
+
+ Ready to Transform Your Photos?
+
+
+ Join the future of portrait photography. Create stunning,
+ professional portraits in seconds.
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/components/landing/faq.tsx b/apps/web/components/landing/faq.tsx
new file mode 100644
index 0000000..e6c7a7c
--- /dev/null
+++ b/apps/web/components/landing/faq.tsx
@@ -0,0 +1,52 @@
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { faq } from "@/constants/data";
+import { Question } from "@phosphor-icons/react/dist/ssr";
+
+export default function FAQ() {
+ return (
+ <>
+
+
+
+
+
+ FAQ
+
+
+ Frequently Asked
+
{" "}
+
+ Questions
+
+
+
+
+ Here are some of the questions that we get asked about PhotoAI.
+
+
+
+ {/* add shadcn accordion component here */}
+
+ {faq.map((item, index) => (
+
+
+ {item.question}
+
+ {item.answer}
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/apps/web/components/landing/features.tsx b/apps/web/components/landing/features.tsx
new file mode 100644
index 0000000..3cc5a2f
--- /dev/null
+++ b/apps/web/components/landing/features.tsx
@@ -0,0 +1,75 @@
+import { features } from "@/constants/data";
+import { ArrowUpRight, Medal } from "@phosphor-icons/react/dist/ssr";
+import { FlickeringGrid } from "../magicui/flickering-grid";
+import { Button } from "../ui/button";
+import { Badge } from "../ui/badge";
+import { Cube } from "@phosphor-icons/react/dist/ssr";
+
+export default function Features() {
+ return (
+ <>
+
+
+
+
+
+ Features
+
+
+ Benefits of using
+
{" "}
+
+ PhotoAI
+
+
+
+
+ PhotoAI is a platform that allows you to create stunning images
+ using
+
+ AI and also let's you customize the images to your liking.
+
+
+
+ {features.map((feature) => (
+
+
+
+
+
+
+
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
+
+
+ {feature.badgeText}{" "}
+
+
+
+
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/apps/web/components/landing/footer.tsx b/apps/web/components/landing/footer.tsx
new file mode 100644
index 0000000..7adf750
--- /dev/null
+++ b/apps/web/components/landing/footer.tsx
@@ -0,0 +1,31 @@
+import { Sparkle } from "@phosphor-icons/react/dist/ssr";
+
+export default function Footer() {
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/apps/web/components/landing/hero.tsx b/apps/web/components/landing/hero.tsx
new file mode 100644
index 0000000..71669f5
--- /dev/null
+++ b/apps/web/components/landing/hero.tsx
@@ -0,0 +1,133 @@
+"use client";
+import {
+ ArrowRight,
+ Cube,
+ CubeFocus,
+ Robot,
+ Rocket,
+ RocketLaunch,
+ ShootingStar,
+ Sliders,
+ Sparkle,
+} from "@phosphor-icons/react/dist/ssr";
+import { Button } from "../ui/button";
+import ImgIllustration from "./illustrations/img-illustration";
+import { Badge } from "../ui/badge";
+import RotatingPeople from "./illustrations/rotating-people";
+import AvatarComponent from "./illustrations/avatar-component";
+import { SignedIn, SignedOut, SignInButton } from "@clerk/nextjs";
+import { useRouter } from "next/navigation";
+
+export default function Hero() {
+ const router = useRouter();
+ return (
+ <>
+
+
+
+ Next-Gen AI
+ Portrait Generation
+
+
+
+ Transform any
+
{" "}
+
+
+ into{" "}
+
+ Magic
+ {" "}
+ with AI
+
+
+
+ Experience the{" "}
+
+ next evolution
+ {" "}
+ in portrait photography.
+
+ Create stunning,{" "}
+
+ professional-quality
+ {" "}
+ portraits that capture your essence in{" "}
+
+ seconds
+
+ .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Top Quality
+
+
+
+ AI-Powered
+
+
+
+ Customizable
+
+
+
+ Instant Results
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/components/landing/illustrations/avatar-component.tsx b/apps/web/components/landing/illustrations/avatar-component.tsx
new file mode 100644
index 0000000..b842638
--- /dev/null
+++ b/apps/web/components/landing/illustrations/avatar-component.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+
+export default function AvatarComponent({ className }: { className?: string }) {
+ return (
+
+
+
+ Trusted by 50+{" "}
+ founders.
+
+
+ );
+}
diff --git a/apps/web/components/landing/illustrations/img-illustration.tsx b/apps/web/components/landing/illustrations/img-illustration.tsx
new file mode 100644
index 0000000..673ca10
--- /dev/null
+++ b/apps/web/components/landing/illustrations/img-illustration.tsx
@@ -0,0 +1,42 @@
+import Image from "next/image";
+
+export default function ImgIllustration() {
+ const IMAGES = [
+ "https://r2-us-west.photoai.com/1739277231-0b2465581e9551abecd467b163d0d48a-1.png",
+ "https://r2-us-west.photoai.com/1739273783-9effbeb7239423cba9629e7dd06f3565-1.png",
+ "https://r2-us-west.photoai.com/1738859038-086cec35785b734c68f99cab1f23d5a2-3.png",
+ ];
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/apps/web/components/landing/illustrations/rotating-people.tsx b/apps/web/components/landing/illustrations/rotating-people.tsx
new file mode 100644
index 0000000..8e7945c
--- /dev/null
+++ b/apps/web/components/landing/illustrations/rotating-people.tsx
@@ -0,0 +1,55 @@
+import React from "react";
+import { Users } from "lucide-react";
+import Image from "next/image";
+import { cn } from "@/lib/utils";
+
+export default function RotatingPeople({ className }: { className?: string }) {
+ const profiles = [
+ "https://r2-us-west.photoai.com/1739277231-0b2465581e9551abecd467b163d0d48a-1.png",
+ "https://r2-us-west.photoai.com/1739273789-920e7410ef180855f9a5718d1e37eb3a-1.png",
+ "https://r2-us-west.photoai.com/1739273783-9effbeb7239423cba9629e7dd06f3565-1.png",
+ "https://r2-us-west.photoai.com/1738861046-1175c64ebe0ecfe10b857e205b3b4a1e-3.png",
+ "https://r2-us-west.photoai.com/1738859038-086cec35785b734c68f99cab1f23d5a2-3.png",
+ "https://r2-us-west.photoai.com/1738859049-0c3f5f8cbb13210cf7bb1e356fd5a30a-3.png",
+ "https://r2-us-west.photoai.com/1739273783-9effbeb7239423cba9629e7dd06f3565-1.png",
+ "https://r2-us-west.photoai.com/1738861046-1175c64ebe0ecfe10b857e205b3b4a1e-3.png",
+ ];
+
+ return (
+
+
+ {profiles.map((profile, index) => {
+ const angle = (index * 360) / profiles.length;
+ const radius = 550; // Adjust this value to change the circle size
+ const radian = (angle * Math.PI) / 180;
+ const x = Math.cos(radian) * radius;
+ const y = Math.sin(radian) * radius;
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/web/components/magicui/flickering-grid.tsx b/apps/web/components/magicui/flickering-grid.tsx
new file mode 100644
index 0000000..f303d4d
--- /dev/null
+++ b/apps/web/components/magicui/flickering-grid.tsx
@@ -0,0 +1,199 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+
+interface FlickeringGridProps extends React.HTMLAttributes {
+ squareSize?: number;
+ gridGap?: number;
+ flickerChance?: number;
+ color?: string;
+ width?: number;
+ height?: number;
+ className?: string;
+ maxOpacity?: number;
+}
+
+export const FlickeringGrid: React.FC = ({
+ squareSize = 4,
+ gridGap = 6,
+ flickerChance = 0.3,
+ color = "rgb(0, 0, 0)",
+ width,
+ height,
+ className,
+ maxOpacity = 0.3,
+ ...props
+}) => {
+ const canvasRef = useRef(null);
+ const containerRef = useRef(null);
+ const [isInView, setIsInView] = useState(false);
+ const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
+
+ const memoizedColor = useMemo(() => {
+ const toRGBA = (color: string) => {
+ if (typeof window === "undefined") {
+ return `rgba(0, 0, 0,`;
+ }
+ const canvas = document.createElement("canvas");
+ canvas.width = canvas.height = 1;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return "rgba(255, 0, 0,";
+ ctx.fillStyle = color;
+ ctx.fillRect(0, 0, 1, 1);
+ const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
+ return `rgba(${r}, ${g}, ${b},`;
+ };
+ return toRGBA(color);
+ }, [color]);
+
+ const setupCanvas = useCallback(
+ (canvas: HTMLCanvasElement, width: number, height: number) => {
+ const dpr = window.devicePixelRatio || 1;
+ canvas.width = width * dpr;
+ canvas.height = height * dpr;
+ canvas.style.width = `${width}px`;
+ canvas.style.height = `${height}px`;
+ const cols = Math.floor(width / (squareSize + gridGap));
+ const rows = Math.floor(height / (squareSize + gridGap));
+
+ const squares = new Float32Array(cols * rows);
+ for (let i = 0; i < squares.length; i++) {
+ squares[i] = Math.random() * maxOpacity;
+ }
+
+ return { cols, rows, squares, dpr };
+ },
+ [squareSize, gridGap, maxOpacity],
+ );
+
+ const updateSquares = useCallback(
+ (squares: Float32Array, deltaTime: number) => {
+ for (let i = 0; i < squares.length; i++) {
+ if (Math.random() < flickerChance * deltaTime) {
+ squares[i] = Math.random() * maxOpacity;
+ }
+ }
+ },
+ [flickerChance, maxOpacity],
+ );
+
+ const drawGrid = useCallback(
+ (
+ ctx: CanvasRenderingContext2D,
+ width: number,
+ height: number,
+ cols: number,
+ rows: number,
+ squares: Float32Array,
+ dpr: number,
+ ) => {
+ ctx.clearRect(0, 0, width, height);
+ ctx.fillStyle = "transparent";
+ ctx.fillRect(0, 0, width, height);
+
+ for (let i = 0; i < cols; i++) {
+ for (let j = 0; j < rows; j++) {
+ const opacity = squares[i * rows + j];
+ ctx.fillStyle = `${memoizedColor}${opacity})`;
+ ctx.fillRect(
+ i * (squareSize + gridGap) * dpr,
+ j * (squareSize + gridGap) * dpr,
+ squareSize * dpr,
+ squareSize * dpr,
+ );
+ }
+ }
+ },
+ [memoizedColor, squareSize, gridGap],
+ );
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ const container = containerRef.current;
+ if (!canvas || !container) return;
+
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ let animationFrameId: number;
+ let gridParams: ReturnType;
+
+ const updateCanvasSize = () => {
+ const newWidth = width || container.clientWidth;
+ const newHeight = height || container.clientHeight;
+ setCanvasSize({ width: newWidth, height: newHeight });
+ gridParams = setupCanvas(canvas, newWidth, newHeight);
+ };
+
+ updateCanvasSize();
+
+ let lastTime = 0;
+ const animate = (time: number) => {
+ if (!isInView) return;
+
+ const deltaTime = (time - lastTime) / 1000;
+ lastTime = time;
+
+ updateSquares(gridParams.squares, deltaTime);
+ drawGrid(
+ ctx,
+ canvas.width,
+ canvas.height,
+ gridParams.cols,
+ gridParams.rows,
+ gridParams.squares,
+ gridParams.dpr,
+ );
+ animationFrameId = requestAnimationFrame(animate);
+ };
+
+ const resizeObserver = new ResizeObserver(() => {
+ updateCanvasSize();
+ });
+
+ resizeObserver.observe(container);
+
+ const intersectionObserver = new IntersectionObserver(
+ ([entry]) => {
+ setIsInView(entry.isIntersecting);
+ },
+ { threshold: 0 },
+ );
+
+ intersectionObserver.observe(canvas);
+
+ if (isInView) {
+ animationFrameId = requestAnimationFrame(animate);
+ }
+
+ return () => {
+ cancelAnimationFrame(animationFrameId);
+ resizeObserver.disconnect();
+ intersectionObserver.disconnect();
+ };
+ }, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
+
+ return (
+
+
+
+ );
+};
diff --git a/apps/web/components/toggler.tsx b/apps/web/components/toggler.tsx
new file mode 100644
index 0000000..46655a6
--- /dev/null
+++ b/apps/web/components/toggler.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import * as React from "react";
+import { useTheme } from "next-themes";
+import { Moon, Sun } from "@phosphor-icons/react";
+
+export function ToggleTheme() {
+ const { setTheme, theme } = useTheme();
+ return (
+ <>
+ setTheme(theme === "light" ? "dark" : "light")}
+ >
+
+
+
+ >
+ );
+}
diff --git a/apps/web/components/ui/accordion.tsx b/apps/web/components/ui/accordion.tsx
new file mode 100644
index 0000000..0378817
--- /dev/null
+++ b/apps/web/components/ui/accordion.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import * as React from "react";
+import * as AccordionPrimitive from "@radix-ui/react-accordion";
+import { ChevronDown } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const Accordion = AccordionPrimitive.Root;
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+AccordionItem.displayName = "AccordionItem";
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ );
+}
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ );
+}
+AccordionContent.displayName = AccordionPrimitive.Content.displayName;
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/apps/web/components/ui/badge.tsx b/apps/web/components/ui/badge.tsx
new file mode 100644
index 0000000..e87d62b
--- /dev/null
+++ b/apps/web/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/apps/web/constants/data.ts b/apps/web/constants/data.ts
new file mode 100644
index 0000000..f01f256
--- /dev/null
+++ b/apps/web/constants/data.ts
@@ -0,0 +1,60 @@
+import {
+ Aperture,
+ MagicWand,
+ Users,
+ Clock,
+} from "@phosphor-icons/react/dist/ssr";
+
+export const features = [
+ {
+ icon: Aperture,
+ badgeText: "100% Quality",
+ title: "Professional Quality",
+ description: "Studio-grade portraits generated in seconds",
+ gradient: "from-blue-500 to-purple-500",
+ },
+ {
+ icon: MagicWand,
+ badgeText: "20+ Models",
+ title: "Advanced AI tools to perfect every detail",
+ description: "Advanced AI tools to perfect every detail",
+ gradient: "from-purple-500 to-pink-500",
+ },
+ {
+ icon: Users,
+ badgeText: "100k+ happy users",
+ title: "Create stunning portraits for the whole family",
+ description: "Create stunning portraits for the whole family",
+ gradient: "from-pink-500 to-red-500",
+ },
+ {
+ icon: Clock,
+ badgeText: "Results in <10s",
+ title: "Get your photos in minutes, not days",
+ description: "Get your photos in minutes, not days",
+ gradient: "from-red-500 to-orange-500",
+ },
+];
+
+export const faq = [
+ {
+ question: "What is PhotoAI?",
+ answer:
+ "PhotoAI is an advanced AI-powered platform that transforms your ideas into stunning, professional-quality images in seconds. Whether you're a designer, content creator, or business owner, our platform helps you generate unique visuals using state-of-the-art artificial intelligence.",
+ },
+ {
+ question: "How does PhotoAI work?",
+ answer:
+ "Simply describe the image you want to create using natural language, and our AI engine will generate high-quality images based on your description. You can further refine your results by adjusting parameters like style, composition, and color scheme. Our advanced algorithms ensure each creation is unique and tailored to your needs.",
+ },
+ {
+ question: "Is there a limit to how many images I can generate?",
+ answer:
+ "The number of images you can generate depends on your subscription plan. Free users can create up to 15 images per month, while our premium plans offer unlimited generations, priority processing, and access to advanced features like higher resolutions and commercial usage rights.",
+ },
+ {
+ question: "Can I use PhotoAI-generated images commercially?",
+ answer:
+ "Yes, premium subscribers receive full commercial rights to their generated images. This means you can use them for business purposes, marketing materials, websites, and social media. However, we recommend reviewing our terms of service for specific usage guidelines and restrictions.",
+ },
+];
diff --git a/apps/web/package.json b/apps/web/package.json
index 300686c..d52ab40 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -11,6 +11,7 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
+ "@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
@@ -40,7 +41,8 @@
"react-hot-toast": "^2.5.1",
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.6",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "motion": "^12.4.2"
},
"devDependencies": {
"@repo/eslint-config": "*",
diff --git a/bun.lockb b/bun.lockb
deleted file mode 100755
index 6aa8ab4..0000000
Binary files a/bun.lockb and /dev/null differ