diff --git a/app/css/style.css b/app/css/style.css index 2e78c217d..9dd11d432 100644 --- a/app/css/style.css +++ b/app/css/style.css @@ -3,6 +3,9 @@ /* Additional styles */ @import './additional-styles/utility-patterns.css' layer(components); @import './additional-styles/theme.css'; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); @plugin "@tailwindcss/forms" { strategy: base; @@ -91,3 +94,137 @@ border-color: var(--color-gray-200, currentColor); } } + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --animate-marquee: marquee var(--duration) infinite linear; + --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; + @keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-100% - var(--gap))); + } + } + @keyframes marquee-vertical { + from { + transform: translateY(0); + } + to { + transform: translateY(calc(-100% - var(--gap))); + } + } +} + +:root { + --radius: 0.625rem; + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 000000000..55870861b --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/css/style.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components/features.tsx b/components/features.tsx index fbdd8759e..914bc1754 100644 --- a/components/features.tsx +++ b/components/features.tsx @@ -1,7 +1,10 @@ +"use client"; + import Image from "next/image"; import BlurredShapeGray from "@/public/images/blurred-shape-gray.svg"; import BlurredShape from "@/public/images/blurred-shape.svg"; import FeaturesImage from "@/public/images/features.png"; +import { motion } from "motion/react"; export default function Features() { return ( @@ -59,7 +62,16 @@ export default function Features() { {/* Items */}
-
+ -
-
+ + -
-
+ + -
-
+ + -
-
+ + -
-
+ + -
+
diff --git a/components/magicui/globe.tsx b/components/magicui/globe.tsx new file mode 100644 index 000000000..4cd6e032a --- /dev/null +++ b/components/magicui/globe.tsx @@ -0,0 +1,128 @@ +"use client"; + +import createGlobe, { COBEOptions } from "cobe"; +import { useMotionValue, useSpring } from "motion/react"; +import { useEffect, useRef } from "react"; + +import { cn } from "@/lib/utils"; + +const MOVEMENT_DAMPING = 1400; + +const GLOBE_CONFIG: COBEOptions = { + width: 800, + height: 800, + onRender: () => {}, + devicePixelRatio: 2, + phi: 0, + theta: 0.3, + dark: 0, + diffuse: 0.4, + mapSamples: 16000, + mapBrightness: 1.2, + baseColor: [1, 1, 1], + markerColor: [251 / 255, 100 / 255, 21 / 255], + glowColor: [1, 1, 1], + markers: [ + { location: [14.5995, 120.9842], size: 0.03 }, + { location: [19.076, 72.8777], size: 0.1 }, + { location: [23.8103, 90.4125], size: 0.05 }, + { location: [30.0444, 31.2357], size: 0.07 }, + { location: [39.9042, 116.4074], size: 0.08 }, + { location: [-23.5505, -46.6333], size: 0.1 }, + { location: [19.4326, -99.1332], size: 0.1 }, + { location: [40.7128, -74.006], size: 0.1 }, + { location: [34.6937, 135.5022], size: 0.05 }, + { location: [41.0082, 28.9784], size: 0.06 }, + ], +}; + +export function Globe({ + className, + config = GLOBE_CONFIG, +}: { + className?: string; + config?: COBEOptions; +}) { + let phi = 0; + let width = 0; + const canvasRef = useRef(null); + const pointerInteracting = useRef(null); + const pointerInteractionMovement = useRef(0); + + const r = useMotionValue(0); + const rs = useSpring(r, { + mass: 1, + damping: 30, + stiffness: 100, + }); + + const updatePointerInteraction = (value: number | null) => { + pointerInteracting.current = value; + if (canvasRef.current) { + canvasRef.current.style.cursor = value !== null ? "grabbing" : "grab"; + } + }; + + const updateMovement = (clientX: number) => { + if (pointerInteracting.current !== null) { + const delta = clientX - pointerInteracting.current; + pointerInteractionMovement.current = delta; + r.set(r.get() + delta / MOVEMENT_DAMPING); + } + }; + + useEffect(() => { + const onResize = () => { + if (canvasRef.current) { + width = canvasRef.current.offsetWidth; + } + }; + + window.addEventListener("resize", onResize); + onResize(); + + const globe = createGlobe(canvasRef.current!, { + ...config, + width: width * 2, + height: width * 2, + onRender: (state) => { + if (!pointerInteracting.current) phi += 0.005; + state.phi = phi + rs.get(); + state.width = width * 2; + state.height = width * 2; + }, + }); + + setTimeout(() => (canvasRef.current!.style.opacity = "1"), 0); + return () => { + globe.destroy(); + window.removeEventListener("resize", onResize); + }; + }, [rs, config]); + + return ( +
+ { + pointerInteracting.current = e.clientX; + updatePointerInteraction(e.clientX); + }} + onPointerUp={() => updatePointerInteraction(null)} + onPointerOut={() => updatePointerInteraction(null)} + onMouseMove={(e) => updateMovement(e.clientX)} + onTouchMove={(e) => + e.touches[0] && updateMovement(e.touches[0].clientX) + } + /> +
+ ); +} diff --git a/components/magicui/marquee.tsx b/components/magicui/marquee.tsx new file mode 100644 index 000000000..fa9c129ba --- /dev/null +++ b/components/magicui/marquee.tsx @@ -0,0 +1,73 @@ +import { cn } from "@/lib/utils"; +import { ComponentPropsWithoutRef } from "react"; + +interface MarqueeProps extends ComponentPropsWithoutRef<"div"> { + /** + * Optional CSS class name to apply custom styles + */ + className?: string; + /** + * Whether to reverse the animation direction + * @default false + */ + reverse?: boolean; + /** + * Whether to pause the animation on hover + * @default false + */ + pauseOnHover?: boolean; + /** + * Content to be displayed in the marquee + */ + children: React.ReactNode; + /** + * Whether to animate vertically instead of horizontally + * @default false + */ + vertical?: boolean; + /** + * Number of times to repeat the content + * @default 4 + */ + repeat?: number; +} + +export function Marquee({ + className, + reverse = false, + pauseOnHover = false, + children, + vertical = false, + repeat = 4, + ...props +}: MarqueeProps) { + return ( +
+ {Array(repeat) + .fill(0) + .map((_, i) => ( +
+ {children} +
+ ))} +
+ ); +} diff --git a/components/testimonials.tsx b/components/testimonials.tsx index bef6dc61b..468639acb 100644 --- a/components/testimonials.tsx +++ b/components/testimonials.tsx @@ -2,6 +2,8 @@ import { useState } from "react"; import useMasonry from "@/utils/useMasonry"; +import { cn } from "@/lib/utils"; +import { Marquee } from "@/components/magicui/marquee"; import Image, { StaticImageData } from "next/image"; import TestimonialImg01 from "@/public/images/testimonial-01.jpg"; import TestimonialImg02 from "@/public/images/testimonial-02.jpg"; @@ -130,12 +132,18 @@ export default function Testimonials() {
{/* Button #1 */}