aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/Footer.tsx36
-rw-r--r--src/app/Header.tsx116
-rw-r--r--src/app/Hero.tsx57
-rw-r--r--src/app/OurServices.tsx121
-rw-r--r--src/app/Testimonials.tsx141
-rw-r--r--src/app/WhyUs.tsx116
-rw-r--r--src/app/about/page.tsx5
-rw-r--r--src/app/api/route.ts3
-rw-r--r--src/app/contact/page.tsx31
-rw-r--r--src/app/get-started/actions.ts76
-rw-r--r--src/app/get-started/page.tsx206
-rw-r--r--src/app/globals.css89
-rw-r--r--src/app/layout.tsx21
-rw-r--r--src/app/page.tsx120
-rw-r--r--src/app/people/page.tsx53
-rw-r--r--src/components/GlassHeader.tsx11
-rw-r--r--src/components/Spinner.tsx24
-rw-r--r--src/components/backgrounds/Grid.tsx35
-rw-r--r--src/components/backgrounds/Polygons.tsx31
-rw-r--r--src/components/old/bentonbox.tsx165
-rw-r--r--src/components/ui/background-ripple-effect.tsx132
-rw-r--r--src/components/ui/canvas-reveal-effect.tsx308
-rw-r--r--src/components/ui/card-spotlight.tsx74
-rw-r--r--src/components/ui/hover-border-gradient.tsx100
-rw-r--r--src/components/ui/wavy-background.tsx132
-rw-r--r--src/components/ui/wooble-card.tsx78
-rw-r--r--src/components/ui/world-map.tsx167
-rw-r--r--src/lib/utils.ts6
28 files changed, 2334 insertions, 120 deletions
diff --git a/src/app/Footer.tsx b/src/app/Footer.tsx
new file mode 100644
index 0000000..a85bc29
--- /dev/null
+++ b/src/app/Footer.tsx
@@ -0,0 +1,36 @@
+import Link from "next/link";
+
+export default function Footer() {
+ return (
+ <footer className="bg-gray-900 text-gray-200 py-8 mt-12">
+ <div className="max-w-7xl mx-auto px-4 sm:px-8 flex flex-col md:flex-row justify-between items-center">
+ <div className="mb-4 md:mb-0 w-full sm:w-auto text-center md:text-left">
+ <span className="text-xl font-bold tracking-tight">
+ Sensible Scholars
+ </span>
+ <p className="text-sm text-gray-400 mt-1">
+ Empowering your learning journey.
+ </p>
+ </div>
+ <nav className="flex space-x-6 mb-4 md:mb-0">
+ <Link href="/" className="hover:text-white transition">
+ Home
+ </Link>
+ <Link href="/about" className="hover:text-white transition">
+ About
+ </Link>
+ <Link href="/blog" className="hover:text-white transition">
+ Blog
+ </Link>
+ <Link href="/contact" className="hover:text-white transition">
+ Contact
+ </Link>
+ </nav>
+ <div className="text-sm text-gray-400">
+ &copy; {new Date().getFullYear()} Sensible Scholars. All rights
+ reserved.
+ </div>
+ </div>
+ </footer>
+ );
+}
diff --git a/src/app/Header.tsx b/src/app/Header.tsx
new file mode 100644
index 0000000..5efe628
--- /dev/null
+++ b/src/app/Header.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import { HoverBorderGradient } from "@/components/ui/hover-border-gradient";
+import { Dialog, DialogPanel } from "@headlessui/react";
+import {
+ ArrowUpCircleIcon,
+ Bars3Icon,
+ XMarkIcon,
+} from "@heroicons/react/24/outline";
+import { motion } from "motion/react";
+import Link from "next/link";
+import { useState } from "react";
+
+export default function Header() {
+ const navigation = [
+ { name: "About Us", href: "/about" },
+ { name: "People", href: "/people" },
+ { name: "Contact", href: "/contact" },
+ ];
+
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+
+ return (
+ <div className="fixed inset-x-0 top-0 z-50">
+ <nav aria-label="Global" className="p-6 lg:px-8">
+ <div className="flex items-center justify-between max-w-7xl mx-auto rounded-full p-1.5 shadow-lg shadow-gray-700/10 bg-white/30 backdrop-blur-sm">
+ <div className="flex">
+ <motion.button
+ whileHover={{ scale: 1.1 }}
+ whileTap={{ scale: 0.9 }}
+ type="button"
+ onClick={() => setMobileMenuOpen(true)}
+ className="inline-flex cursor-pointer items-center justify-center rounded-full p-2 ring-1 ring-inset ring-gray-900/10 hover:ring-gray-900/20 shadow-lg bg-white/50"
+ >
+ <span className="sr-only">Open main menu</span>
+ <Bars3Icon aria-hidden="true" className="size-6" />
+ </motion.button>
+ </div>
+ <div className="lg:flex lg:flex-1 lg:justify-end">
+ <motion.div
+ whileHover={{ scale: 1.15, y: -2, rotate: 1 }}
+ whileTap={{ scale: 0.9 }}
+ initial={{ opacity: 0, y: -20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{
+ type: "spring",
+ stiffness: 400,
+ damping: 17,
+ duration: 0.2,
+ }}
+ >
+ <Link href="/get-started">
+ <HoverBorderGradient
+ containerClassName="rounded-full"
+ as="button"
+ className=" bg-white text-black flex items-center space-x-2 cursor-pointer"
+ >
+ <span className="text-indigo-600 font-semibold">
+ Get Started
+ </span>
+ <ArrowUpCircleIcon className="size-6 my-auto rotate-45 button-gradient rounded-full inline" />
+ </HoverBorderGradient>
+ </Link>
+ </motion.div>
+ </div>
+ </div>
+ </nav>
+ <Dialog open={mobileMenuOpen} onClose={setMobileMenuOpen}>
+ <div className="fixed inset-0 z-50" />
+ <DialogPanel className="fixed inset-y-0 left-0 z-50 w-full overflow-y-auto bg-gray-900 p-6 sm:max-w-sm sm:ring-1 sm:ring-gray-100/10">
+ <div className="flex items-center justify-between">
+ <button
+ type="button"
+ onClick={() => setMobileMenuOpen(false)}
+ className="-m-2.5 rounded-md p-2.5 text-gray-200"
+ >
+ <span className="sr-only">Close menu</span>
+ <XMarkIcon aria-hidden="true" className="size-6" />
+ </button>
+ <a href="#" className="-m-1.5 p-1.5">
+ <span className="sr-only">Your Company</span>
+ <img
+ alt=""
+ src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=500"
+ className="h-8 w-auto"
+ />
+ </a>
+ </div>
+ <div className="mt-6 flow-root">
+ <div className="-my-6 divide-y divide-white/10">
+ <div className="space-y-2 py-6">
+ {navigation.map((item) => (
+ <a
+ key={item.name}
+ href={item.href}
+ className="-mx-3 block rounded-lg px-3 py-2 text-base/7 font-semibold text-white hover:bg-white/5"
+ >
+ {item.name}
+ </a>
+ ))}
+ </div>
+ <div className="py-6">
+ <Link
+ href="/get-started"
+ className="-mx-3 block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-white hover:bg-white/5"
+ >
+ Get Started <span aria-hidden="true">&rarr;</span>
+ </Link>
+ </div>
+ </div>
+ </div>
+ </DialogPanel>
+ </Dialog>
+ </div>
+ );
+}
diff --git a/src/app/Hero.tsx b/src/app/Hero.tsx
new file mode 100644
index 0000000..7398a55
--- /dev/null
+++ b/src/app/Hero.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import { BackgroundRippleEffect } from "@/components/ui/background-ripple-effect";
+import { motion } from "motion/react";
+import Link from "next/link";
+
+export default function Hero() {
+ return (
+ <div className="relative isolate px-6 pt-14 lg:px-8">
+ <BackgroundRippleEffect rows={18} cols={27} />
+ <section className="mx-auto max-w-2xl lg:max-w-4xl py-32 sm:py-48 lg:py-56">
+ <motion.div
+ className="text-center"
+ initial={{ opacity: 0, y: -20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5 }}
+ >
+ <h1 className="relative z-10 text-5xl font-semibold tracking-tight sm:text-7xl">
+ Sensible Scholars <span className="text-gradient">Tutoring</span>
+ </h1>
+ <p className="text-lg font-medium text-pretty text-gray-709 sm:text-xl/8 pt-2 sm:pt-1">
+ Beyond the Classroom: Deeper Learning, Lasting Success.
+ </p>
+ </motion.div>
+ <motion.div
+ className="mt-6 flex justify-center"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5, delay: 0.2 }}
+ >
+ <motion.div
+ className="relative z-10"
+ whileHover={{ scale: 1.25 }}
+ whileTap={{ scale: 1.1 }}
+ transition={{ type: "spring", stiffness: 400, damping: 17 }}
+ // className="relative rounded-full px-3 py-1 text-sm/6 ring-1 ring-black/20 shadow-lg bg-white/30 hover:shadow-indigo-600/20 hover:ring-indigo-600/40 hover:bg-white/50"
+ >
+ <Link
+ href="/get-started"
+ className="relative inline-flex h-8 overflow-hidden rounded-full p-[1px] focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50 shadow-lg"
+ >
+ <span className="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#E2CBFF_0%,#393BB2_50%,#E2CBFF_100%)]" />
+ <span className="inline-flex h-full mx-auto my-auto w-full cursor-pointer items-center justify-center rounded-full bg-white px-3 py-1 text-sm font-medium z-10">
+ <span className="">
+ Get started with our{" "}
+ <span className="font-semibold text-gradient underline decoration-2 decoration-slate-400/70 hover:decoration-slate-400/90">
+ free 20 minute consultation &rarr;
+ </span>
+ </span>
+ </span>
+ </Link>
+ </motion.div>
+ </motion.div>
+ </section>
+ </div>
+ );
+}
diff --git a/src/app/OurServices.tsx b/src/app/OurServices.tsx
new file mode 100644
index 0000000..4f3e5e8
--- /dev/null
+++ b/src/app/OurServices.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import { BackgroundRippleEffect } from "@/components/ui/background-ripple-effect";
+import { CardSpotlight } from "@/components/ui/card-spotlight";
+import { WobbleCard } from "@/components/ui/wooble-card";
+import { WorldMap } from "@/components/ui/world-map";
+import { motion } from "motion/react";
+
+export default function OurServices() {
+ const CheckIcon = () => {
+ return (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="currentColor"
+ className="h-4 w-4 text-blue-500 mt-1 shrink-0"
+ >
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
+ <path
+ d="M12 2c-.218 0 -.432 .002 -.642 .005l-.616 .017l-.299 .013l-.579 .034l-.553 .046c-4.785 .464 -6.732 2.411 -7.196 7.196l-.046 .553l-.034 .579c-.005 .098 -.01 .198 -.013 .299l-.017 .616l-.004 .318l-.001 .324c0 .218 .002 .432 .005 .642l.017 .616l.013 .299l.034 .579l.046 .553c.464 4.785 2.411 6.732 7.196 7.196l.553 .046l.579 .034c.098 .005 .198 .01 .299 .013l.616 .017l.642 .005l.642 -.005l.616 -.017l.299 -.013l.579 -.034l.553 -.046c4.785 -.464 6.732 -2.411 7.196 -7.196l.046 -.553l.034 -.579c.005 -.098 .01 -.198 .013 -.299l.017 -.616l.005 -.642l-.005 -.642l-.017 -.616l-.013 -.299l-.034 -.579l-.046 -.553c-.464 -4.785 -2.411 -6.732 -7.196 -7.196l-.553 -.046l-.579 -.034a28.058 28.058 0 0 0 -.299 -.013l-.616 -.017l-.318 -.004l-.324 -.001zm2.293 7.293a1 1 0 0 1 1.497 1.32l-.083 .094l-4 4a1 1 0 0 1 -1.32 .083l-.094 -.083l-2 -2a1 1 0 0 1 1.32 -1.497l.094 .083l1.293 1.292l3.293 -3.292z"
+ fill="currentColor"
+ strokeWidth="0"
+ />
+ </svg>
+ );
+ };
+ const Step = ({ title }: { title: string }) => {
+ return (
+ <li className="flex gap-2 items-start">
+ <CheckIcon />
+ <p>{title}</p>
+ </li>
+ );
+ };
+
+ return (
+ <section className="relative isolate pt-24 sm:pt-32 mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8">
+ <motion.h2
+ className="text-center text-base/7 font-semibold text-indigo-700"
+ initial={{ opacity: 0, y: -20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5 }}
+ >
+ <span className="text-gradient">Learn in a Better Way</span>
+ </motion.h2>
+ <motion.p
+ className="mx-auto mt-2 max-w-lg text-center text-4xl font-semibold tracking-tight sm:text-5xl"
+ initial={{ opacity: 0, y: -20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5, delay: 0.1 }}
+ >
+ Our Services
+ </motion.p>
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 max-w-7xl mx-auto w-full mt-12">
+ {" "}
+ <motion.div
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.14, duration: 0.5 }}
+ >
+ <div className="p-4 text-center ring-1 ring-indigo-600/40 rounded-lg shadow-lg backdrop-blur-xs bg-white/60">
+ <h3 className="text-xl font-semibold tracking-tight">
+ Tutoring at All Grade Levels
+ </h3>
+ <p className="mt-2 text-sm/6 max-lg:text-center">
+ Meet with experienced tutors who can help you excel in any
+ subject.
+ </p>
+ <ul className="mt-6 space-y-4 text-left">
+ <Step title="K-12 Tutoring" />
+ <Step title="AP Class and Exam Preparation" />
+ <Step title="Undergraduate-Level STEM Tutoring" />{" "}
+ </ul>
+ </div>
+ </motion.div>
+ <motion.div
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.1, duration: 0.5 }}
+ >
+ <div className="p-4 text-center ring-1 ring-indigo-600/40 rounded-lg shadow-lg backdrop-blur-xs bg-white/60">
+ <h3 className="text-xl font-semibold tracking-tight">
+ College Application Assistance
+ </h3>
+ <p className="mt-2 text-sm/6 max-lg:text-center">
+ Have former Ivy League students help you craft the best possible
+ college application.
+ </p>
+ <ul className="mt-6 space-y-4 text-left">
+ <Step title="ACT/SAT Test Prep" />
+ <Step title="College Essay Coaching" />
+ <Step title="Interview Preparation" />
+ </ul>
+ </div>
+ </motion.div>
+ <motion.div
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.1, duration: 0.5 }}
+ >
+ <div className="p-4 text-center ring-1 ring-indigo-600/40 rounded-lg shadow-lg backdrop-blur-xs bg-white/60">
+ <h3 className=" text-xl font-semibold tracking-tight ">
+ Group Review Sessions
+ </h3>
+ <p className="mt-2 text-sm/6 max-lg:text-center">
+ Join group sessions at no cost to prepare for nationwide
+ standardized exams (e.g., SAT, ACT, APs, AMC).
+ </p>
+ <ul className="mt-6 space-y-4 text-left">
+ <Step title="Organization-Wide Review Sessions" />
+ <Step title="Specific Test Taking Strategies" />
+ <Step title="Previous Recordings Available" />
+ </ul>
+ </div>
+ </motion.div>
+ </div>
+ </section>
+ );
+}
diff --git a/src/app/Testimonials.tsx b/src/app/Testimonials.tsx
new file mode 100644
index 0000000..774f38f
--- /dev/null
+++ b/src/app/Testimonials.tsx
@@ -0,0 +1,141 @@
+import { motion } from "motion/react";
+
+const posts = [
+ {
+ id: 1,
+ title: "Boost your conversion rate",
+ href: "#",
+ description:
+ "Illo sint volupta. Error voluptates culpa eligendi. Hic vel totam vitae illo. Non aliquid explicabo necessitatibus unde. Sed exercitationem placeat consectetur nulla deserunt vel. Iusto corrupti dicta.",
+ date: "Mar 16, 2020",
+ datetime: "2020-03-16",
+ category: { title: "AP Calculus", href: "#" },
+ author: {
+ name: "Michael Foster",
+ role: "Co-Founder / CTO",
+ href: "#",
+ imageUrl:
+ "https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
+ },
+ },
+ {
+ id: 2,
+ title: "How to use search engine optimization to drive sales",
+ href: "#",
+ description:
+ "Optio cum necessitatibus dolor voluptatum provident commodi et. Qui aperiam fugiat nemo cumque.",
+ date: "Mar 10, 2020",
+ datetime: "2020-03-10",
+ category: { title: "ACT Prep", href: "#" },
+ author: {
+ name: "Lindsay Walton",
+ role: "Front-end Developer",
+ href: "#",
+ imageUrl:
+ "https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
+ },
+ },
+ {
+ id: 3,
+ title: "Improve your customer experience",
+ href: "#",
+ description:
+ "Cupiditate maiores ullam eveniet adipisci in doloribus nulla minus. Voluptas iusto libero adipisci rem et corporis. Nostrud sint anim sunt aliqua. Nulla eu labore irure incididunt velit cillum quis magna dolore.",
+ date: "Feb 12, 2020",
+ datetime: "2020-02-12",
+ category: { title: "HS Algebra", href: "#" },
+ author: {
+ name: "Tom Cook",
+ role: "Director of Product",
+ href: "#",
+ imageUrl:
+ "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
+ },
+ },
+];
+
+export default function Testimonials() {
+ return (
+ <>
+ <div
+ aria-hidden="true"
+ className="absolute inset-x-0 -z-10 transform-gpu overflow-hidden blur-3xl -translate-y-60 sm:-translate-y-80"
+ >
+ <div
+ style={{
+ clipPath:
+ "polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
+ }}
+ className="relative rotate-12 aspect-1000/900 sm:aspect-1155/700 w-144.5-translate-x-1/2 bg-linear-to-r from-indigo-600 via-red-200 to-red-500 opacity-30 sm:w-288.75 max-w-7xl mx-auto"
+ />
+ </div>
+
+ <div className="pt-24 sm:pt-32">
+ <div className="mx-auto max-w-7xl px-6 lg:px-8">
+ <motion.div
+ className="mx-auto max-w-2xl lg:mx-0"
+ initial={{ opacity: 0, y: -20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.25, delay: 0.1 }}
+ >
+ <h2 className="text-4xl font-semibold tracking-tight text-pretty text-gray-900 sm:text-5xl">
+ From the Students
+ </h2>
+ <p className="mt-2 text-lg/8 text-gray-600">
+ Listen to what our students have to say about their experiences.
+ </p>
+ </motion.div>
+ <motion.div
+ className="mx-auto mt-10 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-16 border-t border-indigo-200 pt-10 sm:mt-16 sm:pt-16 lg:mx-0 lg:max-w-none lg:grid-cols-3"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5, delay: 0.2 }}
+ >
+ {posts.map((post) => (
+ <article
+ key={post.id}
+ className="flex max-w-xl flex-col items-start justify-between"
+ >
+ <div className="flex items-center gap-x-4 text-xs">
+ <time dateTime={post.datetime} className="text-gray-500">
+ {post.date}
+ </time>
+ <p className="relative z-10 rounded-full bg-indigo-100 px-3 py-1.5 font-medium text-gray-600 hover:bg-indigo-200">
+ {post.category.title}
+ </p>
+ </div>
+ <div className="group relative grow">
+ <h3 className="mt-3 text-lg/6 font-semibold text-gray-900 group-hover:text-gray-600">
+ <a href={post.href}>
+ <span className="absolute inset-0" />
+ {post.title}
+ </a>
+ </h3>
+ <p className="mt-5 line-clamp-3 text-sm/6 text-gray-600">
+ {post.description}
+ </p>
+ </div>
+ <div className="relative mt-8 flex items-center gap-x-4 justify-self-end">
+ <img
+ alt=""
+ src={post.author.imageUrl}
+ className="size-10 rounded-full bg-gray-50"
+ />
+ <div className="text-sm/6">
+ <p className="font-semibold text-gray-900">
+ <a href={post.author.href}>
+ <span className="absolute inset-0" />
+ {post.author.name}
+ </a>
+ </p>
+ <p className="text-gray-600">{post.author.role}</p>
+ </div>
+ </div>
+ </article>
+ ))}
+ </motion.div>
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/src/app/WhyUs.tsx b/src/app/WhyUs.tsx
new file mode 100644
index 0000000..bb92485
--- /dev/null
+++ b/src/app/WhyUs.tsx
@@ -0,0 +1,116 @@
+import { BackgroundRippleEffect } from "@/components/ui/background-ripple-effect";
+import {
+ AcademicCapIcon,
+ BanknotesIcon,
+ CalendarDateRangeIcon,
+ CloudArrowUpIcon,
+} from "@heroicons/react/24/solid";
+import { motion } from "motion/react";
+import Image from "next/image";
+
+const features = [
+ {
+ name: "High Quality Tutors",
+ description:
+ "We only hire tutors who have been high-performing students with previous teaching experience. Most of our tutors are currently pursuing PhDs or Masters in their respective fields.",
+ icon: AcademicCapIcon,
+ },
+ {
+ name: "Negotiable Pricing",
+ description:
+ "We determine pricing on a case-by-case basis during our free consultation to best accommodate the needs of both students and tutors.",
+ icon: BanknotesIcon,
+ },
+ {
+ name: "Flexible Scheduling",
+ description:
+ "We offer both in-person and online session options and can accommodate different time zones.",
+ icon: CalendarDateRangeIcon,
+ },
+ {
+ name: "Modern, Accessible Platform",
+ description:
+ "Our platform uses the latest web technologies to ensure a seamless connection between students and tutors on all devices.",
+ icon: CloudArrowUpIcon,
+ },
+];
+
+export default function WhyUs() {
+ return (
+ <section className="overflow-hidden pt-24 sm:pt-32">
+ <div className="mx-auto max-w-7xl px-6 lg:px-8">
+ <div className="mx-auto grid items-center max-w-2xl grid-cols-1 gap-x-8 gap-y-16 sm:gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-2">
+ <div className="lg:mt-4 lg:pr-8">
+ <div className="lg:max-w-lg">
+ <motion.h2
+ className="mt-2 text-4xl font-semibold tracking-tight text-pretty text-gray-900 sm:text-5xl"
+ initial={{ opacity: 0, y: -20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5, delay: 0.1 }}
+ >
+ Why Choose{" "}
+ <span className="text-gradient">Sensible Scholars?</span>
+ </motion.h2>
+ <motion.p
+ className="pt-4 text-base/7 text-indigo-600/80"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.25, delay: 0.1 }}
+ >
+ We make learning the best it can possibly be.
+ </motion.p>
+ <dl className="mt-6 max-w-xl space-y-8 text-base/7 text-gray-600 lg:max-w-none">
+ {features.map((feature) => (
+ <motion.div
+ key={feature.name}
+ className="relative pl-9"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{
+ duration: 0.5,
+ delay: 0.1 + features.indexOf(feature) * 0.05,
+ }}
+ >
+ <dt className="inline font-semibold text-gray-900">
+ <feature.icon
+ aria-hidden="true"
+ className="absolute top-1 left-1 size-5 text-indigo-600"
+ />
+ {feature.name}
+ </dt>{" "}
+ <dd className="inline">{feature.description}</dd>
+ </motion.div>
+ ))}
+ </dl>
+ </div>
+ </div>
+ <motion.div
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5, delay: 0.1 }}
+ >
+ <Image
+ alt="Product screenshot"
+ src="/portal-page.png"
+ width={2432}
+ height={1442}
+ className="w-3xl max-w-none rounded-xl shadow-xl ring-1 ring-gray-400/10 sm:w-228 md:-ml-4 lg:-ml-0"
+ />
+ </motion.div>
+ </div>
+ </div>
+ <div
+ aria-hidden="true"
+ className="absolute inset-x-0 -z-10 transform-gpu overflow-hidden blur-3xl sm:-translate-y-30 sm:-translate-x-80"
+ >
+ <div
+ style={{
+ clipPath:
+ "polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
+ }}
+ className="relative rotate-180 aspect-1000/900 sm:aspect-1155/700 w-144.5-translate-x-1/2 bg-linear-to-r from-indigo-600 via-red-200 to-red-500 opacity-30 sm:w-288.75 max-w-7xl mx-auto"
+ />
+ </div>
+ </section>
+ );
+}
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
new file mode 100644
index 0000000..26cdb81
--- /dev/null
+++ b/src/app/about/page.tsx
@@ -0,0 +1,5 @@
+'use client';
+
+export default function Home() {
+ return (<div>About Us Page - Coming Soon!</div>);
+} \ No newline at end of file
diff --git a/src/app/api/route.ts b/src/app/api/route.ts
new file mode 100644
index 0000000..a413bcd
--- /dev/null
+++ b/src/app/api/route.ts
@@ -0,0 +1,3 @@
+export async function GET(request: Request) {
+ return new Response("Hello, this is the API route!");
+} \ No newline at end of file
diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx
new file mode 100644
index 0000000..550498d
--- /dev/null
+++ b/src/app/contact/page.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { motion } from "motion/react";
+
+export default function ContactPage() {
+ const box = {
+ width: 100,
+ height: 100,
+ backgroundColor: "#ff0000",
+ marginTop: 20,
+ };
+
+ return (
+ <motion.div
+ className="flex items-center justify-center min-h-screen bg-gray-100"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ exit={{ opacity: 0 }}
+ transition={{ duration: 10 }}
+ >
+ <h1 className="text-4xl font-bold text-gray-800">
+ Contact Us Page - Coming Soon!
+ </h1>
+ <motion.div
+ style={box}
+ animate={{ rotate: 360 }}
+ transition={{ duration: 10 }}
+ />
+ </motion.div>
+ );
+}
diff --git a/src/app/get-started/actions.ts b/src/app/get-started/actions.ts
new file mode 100644
index 0000000..1e5f439
--- /dev/null
+++ b/src/app/get-started/actions.ts
@@ -0,0 +1,76 @@
+"use server";
+
+import { createTransport } from "nodemailer";
+
+const noSend = true; // for testing
+
+if (noSend) {
+ console.log("Emails are disabled - no emails will be sent");
+}
+
+export default async function logFormData(
+ prevState: { message: string; error: boolean },
+ formData: FormData
+) {
+ // wait 15 sec
+ await new Promise<void>((resolve) => setTimeout(resolve, 2000));
+
+ // Create a test account or replace with real credentials.
+ const transporter = createTransport({
+ host: "mail.mfoi.dev",
+ port: 465,
+ secure: true, // true for 465, false for other ports
+ auth: {
+ user: "test@mfoi.dev",
+ pass: "Fakrum-5hapzo-fivkeb", // TODO: put in env variable
+ },
+ });
+
+ const email_content = `
+<h2>New Consulting Request</h2>
+<p><strong>First Name: </strong>${formData.get("firstname")}</p>
+<p><strong>Last Name: </strong>${formData.get("lastname")}</p>
+<p><strong>Email: </strong>${formData.get("email")}</p>${
+ formData.get("phonenumber") &&
+ `<p><strong>Phone Number: </strong>${formData.get("phonenumber")}</p>`
+ }
+<p><strong>Message: </strong><br />${formData.get("message")}</p>
+<hr />
+<p><strong>Submitted at:</strong> ${new Date().toLocaleString()}</p>
+ `;
+
+ const full_name = `${formData.get("firstname")} ${formData.get("lastname")}`;
+
+ if (noSend) {
+ console.log("Email sending is disabled. Email content:");
+ console.log(email_content);
+ return {
+ message:
+ "Successfully submitted your consultation request - email sending is disabled for testing purposes.",
+ error: false,
+ };
+ }
+
+ try {
+ const info = await transporter.sendMail({
+ from: '"sensiblescholars.com" <test@mfoi.dev>',
+ to: "test@mfoi.dev",
+ subject: `New Consultation Request from ${full_name}!`,
+ html: email_content,
+ });
+ console.log("Message sent:", info.messageId);
+ } catch (error) {
+ console.error("Error sending email:", error); // Handle errors
+ return {
+ message:
+ "Failed to send email. This has been reported. Please try again later, and sorry for any inconvenience.",
+ error: true,
+ };
+ }
+
+ return {
+ message:
+ "Successfully submitted your consultation request - expect to hear back soon via email!",
+ error: false,
+ };
+}
diff --git a/src/app/get-started/page.tsx b/src/app/get-started/page.tsx
new file mode 100644
index 0000000..75f04b0
--- /dev/null
+++ b/src/app/get-started/page.tsx
@@ -0,0 +1,206 @@
+"use client";
+
+import Spinner from "@/components/Spinner";
+import { motion } from "motion/react";
+import Form from "next/form";
+import { useActionState } from "react";
+import logFormData from "./actions";
+
+// Debug states for testing UI without sending email
+const debugSuccess = {
+ message:
+ "Sucessfully submitted your consultation request - expect to hear back soon via email!",
+ error: false,
+};
+
+const debugError = {
+ message:
+ "Failed to send email. This has been reported. Please try again later, and sorry for any inconvenience.",
+ error: true,
+};
+
+const initialState = {
+ message: "",
+ error: false,
+};
+
+export default function Home() {
+ // TODO: look up webdevsimplified video on forms in nextjs
+ const [state, formAction, pending] = useActionState(
+ logFormData,
+ initialState
+ );
+
+ return (
+ <div className="container mx-auto isolate px-4 py-6 sm:py-12 xl:py-16">
+ <div
+ aria-hidden="true"
+ className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80"
+ >
+ <div
+ style={{
+ clipPath:
+ "polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
+ }}
+ className="relative left-1/2 -z-10 aspect-1155/678 w-144.5 max-w-none -translate-x-1/2 rotate-30 bg-linear-to-br from-indigo-600 to-red-300 opacity-50 sm:left-[calc(50%-40rem)] sm:w-288.75"
+ />
+ </div>
+ <div className="mx-auto max-w-3xl outline outline-gray-700/20 rounded-xl p-6 shadow-lg shadow-gray-700/10 backdrop-blur-lg sm:p-10 bg-white/30">
+ <motion.div
+ className="mx-auto max-w-2xl text-center"
+ initial={{ opacity: 0, y: -20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5 }}
+ >
+ <h2 className="text-4xl font-semibold tracking-tight text-balance text-gray-900 sm:text-5xl">
+ Schedule a <span className="text-gradient">Free Consultation</span>
+ </h2>
+ <p className="mt-6 text-lg text-gray-600">
+ We want to have a 20 minute conversation with you to discuss your
+ needs and talk pricing - completely free and with no obligation.
+ </p>
+ </motion.div>
+
+ <Form action={formAction} className="mx-auto mt-8">
+ <motion.div
+ className="grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2"
+ initial={{ opacity: 0, y: 20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5, delay: 0.1 }}
+ >
+ <div>
+ <label
+ htmlFor="firstname"
+ className="block text-sm/6 font-semibold text-gray-900"
+ >
+ First name
+ </label>
+ <div className="mt-2.5">
+ <input
+ id="firstname"
+ name="firstname"
+ type="text"
+ auto-complete="given-name"
+ className="block w-full rounded-md bg-white px-3.5 py-2 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600"
+ placeholder="First Name"
+ required
+ />
+ </div>
+ </div>
+ <div>
+ <label
+ htmlFor="lastname"
+ className="block text-sm/6 font-semibold text-gray-900"
+ >
+ Last Name
+ </label>
+ <div className="mt-2.5">
+ <input
+ id="lastname"
+ name="lastname"
+ type="text"
+ auto-complete="familyname"
+ className="block w-full rounded-md bg-white px-3.5 py-2 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600"
+ placeholder="Last Name"
+ required
+ />
+ </div>
+ </div>
+ <div>
+ <label
+ htmlFor="email"
+ className="block text-sm/6 font-semibold text-gray-900"
+ >
+ Email
+ </label>
+ <div className="mt-2.5">
+ <input
+ id="email"
+ name="email"
+ type="email"
+ auto-complete="email"
+ className="block w-full rounded-md bg-white px-3.5 py-2 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600"
+ placeholder="email@domain.com"
+ required
+ />
+ </div>
+ </div>
+ <div>
+ <label
+ htmlFor="phonenumber"
+ className="block text-sm/6 font-semibold text-gray-900"
+ >
+ Phone Number (optional)
+ </label>
+ <div className="mt-2.5">
+ <input
+ id="phonenumber"
+ name="phonenumber"
+ type="tel"
+ auto-complete="tel"
+ className="block w-full rounded-md bg-white px-3.5 py-2 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600"
+ placeholder="(216) 555-1234"
+ />
+ </div>
+ </div>
+ <div className="sm:col-span-2">
+ <label
+ htmlFor="message"
+ className="block text-sm/6 font-semibold text-gray-900"
+ >
+ Message
+ </label>
+ <div className="mt-2.5">
+ <textarea
+ id="message"
+ name="message"
+ rows={4}
+ className="block w-full rounded-md bg-white px-3.5 py-2 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600"
+ placeholder="Please include a brief message about your (or your child's) needs - including subjects, grade levels, and any specific requests."
+ defaultValue={""}
+ ></textarea>
+ </div>
+ </div>
+ <div className="sm:col-span-2">
+ <motion.button
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.9 }}
+ transition={{ type: "spring", stiffness: 400, damping: 17 }}
+ disabled={pending}
+ type="submit"
+ className="block w-full
+ rounded-md button-gradient
+ px-3.5 py-2.5 text-center text-md font-semibold text-white shadow-xs
+ hover:shadow-sm
+ disabled:bg-gray-600
+ hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ {pending ? (
+ <div className="flex items-center justify-center">
+ <Spinner /> <span>Submitting...</span>
+ </div>
+ ) : (
+ "Submit"
+ )}
+ </motion.button>
+ </div>
+ <div className="sm:col-span-2" hidden={!state.message}>
+ <div className="text-center block w-full">
+ {state.error ? (
+ <p className="font-semibold bg-red-50 rounded-md text-md outline-1 -outline-offset-1 outline-red-700 px-3 py-2 text-red-800">
+ <span className="font-bold">Error(!): </span>
+ {state.message}
+ </p>
+ ) : (
+ <p className="font-semibold bg-green-50 rounded-md text-md outline-1 -outline-offset-1 outline-green-700 px-3 py-2 text-green-800">
+ {state.message}
+ </p>
+ )}
+ </div>
+ </div>
+ </motion.div>
+ </Form>
+ </div>
+ </div>
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index a2dc41e..b8ffc86 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,26 +1,101 @@
@import "tailwindcss";
:root {
- --background: #ffffff;
+ --background: var(--color-white);
--foreground: #171717;
+
+ --background-gradient: radial-gradient(
+ circle at 30% -30%,
+ var(--color-red-200),
+ var(--color-white),
+ var(--color-red-400)
+ );
+
+ /* --gradient-red: var(--color-red-200); */
+
+ --accent-color: var(--color-amber-900);
}
+/* @media (prefers-color-scheme: dark) {
+ :root {
+ --background: #0a0a0a;
+ --foreground: #ededed;
+
+ --background-gradient: radial-gradient(
+ circle at 30% -30%,
+ var(--color-red-900),
+ var(--color-gray-700),
+ var(--color-red-800)
+ );
+ }
+} */
+
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
+ --color-accent: var(--accent-color);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
- }
+html {
+ scroll-behavior: smooth;
}
body {
background: var(--background);
color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+
+ /* background: radial-gradient(
+ circle at bottom left,
+ transparent 25%,
+ #f61a1a 25.5%,
+ #f61a1a 36%,
+ transparent 37%,
+ transparent 100%
+ ),
+ radial-gradient(
+ circle at top right,
+ transparent 34%,
+ #f61a1a 34.5%,
+ #f61a1a 45.5%,
+ transparent 46%,
+ transparent 100%
+ );
+ background-size: 3em 3em;
+ background-color: #f1f1f1;
+ opacity: 0.85;
+
+ /* Add texture to background
+ background-image: url("/public/white-carbon.png");
+ background-repeat: repeat;
+ background-size: contain; */
+}
+
+@layer utilities {
+ .text-gradient {
+ @apply bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent;
+ }
+ .button-gradient {
+ @apply bg-gradient-to-r from-blue-600 to-purple-600 text-white;
+ }
+}
+
+@theme inline {
+ --animate-cell-ripple: cell-ripple var(--duration, 200ms) ease-out none 1
+ var(--delay, 0ms);
+
+ @keyframes cell-ripple {
+ 0% {
+ opacity: 0.4;
+ }
+ 50% {
+ opacity: 0.8;
+ }
+ 100% {
+ opacity: 0.4;
+ }
+ }
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f7fa87e..7f80895 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,20 +1,9 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
-});
-
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Sensible Scholars",
+ description: "Beyond the Classroom: Deeper Learning, Lasting Success.",
};
export default function RootLayout({
@@ -24,11 +13,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
- <body
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
- >
- {children}
- </body>
+ <body className={`antialiased`}>{children}</body>
</html>
);
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index a932894..0c59aab 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,103 +1,33 @@
-import Image from "next/image";
+"use client";
+
+import Grid from "@/components/backgrounds/Grid";
+import Polygons from "@/components/backgrounds/Polygons";
+import Footer from "./Footer";
+import Header from "./Header";
+import Hero from "./Hero";
+import OurServices from "./OurServices";
+import Testimonials from "./Testimonials";
+import WhyUs from "./WhyUs";
export default function Home() {
return (
- <div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
- <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
- <Image
- className="dark:invert"
- src="/next.svg"
- alt="Next.js logo"
- width={180}
- height={38}
- priority
- />
- <ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
- <li className="mb-2 tracking-[-.01em]">
- Get started by editing{" "}
- <code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
- src/app/page.tsx
- </code>
- .
- </li>
- <li className="tracking-[-.01em]">
- Save and see your changes instantly.
- </li>
- </ol>
-
- <div className="flex gap-4 items-center flex-col sm:flex-row">
- <a
- className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Image
- className="dark:invert"
- src="/vercel.svg"
- alt="Vercel logomark"
- width={20}
- height={20}
- />
- Deploy now
- </a>
- <a
- className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
- href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- Read our docs
- </a>
- </div>
+ <div className="relative min-h-screen overflow-x-hidden">
+ <header>
+ <Header />
+ </header>
+ <main>
+ <Hero />
+ <WhyUs />
+ <OurServices />
+ <Testimonials />
</main>
- <footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
- <a
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
- href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Image
- aria-hidden
- src="/file.svg"
- alt="File icon"
- width={16}
- height={16}
- />
- Learn
- </a>
- <a
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
- href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Image
- aria-hidden
- src="/window.svg"
- alt="Window icon"
- width={16}
- height={16}
- />
- Examples
- </a>
- <a
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
- href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- <Image
- aria-hidden
- src="/globe.svg"
- alt="Globe icon"
- width={16}
- height={16}
- />
- Go to nextjs.org →
- </a>
+
+ <footer>
+ <Footer />
</footer>
+
+ {/* <Grid /> */}
+ <Polygons />
</div>
);
}
diff --git a/src/app/people/page.tsx b/src/app/people/page.tsx
new file mode 100644
index 0000000..3df1ac4
--- /dev/null
+++ b/src/app/people/page.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+export default function Page() {
+ const peopleInfo = [
+ {
+ name: "Joseph",
+ role: "Founder",
+ bio: "Bio - Quia illum aut in beatae. Possimus dolores aliquid accusantium aut in ut non assumenda. Enim iusto molestias aut deleniti eos aliquid magnam molestiae. At et non possimus ab. Magni labore molestiae nulla qui.",
+ imageUrl: "/globe.svg",
+ },
+ {
+ name: "Michael",
+ role: "Co-Founder",
+ bio: "Bio - Quia illum aut in beatae. Possimus dolores aliquid accusantium aut in ut non assumenda. Enim iusto molestias aut deleniti eos aliquid magnam molestiae. At et non possimus ab. Magni labore molestiae nulla qui.",
+ imageUrl: "/globe.svg",
+ },
+ ];
+
+ const peopleList = peopleInfo.map((person) => (
+ <div key={person.name} className="flex items-start gap-4 text-left">
+ <img
+ className="w-16 rounded-full object-cover"
+ src={person.imageUrl}
+ alt={`Headshot photo of ${person.name}`}
+ />
+ <div>
+ <h3 className="text-lg font-semibold">{person.name}</h3>
+ <p className="text-sm text-gray-500">{person.role}</p>
+ <p className="mt-2 text-sm/6 text-gray-700">{person.bio}</p>
+ </div>
+ </div>
+ ));
+
+ return (
+ <div className="container mx-auto h-lvh px-6 py-10">
+ <div className="text-center pb-8">
+ <h1 className="text-2xl font-semibold">
+ Meet{" "}
+ <span className="bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
+ Our Team
+ </span>
+ </h1>
+ <p className="max-w-2xl mx-auto my-6 text-gray-700 dark:text-gray-500">
+ We are a passionate group of educators dedicated to making learning
+ accessible and engaging for everyone.
+ </p>
+ </div>
+ <div className="grid grid-cols-1 gap-8 mt-8 sm:grid-cols-2 xl:mt-16">
+ {peopleList}
+ </div>
+ </div>
+ );
+}
diff --git a/src/components/GlassHeader.tsx b/src/components/GlassHeader.tsx
new file mode 100644
index 0000000..fd5a5aa
--- /dev/null
+++ b/src/components/GlassHeader.tsx
@@ -0,0 +1,11 @@
+export default function GlassHeader() {
+ return (
+ <div className="w-full bg-white/30 backdrop-blur-lg shadow-md shadow-gray-700/10">
+ <div className="container mx-auto px-6 py-4">
+ <h1 className="text-2xl font-semibold text-gray-900">
+ Sensible Scholars
+ </h1>
+ </div>
+ </div>
+ );
+}
diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx
new file mode 100644
index 0000000..96b6b6e
--- /dev/null
+++ b/src/components/Spinner.tsx
@@ -0,0 +1,24 @@
+export default function Spinner() {
+ return (
+ <svg
+ className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ >
+ <circle
+ className="opacity-25"
+ cx="12"
+ cy="12"
+ r="10"
+ stroke="currentColor"
+ strokeWidth="4"
+ ></circle>
+ <path
+ className="opacity-75"
+ fill="currentColor"
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+ ></path>
+ </svg>
+ );
+}
diff --git a/src/components/backgrounds/Grid.tsx b/src/components/backgrounds/Grid.tsx
new file mode 100644
index 0000000..7198933
--- /dev/null
+++ b/src/components/backgrounds/Grid.tsx
@@ -0,0 +1,35 @@
+export default function Grid() {
+ return (
+ <div className="absolute inset-0 -z-10 overflow-hidden">
+ <svg
+ aria-hidden="true"
+ className="absolute top-0 left-[max(50%,25rem)] h-256 w-512 -translate-x-1/2 mask-[radial-gradient(64rem_64rem_at_top,white,transparent)] stroke-gray-200"
+ >
+ <defs>
+ <pattern
+ x="50%"
+ y={-1}
+ id="e813992c-7d03-4cc4-a2bd-151760b470a0"
+ width={200}
+ height={200}
+ patternUnits="userSpaceOnUse"
+ >
+ <path d="M100 200V.5M.5 .5H200" fill="none" />
+ </pattern>
+ </defs>
+ <svg x="50%" y={-1} className="overflow-visible fill-red-50">
+ <path
+ d="M-100.5 0h201v201h-201Z M699.5 0h201v201h-201Z M499.5 400h201v201h-201Z M-300.5 600h201v201h-201Z"
+ strokeWidth={0}
+ />
+ </svg>
+ <rect
+ fill="url(#e813992c-7d03-4cc4-a2bd-151760b470a0)"
+ width="100%"
+ height="100%"
+ strokeWidth={0}
+ />
+ </svg>
+ </div>
+ );
+}
diff --git a/src/components/backgrounds/Polygons.tsx b/src/components/backgrounds/Polygons.tsx
new file mode 100644
index 0000000..c6f8ab6
--- /dev/null
+++ b/src/components/backgrounds/Polygons.tsx
@@ -0,0 +1,31 @@
+export default function Polygons() {
+ return (
+ <>
+ <div
+ aria-hidden="true"
+ className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-100"
+ >
+ <div
+ style={{
+ clipPath:
+ "polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
+ }}
+ className="relative left-[calc(50%-11rem)] aspect-1155/800 w-144.5 -translate-x-1/2 rotate-30 bg-linear-to-tr from-[#ff80b5] to-[#ff0000] opacity-50 sm:left-[calc(50%-30rem)] sm:w-288.75"
+ />
+ </div>
+
+ <div
+ aria-hidden="true"
+ className="absolute inset-x-0 top-[calc(30rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(20rem)]"
+ >
+ <div
+ style={{
+ clipPath:
+ "polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
+ }}
+ className="relative left-[calc(50%-15rem)] aspect-1155/900 sm:aspect-1155/700 w-144.5-translate-x-1/2 bg-linear-to-tr from-red-300 to-indigo-400 opacity-50 sm:left-[calc(50%-40rem)] sm:w-288.75"
+ />
+ </div>
+ </>
+ );
+}
diff --git a/src/components/old/bentonbox.tsx b/src/components/old/bentonbox.tsx
new file mode 100644
index 0000000..5a3afca
--- /dev/null
+++ b/src/components/old/bentonbox.tsx
@@ -0,0 +1,165 @@
+"use client";
+
+import { motion } from "motion/react";
+
+export default function OurServices() {
+ return (
+ <div className="pt-24 sm:pt-32">
+ <section className="mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8">
+ <motion.h2
+ className="text-center text-base/7 font-semibold text-indigo-700"
+ initial={{ opacity: 0, y: -20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5 }}
+ >
+ Learn In a Better Way
+ </motion.h2>
+ <motion.p
+ className="mx-auto mt-2 max-w-lg text-center text-4xl font-semibold tracking-tight sm:text-5xl"
+ initial={{ opacity: 0, y: -20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.5, delay: 0.1 }}
+ >
+ Our Services
+ </motion.p>
+ <div className="mt-10 grid gap-4 sm:mt-16 lg:grid-cols-3 lg:grid-rows-2">
+ <motion.div
+ className="relative lg:row-span-2"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.2, duration: 0.2 }}
+ >
+ <div className="absolute inset-px rounded-lg bg-white/30 outline outline-black/5 lg:rounded-l-4xl" />
+ <div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] lg:rounded-l-[calc(2rem+1px)]">
+ <div className="px-8 pt-8 pb-3 sm:px-10 sm:pt-10 sm:pb-0">
+ <p className="mt-2 text-lg font-medium tracking-tight max-lg:text-center">
+ Tutoring at All Grade Levels
+ </p>
+ <p className="mt-2 text-sm/6 text-gray-600 max-lg:text-center">
+ Our tutors cover a wide range of subjects and grade levels,
+ </p>
+ </div>
+ <div className="@container relative min-h-120 w-full grow max-lg:mx-auto max-lg:max-w-sm">
+ <div className="absolute inset-x-10 top-10 bottom-0 overflow-hidden rounded-t-[12cqw] border-x-[3cqw] border-t-[3cqw] border-gray-700 bg-gray-900 outline outline-white/20">
+ <img
+ alt=""
+ src="https://tailwindcss.com/plus-assets/img/component-images/bento-03-mobile-friendly.png"
+ className="size-full object-cover object-top"
+ />
+ </div>
+ </div>
+ </div>
+ <div className="pointer-events-none absolute inset-px rounded-lg shadow-sm outline outline-white/15 lg:rounded-l-4xl" />
+ </motion.div>
+ <motion.div
+ className="relative max-lg:row-start-1"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{
+ opacity: 1,
+ y: 0,
+ transition: { delay: 0.2, duration: 0.2 },
+ }}
+ >
+ <div className="absolute inset-px rounded-lg bg-white/30 outline outline-black/5 max-lg:rounded-t-4xl" />
+ <div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] max-lg:rounded-t-[calc(2rem+1px)]">
+ <div className="px-8 pt-8 sm:px-10 sm:pt-10">
+ <p className="mt-2 text-lg font-medium tracking-tight max-lg:text-center">
+ SAT / ACT Prep
+ </p>
+ <p className="mt-2 text-sm/6 text-gray-600 max-lg:text-center">
+ We only hire tutors who have been high-performing students
+ with previous teaching experience. Most of our tutors are
+ currently pursuing PhDs or Masters in their respective fields.
+ </p>
+ </div>
+ <div className="flex flex-1 items-center justify-center px-8 max-lg:pt-10 max-lg:pb-12 sm:px-10 lg:pb-2">
+ <img
+ alt=""
+ src="https://tailwindcss.com/plus-assets/img/component-images/dark-bento-03-performance.png"
+ className="w-full max-lg:max-w-xs"
+ />
+ </div>
+ </div>
+ <div className="pointer-events-none absolute inset-px rounded-lg shadow-sm outline outline-white/15 max-lg:rounded-t-4xl" />
+ </motion.div>
+ <motion.div
+ className="relative max-lg:row-start-3 lg:col-start-2 lg:row-start-2"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{
+ opacity: 1,
+ y: 0,
+ transition: { delay: 0.2, duration: 0.2 },
+ }}
+ >
+ <div className="absolute inset-px rounded-lg bg-white/30 outline outline-black/5" />
+ <div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)]">
+ <div className="px-8 pt-8 sm:px-10 sm:pt-10">
+ <p className="mt-2 text-lg font-medium tracking-tight max-lg:text-center">
+ Secure Technological Integration
+ </p>
+ <p className="mt-2 text-sm/6 text-gray-700 max-lg:text-center">
+ While applying technology to make learning the best it can be,
+ we prioritize security and privacy to protect our users' data
+ and ensure a safe learning environment.
+ </p>
+ </div>
+ <div className="@container flex flex-1 items-center max-lg:py-6 lg:pb-2">
+ <img
+ alt=""
+ src="https://tailwindcss.com/plus-assets/img/component-images/dark-bento-03-security.png"
+ className="h-[min(152px,40cqw)] object-cover"
+ />
+ </div>
+ </div>
+ <div className="pointer-events-none absolute inset-px rounded-lg shadow-sm outline outline-white/15" />
+ </motion.div>
+ <motion.div
+ className="relative lg:row-span-2"
+ initial={{ opacity: 0, y: 20 }}
+ whileInView={{
+ opacity: 1,
+ y: 0,
+ transition: { delay: 0.2, duration: 0.2 },
+ }}
+ >
+ <div className="absolute inset-px rounded-lg bg-white/30 outline outline-black/5 max-lg:rounded-b-4xl lg:rounded-r-4xl" />
+ <div className="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] max-lg:rounded-b-[calc(2rem+1px)] lg:rounded-r-[calc(2rem+1px)]">
+ <div className="px-8 pt-8 pb-3 sm:px-10 sm:pt-10 sm:pb-0">
+ <p className="mt-2 text-lg font-medium tracking-tight max-lg:text-center">
+ Competitive Math and Coding Counseling
+ </p>
+ <p className="mt-2 text-sm/6 text-gray-600 max-lg:text-center">
+ Our handcrafted AI learns from your sessions and provides
+ personalized summaries, practice problems, and study plans to
+ help you succeed.
+ </p>
+ </div>
+ <div className="relative min-h-120 w-full grow">
+ <div className="absolute top-10 right-0 bottom-0 left-10 overflow-hidden rounded-tl-xl bg-gray-900/60 outline outline-white/10">
+ <div className="flex bg-gray-900 outline outline-white/5">
+ <div className="-mb-px flex text-sm/6 font-medium text-gray-400">
+ <div className="border-r border-b border-r-white/10 border-b-white/20 bg-white/5 px-4 py-2 text-white">
+ Week1_Session_Precalc.md
+ </div>
+ <div className="border-r border-gray-600/10 px-4 py-2">
+ Week2_Session_Precalc.md
+ </div>
+ </div>
+ </div>
+ <div className="px-6 pt-6 pb-14">
+ <p className="text-sm/6 text-gray-400">AI Summary:</p>
+ <p className="text-sm/6 text-gray-400">
+ This is a summary of the AI's findings and recommendations
+ based on the user's input and interactions.
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="pointer-events-none absolute inset-px rounded-lg shadow-sm outline outline-white/15 max-lg:rounded-b-4xl lg:rounded-r-4xl" />
+ </motion.div>
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/src/components/ui/background-ripple-effect.tsx b/src/components/ui/background-ripple-effect.tsx
new file mode 100644
index 0000000..d9d265b
--- /dev/null
+++ b/src/components/ui/background-ripple-effect.tsx
@@ -0,0 +1,132 @@
+"use client";
+import React, { useMemo, useRef, useState } from "react";
+import { cn } from "@/lib/utils";
+
+export const BackgroundRippleEffect = ({
+ rows = 8,
+ cols = 27,
+ cellSize = 56,
+}: {
+ rows?: number;
+ cols?: number;
+ cellSize?: number;
+}) => {
+ const [clickedCell, setClickedCell] = useState<{
+ row: number;
+ col: number;
+ } | null>(null);
+ const [rippleKey, setRippleKey] = useState(0);
+ const ref = useRef<any>(null);
+
+ return (
+ <div
+ ref={ref}
+ className={cn(
+ "absolute inset-0 h-full w-full",
+ "[--cell-border-color:var(--color-neutral-300)] [--cell-fill-color:var(--color-neutral-100)] [--cell-shadow-color:var(--color-neutral-500)]"
+ )}
+ >
+ <div className="relative h-auto w-auto overflow-hidden">
+ <div className="pointer-events-none absolute inset-0 z-[2] h-full w-full overflow-hidden" />
+ <DivGrid
+ key={`base-${rippleKey}`}
+ className="mask-radial-from-20% mask-radial-at-top opacity-600"
+ rows={rows}
+ cols={cols}
+ cellSize={cellSize}
+ borderColor="var(--cell-border-color)"
+ fillColor="var(--cell-fill-color)"
+ clickedCell={clickedCell}
+ onCellClick={(row, col) => {
+ setClickedCell({ row, col });
+ setRippleKey((k) => k + 1);
+ }}
+ interactive
+ />
+ </div>
+ </div>
+ );
+};
+
+type DivGridProps = {
+ className?: string;
+ rows: number;
+ cols: number;
+ cellSize: number; // in pixels
+ borderColor: string;
+ fillColor: string;
+ clickedCell: { row: number; col: number } | null;
+ onCellClick?: (row: number, col: number) => void;
+ interactive?: boolean;
+};
+
+type CellStyle = React.CSSProperties & {
+ ["--delay"]?: string;
+ ["--duration"]?: string;
+};
+
+const DivGrid = ({
+ className,
+ rows = 7,
+ cols = 30,
+ cellSize = 56,
+ borderColor = "#3f3f46",
+ fillColor = "rgba(14,165,233,0.3)",
+ clickedCell = null,
+ onCellClick = () => {},
+ interactive = true,
+}: DivGridProps) => {
+ const cells = useMemo(
+ () => Array.from({ length: rows * cols }, (_, idx) => idx),
+ [rows, cols]
+ );
+
+ const gridStyle: React.CSSProperties = {
+ display: "grid",
+ gridTemplateColumns: `repeat(${cols}, ${cellSize}px)`,
+ gridTemplateRows: `repeat(${rows}, ${cellSize}px)`,
+ width: cols * cellSize,
+ height: rows * cellSize,
+ marginInline: "auto",
+ };
+
+ return (
+ <div className={cn("relative z-[3]", className)} style={gridStyle}>
+ {cells.map((idx) => {
+ const rowIdx = Math.floor(idx / cols);
+ const colIdx = idx % cols;
+ const distance = clickedCell
+ ? Math.hypot(clickedCell.row - rowIdx, clickedCell.col - colIdx)
+ : 0;
+ const delay = clickedCell ? Math.max(0, distance * 55) : 0; // ms
+ const duration = 200 + distance * 80; // ms
+
+ const style: CellStyle = clickedCell
+ ? {
+ "--delay": `${delay}ms`,
+ "--duration": `${duration}ms`,
+ }
+ : {};
+
+ return (
+ <div
+ key={idx}
+ className={cn(
+ "cell relative border-[0.5px] opacity-40 transition-opacity duration-150 will-change-transform hover:opacity-80",
+ clickedCell && "animate-cell-ripple [animation-fill-mode:none]",
+ !interactive && "pointer-events-none"
+ )}
+ style={{
+ backgroundColor: fillColor,
+ borderColor: borderColor,
+ ...style,
+ }}
+ onClick={
+ interactive ? () => onCellClick?.(rowIdx, colIdx) : undefined
+ }
+ />
+ );
+ })}
+ </div>
+ );
+};
diff --git a/src/components/ui/canvas-reveal-effect.tsx b/src/components/ui/canvas-reveal-effect.tsx
new file mode 100644
index 0000000..f84d7a8
--- /dev/null
+++ b/src/components/ui/canvas-reveal-effect.tsx
@@ -0,0 +1,308 @@
+"use client";
+import { cn } from "@/lib/utils";
+import { Canvas, useFrame, useThree } from "@react-three/fiber";
+import React, { useMemo, useRef } from "react";
+import * as THREE from "three";
+
+export const CanvasRevealEffect = ({
+ animationSpeed = 0.4,
+ opacities = [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1],
+ colors = [[0, 255, 255]],
+ containerClassName,
+ dotSize,
+ showGradient = false,
+}: {
+ /**
+ * 0.1 - slower
+ * 1.0 - faster
+ */
+ animationSpeed?: number;
+ opacities?: number[];
+ colors?: number[][];
+ containerClassName?: string;
+ dotSize?: number;
+ showGradient?: boolean;
+}) => {
+ return (
+ <div className={cn("h-full relative bg-white w-full", containerClassName)}>
+ <div className="h-full w-full">
+ <DotMatrix
+ colors={colors ?? [[0, 255, 255]]}
+ dotSize={dotSize ?? 3}
+ opacities={
+ opacities ?? [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1]
+ }
+ shader={`
+ float animation_speed_factor = ${animationSpeed.toFixed(1)};
+ float intro_offset = distance(u_resolution / 2.0 / u_total_size, st2) * 0.01 + (random(st2) * 0.15);
+ opacity *= step(intro_offset, u_time * animation_speed_factor);
+ opacity *= clamp((1.0 - step(intro_offset + 0.1, u_time * animation_speed_factor)) * 1.25, 1.0, 1.25);
+ `}
+ center={["x", "y"]}
+ />
+ </div>
+ {showGradient && (
+ <div className="absolute inset-0 bg-gradient-to-t from-gray-950 to-[84%]" />
+ )}
+ </div>
+ );
+};
+
+interface DotMatrixProps {
+ colors?: number[][];
+ opacities?: number[];
+ totalSize?: number;
+ dotSize?: number;
+ shader?: string;
+ center?: ("x" | "y")[];
+}
+
+const DotMatrix: React.FC<DotMatrixProps> = ({
+ colors = [[0, 0, 0]],
+ opacities = [0.04, 0.04, 0.04, 0.04, 0.04, 0.08, 0.08, 0.08, 0.08, 0.14],
+ totalSize = 4,
+ dotSize = 2,
+ shader = "",
+ center = ["x", "y"],
+}) => {
+ const uniforms = React.useMemo(() => {
+ let colorsArray = [
+ colors[0],
+ colors[0],
+ colors[0],
+ colors[0],
+ colors[0],
+ colors[0],
+ ];
+ if (colors.length === 2) {
+ colorsArray = [
+ colors[0],
+ colors[0],
+ colors[0],
+ colors[1],
+ colors[1],
+ colors[1],
+ ];
+ } else if (colors.length === 3) {
+ colorsArray = [
+ colors[0],
+ colors[0],
+ colors[1],
+ colors[1],
+ colors[2],
+ colors[2],
+ ];
+ }
+
+ return {
+ u_colors: {
+ value: colorsArray.map((color) => [
+ color[0] / 255,
+ color[1] / 255,
+ color[2] / 255,
+ ]),
+ type: "uniform3fv",
+ },
+ u_opacities: {
+ value: opacities,
+ type: "uniform1fv",
+ },
+ u_total_size: {
+ value: totalSize,
+ type: "uniform1f",
+ },
+ u_dot_size: {
+ value: dotSize,
+ type: "uniform1f",
+ },
+ };
+ }, [colors, opacities, totalSize, dotSize]);
+
+ return (
+ <Shader
+ source={`
+ precision mediump float;
+ in vec2 fragCoord;
+
+ uniform float u_time;
+ uniform float u_opacities[10];
+ uniform vec3 u_colors[6];
+ uniform float u_total_size;
+ uniform float u_dot_size;
+ uniform vec2 u_resolution;
+ out vec4 fragColor;
+ float PHI = 1.61803398874989484820459;
+ float random(vec2 xy) {
+ return fract(tan(distance(xy * PHI, xy) * 0.5) * xy.x);
+ }
+ float map(float value, float min1, float max1, float min2, float max2) {
+ return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
+ }
+ void main() {
+ vec2 st = fragCoord.xy;
+ ${
+ center.includes("x")
+ ? "st.x -= abs(floor((mod(u_resolution.x, u_total_size) - u_dot_size) * 0.5));"
+ : ""
+ }
+ ${
+ center.includes("y")
+ ? "st.y -= abs(floor((mod(u_resolution.y, u_total_size) - u_dot_size) * 0.5));"
+ : ""
+ }
+ float opacity = step(0.0, st.x);
+ opacity *= step(0.0, st.y);
+
+ vec2 st2 = vec2(int(st.x / u_total_size), int(st.y / u_total_size));
+
+ float frequency = 5.0;
+ float show_offset = random(st2);
+ float rand = random(st2 * floor((u_time / frequency) + show_offset + frequency) + 1.0);
+ opacity *= u_opacities[int(rand * 10.0)];
+ opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.x / u_total_size));
+ opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.y / u_total_size));
+
+ vec3 color = u_colors[int(show_offset * 6.0)];
+
+ ${shader}
+
+ fragColor = vec4(color, opacity);
+ fragColor.rgb *= fragColor.a;
+ }`}
+ uniforms={uniforms}
+ maxFps={60}
+ />
+ );
+};
+
+type Uniforms = {
+ [key: string]: {
+ value: number[] | number[][] | number;
+ type: string;
+ };
+};
+const ShaderMaterial = ({
+ source,
+ uniforms,
+ maxFps = 60,
+}: {
+ source: string;
+ hovered?: boolean;
+ maxFps?: number;
+ uniforms: Uniforms;
+}) => {
+ const { size } = useThree();
+ const ref = useRef<THREE.Mesh>();
+ let lastFrameTime = 0;
+
+ useFrame(({ clock }) => {
+ if (!ref.current) return;
+ const timestamp = clock.getElapsedTime();
+ if (timestamp - lastFrameTime < 1 / maxFps) {
+ return;
+ }
+ lastFrameTime = timestamp;
+
+ const material: any = ref.current.material;
+ const timeLocation = material.uniforms.u_time;
+ timeLocation.value = timestamp;
+ });
+
+ const getUniforms = () => {
+ const preparedUniforms: any = {};
+
+ for (const uniformName in uniforms) {
+ const uniform: any = uniforms[uniformName];
+
+ switch (uniform.type) {
+ case "uniform1f":
+ preparedUniforms[uniformName] = { value: uniform.value, type: "1f" };
+ break;
+ case "uniform3f":
+ preparedUniforms[uniformName] = {
+ value: new THREE.Vector3().fromArray(uniform.value),
+ type: "3f",
+ };
+ break;
+ case "uniform1fv":
+ preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" };
+ break;
+ case "uniform3fv":
+ preparedUniforms[uniformName] = {
+ value: uniform.value.map((v: number[]) =>
+ new THREE.Vector3().fromArray(v)
+ ),
+ type: "3fv",
+ };
+ break;
+ case "uniform2f":
+ preparedUniforms[uniformName] = {
+ value: new THREE.Vector2().fromArray(uniform.value),
+ type: "2f",
+ };
+ break;
+ default:
+ console.error(`Invalid uniform type for '${uniformName}'.`);
+ break;
+ }
+ }
+
+ preparedUniforms["u_time"] = { value: 0, type: "1f" };
+ preparedUniforms["u_resolution"] = {
+ value: new THREE.Vector2(size.width * 2, size.height * 2),
+ }; // Initialize u_resolution
+ return preparedUniforms;
+ };
+
+ // Shader material
+ const material = useMemo(() => {
+ const materialObject = new THREE.ShaderMaterial({
+ vertexShader: `
+ precision mediump float;
+ in vec2 coordinates;
+ uniform vec2 u_resolution;
+ out vec2 fragCoord;
+ void main(){
+ float x = position.x;
+ float y = position.y;
+ gl_Position = vec4(x, y, 0.0, 1.0);
+ fragCoord = (position.xy + vec2(1.0)) * 0.5 * u_resolution;
+ fragCoord.y = u_resolution.y - fragCoord.y;
+ }
+ `,
+ fragmentShader: source,
+ uniforms: getUniforms(),
+ glslVersion: THREE.GLSL3,
+ blending: THREE.CustomBlending,
+ blendSrc: THREE.SrcAlphaFactor,
+ blendDst: THREE.OneFactor,
+ });
+
+ return materialObject;
+ }, [size.width, size.height, source]);
+
+ return (
+ <mesh ref={ref as any}>
+ <planeGeometry args={[2, 2]} />
+ <primitive object={material} attach="material" />
+ </mesh>
+ );
+};
+
+const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
+ return (
+ <Canvas className="absolute inset-0 h-full w-full">
+ <ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
+ </Canvas>
+ );
+};
+interface ShaderProps {
+ source: string;
+ uniforms: {
+ [key: string]: {
+ value: number[] | number[][] | number;
+ type: string;
+ };
+ };
+ maxFps?: number;
+}
diff --git a/src/components/ui/card-spotlight.tsx b/src/components/ui/card-spotlight.tsx
new file mode 100644
index 0000000..f0b17d7
--- /dev/null
+++ b/src/components/ui/card-spotlight.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import { useMotionValue, motion, useMotionTemplate } from "motion/react";
+import React, { MouseEvent as ReactMouseEvent, useState } from "react";
+import { CanvasRevealEffect } from "@/components/ui/canvas-reveal-effect";
+import { cn } from "@/lib/utils";
+
+export const CardSpotlight = ({
+ children,
+ radius = 350,
+ color = "#ffffff33",
+ className,
+ ...props
+}: {
+ radius?: number;
+ color?: string;
+ children: React.ReactNode;
+} & React.HTMLAttributes<HTMLDivElement>) => {
+ const mouseX = useMotionValue(0);
+ const mouseY = useMotionValue(0);
+ function handleMouseMove({
+ currentTarget,
+ clientX,
+ clientY,
+ }: ReactMouseEvent<HTMLDivElement>) {
+ let { left, top } = currentTarget.getBoundingClientRect();
+
+ mouseX.set(clientX - left);
+ mouseY.set(clientY - top);
+ }
+
+ const [isHovering, setIsHovering] = useState(false);
+ const handleMouseEnter = () => setIsHovering(true);
+ const handleMouseLeave = () => setIsHovering(false);
+ return (
+ <div
+ className={cn(
+ "group/spotlight p-10 rounded-md relative border border-neutral-800 bg-black dark:border-neutral-800",
+ className
+ )}
+ onMouseMove={handleMouseMove}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ {...props}
+ >
+ <motion.div
+ className="pointer-events-none absolute z-0 -inset-px rounded-md opacity-0 transition duration-300 group-hover/spotlight:opacity-100"
+ style={{
+ backgroundColor: color,
+ maskImage: useMotionTemplate`
+ radial-gradient(
+ ${radius}px circle at ${mouseX}px ${mouseY}px,
+ white,
+ transparent 80%
+ )
+ `,
+ }}
+ >
+ {isHovering && (
+ <CanvasRevealEffect
+ animationSpeed={5}
+ containerClassName="bg-transparent absolute inset-0 pointer-events-none"
+ colors={[
+ [59, 130, 246],
+ [139, 92, 246],
+ ]}
+ dotSize={3}
+ />
+ )}
+ </motion.div>
+ {children}
+ </div>
+ );
+};
diff --git a/src/components/ui/hover-border-gradient.tsx b/src/components/ui/hover-border-gradient.tsx
new file mode 100644
index 0000000..898f575
--- /dev/null
+++ b/src/components/ui/hover-border-gradient.tsx
@@ -0,0 +1,100 @@
+"use client";
+import React, { useState, useEffect, useRef } from "react";
+
+import { motion } from "motion/react";
+import { cn } from "@/lib/utils";
+
+type Direction = "TOP" | "LEFT" | "BOTTOM" | "RIGHT";
+
+export function HoverBorderGradient({
+ children,
+ containerClassName,
+ className,
+ as: Tag = "button",
+ duration = 1,
+ clockwise = true,
+ ...props
+}: React.PropsWithChildren<
+ {
+ as?: React.ElementType;
+ containerClassName?: string;
+ className?: string;
+ duration?: number;
+ clockwise?: boolean;
+ } & React.HTMLAttributes<HTMLElement>
+>) {
+ const [hovered, setHovered] = useState<boolean>(false);
+ const [direction, setDirection] = useState<Direction>("TOP");
+
+ const rotateDirection = (currentDirection: Direction): Direction => {
+ const directions: Direction[] = ["TOP", "LEFT", "BOTTOM", "RIGHT"];
+ const currentIndex = directions.indexOf(currentDirection);
+ const nextIndex = clockwise
+ ? (currentIndex - 1 + directions.length) % directions.length
+ : (currentIndex + 1) % directions.length;
+ return directions[nextIndex];
+ };
+
+ const movingMap: Record<Direction, string> = {
+ TOP: "radial-gradient(20.7% 50% at 50% 0%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
+ LEFT: "radial-gradient(16.6% 43.1% at 0% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
+ BOTTOM:
+ "radial-gradient(20.7% 50% at 50% 100%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
+ RIGHT:
+ "radial-gradient(16.2% 41.199999999999996% at 100% 50%, hsl(0, 0%, 100%) 0%, rgba(255, 255, 255, 0) 100%)",
+ };
+
+ const highlight =
+ "radial-gradient(75% 181.15942028985506% at 50% 50%, #3275F8 0%, rgba(255, 255, 255, 0) 100%)";
+
+ useEffect(() => {
+ if (!hovered) {
+ const interval = setInterval(() => {
+ setDirection((prevState) => rotateDirection(prevState));
+ }, duration * 1000);
+ return () => clearInterval(interval);
+ }
+ }, [hovered]);
+ return (
+ <Tag
+ onMouseEnter={(event: React.MouseEvent<HTMLDivElement>) => {
+ setHovered(true);
+ }}
+ onMouseLeave={() => setHovered(false)}
+ className={cn(
+ "relative flex rounded-full border border-indigo-300/50 content-center bg-black/20 hover:bg-black/10 transition duration-500 items-center flex-col flex-nowrap gap-10 h-min justify-center overflow-visible p-px decoration-clone w-fit",
+ containerClassName
+ )}
+ {...props}
+ >
+ <div
+ className={cn(
+ "w-auto text-white z-10 bg-black px-4 py-2 rounded-[inherit]",
+ className
+ )}
+ >
+ {children}
+ </div>
+ <motion.div
+ className={cn(
+ "flex-none inset-0 overflow-hidden absolute z-0 rounded-[inherit]"
+ )}
+ style={{
+ filter: "blur(2px)",
+ position: "absolute",
+ width: "100%",
+ height: "110%",
+ margin: "auto",
+ }}
+ initial={{ background: movingMap[direction] }}
+ animate={{
+ background: hovered
+ ? [movingMap[direction], highlight]
+ : movingMap[direction],
+ }}
+ transition={{ ease: "linear", duration: duration ?? 1 }}
+ />
+ <div className="bg-black absolute z-1 flex-none inset-[2px] rounded-[100px]" />
+ </Tag>
+ );
+}
diff --git a/src/components/ui/wavy-background.tsx b/src/components/ui/wavy-background.tsx
new file mode 100644
index 0000000..4fbc374
--- /dev/null
+++ b/src/components/ui/wavy-background.tsx
@@ -0,0 +1,132 @@
+"use client";
+import { cn } from "@/lib/utils";
+import React, { useEffect, useRef, useState } from "react";
+import { createNoise3D } from "simplex-noise";
+
+export const WavyBackground = ({
+ children,
+ className,
+ containerClassName,
+ colors,
+ waveWidth,
+ backgroundFill,
+ blur = 10,
+ speed = "fast",
+ waveOpacity = 0.5,
+ ...props
+}: {
+ children?: any;
+ className?: string;
+ containerClassName?: string;
+ colors?: string[];
+ waveWidth?: number;
+ backgroundFill?: string;
+ blur?: number;
+ speed?: "slow" | "fast";
+ waveOpacity?: number;
+ [key: string]: any;
+}) => {
+ const noise = createNoise3D();
+ let w: number,
+ h: number,
+ nt: number,
+ i: number,
+ x: number,
+ ctx: any,
+ canvas: any;
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+ const getSpeed = () => {
+ switch (speed) {
+ case "slow":
+ return 0.001;
+ case "fast":
+ return 0.002;
+ default:
+ return 0.001;
+ }
+ };
+
+ const init = () => {
+ canvas = canvasRef.current;
+ ctx = canvas.getContext("2d");
+ w = ctx.canvas.width = window.innerWidth;
+ h = ctx.canvas.height = window.innerHeight;
+ ctx.filter = `blur(${blur}px)`;
+ nt = 0;
+ window.onresize = function () {
+ w = ctx.canvas.width = window.innerWidth;
+ h = ctx.canvas.height = window.innerHeight;
+ ctx.filter = `blur(${blur}px)`;
+ };
+ render();
+ };
+
+ const waveColors = colors ?? [
+ "#38bdf8",
+ "#818cf8",
+ "#c084fc",
+ "#e879f9",
+ "#22d3ee",
+ ];
+ const drawWave = (n: number) => {
+ nt += getSpeed();
+ for (i = 0; i < n; i++) {
+ ctx.beginPath();
+ ctx.lineWidth = waveWidth || 50;
+ ctx.strokeStyle = waveColors[i % waveColors.length];
+ for (x = 0; x < w; x += 5) {
+ var y = noise(x / 800, 0.3 * i, nt) * 100;
+ ctx.lineTo(x, y + h * 0.5); // adjust for height, currently at 50% of the container
+ }
+ ctx.stroke();
+ ctx.closePath();
+ }
+ };
+
+ let animationId: number;
+ const render = () => {
+ ctx.fillStyle = backgroundFill || "black";
+ ctx.globalAlpha = waveOpacity || 0.5;
+ ctx.fillRect(0, 0, w, h);
+ drawWave(5);
+ animationId = requestAnimationFrame(render);
+ };
+
+ useEffect(() => {
+ init();
+ return () => {
+ cancelAnimationFrame(animationId);
+ };
+ }, []);
+
+ const [isSafari, setIsSafari] = useState(false);
+ useEffect(() => {
+ // I'm sorry but i have got to support it on safari.
+ setIsSafari(
+ typeof window !== "undefined" &&
+ navigator.userAgent.includes("Safari") &&
+ !navigator.userAgent.includes("Chrome")
+ );
+ }, []);
+
+ return (
+ <div
+ className={cn(
+ "h-screen flex flex-col items-center justify-center",
+ containerClassName
+ )}
+ >
+ <canvas
+ className="absolute inset-0 z-0"
+ ref={canvasRef}
+ id="canvas"
+ style={{
+ ...(isSafari ? { filter: `blur(${blur}px)` } : {}),
+ }}
+ ></canvas>
+ <div className={cn("relative z-10", className)} {...props}>
+ {children}
+ </div>
+ </div>
+ );
+};
diff --git a/src/components/ui/wooble-card.tsx b/src/components/ui/wooble-card.tsx
new file mode 100644
index 0000000..05c059a
--- /dev/null
+++ b/src/components/ui/wooble-card.tsx
@@ -0,0 +1,78 @@
+"use client";
+import { cn } from "@/lib/utils";
+import { motion } from "motion/react";
+import React, { useState } from "react";
+
+export const WobbleCard = ({
+ children,
+ containerClassName,
+ className,
+}: {
+ children: React.ReactNode;
+ containerClassName?: string;
+ className?: string;
+}) => {
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
+ const [isHovering, setIsHovering] = useState(false);
+
+ const handleMouseMove = (event: React.MouseEvent<HTMLElement>) => {
+ const { clientX, clientY } = event;
+ const rect = event.currentTarget.getBoundingClientRect();
+ const x = (clientX - (rect.left + rect.width / 2)) / 20;
+ const y = (clientY - (rect.top + rect.height / 2)) / 20;
+ setMousePosition({ x, y });
+ };
+ return (
+ <motion.section
+ onMouseMove={handleMouseMove}
+ onMouseEnter={() => setIsHovering(true)}
+ onMouseLeave={() => {
+ setIsHovering(false);
+ setMousePosition({ x: 0, y: 0 });
+ }}
+ style={{
+ transform: isHovering
+ ? `translate3d(${mousePosition.x}px, ${mousePosition.y}px, 0) scale3d(1, 1, 1)`
+ : "translate3d(0px, 0px, 0) scale3d(1, 1, 1)",
+ transition: "transform 0.1s ease-out",
+ }}
+ className={cn(
+ "mx-auto w-full bg-indigo-800 relative rounded-2xl overflow-hidden",
+ containerClassName
+ )}
+ >
+ <div
+ className="relative h-full [background-image:radial-gradient(88%_100%_at_top,rgba(255,255,255,0.5),rgba(255,255,255,0))] sm:mx-0 sm:rounded-2xl overflow-hidden"
+ style={{
+ boxShadow:
+ "0 10px 32px rgba(34, 42, 53, 0.12), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.05), 0 4px 6px rgba(34, 42, 53, 0.08), 0 24px 108px rgba(47, 48, 55, 0.10)",
+ }}
+ >
+ <motion.div
+ style={{
+ transform: isHovering
+ ? `translate3d(${-mousePosition.x}px, ${-mousePosition.y}px, 0) scale3d(1.03, 1.03, 1)`
+ : "translate3d(0px, 0px, 0) scale3d(1, 1, 1)",
+ transition: "transform 0.1s ease-out",
+ }}
+ className={cn("h-full px-4 py-20 sm:px-10", className)}
+ >
+ <Noise />
+ {children}
+ </motion.div>
+ </div>
+ </motion.section>
+ );
+};
+
+const Noise = () => {
+ return (
+ <div
+ className="absolute inset-0 w-full h-full scale-[1.2] transform opacity-10 [mask-image:radial-gradient(#fff,transparent,75%)]"
+ style={{
+ backgroundImage: "url(/noise.webp)",
+ backgroundSize: "30%",
+ }}
+ ></div>
+ );
+};
diff --git a/src/components/ui/world-map.tsx b/src/components/ui/world-map.tsx
new file mode 100644
index 0000000..d6c9891
--- /dev/null
+++ b/src/components/ui/world-map.tsx
@@ -0,0 +1,167 @@
+"use client";
+
+import { useRef } from "react";
+import { motion } from "motion/react";
+import DottedMap from "dotted-map";
+
+import { useTheme } from "next-themes";
+
+interface MapProps {
+ dots?: Array<{
+ start: { lat: number; lng: number; label?: string };
+ end: { lat: number; lng: number; label?: string };
+ }>;
+ lineColor?: string;
+}
+
+export function WorldMap({ dots = [], lineColor = "#0ea5e9" }: MapProps) {
+ const svgRef = useRef<SVGSVGElement>(null);
+ const map = new DottedMap({ height: 100, grid: "diagonal" });
+
+ const { theme } = useTheme();
+
+ const svgMap = map.getSVG({
+ radius: 0.22,
+ color: theme === "dark" ? "#FFFFFF40" : "#00000040",
+ shape: "circle",
+ backgroundColor: theme === "dark" ? "black" : "white",
+ });
+
+ const projectPoint = (lat: number, lng: number) => {
+ const x = (lng + 180) * (800 / 360);
+ const y = (90 - lat) * (400 / 180);
+ return { x, y };
+ };
+
+ const createCurvedPath = (
+ start: { x: number; y: number },
+ end: { x: number; y: number }
+ ) => {
+ const midX = (start.x + end.x) / 2;
+ const midY = Math.min(start.y, end.y) - 50;
+ return `M ${start.x} ${start.y} Q ${midX} ${midY} ${end.x} ${end.y}`;
+ };
+
+ return (
+ <div className="w-full aspect-[2/1] dark:bg-black bg-white rounded-lg relative font-sans">
+ <img
+ src={`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`}
+ className="h-full w-full [mask-image:linear-gradient(to_bottom,transparent,white_10%,white_90%,transparent)] pointer-events-none select-none"
+ alt="world map"
+ height="495"
+ width="1056"
+ draggable={false}
+ />
+ <svg
+ ref={svgRef}
+ viewBox="0 0 800 400"
+ className="w-full h-full absolute inset-0 pointer-events-none select-none"
+ >
+ {dots.map((dot, i) => {
+ const startPoint = projectPoint(dot.start.lat, dot.start.lng);
+ const endPoint = projectPoint(dot.end.lat, dot.end.lng);
+ return (
+ <g key={`path-group-${i}`}>
+ <motion.path
+ d={createCurvedPath(startPoint, endPoint)}
+ fill="none"
+ stroke="url(#path-gradient)"
+ strokeWidth="1"
+ initial={{
+ pathLength: 0,
+ }}
+ animate={{
+ pathLength: 1,
+ }}
+ transition={{
+ duration: 0,
+ delay: 0.5 * i,
+ ease: "easeOut",
+ }}
+ key={`start-upper-${i}`}
+ ></motion.path>
+ </g>
+ );
+ })}
+
+ <defs>
+ <linearGradient id="path-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
+ <stop offset="0%" stopColor="white" stopOpacity="0" />
+ <stop offset="5%" stopColor={lineColor} stopOpacity="1" />
+ <stop offset="95%" stopColor={lineColor} stopOpacity="1" />
+ <stop offset="100%" stopColor="white" stopOpacity="0" />
+ </linearGradient>
+ </defs>
+
+ {dots.map((dot, i) => (
+ <g key={`points-group-${i}`}>
+ <g key={`start-${i}`}>
+ <circle
+ cx={projectPoint(dot.start.lat, dot.start.lng).x}
+ cy={projectPoint(dot.start.lat, dot.start.lng).y}
+ r="2"
+ fill={lineColor}
+ />
+ <circle
+ cx={projectPoint(dot.start.lat, dot.start.lng).x}
+ cy={projectPoint(dot.start.lat, dot.start.lng).y}
+ r="2"
+ fill={lineColor}
+ opacity="0.5"
+ >
+ <animate
+ attributeName="r"
+ from="2"
+ to="8"
+ dur="1.5s"
+ begin="0s"
+ repeatCount="indefinite"
+ />
+ <animate
+ attributeName="opacity"
+ from="0.5"
+ to="0"
+ dur="1.5s"
+ begin="0s"
+ repeatCount="indefinite"
+ />
+ </circle>
+ </g>
+ <g key={`end-${i}`}>
+ <circle
+ cx={projectPoint(dot.end.lat, dot.end.lng).x}
+ cy={projectPoint(dot.end.lat, dot.end.lng).y}
+ r="2"
+ fill={lineColor}
+ />
+ <circle
+ cx={projectPoint(dot.end.lat, dot.end.lng).x}
+ cy={projectPoint(dot.end.lat, dot.end.lng).y}
+ r="2"
+ fill={lineColor}
+ opacity="0.5"
+ >
+ <animate
+ attributeName="r"
+ from="2"
+ to="8"
+ dur="1.5s"
+ begin="0s"
+ repeatCount="indefinite"
+ />
+ <animate
+ attributeName="opacity"
+ from="0.5"
+ to="0"
+ dur="1.5s"
+ begin="0s"
+ repeatCount="indefinite"
+ />
+ </circle>
+ </g>
+ </g>
+ ))}
+ </svg>
+ </div>
+ );
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..cec6ac9
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}