Skip to content

Commit 67dafd2

Browse files
authored
Add files via upload
1 parent adef78e commit 67dafd2

30 files changed

Lines changed: 1965 additions & 0 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { motion } from "framer-motion";
2+
import Divider from "@/components/Divider";
3+
import TechBadge from "@/components/TechBadge";
4+
import Marquee from "react-fast-marquee";
5+
import type { Tech } from "../../typings";
6+
7+
export default function AboutCard({ title, description, tech, direction, span, gradient, delay }: { title: string, description: string, tech?: Tech[], direction: 'top' | 'bottom' | 'left' | 'right', span: 1 | 2, gradient: string, delay: number }) {
8+
return (
9+
<>
10+
<motion.li
11+
className={`${span === 1 ? 'min-[940px]:col-span-1 col-span-2' : 'col-span-2'} `}
12+
initial={{ transform: `translate${direction === 'top' || direction === 'bottom' ? 'Y' : 'X'}(${direction === 'top' || direction === 'left' ? '-' : ''}30px)`, opacity: 0 }}
13+
whileInView={{ transform: `translate${direction === 'top' || direction === 'bottom' ? 'Y' : 'X'}(0px)`, opacity: 100 }}
14+
transition={{ duration: 0.5, delay: delay, ease: [0.39, 0.21, 0.12, 0.96], }}
15+
viewport={{ amount: 0.1, once: true }}
16+
>
17+
<div className={`${gradient} from-primary to-secondary p-4 flex flex-col rounded-lg border-1 border-accent shadow-2xl shadow-background`}>
18+
<h2 className="text-center font-semibold text-4xl">
19+
{title}
20+
</h2>
21+
<p className="text-center text-xl mb-2">
22+
{description}
23+
</p>
24+
{tech &&
25+
<>
26+
<Divider />
27+
<Marquee pauseOnHover speed={70} className="my-2">
28+
<ul className="flex flex-row">
29+
{tech.map((tech: Tech) => (
30+
<TechBadge key={tech.title} title={tech.title} icon={tech.icon} link={tech.link} />
31+
))}
32+
</ul>
33+
</Marquee>
34+
<Divider />
35+
</>
36+
}
37+
</div>
38+
</motion.li>
39+
</>
40+
);
41+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Button({ label, onClick, width }: { label: string, onClick: () => void, width: string }) {
2+
return (
3+
<button onClick={onClick} className={`${width} bg-secondary hover:bg-accent duration-300 border-1 border-accent px-2 py-1.5 text-lg font-medium flex items-center justify-center rounded-lg`}>
4+
{label}
5+
</button>
6+
);
7+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { useEffect, useState } from 'react';
2+
3+
interface Sparkle {
4+
id: number;
5+
x: number;
6+
y: number;
7+
opacity: number;
8+
vx: number;
9+
vy: number;
10+
size: number;
11+
}
12+
13+
export default function CursorSparkles() {
14+
const [sparkles, setSparkles] = useState<Sparkle[]>([]);
15+
const [sparkleId, setSparkleId] = useState(0);
16+
17+
useEffect(() => {
18+
const handleMouseMove = (e: MouseEvent) => {
19+
// Create a few sparkles at the cursor position
20+
const newSparkles: Sparkle[] = [];
21+
const sparkleCount = 2;
22+
23+
for (let i = 0; i < sparkleCount; i++) {
24+
const angle = Math.random() * Math.PI * 2;
25+
const speed = 0.5 + Math.random() * 1;
26+
const vx = Math.cos(angle) * speed;
27+
const vy = Math.sin(angle) * speed;
28+
const size = 2 + Math.random() * 3;
29+
30+
newSparkles.push({
31+
id: sparkleId + i,
32+
x: e.clientX,
33+
y: e.clientY,
34+
opacity: 1,
35+
vx,
36+
vy,
37+
size,
38+
});
39+
}
40+
41+
setSparkleId(prev => prev + sparkleCount);
42+
setSparkles(prev => [...prev, ...newSparkles]);
43+
};
44+
45+
window.addEventListener('mousemove', handleMouseMove);
46+
47+
// Animate sparkles
48+
const interval = setInterval(() => {
49+
setSparkles(prev =>
50+
prev
51+
.map(sparkle => ({
52+
...sparkle,
53+
x: sparkle.x + sparkle.vx,
54+
y: sparkle.y + sparkle.vy,
55+
opacity: sparkle.opacity - 0.03,
56+
vx: sparkle.vx * 0.98,
57+
vy: sparkle.vy * 0.98,
58+
}))
59+
.filter(sparkle => sparkle.opacity > 0)
60+
);
61+
}, 16); // ~60fps
62+
63+
return () => {
64+
window.removeEventListener('mousemove', handleMouseMove);
65+
clearInterval(interval);
66+
};
67+
}, [sparkleId]);
68+
69+
return (
70+
<div className="fixed inset-0 pointer-events-none z-[9999]">
71+
{sparkles.map(sparkle => (
72+
<div
73+
key={sparkle.id}
74+
className="absolute rounded-full bg-white"
75+
style={{
76+
left: `${sparkle.x}px`,
77+
top: `${sparkle.y}px`,
78+
width: `${sparkle.size}px`,
79+
height: `${sparkle.size}px`,
80+
opacity: sparkle.opacity,
81+
transform: 'translate(-50%, -50%)',
82+
boxShadow: `0 0 ${sparkle.size * 2}px ${sparkle.size}px rgba(255, 255, 255, ${sparkle.opacity * 0.8})`,
83+
}}
84+
/>
85+
))}
86+
</div>
87+
);
88+
}
89+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Divider() {
2+
return (<div className="h-0.5 w-full rounded-lg bg-gradient-to-r from-secondary via-accent to-secondary" />);
3+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { motion } from "framer-motion";
2+
import Link from "next/link";
3+
import { useState } from "react";
4+
import Modal from "@/components/Modal";
5+
import MediaCarousel from "@/components/MediaCarousel";
6+
import Button from "@/components/Button";
7+
import LinkButton from "@/components/LinkButton";
8+
import Divider from "@/components/Divider";
9+
10+
export default function ExperienceCard({ url, title, fullDescription, cardDescription, cardImage, media, delay, gradient, myRole, timeline }: { url?: string, title: string, fullDescription: string[], cardDescription: string, cardImage: string, media: string[], delay: number, gradient: string, myRole: string, timeline: string }) {
11+
const [modalOpen, setModalOpen] = useState(false);
12+
13+
return (
14+
<>
15+
<motion.li
16+
className="group flex"
17+
initial={{ transform: 'translateY(-30px)', opacity: 0 }}
18+
whileInView={{ transform: 'translateY(0px)', opacity: 100 }}
19+
transition={{ duration: 0.5, delay: delay, ease: [0.39, 0.21, 0.12, 0.96], }}
20+
viewport={{ amount: 0.1, once: true }}
21+
>
22+
<div className={`p-4 flex md:flex-row flex-col gap-6 ${gradient} from-primary to-secondary rounded-lg border-1 border-accent shadow-2xl shadow-background items-center`}>
23+
<img alt="" draggable={false} className="rounded-lg md:h-[14rem] sm:h-[12rem] h-[10rem] duration-300" src={cardImage} />
24+
<div className="flex flex-col">
25+
<h2 className="md:text-left text-center font-semibold text-4xl">
26+
{myRole}
27+
</h2>
28+
<h3 className="md:text-left text-center font-normal text-2xl">
29+
{title} | <span className="brightness-75">{timeline}</span>
30+
</h3>
31+
<Divider />
32+
<p className="md:text-left text-center text-lg mt-1">
33+
{cardDescription}
34+
</p>
35+
<div className="flex row gap-4 mt-2">
36+
{url &&
37+
<LinkButton label="Visit Website" link={url} width="w-1/2" />
38+
}
39+
<Button label="View More" onClick={() => setModalOpen(true)} width={`${url ? 'w-1/2' : 'w-full'}`} />
40+
</div>
41+
</div>
42+
<Modal open={modalOpen} setOpen={setModalOpen}>
43+
<MediaCarousel media={media} />
44+
<div className="flex lg:flex-row flex-col justify-between mt-6 px-3">
45+
<div className="flex flex-col">
46+
<div className="flex flex-row gap-2 items-center">
47+
<h1 className="sm:text-4xl text-3xl font-bold">{title}</h1>
48+
{url &&
49+
<Link href={url} target="_blank" className="bg-middle hover:bg-secondary duration-300 border-1 border-accent p-1.5 rounded-full">
50+
<svg xmlns="http://www.w3.org/2000/svg" className="w-6 h-6 fill-white" viewBox="0 0 16 16">
51+
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z" />
52+
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z" />
53+
</svg>
54+
</Link>
55+
}
56+
</div>
57+
<div className="flex flex-col mt-2">
58+
<h2 className="sm:text-2xl text-xl font-semibold">My Role</h2>
59+
<p className="sm:text-lg text-base">{myRole}</p>
60+
</div>
61+
<div className="flex flex-col mt-2">
62+
<h2 className="sm:text-2xl text-xl font-semibold">Timeline</h2>
63+
<p className="sm:text-lg text-base">{timeline}</p>
64+
</div>
65+
</div>
66+
<div className="w-auto h-0.5 bg-accent rounded-lg lg:hidden block my-2" />
67+
<div className="w-0.5 h-auto bg-accent rounded-lg lg:block hidden" />
68+
<div className="flex flex-col">
69+
<h2 className="sm:text-2xl text-xl font-semibold">
70+
Overview
71+
</h2>
72+
<div className="max-h-[16.5rem] overflow-y-auto bg-neutral-800 border-1 border-accent rounded-lg p-2">
73+
{fullDescription.map((desc, i) => (
74+
<p key={i} className="sm:text-lg text-base first:mt-0 mt-2 max-w-[28rem]">{desc}</p>
75+
))}
76+
</div>
77+
</div>
78+
</div>
79+
</Modal>
80+
</div>
81+
</motion.li>
82+
</>
83+
);
84+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { useId } from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
// MagicUI Component
6+
// https://magicui.design/docs/components/grid-pattern
7+
8+
interface GridPatternProps {
9+
width?: any;
10+
height?: any;
11+
x?: any;
12+
y?: any;
13+
squares?: Array<[x: number, y: number]>;
14+
strokeDasharray?: any;
15+
className?: string;
16+
[key: string]: any;
17+
}
18+
19+
export function GridPattern({
20+
width = 40,
21+
height = 40,
22+
x = -1,
23+
y = -1,
24+
strokeDasharray = 0,
25+
squares,
26+
className,
27+
...props
28+
}: GridPatternProps) {
29+
const id = useId();
30+
31+
return (
32+
<svg
33+
aria-hidden="true"
34+
className={cn(
35+
"pointer-events-none absolute inset-0 h-full w-full fill-primary stroke-primary",
36+
className,
37+
)}
38+
{...props}
39+
>
40+
<defs>
41+
<pattern
42+
id={id}
43+
width={width}
44+
height={height}
45+
patternUnits="userSpaceOnUse"
46+
x={x}
47+
y={y}
48+
>
49+
<path
50+
d={`M.5 ${height}V.5H${width}`}
51+
fill="none"
52+
strokeDasharray={strokeDasharray}
53+
/>
54+
</pattern>
55+
</defs>
56+
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
57+
{squares && (
58+
<svg x={x} y={y} className="overflow-visible">
59+
{squares.map(([x, y]) => (
60+
<rect
61+
strokeWidth="0"
62+
key={`${x}-${y}`}
63+
width={width - 1}
64+
height={height - 1}
65+
x={x * width + 1}
66+
y={y * height + 1}
67+
/>
68+
))}
69+
</svg>
70+
)}
71+
</svg>
72+
);
73+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Link from "next/link";
2+
3+
export default function LinkButton({ label, link, width }: { label: string, link: string, width: string }) {
4+
return (
5+
<Link href={link} target="_blank" className={`${width} bg-secondary hover:bg-accent duration-300 border-1 border-accent px-2 py-1.5s text-lg font-medium flex items-center justify-center rounded-lg`}>
6+
{label}
7+
</Link>
8+
);
9+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useEffect, useRef, useState } from 'react'
2+
import { ArrowLeft, ArrowRight } from 'lucide-react'
3+
4+
export default function MediaCarousel({ media }: { media: string[] }) {
5+
const [currentMedia, setCurrentMedia] = useState(0)
6+
const refs = useRef<HTMLDivElement[]>([])
7+
8+
useEffect(() => {
9+
refs.current = refs.current.slice(0, media.length)
10+
}, [media.length])
11+
12+
const scrollToMedia = (i: number) => {
13+
setCurrentMedia(i)
14+
15+
refs.current[i]?.scrollIntoView({
16+
behavior: 'smooth',
17+
block: 'nearest',
18+
inline: 'start',
19+
})
20+
}
21+
22+
const nextMedia = () => {
23+
scrollToMedia((currentMedia + 1) % media.length)
24+
}
25+
26+
const previousMedia = () => {
27+
scrollToMedia((currentMedia - 1 + media.length) % media.length)
28+
}
29+
30+
return (
31+
<div className="scrollbar flex w-full snap-x snap-mandatory overflow-x-auto rounded-lg media-carousel">
32+
{media.length > 1 && <CarouselControl isLeft handleNext={nextMedia} handlePrevious={previousMedia} />}
33+
{media.map((src, i) => (
34+
<div
35+
ref={(el) => { if (el) refs.current[i] = el }}
36+
key={i}
37+
className="flex w-full flex-shrink-0 snap-center justify-center rounded-lg"
38+
>
39+
{src.includes('.mp4') ?
40+
<video
41+
src={src}
42+
muted
43+
loop
44+
controls
45+
/>
46+
:
47+
<img
48+
src={src}
49+
alt={`Media ${i}`}
50+
/>
51+
}
52+
</div>
53+
))}
54+
{media.length > 1 && <CarouselControl handleNext={nextMedia} handlePrevious={previousMedia} />}
55+
</div>
56+
)
57+
}
58+
59+
function CarouselControl({ isLeft = false, handleNext, handlePrevious }: { isLeft?: boolean, handleNext: () => void, handlePrevious: () => void }) {
60+
return (
61+
<button
62+
type="button"
63+
onClick={!isLeft ? handleNext : handlePrevious}
64+
className={`absolute z-10 flex h-8 w-8 items-center justify-center self-center rounded-full bg-neutral-700 text-sm text-white opacity-60 md:text-2xl ${isLeft ? 'md:left-8 left-6' : 'md:right-8 right-6'}`}
65+
>
66+
{isLeft ? <ArrowLeft className="h-4 w-4" /> : <ArrowRight className="h-4 w-4" />}
67+
</button>
68+
)
69+
}

0 commit comments

Comments
 (0)