Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions src/components/ContactModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { motion, AnimatePresence } from "framer-motion";
import { X, Mail, Linkedin, Github, Send } from "lucide-react";
import { Magnetic } from "./Motion";
import { useState } from "react";
import { toast } from "sonner";

interface ContactModalProps {
isOpen: boolean;
onClose: () => void;
}

export function ContactModal({ isOpen, onClose }: ContactModalProps) {
const [pending, setPending] = useState(false);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setPending(true);
// Simulate API call
setTimeout(() => {
setPending(false);
toast.success("Message sent! I'll get back to you soon.");
onClose();
}, 1500);
};

return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/80 backdrop-blur-xl p-4 md:p-8"
>
<motion.div
initial={{ scale: 0.9, y: 20, opacity: 0 }}
animate={{ scale: 1, y: 0, opacity: 1 }}
exit={{ scale: 0.9, y: 20, opacity: 0 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="relative grid w-full max-w-6xl gap-8 overflow-hidden rounded-[2rem] border border-border bg-card p-8 md:grid-cols-2 md:p-16"
>
{/* Close button */}
<div className="absolute right-6 top-6 z-10">
<Magnetic>
<button
onClick={onClose}
className="flex h-12 w-12 items-center justify-center rounded-full border border-border bg-card transition-colors hover:bg-muted"
>
<X size={20} />
</button>
</Magnetic>
</div>

{/* Content */}
<div className="flex flex-col justify-between space-y-12">
<div>
<p className="text-xs font-semibold tracking-[0.3em] text-accent uppercase">[ CONTACT ]</p>
<h2 className="mt-6 font-display text-5xl sm:text-7xl">Let's start a<br />conversation.</h2>
<p className="mt-6 max-w-md text-lg text-muted-foreground leading-relaxed">
I'm always open to discussing new projects, creative ideas or opportunities to be part of your visions.
</p>
</div>

<div className="space-y-6">
<a href="mailto:rohan@example.com" className="flex items-center gap-4 text-xl hover:text-accent transition-colors">
<Mail className="text-accent" /> rohan@example.com
</a>
<div className="flex gap-4">
<Magnetic>
<a href="#" className="flex h-12 w-12 items-center justify-center rounded-full border border-border hover:bg-muted transition-colors">
<Linkedin size={20} />
</a>
</Magnetic>
<Magnetic>
<a href="#" className="flex h-12 w-12 items-center justify-center rounded-full border border-border hover:bg-muted transition-colors">
<Github size={20} />
</a>
</Magnetic>
</div>
</div>
</div>

{/* Form */}
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div className="space-y-2">
<label className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">Full Name</label>
<input
required
type="text"
placeholder="John Doe"
className="w-full rounded-2xl border border-border bg-muted/50 px-6 py-4 outline-none transition-all focus:border-accent focus:ring-1 focus:ring-accent"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">Email Address</label>
<input
required
type="email"
placeholder="john@example.com"
className="w-full rounded-2xl border border-border bg-muted/50 px-6 py-4 outline-none transition-all focus:border-accent focus:ring-1 focus:ring-accent"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">Message</label>
<textarea
required
rows={4}
placeholder="How can I help you?"
className="w-full rounded-2xl border border-border bg-muted/50 px-6 py-4 outline-none transition-all focus:border-accent focus:ring-1 focus:ring-accent resize-none"
/>
</div>
<button
disabled={pending}
className="group relative mt-2 flex items-center justify-center gap-3 overflow-hidden rounded-2xl bg-primary py-5 font-display text-xl text-primary-foreground transition-all active:scale-[0.98] disabled:opacity-50"
>
<span className="relative z-10 flex items-center gap-2">
{pending ? "SENDING..." : "SEND MESSAGE"}
<Send size={18} className={pending ? "animate-pulse" : ""} />
</span>
<span className="absolute inset-0 -translate-x-full bg-accent transition-transform duration-500 ease-[0.22,1,0.36,1] group-hover:translate-x-0" />
</button>
</form>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
33 changes: 33 additions & 0 deletions src/components/Motion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,40 @@ export function ScrambleText({ text, className = "" }: { text: string; className
);
}

export function Tilt({ children, intensity = 15 }: { children: ReactNode; intensity?: number }) {
const ref = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useSpring(useTransform(y, [-0.5, 0.5], [intensity, -intensity]), { stiffness: 150, damping: 15 });
const rotateY = useSpring(useTransform(x, [-0.5, 0.5], [-intensity, intensity]), { stiffness: 150, damping: 15 });

const handleMouseMove = (e: React.MouseEvent) => {
if (!ref.current) return;
const { left, top, width, height } = ref.current.getBoundingClientRect();
x.set((e.clientX - left) / width - 0.5);
y.set((e.clientY - top) / height - 0.5);
};

const handleMouseLeave = () => {
x.set(0);
y.set(0);
};

return (
<motion.div
ref={ref}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{ rotateX, rotateY, perspective: 1000 }}
className="h-full w-full"
>
{children}
</motion.div>
);
}

export function PageTransition({ children, routeKey }: { children: ReactNode; routeKey: string }) {

return (
<AnimatePresence mode="wait">
<motion.div key={routeKey} className="relative">
Expand Down
19 changes: 9 additions & 10 deletions src/components/SiteHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { useState } from "react";
import { useTheme } from "@/hooks/use-theme";
import { useContactModal } from "@/hooks/use-contact-modal";
import { Moon, Sun } from "lucide-react";

const links: { label: string; to: string; hash?: string }[] = [
Expand All @@ -26,6 +27,7 @@ export function ThemeToggle() {

export function SiteHeader() {
const [open, setOpen] = useState(false);
const { open: openContact } = useContactModal();

return (
<motion.header
Expand Down Expand Up @@ -54,14 +56,13 @@ export function SiteHeader() {

<div className="flex items-center gap-3">
<ThemeToggle />
<Link
to="/"
hash="contact"
<button
onClick={openContact}
className="group relative hidden overflow-hidden rounded-full bg-primary px-4 py-2 text-xs font-semibold text-primary-foreground sm:inline-block sm:px-5 sm:text-sm"
>
<span className="relative z-10">CONTACT</span>
<span className="absolute inset-0 -translate-x-full bg-accent transition-transform duration-500 ease-[0.22,1,0.36,1] group-hover:translate-x-0" />
</Link>
</button>
<button
aria-label="Menu"
onClick={() => setOpen((v) => !v)}
Expand Down Expand Up @@ -96,14 +97,12 @@ export function SiteHeader() {
{l.label}
</Link>
))}
<Link
to="/"
hash="contact"
onClick={() => setOpen(false)}
className="mt-1 rounded-xl bg-primary px-4 py-3 text-center text-sm font-semibold text-primary-foreground"
<button
onClick={() => { setOpen(false); openContact(); }}
className="mt-1 rounded-xl bg-primary px-4 py-3 text-center text-sm font-semibold text-primary-foreground w-full"
>
CONTACT
</Link>
</button>
</nav>
</motion.div>
)}
Expand Down
27 changes: 27 additions & 0 deletions src/hooks/use-contact-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useState, useEffect } from "react";

const EVENT_NAME = "toggle-contact-modal";

export function useContactModal() {
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
const handleToggle = (e: Event) => {
const customEvent = e as CustomEvent<{ isOpen: boolean }>;
setIsOpen(customEvent.detail.isOpen);
};

window.addEventListener(EVENT_NAME, handleToggle);
return () => window.removeEventListener(EVENT_NAME, handleToggle);
}, []);

const open = () => {
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: { isOpen: true } }));
};

const close = () => {
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: { isOpen: false } }));
};

return { isOpen, open, close };
}
7 changes: 7 additions & 0 deletions src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,15 @@ function RootShell({ children }: { children: React.ReactNode }) {
);
}

import { ContactModal } from "@/components/ContactModal";
import { useContactModal } from "@/hooks/use-contact-modal";
import { Toaster } from "sonner";

function RootComponent() {
const { queryClient } = Route.useRouteContext();
// Show loader only once per browser session
const [loaded, setLoaded] = useState(false);
const { isOpen, close } = useContactModal();

const routerState = useRouterState();
const currentPath = routerState.location.pathname;
Expand Down Expand Up @@ -105,6 +110,8 @@ function RootComponent() {
<Outlet />
</PageTransition>
</motion.div>
<ContactModal isOpen={isOpen} onClose={close} />
<Toaster position="bottom-right" />
</QueryClientProvider>
);
}
Expand Down
Loading
Loading