Personal Website for @jaspermayone.com jaspermayone.com
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

oooh shiny

+274 -43
+18 -7
src/app/page.tsx
··· 5 5 import Email from "@/components/email"; 6 6 import styles from "@/styles/Home.module.css"; 7 7 import CommitHash from "@/components/helpers/commitHash"; 8 + import SquigglyLine from "@/components/SquigglyLine"; 8 9 import { 9 10 SiGithub, 10 11 SiInstagram, ··· 14 15 SiBluesky, 15 16 } from "react-icons/si"; 16 17 import RoundedImage from "@/components/RoundedImage"; 18 + import Experience from "@/components/experience"; 19 + import AnimatedTitle from "@/components/AnimatedTitle"; 17 20 18 21 export default function Home({}) { 19 22 const [selectedTab, setSelectedTab] = useState("Homepage"); ··· 38 41 case "Homepage": 39 42 return ( 40 43 <> 41 - <div className="flex flex-col md:flex-row gap-8 items-center"> 44 + <div className="flex flex-col md:flex-row gap-8 items-center -mt-8"> 42 45 <div className="w-64 flex-shrink-0"> 43 46 <RoundedImage 44 47 src="/images/jmdark-min.jpg" ··· 83 86 </div> 84 87 </div> 85 88 </div> 86 - <div className="py-5" /> 87 - <Email /> 89 + <div className="py-2.5" /> 90 + <SquigglyLine 91 + width="104%" 92 + frequency={175} 93 + amplitude={1} 94 + className="-ml-10" 95 + /> 96 + <div className="py-1.5" /> 97 + <div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-2"> 98 + <Experience /> 99 + <Email /> 100 + </div> 88 101 </> 89 102 ); 90 103 case "Portfolio": ··· 122 135 > 123 136 <div className={styles.container}> 124 137 <div className={styles.top}> 125 - <h1 className={styles.title} title="Jasper Mayone's website"> 126 - Jasper Mayone 127 - </h1> 138 + <AnimatedTitle /> 128 139 <div className={styles.menuContainer}> 129 140 <div className={styles.menu} aria-label="main menu"> 130 141 {menuItems.map((item) => ( ··· 210 221 {renderContent()} 211 222 </motion.div> 212 223 </div> 213 - <footer className="flex flex-col items-center mb-3.5"> 224 + <footer className="flex flex-col items-center mb-3.5 -mt-4"> 214 225 <div className="flex items-center justify-center"> 215 226 <p className="text-xs mr-1.5"> 216 227 © {new Date().getFullYear()} Jasper Mayone
+52
src/components/AnimatedTitle.tsx
··· 1 + import React, { useState, useEffect } from "react"; 2 + import styles from "@/styles/AnimatedTitle.module.css"; 3 + 4 + const AnimatedTitle = () => { 5 + const [activeIndex, setActiveIndex] = useState(-1); 6 + const firstWord = "Jasper"; 7 + const secondWord = "Mayone"; 8 + const totalLength = firstWord.length + secondWord.length; 9 + 10 + useEffect(() => { 11 + const animate = () => { 12 + let currentIndex = -1; 13 + const interval = setInterval(() => { 14 + currentIndex = (currentIndex + 1) % (totalLength + 1); 15 + setActiveIndex(currentIndex === totalLength ? -1 : currentIndex); 16 + }, 300); 17 + return () => clearInterval(interval); 18 + }; 19 + const animation = animate(); 20 + return () => animation(); 21 + }, []); 22 + 23 + return ( 24 + <h1 className={styles.title} title="Jasper Mayone's website"> 25 + {firstWord.split("").map((letter, index) => ( 26 + <span 27 + key={`first-${index}`} 28 + className="transition-colors duration-300" 29 + style={{ 30 + color: index === activeIndex ? "#4299e1" : "inherit", 31 + }} 32 + > 33 + {letter} 34 + </span> 35 + ))}{" "} 36 + {secondWord.split("").map((letter, index) => ( 37 + <span 38 + key={`second-${index}`} 39 + className="transition-colors duration-300" 40 + style={{ 41 + color: 42 + index + firstWord.length === activeIndex ? "#4299e1" : "inherit", 43 + }} 44 + > 45 + {letter} 46 + </span> 47 + ))} 48 + </h1> 49 + ); 50 + }; 51 + 52 + export default AnimatedTitle;
+71
src/components/SquigglyLine.tsx
··· 1 + // components/SquigglyLine.tsx 2 + "use client"; 3 + 4 + import { FC, useMemo } from "react"; 5 + 6 + interface SquigglyLineProps { 7 + width?: string | number; 8 + height?: string | number; 9 + color?: string; 10 + className?: string; 11 + frequency?: number; // How many waves 12 + amplitude?: number; // Height of waves 13 + } 14 + 15 + const SquigglyLine: FC<SquigglyLineProps> = ({ 16 + width = "100%", 17 + height = "8", 18 + color = "#4299e1", 19 + className = "", 20 + frequency = 6, // Default number of waves 21 + amplitude = 1, // Default wave height 22 + }) => { 23 + const generatePath = useMemo(() => { 24 + // Convert width to number if it's a string with units 25 + const numWidth = 26 + typeof width === "string" ? parseFloat(width) || 100 : width; 27 + 28 + const points = []; 29 + const segments = frequency * 2; // Two control points per wave 30 + const segmentWidth = numWidth / segments; 31 + 32 + for (let i = 0; i <= segments; i++) { 33 + const x = i * segmentWidth; 34 + const y = i % 2 === 0 ? 0.5 : i % 4 === 1 ? -amplitude : amplitude; 35 + // @ts-expect-error 36 + points.push(`${x},${y}`); 37 + } 38 + 39 + // Create the path using quadratic curves 40 + let path = `M${points[0]}`; 41 + for (let i = 1; i < points.length; i++) { 42 + // @ts-expect-error 43 + const [x, y] = points[i].split(","); 44 + path += ` Q${x},${y} ${x},${y}`; 45 + } 46 + 47 + return path; 48 + }, [width, frequency, amplitude]); 49 + 50 + return ( 51 + <svg 52 + width={width} 53 + height={height} 54 + className={`overflow-visible ${className}`} 55 + preserveAspectRatio="none" 56 + viewBox={`0 -${amplitude} ${typeof width === "number" ? width : 100} ${amplitude * 2}`} 57 + > 58 + <path 59 + d={generatePath} 60 + style={{ 61 + stroke: color, 62 + strokeWidth: "1.5px", 63 + vectorEffect: "non-scaling-stroke", 64 + fill: "none", 65 + }} 66 + /> 67 + </svg> 68 + ); 69 + }; 70 + 71 + export default SquigglyLine;
+1 -1
src/components/email.tsx
··· 64 64 return ( 65 65 <> 66 66 <div className="space-y" aria-label="Jasper's Newsletter Signup"> 67 - <p className="font-[450]">Newsletter</p> 67 + <p className="font-medium text-xl">Newsletter</p> 68 68 <p className="text-gray-500"> 69 69 Join my newsletter to get updates on new projects. 70 70 </p>
+52
src/components/experience.tsx
··· 1 + import React from "react"; 2 + import { 3 + Accordion, 4 + AccordionContent, 5 + AccordionItem, 6 + AccordionTrigger, 7 + } from "@/components/ui/accordion"; 8 + 9 + interface ExperienceItem { 10 + title: string; 11 + role: string; 12 + date: string; 13 + link: string; 14 + } 15 + 16 + import { experience } from "@/lib/experience"; 17 + 18 + export default function Experience() { 19 + const midpoint = Math.ceil(experience.length / 2); 20 + const leftColumn = experience.slice(0, midpoint); 21 + const rightColumn = experience.slice(midpoint); 22 + 23 + const ExperienceColumn = ({ items }: { items: ExperienceItem[] }) => ( 24 + <div className="flex-1"> 25 + {items.map((item, index) => ( 26 + <div key={item.title} className="mb-2"> 27 + <Accordion type="single" collapsible className="w-full"> 28 + <AccordionItem value={index.toString()} className="border-b-0"> 29 + <AccordionTrigger className="py-2 hover:no-underline hover:bg-gray-50 rounded-lg px-3 text-sm"> 30 + <div className="text-left">{item.title}</div> 31 + </AccordionTrigger> 32 + <AccordionContent className="px-3 py-2"> 33 + <p className="text-gray-700 text-sm">{item.role}</p> 34 + <p className="text-xs text-gray-500 mt-1">{item.date}</p> 35 + </AccordionContent> 36 + </AccordionItem> 37 + </Accordion> 38 + </div> 39 + ))} 40 + </div> 41 + ); 42 + 43 + return ( 44 + <div className="w-full max-w-6xl mx-auto"> 45 + <h2 className="font-medium text-xl pb-4">Experience</h2> 46 + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 47 + <ExperienceColumn items={leftColumn} /> 48 + <ExperienceColumn items={rightColumn} /> 49 + </div> 50 + </div> 51 + ); 52 + }
+69
src/lib/experience.tsx
··· 1 + interface ExperienceItem { 2 + title: string; 3 + role: string; 4 + date: string; 5 + link: string; 6 + } 7 + 8 + export const experience: ExperienceItem[] = [ 9 + { 10 + title: "Jewish Community of Greater Stowe (JCOGS), Stowe, VT", 11 + role: "Part Time Technology Coordinator", 12 + date: "September 2023 - Present", 13 + link: "https://www.jcogs.org/", 14 + }, 15 + { 16 + title: "The Hack Foundation, Shelburne, VT", 17 + role: "Gap Year Software Engineer", 18 + date: "September 2023 - July 2024", 19 + link: "https://www.hackclub.com/", 20 + }, 21 + { 22 + title: "Harwood Union High School, Duxbury, VT", 23 + role: "Student Tech Assistant", 24 + date: "September 2021 - June 2024", 25 + link: "https://huusd.org/", 26 + }, 27 + { 28 + title: "Three Mountain Associates, Waitsfield, VT", 29 + role: "Rep in training/Intern", 30 + date: "June 2018 - Present", 31 + link: "https://threemountainassociates.com/", 32 + }, 33 + { 34 + title: "Signal Kitchen, Burlington, VT", 35 + role: "Event Registration Coordinator & Assistant to Event Director, Bookshop Manager", 36 + date: "February 2023 - February 2024", 37 + link: "https://signalkitchen.com/", 38 + }, 39 + { 40 + title: "Sadie Dog LLC, Warren, VT", 41 + role: "Busser / Event Staff", 42 + date: "November 2022 - December 2023", 43 + link: "https://www.pitcherinn.com/", 44 + }, 45 + { 46 + title: "Valley Players Theater, Waitsfield, VT", 47 + role: "Lighting Designer & Operator", 48 + date: "December 2019 - November 2023", 49 + link: "https://valleyplayers.com/", 50 + }, 51 + { 52 + title: "DeJames Hospitality, Waitsfield, VT", 53 + role: "Busser / Event Staff", 54 + date: "May 2022 - May 2023", 55 + link: "https://theroundbarn.com/", 56 + }, 57 + { 58 + title: "Sugarbush Soaring Association, Warren, VT", 59 + role: "Line Crew Member", 60 + date: "July 2021 - September 2021", 61 + link: "https://sugarbushsoaring.com/", 62 + }, 63 + { 64 + title: "Mad River Valley Television, Waitsfield, VT", 65 + role: "Media Intern", 66 + date: "May 2020 - October 2020", 67 + link: "https://mrvtv.com/", 68 + }, 69 + ];
+5
src/styles/AnimatedTitle.module.css
··· 1 + .title { 2 + margin: 0; /* Remove default margin */ 3 + font-size: 3.5em; 4 + font-family: "Cute Notes", sans-serif; 5 + }
-29
src/styles/DotsBackground.module.css
··· 1 - /* DotsBackground.module.css */ 2 - .container { 3 - position: absolute; /* Use absolute positioning to cover the viewport */ 4 - top: 0; 5 - left: 0; 6 - width: 100vw; /* Full viewport width */ 7 - height: 100vh; /* Full viewport height */ 8 - overflow: hidden; 9 - z-index: -10; 10 - } 11 - 12 - .dot { 13 - position: absolute; 14 - border-radius: 50%; 15 - opacity: 0.7; 16 - animation: move 5s ease-in-out infinite; 17 - } 18 - 19 - /* Adjust the keyframes to ensure dots stay within the container */ 20 - @keyframes move { 21 - 0% { 22 - transform: translateY(0); /* Adjust if needed */ 23 - } 24 - 100% { 25 - transform: translateY( 26 - calc(100% - 100px) 27 - ); /* Ensure dots don’t exceed container height */ 28 - } 29 - }
+6 -6
src/styles/Home.module.css
··· 64 64 } 65 65 66 66 .menu p:hover { 67 - color: #78cbf2; /* Change text color when hovered (adjust to desired color) */ 67 + color: #4299e1; /* Change text color when hovered (adjust to desired color) */ 68 68 } 69 69 70 70 .menu p.selected { 71 71 text-decoration: underline wavy; 72 - text-decoration-color: #78cbf2; /* Wavy underline for selected items */ 72 + text-decoration-color: #4299e1; /* Wavy underline for selected items */ 73 73 } 74 74 75 75 .profilePhoto { ··· 105 105 } 106 106 107 107 .footerLink { 108 - color: #78cbf2; /* Link color */ 108 + color: #4299e1; /* Link color */ 109 109 text-decoration: underline wavy; /* Add an underline */ 110 110 transition: color 0.3s ease; /* Smooth color transition on hover */ 111 111 } 112 112 113 113 .lnk { 114 - color: #78cbf2; /* Link color */ 114 + color: #4299e1; /* Link color */ 115 115 text-decoration: underline wavy; /* Add an underline */ 116 116 transition: color 0.3s ease; /* Smooth color transition on hover */ 117 117 } 118 118 119 119 .lnk:hover { 120 - color: #408da3; /* Change color on hover */ 120 + color: #003e73; /* Change color on hover */ 121 121 } 122 122 123 123 .footerLink:hover { 124 - color: #408da3; /* Change color on hover */ 124 + color: #003e73; /* Change color on hover */ 125 125 }