fixed gradient animation

This commit is contained in:
liyunze 2025-12-01 14:48:03 +08:00
parent 389b1388be
commit 9c2a7cb269
19 changed files with 559 additions and 378 deletions

View file

@ -7,6 +7,7 @@
"@astrojs/mdx": "^4.3.0",
"@astrojs/react": "^4.3.0",
"@astrojs/svelte": "^7.1.0",
"@radix-ui/react-tabs": "^1.1.13",
"@tailwindcss/vite": "^4.1.8",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
@ -197,6 +198,36 @@
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="],

View file

@ -13,6 +13,7 @@
"@astrojs/mdx": "^4.3.0",
"@astrojs/react": "^4.3.0",
"@astrojs/svelte": "^7.1.0",
"@radix-ui/react-tabs": "^1.1.13",
"@tailwindcss/vite": "^4.1.8",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",

View file

@ -16,10 +16,9 @@ import Line from "../global/NetworkLine.astro";
</div>
<hr class="mb-3 w-full" />
<p class="mb-3">
Led implementation of frontend development, while also
developing and intergating a CMS on the backend for
website development. Actively contributing to LLM
product research and development.
Led development of Astro websites with Payload CMS
backend implementation, manage Github Actions CI/CD and
deployments.
</p>
<h3 class="mb-1 text-lg font-bold">Relevant skills:</h3>
<div
@ -49,8 +48,10 @@ import Line from "../global/NetworkLine.astro";
</div>
<hr class="mb-3 w-full" />
<p class="mb-3">
Awarded a 50% Vice-Chancellor Scholarship to study
Bachelor's in Computer Science.
Awarded 50% Vice-Chancellor Scholarship to study
Bachelor's in Computer Science. Active committee member
in cybersecurity association and software engineering
club.
</p>
<h3 class="mb-1 text-lg font-bold">Relevant skills:</h3>
<div

View file

@ -0,0 +1,242 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export default function Experience() {
return (
<Tabs
defaultValue="experience"
className="w-[90%] md:w-[600px] lg:w-1/2"
>
<TabsList className="w-full">
<TabsTrigger value="experience">Experience</TabsTrigger>
<TabsTrigger value="education">Education</TabsTrigger>
<TabsTrigger value="extracurricular">
Extracurricular
</TabsTrigger>
</TabsList>
<TabsContent
value="experience"
className="bg-secondary rounded-md px-5 py-5"
>
{ExperienceTabContent()}
</TabsContent>
<TabsContent
value="education"
className="bg-secondary rounded-md px-5 py-5"
>
{EducationTabContent()}
</TabsContent>
<TabsContent
value="extracurricular"
className="bg-secondary rounded-md px-5 py-5"
>
{ExtracurricularTabContent()}
</TabsContent>
</Tabs>
);
}
function ExperienceTabContent() {
return (
<>
{Position({
title: "Lead Full Stack Developer",
institution: "Hyson Horizon",
dateRange: "Dec 2024 - Now",
country: "Malaysia",
summary:
"Led development of clients' Astro websites with Payload CMS backend implementation, managed Github Actions CI/CD and deployments.",
className: "pb-5",
})}
{Position({
title: "Frontend Web Developer Intern",
institution: "Studio20",
dateRange: "Jan 2024 - Apr 2024",
country: "Malaysia",
summary:
"Developed WordPress sites and NextJS apps for clients. Used lazy-loading to optimise page load speeds. Ensured CMS allowed easy yet flexible customisations for clients.",
})}
</>
);
}
function EducationTabContent() {
return (
<>
{Position({
title: "Bachelors of Computer Science",
dateRange: "June 2024 - Now",
country: "Australia",
summary: `Awarded 50% Vice-Chancellor Scholarship to study
Bachelor's in Computer Science, minoring in Full Stack Development. Active committee member in cybersecurity association and software engineering club.`,
className: "pb-5",
})}
{Position({
title: "Diploma in Computer Science",
dateRange: "Jan 2022 - Apr 2024",
country: "Malaysia",
summary: `Focused in Data Science, took on role as President of Computer Programming Club and Vice President of Diploma Student Union.`,
})}
</>
);
}
function ExtracurricularTabContent() {
return (
<>
{Position({
title: "University Cybersecurity Association, Development Committee",
dateRange: "Nov 2025 - Now",
country: "Australia",
summary: `Leading frontend development club's revamped website using Astro framework with Svelte components.`,
className: "pb-5",
})}
{Position({
title: "University Software Engineering Club Committee",
dateRange: "Sep 2025 - Now",
country: "Australia",
summary: `Leading and contributing to various club projects:`,
className: "pb-5",
highlights: [
`Rust Discord Bot, includes club membership verification feature using Supabase.`,
`Python web scraping AI project, using BeautifulSoup4 and Gemini API.`,
],
})}
{Position({
title: "Malaysian Association, Marketing Officer",
dateRange: "Mar 2025 - Oct 2025",
country: "Australia",
summary: `Contributed to marketing strategy through video ideation, execution (including on-screen presence and filming), and video editing.`,
})}
</>
);
}
// ICONS AND SUBCOMPONENTS
function LocationIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z"
/>
</svg>
);
}
function CalendarIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z"
/>
</svg>
);
}
function BriefCaseIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 0 0 .75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 0 0-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0 1 12 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 0 1-.673-.38m0 0A2.18 2.18 0 0 1 3 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 0 1 3.413-.387m7.5 0V5.25A2.25 2.25 0 0 0 13.5 3h-3a2.25 2.25 0 0 0-2.25 2.25v.894m7.5 0a48.667 48.667 0 0 0-7.5 0M12 12.75h.008v.008H12v-.008Z"
/>
</svg>
);
}
const lineMoveCSS = `
@keyframes line-move {
0% { background-position: 0% 50%; }
100% { background-position: 0% -100%; }
}
.animate-line-move {
animation: line-move 2s linear infinite;
background-size: 100% 200%;
}
`;
function Position({
title,
institution,
dateRange,
country,
summary,
className = "",
highlights = [],
}: {
title: string;
institution?: string;
dateRange: string;
country: string;
summary: string;
className?: string;
highlights?: Array<string>;
}) {
return (
<>
<style>{lineMoveCSS}</style>
<div className="position-el flex flex-row items-start gap-3">
<div className="flex flex-col items-center justify-start gap-2 self-stretch pt-2">
<div className="bg-accent h-2 w-2 rounded-full"></div>
<div className="animate-line-move w-[1px] flex-grow bg-gradient-to-b from-gray-800 from-0% via-gray-300 via-5% to-gray-800 to-10%"></div>
</div>
<div className={className}>
<h2 className="text-lg font-bold">{title}</h2>
<div className="mt-1 flex flex-col items-start justify-start gap-1">
<div className="text-muted-foreground flex flex-row items-center justify-start gap-1 bg-size-[100%_200%] text-xs">
{CalendarIcon()} {dateRange}
</div>
<div className="text-muted-foreground mb-2 flex w-full flex-row items-center justify-start gap-5 text-xs">
{institution && (
<div className="flex flex-row items-center justify-start gap-1">
{BriefCaseIcon()} {institution}
</div>
)}
<div className="flex flex-row items-center justify-start gap-1">
{LocationIcon()} {country}
</div>
</div>
</div>
<p className="text-base">{summary}</p>
<ul className="list-inside list-disc">
{highlights.map((highlight) => (
<li>{highlight}</li>
))}
</ul>
</div>
</div>
</>
);
}

View file

@ -1,35 +0,0 @@
---
import Card from "../global/Card.astro";
import Line from "../global/NetworkLine.astro";
---
<div id="Experience" class="h-fit">
<div class="flex flex-col items-center">
<div class="w-[90%] md:w-[600px] lg:w-1/2">
<Line height="50px" />
<Card primaryColor="#FFAC21" className="view-fadeUp">
<div class="px-4 py-4 md:px-8 md:py-8">
<div class="mb-3 flex w-full flex-col justify-start gap-1">
<h2 class="text-xl font-bold">
Diploma in Computer Science student
</h2>
<h3 class="font-bold">
@ Methodist College Kuala Lumpur
</h3>
<h3>Jan 2022 - June 2024</h3>
</div>
<hr class="mb-3 w-full" />
<h3 class="mb-1 text-lg font-bold">Achievements</h3>
<ul class="list-inside list-disc">
<li>
4-time Dean's List Award recipient (&gt;3.5 GPA)
</li>
<li>3.8 CGPA</li>
<li>President of Computer Programming Club</li>
<li>Vice President of Diploma Union</li>
</ul>
</div>
</Card>
</div>
</div>
</div>

View file

@ -11,7 +11,6 @@ type Props = {
const {
href,
primaryColor,
shadowColor = "rgb(12,12,12)",
bgColor = "var(--color-secondary)",
scaleOnHover = false,

View file

@ -4,23 +4,23 @@ import { HoverNavigation } from "@/components/ui/NavigationBar";
const navLinks = [
{
label: "Home",
link: "/",
link: "/#home",
},
{
label: "Experience",
link: "/experience",
link: "/#experience",
},
{
label: "Projects",
link: "/projects",
link: "/#projects",
},
{
label: "Skills",
link: "/skills",
link: "/#skills",
},
{
label: "Contact",
link: "/contact",
link: "/#contact",
},
];
---

View file

@ -18,23 +18,7 @@ import TwitchPlays from "../images/projects/twitchplays.webp";
chess engine to determine the best moves. Then uses
PyAutoGUI to play the moves on the browser.
`}
tags={[
{
text: "Python",
textColor: "white",
bgColor: "var(--color-python)",
},
{
text: "OpenCV",
textColor: "white",
bgColor: "var(--color-opencv)",
},
{
text: "PyAutoGUI",
textColor: "white",
bgColor: "black",
},
]}
tags={["Python", "OpenCV", "PyAutoGUI"]}
client:visible
/>
<Project
@ -47,18 +31,7 @@ import TwitchPlays from "../images/projects/twitchplays.webp";
Chatters can enter messages such as "up up down down left
right B A" and the Python program will trigger the
corresponding keypresses.`}
tags={[
{
text: "win32API",
bgColor: "var(--color-win32api)",
textColor: "white",
},
{
text: "Python",
bgColor: "var(--color-python)",
textColor: "white",
},
]}
tags={["win32API", "Python"]}
/>
</div>
</div>

View file

@ -1,12 +1,6 @@
---
import { Image } from "astro:assets";
interface Tag {
text: string;
textColor: string;
bgColor: string;
}
interface Props {
position?: "left" | "right";
image?: ImageMetadata;
@ -15,7 +9,7 @@ interface Props {
title: string;
description: string;
label?: string;
tags?: Tag[];
tags?: Array<string>;
}
const {
@ -117,7 +111,7 @@ const leftProject = position == "left";
<div
class="my-1 mr-1 w-fit rounded-full px-3 py-1 transition-colors duration-300 text-white bg-secondary"
>
{tag.text}
{tag}
</div>
))
}

View file

@ -19,23 +19,7 @@ import Project from "./Project.astro";
title="Coworking Guide"
label="Visit Site"
description="A guide to coworking streams for streamers, covers wide range of topics from tips on how to get started with setting up stream to how to grow your audience!"
tags={[
{
text: "Astro",
bgColor: "white",
textColor: "black",
},
{
text: "TypeScript",
bgColor: "#3178C6",
textColor: "white",
},
{
text: "Markdown",
bgColor: "black",
textColor: "white",
},
]}
tags={["Astro", "Typescript", "Markdown"]}
/>
<VideoProject
@ -48,23 +32,7 @@ import Project from "./Project.astro";
description={`
A script that plays AimLab, a game that helps improve your aim in FPS games. Uses OpenCV to detect the targets and win32API to move the mouse and click.
`}
tags={[
{
text: "Python",
textColor: "white",
bgColor: "var(--color-python)",
},
{
text: "OpenCV",
textColor: "white",
bgColor: "var(--color-opencv)",
},
{
text: "win32API",
textColor: "white",
bgColor: "var(--color-win32api)",
},
]}
tags={["Python", "OpenCV", "win32API"]}
/>
<Project
@ -75,18 +43,7 @@ import Project from "./Project.astro";
label="Visit Site"
title="We're not really strangers"
description="This is a digital adaptation of the popular card game designed to foster meaningful connections through engaging questions and prompts. Made into a PWA for offline usage on mobile!"
tags={[
{
text: "React",
bgColor: "var(--color-react)",
textColor: "black",
},
{
text: "TypeScript",
bgColor: "var(--color-typescript)",
textColor: "white",
},
]}
tags={["React", "Typescript"]}
/>
<Project
@ -99,23 +56,7 @@ import Project from "./Project.astro";
to add their own tasks to the widget for accountability!
Easy to setup and the customization possibilities are
endless!`}
tags={[
{
text: "HTML",
bgColor: "var(--color-html)",
textColor: "white",
},
{
text: "CSS",
bgColor: "var(--color-blue-600)",
textColor: "white",
},
{
text: "Javascript",
bgColor: "var(--color-yellow-500)",
textColor: "black",
},
]}
tags={["HTML", "CSS", "Javascript"]}
>
<TaskWidget
classList={`

View file

@ -1,12 +1,6 @@
<script lang="ts">
import { onMount } from "svelte";
interface Tag {
text: string;
textColor: string;
bgColor: string;
}
interface Props {
position?: "left" | "right";
image_src: string;
@ -14,7 +8,7 @@
video_src: string;
title: string;
description: string;
tags?: Tag[];
tags?: Array<string>;
}
let {
@ -137,7 +131,7 @@
w-fit rounded-full px-3
py-1 text-white transition-colors duration-300"
>
{tag.text}
{tag}
</div>
{/each}
</div>

View file

@ -1,18 +1,10 @@
---
import Card from "../global/Card.astro";
interface Tag {
text: string;
bgColor: string;
textColor?: string;
hoverTextColor?: string;
className?: string;
}
interface Props {
title: string;
tags?: Tag[];
primaryColor: string;
tags?: Array<string>;
primaryColor?: string;
className?: string;
}
@ -35,7 +27,7 @@ const { title, tags, primaryColor, className = "" } = Astro.props;
<div
class={`tag-${index} my-1 mr-1 w-fit cursor-default rounded-full bg-[#303030] px-3 py-1 text-white transition-colors duration-300`}
>
{tag.text}
{tag}
</div>
))
}

View file

@ -13,51 +13,16 @@ import Skill from "./Skill.astro";
title="Frontend Development"
primaryColor="#19a7ce"
tags={[
{
text: "HTML",
bgColor: "var(--color-html)",
},
{
text: "CSS",
bgColor: "var(--color-blue-700)",
},
{
text: "Javascript",
bgColor: "var(--color-yellow-400)",
textColor: "black",
},
{
text: "Typescript",
bgColor: "var(--color-typescript)",
},
{
text: "Svelte",
bgColor: "var(--color-svelte)",
},
{
text: "React",
bgColor: "var(--color-react)",
textColor: "black",
},
{
text: "Vue",
bgColor: "var(--color-green-500)",
textColor: "black",
},
{
text: "WordPress",
bgColor: "var(--color-wordpress)",
},
{
text: "Astro",
bgColor: "white",
textColor: "black",
},
{
text: "TailwindCSS",
bgColor: "var(--color-tailwind)",
textColor: "black",
},
"HTML",
"CSS",
"Javascript",
"Typescript",
"Svelte",
"React",
"Vue",
"WordPress",
"Astro",
"TailwindCSS",
]}
/>
<Line height="50px" />
@ -66,38 +31,15 @@ import Skill from "./Skill.astro";
title="Backend Development"
primaryColor="#474F7A"
tags={[
{
text: "Python",
bgColor: "var(--color-python)",
},
{
text: "Typescript",
bgColor: "var(--color-typescript)",
},
{
text: "C#",
bgColor: "var(--color-csharp)",
},
{
text: "Kotlin",
bgColor: "var(--color-kotlin)",
},
{
text: "PHP",
bgColor: "var(--color-php)",
},
{
text: "PostgreSQL",
bgColor: "var(--color-postgresql)",
},
{
text: "MySQL",
bgColor: "var(--color-mysql)",
},
{
text: "MongoDB",
bgColor: "var(--color-mongodb)",
},
"Python",
"Typescript",
"C#",
"Kotlin",
"PHP",
"PostgreSQL",
"MySQL",
"MongoDB",
"Supabase",
]}
/>
<Line height="50px" />
@ -106,39 +48,14 @@ import Skill from "./Skill.astro";
title="Data Science"
primaryColor="#a1eebd"
tags={[
{
text: "Python",
bgColor: "var(--color-python)",
},
{
text: "R",
bgColor: "var(--color-cyan-500)",
},
{
text: "OpenCV",
bgColor: "var(--color-opencv)",
},
{
text: "SAS",
bgColor: "var(--color-blue-500)",
},
{
text: "MySQL",
bgColor: "var(--color-mysql)",
},
{
text: "Pandas",
bgColor: "var(--color-blue-800)",
},
{
text: "Selenium",
bgColor: "var(--color-green-700)",
},
{
text: "BeautifulSoup4",
bgColor: "white",
textColor: "black",
},
"Python",
"R",
"OpenCV",
"SAS",
"MySQL",
"Pandas",
"Selenium",
"BeautifulSoup4",
]}
/>
</div>

View file

@ -49,54 +49,9 @@ export const HoverNavigation = ({
className?: string;
}) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const { scrollY } = useScroll();
const [navVisible, setNavVisible] = useState(true);
const [availableVisible, setAvailableVisible] = useState(false);
const [onContactPage, setOnContactPage] = useState(false);
const [hamburgerOpen, setHamburgerOpen] = useState(false);
const { width } = useWindowDimensions();
// if on contact page, do not do width 0%
useEffect(() => {
setOnContactPage(window.location.href.includes("/contact"));
}, []);
useEffect(() => {
updateNavBarState(width);
}, [width]);
const updateNavBarState = (
width: number,
scrollPosition: number = 0,
scrollUp: boolean = false,
) => {
if (width && width < 1024) {
setNavVisible(false);
setAvailableVisible(true);
} else if (scrollPosition < 50) {
setNavVisible(true);
setAvailableVisible(false);
} else if (!onContactPage) {
setNavVisible(scrollUp);
setAvailableVisible(!scrollUp);
} else {
setNavVisible(true);
setAvailableVisible(false);
}
};
useMotionValueEvent(scrollY, "change", (current) => {
// Check if current is not undefined and is a number
if (typeof current === "number") {
let direction = current! - scrollY.getPrevious()!;
let scrollUp = direction <= 0;
let scrollProgress = scrollY.get();
updateNavBarState(width, scrollProgress, scrollUp);
}
});
return (
<div className="bg-secondary/50 fixed top-5 left-1/2 z-30 box-border flex w-11/12 -translate-x-1/2 items-center justify-center overflow-auto rounded-[40px] border-2 border-solid border-gray-700 px-5 py-3 backdrop-blur-md sm:w-4/5 lg:w-2/3 xl:w-auto">
<div className="flex flex-col">
@ -112,13 +67,7 @@ export const HoverNavigation = ({
</a>
</div>
<div className="flex flex-col items-center justify-center">
<motion.div
animate={{
width: availableVisible ? "auto" : "0px",
opacity: availableVisible ? "100%" : "0%",
}}
className="flex w-auto flex-row items-center justify-center pr-2 whitespace-nowrap opacity-100 lg:w-0 lg:opacity-0"
>
<motion.div className="flex w-auto flex-row items-center justify-center pr-2 whitespace-nowrap opacity-100 lg:w-0 lg:opacity-0">
<a
tabIndex={0}
href="/contact"
@ -138,10 +87,6 @@ export const HoverNavigation = ({
>
<motion.ul
className="hidden w-0 flex-row items-center justify-center overflow-hidden lg:flex lg:w-auto"
animate={{
width: navVisible ? "auto" : "0px",
opacity: navVisible ? "100%" : "0%",
}}
transition={{
ease: "easeInOut",
}}

View file

@ -0,0 +1,141 @@
"use client";
import React from "react";
import {
motion,
useAnimationFrame,
useMotionTemplate,
useMotionValue,
useTransform,
} from "motion/react";
import { useRef } from "react";
import { cn } from "@/lib/utils";
export function Button({
borderRadius = "1.75rem",
children,
as: Component = "button",
containerClassName,
borderClassName,
duration,
className,
...otherProps
}: {
borderRadius?: string;
children: React.ReactNode;
as?: any;
containerClassName?: string;
borderClassName?: string;
duration?: number;
className?: string;
[key: string]: any;
}) {
return (
<Component
className={cn(
"relative h-16 w-fit overflow-hidden bg-transparent p-[1px] text-xl",
containerClassName,
)}
style={{
borderRadius: borderRadius,
}}
{...otherProps}
>
<div
className="absolute inset-0"
style={{ borderRadius: `calc(${borderRadius} * 0.96)` }}
>
<MovingBorder duration={duration} rx="30%" ry="30%">
<div
className={cn(
"h-20 w-20 bg-[radial-gradient(#72ffff_40%,transparent_60%)] opacity-[0.8]",
borderClassName,
)}
/>
</MovingBorder>
</div>
<div
className={cn(
"relative flex h-full w-full items-center justify-center border border-slate-800 bg-slate-900/[0.8] text-sm text-white antialiased backdrop-blur-xl",
className,
)}
style={{
borderRadius: `calc(${borderRadius} * 0.96)`,
}}
>
{children}
</div>
</Component>
);
}
export const MovingBorder = ({
children,
duration = 3000,
rx,
ry,
...otherProps
}: {
children: React.ReactNode;
duration?: number;
rx?: string;
ry?: string;
[key: string]: any;
}) => {
// @ts-expect-error
const pathRef = useRef<any>();
const progress = useMotionValue<number>(0);
useAnimationFrame((time) => {
const length = pathRef.current?.getTotalLength();
if (length) {
const pxPerMillisecond = length / duration;
progress.set((time * pxPerMillisecond) % length);
}
});
const x = useTransform(
progress,
(val) => pathRef.current?.getPointAtLength(val).x,
);
const y = useTransform(
progress,
(val) => pathRef.current?.getPointAtLength(val).y,
);
const transform = useMotionTemplate`translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)`;
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
className="absolute h-full w-full"
width="100%"
height="100%"
{...otherProps}
>
<rect
fill="none"
width="100%"
height="100%"
rx={rx}
ry={ry}
ref={pathRef}
/>
</svg>
<motion.div
style={{
position: "absolute",
top: 0,
left: 0,
display: "inline-block",
transform,
}}
>
{children}
</motion.div>
</>
);
};

View file

@ -0,0 +1,66 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"text-primary inline-flex h-9 w-fit items-center justify-center rounded-lg bg-[#101010] p-[3px]",
className,
)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-secondary focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-primary hover:bg-secondary/20 inline-flex h-[calc(100%-1px)] flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-5 text-base font-medium whitespace-nowrap transition-[color,box-shadow] duration-300 focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };

View file

@ -1,18 +0,0 @@
---
import ExperienceComponent from "../components/experience/ExperienceComponent.astro";
import MoreExperience from "../components/experience/MoreExperience.astro";
import Footer from "../components/global/FooterComponent.astro";
import PageLayout from "@/layouts/PageLayout.astro";
---
<PageLayout
title="Experience"
description="Find out RythonDev's previous education"
thumbnail="/images/profile/contact_photo.webp"
>
<div id="Experience" class="pt-30">
<ExperienceComponent />
<MoreExperience />
</div>
<Footer />
</PageLayout>

View file

@ -2,11 +2,12 @@
import NetworkLine from "../components/global/NetworkLine.astro";
import Intro from "../components/home/Intro.astro";
import Projects from "../components/projects/ProjectsComponent.astro";
import Experience from "../components/experience/ExperienceComponent.astro";
import Experience from "../components/experience/ExperienceComponent.tsx";
import Skills from "../components/skills/SkillsComponent.astro";
import Contact from "../components/contact/ContactComponent.astro";
import Footer from "../components/global/FooterComponent.astro";
import PageLayout from "@/layouts/PageLayout.astro";
import { Button } from "@/components/ui/moving-border";
---
<PageLayout
@ -18,17 +19,31 @@ import PageLayout from "@/layouts/PageLayout.astro";
<Intro />
</div>
<NetworkLine height="70px" />
<div id="education">
<div class="mx-auto mb-10 h-[1px] w-1/3 bg-[#404040]"></div>
<Experience />
<div
id="experience"
class="flex scroll-mt-18 flex-col items-center justify-center"
>
<!-- horizontal line -->
<hr class="mx-auto mb-10 h-[1px] w-1/3 border-[#404040]" />
<Experience client:visible />
</div>
<div id="projects">
<div class="pt-20">
<div id="projects" class="scroll-mt-12">
<div class="flex flex-col items-center justify-center pt-20">
<h1 class="mb-10 text-center text-5xl">Projects</h1>
<Projects />
<Button
client:visible
borderRadius="1.75rem"
className="bg-secondary cursor-pointer border-slate-800 px-5 text-white transition-all duration-300 hover:brightness-125"
duration={2000}
as={"a"}
href="/projects"
>
VIEW ALL PROJECTS
</Button>
</div>
</div>
<div id="skills">
<div id="skills" class="scroll-mt-16">
<Skills />
</div>
<div id="contact" class="pt-20">
@ -36,6 +51,7 @@ import PageLayout from "@/layouts/PageLayout.astro";
</div>
<Footer />
</PageLayout>
<script is:inline>
location.search.includes("?uwu") &&
document.documentElement.classList.add("uwu");

View file

@ -1,34 +1,15 @@
@import "tailwindcss";
@theme {
--color-html: #b11700;
--color-typescript: #3178c6;
--color-svelte: #b11700;
--color-react: #00d8ff;
--color-wordpress: #21759b;
--color-htmx: #3366cc;
--color-tailwind: #07b6d4;
--color-python: #3776ab;
--color-csharp: #672079;
--color-java: #ed8b00;
--color-kotlin: #7f52ff;
--color-php: #3d5694;
--color-go: #006080;
--color-rust: #b11700;
--color-postgresql: #4479a1;
--color-mysql: #1d6998;
--color-mongodb: #09934e;
--color-opencv: #10652f;
--color-win32api: #0078d4;
--color-primary: #fafafa;
--color-secondary: #1d1c20;
--color-accent: #72ffff;
--color-muted-foreground: #ccc;
--animate-fadeIn: fadeIn 0.5s ease-in-out;
--animate-fadeUp: fadeUp 0.5s ease-in-out;
--animate-glow: glow 2s ease infinite;
/* --gradient-primary: */
@keyframes fadeIn {
0% {
opacity: 0;