updated experience component

This commit is contained in:
liyunze 2025-11-30 22:59:00 +08:00
parent 389b1388be
commit 1a4213879b
19 changed files with 545 additions and 376 deletions

View file

@ -7,6 +7,7 @@
"@astrojs/mdx": "^4.3.0", "@astrojs/mdx": "^4.3.0",
"@astrojs/react": "^4.3.0", "@astrojs/react": "^4.3.0",
"@astrojs/svelte": "^7.1.0", "@astrojs/svelte": "^7.1.0",
"@radix-ui/react-tabs": "^1.1.13",
"@tailwindcss/vite": "^4.1.8", "@tailwindcss/vite": "^4.1.8",
"@types/react": "^19.1.9", "@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",
@ -197,6 +198,36 @@
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], "@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=="], "@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=="], "@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/mdx": "^4.3.0",
"@astrojs/react": "^4.3.0", "@astrojs/react": "^4.3.0",
"@astrojs/svelte": "^7.1.0", "@astrojs/svelte": "^7.1.0",
"@radix-ui/react-tabs": "^1.1.13",
"@tailwindcss/vite": "^4.1.8", "@tailwindcss/vite": "^4.1.8",
"@types/react": "^19.1.9", "@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",

View file

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

View file

@ -0,0 +1,228 @@
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>
);
}
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 (
<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="w-[1px] flex-grow bg-gray-400"></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 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 { const {
href, href,
primaryColor,
shadowColor = "rgb(12,12,12)", shadowColor = "rgb(12,12,12)",
bgColor = "var(--color-secondary)", bgColor = "var(--color-secondary)",
scaleOnHover = false, scaleOnHover = false,

View file

@ -4,23 +4,23 @@ import { HoverNavigation } from "@/components/ui/NavigationBar";
const navLinks = [ const navLinks = [
{ {
label: "Home", label: "Home",
link: "/", link: "/#home",
}, },
{ {
label: "Experience", label: "Experience",
link: "/experience", link: "/#experience",
}, },
{ {
label: "Projects", label: "Projects",
link: "/projects", link: "/#projects",
}, },
{ {
label: "Skills", label: "Skills",
link: "/skills", link: "/#skills",
}, },
{ {
label: "Contact", 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 chess engine to determine the best moves. Then uses
PyAutoGUI to play the moves on the browser. PyAutoGUI to play the moves on the browser.
`} `}
tags={[ tags={["Python", "OpenCV", "PyAutoGUI"]}
{
text: "Python",
textColor: "white",
bgColor: "var(--color-python)",
},
{
text: "OpenCV",
textColor: "white",
bgColor: "var(--color-opencv)",
},
{
text: "PyAutoGUI",
textColor: "white",
bgColor: "black",
},
]}
client:visible client:visible
/> />
<Project <Project
@ -47,18 +31,7 @@ import TwitchPlays from "../images/projects/twitchplays.webp";
Chatters can enter messages such as "up up down down left Chatters can enter messages such as "up up down down left
right B A" and the Python program will trigger the right B A" and the Python program will trigger the
corresponding keypresses.`} corresponding keypresses.`}
tags={[ tags={["win32API", "Python"]}
{
text: "win32API",
bgColor: "var(--color-win32api)",
textColor: "white",
},
{
text: "Python",
bgColor: "var(--color-python)",
textColor: "white",
},
]}
/> />
</div> </div>
</div> </div>

View file

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

View file

@ -19,23 +19,7 @@ import Project from "./Project.astro";
title="Coworking Guide" title="Coworking Guide"
label="Visit Site" 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!" 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={[ tags={["Astro", "Typescript", "Markdown"]}
{
text: "Astro",
bgColor: "white",
textColor: "black",
},
{
text: "TypeScript",
bgColor: "#3178C6",
textColor: "white",
},
{
text: "Markdown",
bgColor: "black",
textColor: "white",
},
]}
/> />
<VideoProject <VideoProject
@ -48,23 +32,7 @@ import Project from "./Project.astro";
description={` 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. 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={[ tags={["Python", "OpenCV", "win32API"]}
{
text: "Python",
textColor: "white",
bgColor: "var(--color-python)",
},
{
text: "OpenCV",
textColor: "white",
bgColor: "var(--color-opencv)",
},
{
text: "win32API",
textColor: "white",
bgColor: "var(--color-win32api)",
},
]}
/> />
<Project <Project
@ -75,18 +43,7 @@ import Project from "./Project.astro";
label="Visit Site" label="Visit Site"
title="We're not really strangers" 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!" 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={[ tags={["React", "Typescript"]}
{
text: "React",
bgColor: "var(--color-react)",
textColor: "black",
},
{
text: "TypeScript",
bgColor: "var(--color-typescript)",
textColor: "white",
},
]}
/> />
<Project <Project
@ -99,23 +56,7 @@ import Project from "./Project.astro";
to add their own tasks to the widget for accountability! to add their own tasks to the widget for accountability!
Easy to setup and the customization possibilities are Easy to setup and the customization possibilities are
endless!`} endless!`}
tags={[ tags={["HTML", "CSS", "Javascript"]}
{
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",
},
]}
> >
<TaskWidget <TaskWidget
classList={` classList={`

View file

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

View file

@ -1,18 +1,10 @@
--- ---
import Card from "../global/Card.astro"; import Card from "../global/Card.astro";
interface Tag {
text: string;
bgColor: string;
textColor?: string;
hoverTextColor?: string;
className?: string;
}
interface Props { interface Props {
title: string; title: string;
tags?: Tag[]; tags?: Array<string>;
primaryColor: string; primaryColor?: string;
className?: string; className?: string;
} }
@ -35,7 +27,7 @@ const { title, tags, primaryColor, className = "" } = Astro.props;
<div <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`} 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> </div>
)) ))
} }

View file

@ -13,51 +13,16 @@ import Skill from "./Skill.astro";
title="Frontend Development" title="Frontend Development"
primaryColor="#19a7ce" primaryColor="#19a7ce"
tags={[ tags={[
{ "HTML",
text: "HTML", "CSS",
bgColor: "var(--color-html)", "Javascript",
}, "Typescript",
{ "Svelte",
text: "CSS", "React",
bgColor: "var(--color-blue-700)", "Vue",
}, "WordPress",
{ "Astro",
text: "Javascript", "TailwindCSS",
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",
},
]} ]}
/> />
<Line height="50px" /> <Line height="50px" />
@ -66,38 +31,15 @@ import Skill from "./Skill.astro";
title="Backend Development" title="Backend Development"
primaryColor="#474F7A" primaryColor="#474F7A"
tags={[ tags={[
{ "Python",
text: "Python", "Typescript",
bgColor: "var(--color-python)", "C#",
}, "Kotlin",
{ "PHP",
text: "Typescript", "PostgreSQL",
bgColor: "var(--color-typescript)", "MySQL",
}, "MongoDB",
{ "Supabase",
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)",
},
]} ]}
/> />
<Line height="50px" /> <Line height="50px" />
@ -106,39 +48,14 @@ import Skill from "./Skill.astro";
title="Data Science" title="Data Science"
primaryColor="#a1eebd" primaryColor="#a1eebd"
tags={[ tags={[
{ "Python",
text: "Python", "R",
bgColor: "var(--color-python)", "OpenCV",
}, "SAS",
{ "MySQL",
text: "R", "Pandas",
bgColor: "var(--color-cyan-500)", "Selenium",
}, "BeautifulSoup4",
{
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",
},
]} ]}
/> />
</div> </div>

View file

@ -49,54 +49,9 @@ export const HoverNavigation = ({
className?: string; className?: string;
}) => { }) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); 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 [hamburgerOpen, setHamburgerOpen] = useState(false);
const { width } = useWindowDimensions(); 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 ( 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="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"> <div className="flex flex-col">
@ -112,13 +67,7 @@ export const HoverNavigation = ({
</a> </a>
</div> </div>
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<motion.div <motion.div className="flex w-auto flex-row items-center justify-center pr-2 whitespace-nowrap opacity-100 lg:w-0 lg:opacity-0">
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"
>
<a <a
tabIndex={0} tabIndex={0}
href="/contact" href="/contact"
@ -138,10 +87,6 @@ export const HoverNavigation = ({
> >
<motion.ul <motion.ul
className="hidden w-0 flex-row items-center justify-center overflow-hidden lg:flex lg:w-auto" 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={{ transition={{
ease: "easeInOut", 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 NetworkLine from "../components/global/NetworkLine.astro";
import Intro from "../components/home/Intro.astro"; import Intro from "../components/home/Intro.astro";
import Projects from "../components/projects/ProjectsComponent.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 Skills from "../components/skills/SkillsComponent.astro";
import Contact from "../components/contact/ContactComponent.astro"; import Contact from "../components/contact/ContactComponent.astro";
import Footer from "../components/global/FooterComponent.astro"; import Footer from "../components/global/FooterComponent.astro";
import PageLayout from "@/layouts/PageLayout.astro"; import PageLayout from "@/layouts/PageLayout.astro";
import { Button } from "@/components/ui/moving-border";
--- ---
<PageLayout <PageLayout
@ -18,17 +19,31 @@ import PageLayout from "@/layouts/PageLayout.astro";
<Intro /> <Intro />
</div> </div>
<NetworkLine height="70px" /> <NetworkLine height="70px" />
<div id="education"> <div
<div class="mx-auto mb-10 h-[1px] w-1/3 bg-[#404040]"></div> id="experience"
<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>
<div id="projects"> <div id="projects" class="scroll-mt-12">
<div class="pt-20"> <div class="flex flex-col items-center justify-center pt-20">
<h1 class="mb-10 text-center text-5xl">Projects</h1> <h1 class="mb-10 text-center text-5xl">Projects</h1>
<Projects /> <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> </div>
<div id="skills"> <div id="skills" class="scroll-mt-16">
<Skills /> <Skills />
</div> </div>
<div id="contact" class="pt-20"> <div id="contact" class="pt-20">
@ -36,6 +51,7 @@ import PageLayout from "@/layouts/PageLayout.astro";
</div> </div>
<Footer /> <Footer />
</PageLayout> </PageLayout>
<script is:inline> <script is:inline>
location.search.includes("?uwu") && location.search.includes("?uwu") &&
document.documentElement.classList.add("uwu"); document.documentElement.classList.add("uwu");

View file

@ -1,27 +1,10 @@
@import "tailwindcss"; @import "tailwindcss";
@theme { @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-primary: #fafafa;
--color-secondary: #1d1c20; --color-secondary: #1d1c20;
--color-accent: #72ffff;
--color-muted-foreground: #ccc;
--animate-fadeIn: fadeIn 0.5s ease-in-out; --animate-fadeIn: fadeIn 0.5s ease-in-out;
--animate-fadeUp: fadeUp 0.5s ease-in-out; --animate-fadeUp: fadeUp 0.5s ease-in-out;