initial commit: setup astro + vue + supabase
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Supabase Configuration
|
||||||
|
# Replace these with your actual credentials from the Supabase Dashboard
|
||||||
|
SUPABASE_URL=https://your-project-id.supabase.co
|
||||||
|
SUPABASE_ANON_KEY=your-anon-key
|
||||||
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Build & Output
|
||||||
|
dist/
|
||||||
|
.vercel/
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Environment Variables & Secrets
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE & Editors
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
53
README.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Yoga Pangestu - Portfolio Website
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Welcome to my personal portfolio website! This project is a modern, responsive, and performance-focused web application built with **Astro**. It serves as a digital showcase for my projects, skills, and professional journey as a Full Stack Developer.
|
||||||
|
|
||||||
|
## About Me
|
||||||
|
|
||||||
|
I am a **Full Stack Developer** with over 3 years of experience in full-stack web application development. I specialize in building scalable, high-performance, and secure digital solutions, covering all stages from system design to deployment.
|
||||||
|
|
||||||
|
> "Coding, Sleep, Repeat..."
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ⚡ **High Performance** – Built with **Astro** for blazing fast load times.
|
||||||
|
- 🎨 **Modern UI/UX** – Styled with **TailwindCSS** and **Framer Motion** for smooth animations.
|
||||||
|
- <20> **Fully Responsive** – Optimized for all devices, from mobile to desktop.
|
||||||
|
- <20> **Dark Mode Support** – Seamless theme switching.
|
||||||
|
- <20> **Project Showcase** – Detailed case studies of my work, including:
|
||||||
|
- **Webdesaku**
|
||||||
|
- **CSR Purwakarta**
|
||||||
|
- **Sibadeksa**
|
||||||
|
- **Yadi Parfum**
|
||||||
|
- and more...
|
||||||
|
- 📖 **Blog & Insights** – Sharing knowledge and updates on web technologies.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
This portfolio is built using a modern stack:
|
||||||
|
|
||||||
|
- **Astro** – Static Site Generation (SSG) for performance.
|
||||||
|
- **React** – Component-based UI architecture.
|
||||||
|
- **TailwindCSS** – Utility-first CSS framework.
|
||||||
|
- **TypeScript** – Type-safe code for better maintainability.
|
||||||
|
- **Framer Motion** – For interactive animations.
|
||||||
|
- **MDX** – For writing content-rich blog posts and project details.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- `src/data/projects` – MDX files for project descriptions.
|
||||||
|
- `src/lib/constants` – Configuration for profile info, tech stack, and site metadata.
|
||||||
|
- `src/pages` – Application routes and views.
|
||||||
|
- `public` – Static assets.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Feel free to reach out for collaborations or inquiries!
|
||||||
|
|
||||||
|
- **Email**: info.pangestuyoga@gmail.com
|
||||||
18
astro.config.mjs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
import vue from "@astrojs/vue";
|
||||||
|
import mdx from "@astrojs/mdx";
|
||||||
|
import icon from "astro-icon";
|
||||||
|
import vercel from "@astrojs/vercel";
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://pangestu.vercel.app',
|
||||||
|
adapter: vercel(),
|
||||||
|
integrations: [vue(), mdx(), icon()],
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
},
|
||||||
|
});
|
||||||
19
eslint.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import eslintPluginAstro from "eslint-plugin-astro";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
...eslintPluginAstro.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ rules: { "no-console": "error" } },
|
||||||
|
{ ignores: ["dist/**", ".astro", "public/pagefind/**"] },
|
||||||
|
];
|
||||||
|
|
||||||
11619
package-lock.json
generated
Normal file
57
package.json
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"name": "pangestu-portfolio",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/mdx": "^4.2.1",
|
||||||
|
"@astrojs/vercel": "^9.0.5",
|
||||||
|
"@astrojs/vue": "^5.1.4",
|
||||||
|
"@fontsource/geist-sans": "^5.2.5",
|
||||||
|
"@fontsource/jetbrains-mono": "^5.2.5",
|
||||||
|
"@fontsource/roboto-condensed": "^5.2.5",
|
||||||
|
"@supabase/supabase-js": "^2.110.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@tailwindcss/vite": "^4.0.16",
|
||||||
|
"@typescript-eslint/parser": "^8.28.0",
|
||||||
|
"@vercel/analytics": "^1.5.0",
|
||||||
|
"astro": "^5.5.4",
|
||||||
|
"astro-icon": "^1.1.5",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"developer-icons": "^5.3.2",
|
||||||
|
"eslint": "^9.23.0",
|
||||||
|
"eslint-plugin-astro": "^1.3.1",
|
||||||
|
"framer-motion": "^12.6.0",
|
||||||
|
"install": "^0.13.0",
|
||||||
|
"lucide-react": "^0.484.0",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"mdast-util-from-markdown": "^2.0.2",
|
||||||
|
"mdast-util-to-string": "^4.0.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"reading-time": "^1.5.0",
|
||||||
|
"sharp": "^0.33.5",
|
||||||
|
"tailwind-merge": "^3.0.2",
|
||||||
|
"tailwindcss": "^4.0.16",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "^5.8.2",
|
||||||
|
"typescript-eslint": "^8.28.0",
|
||||||
|
"vue": "^3.5.39"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/devicon": "^1.2.62",
|
||||||
|
"@iconify-json/fluent-color": "^1.2.21",
|
||||||
|
"@iconify-json/logos": "^1.2.11",
|
||||||
|
"@iconify-json/lucide": "^1.2.112",
|
||||||
|
"@iconify-json/simple-icons": "^1.2.86",
|
||||||
|
"@iconify-json/skill-icons": "^1.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
5527
pnpm-lock.yaml
generated
Normal file
9
public/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
BIN
public/og-images/projects/csr.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/og-images/projects/jelita-florist.png
Normal file
|
After Width: | Height: | Size: 527 KiB |
BIN
public/og-images/projects/katalis.png
Normal file
|
After Width: | Height: | Size: 737 KiB |
BIN
public/og-images/projects/ppid.png
Normal file
|
After Width: | Height: | Size: 647 KiB |
BIN
public/og-images/projects/sibadeksa.png
Normal file
|
After Width: | Height: | Size: 667 KiB |
BIN
public/og-images/projects/simedkom.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/og-images/projects/webdesaku.png
Normal file
|
After Width: | Height: | Size: 435 KiB |
BIN
public/og-images/projects/yadi-parfum.png
Normal file
|
After Width: | Height: | Size: 905 KiB |
BIN
public/screenshot-dark.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
public/screenshot-light.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
src/assets/avatar.png
Normal file
|
After Width: | Height: | Size: 1001 KiB |
63
src/components/back-to-top.astro
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
import { ChevronUp } from "lucide-react";
|
||||||
|
import { buttonVariants, cn } from "@/lib/utils";
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("astro:page-load", () => {
|
||||||
|
function handleScroll() {
|
||||||
|
const button = document.querySelector("#back-to-top");
|
||||||
|
if (!button) return;
|
||||||
|
if (window.scrollY < window.innerHeight) {
|
||||||
|
button.classList.add("scale-0");
|
||||||
|
} else {
|
||||||
|
button.classList.remove("scale-0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function backToTop() {
|
||||||
|
const button = document.querySelector("#back-to-top");
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
document.body.scrollTop = 0; // For Safari
|
||||||
|
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE, and Opera
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check scroll position on page load and scroll events
|
||||||
|
handleScroll();
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the back to top functionality
|
||||||
|
backToTop();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Go to page start after page swap */
|
||||||
|
document.addEventListener("astro:after-swap", () => {
|
||||||
|
window.scrollTo({ left: 0, top: 0, behavior: "instant" });
|
||||||
|
|
||||||
|
// Need to wait a moment for the button to be available in the DOM after swap
|
||||||
|
setTimeout(() => {
|
||||||
|
const button = document.querySelector("#back-to-top");
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
if (window.scrollY < window.innerHeight) {
|
||||||
|
button.classList.add("scale-0");
|
||||||
|
} else {
|
||||||
|
button.classList.remove("scale-0");
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<button
|
||||||
|
id="back-to-top"
|
||||||
|
aria-label="back-to-top-button"
|
||||||
|
title="Back to Top Button"
|
||||||
|
class={cn(
|
||||||
|
buttonVariants({ size: "icon" }),
|
||||||
|
"fixed right-4 bottom-[4rem] scale-0 z-50 cursor-pointer duration-150 transition-transform rounded-full lg:left-[calc(100vw/2+400px)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ChevronUp />
|
||||||
|
</button>
|
||||||
11
src/components/box/content.astro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={["p-4 text-sm", className]}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
11
src/components/box/header.astro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { class: className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={["relative px-4 py-2", className]}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
11
src/components/box/index.astro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class:list={["md:border-x", className]}>
|
||||||
|
<slot />
|
||||||
|
</section>
|
||||||
11
src/components/box/title.astro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<h2 class:list={["text-2xl font-semibold", className]}>
|
||||||
|
<slot />
|
||||||
|
</h2>
|
||||||
13
src/components/footer.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<footer class="w-full border-t border-dashed md:border-b">
|
||||||
|
<div
|
||||||
|
class="mx-auto flex max-w-3xl flex-col-reverse items-center justify-between gap-4 border-dashed px-4 py-3 md:border-x lg:flex-row"
|
||||||
|
>
|
||||||
|
<p class="text-sm text-foreground/70">
|
||||||
|
Created by <a
|
||||||
|
href="https://github.com/pangestuyoga"
|
||||||
|
class="font-medium text-primary underline underline-offset-4"
|
||||||
|
>Yoga Pangestu</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
61
src/components/mode-toggle.astro
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
import { Moon, Sun } from "lucide-react";
|
||||||
|
import { cn, buttonVariants } from "@/lib/utils";
|
||||||
|
---
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="theme-toggle"
|
||||||
|
class={cn(
|
||||||
|
buttonVariants({ size: "icon", variant: "ghost" }),
|
||||||
|
"cursor-pointer rounded-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Sun
|
||||||
|
id="sun-icon"
|
||||||
|
className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90"
|
||||||
|
/>
|
||||||
|
<Moon
|
||||||
|
id="moon-icon"
|
||||||
|
className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0"
|
||||||
|
/>
|
||||||
|
<span class="sr-only">Toggle theme</span>
|
||||||
|
</button>
|
||||||
|
<script is:inline>
|
||||||
|
// Check for theme preference in localStorage or system preference
|
||||||
|
const getThemePreference = () => {
|
||||||
|
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
|
||||||
|
return localStorage.getItem("theme");
|
||||||
|
}
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set theme on document and in localStorage
|
||||||
|
const setThemeAppearance = theme => {
|
||||||
|
if (theme === "dark") {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
}
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply the theme immediately to avoid flash
|
||||||
|
const theme = getThemePreference();
|
||||||
|
setThemeAppearance(theme);
|
||||||
|
|
||||||
|
document.addEventListener("astro:page-load", () => {
|
||||||
|
const toggleButton = document.getElementById("theme-toggle");
|
||||||
|
|
||||||
|
// Toggle theme function
|
||||||
|
const toggleMode = () => {
|
||||||
|
const isDarkMode = document.documentElement.classList.contains("dark");
|
||||||
|
const newTheme = isDarkMode ? "light" : "dark";
|
||||||
|
setThemeAppearance(newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add click event listener
|
||||||
|
toggleButton?.addEventListener("click", toggleMode);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
31
src/components/navbar/index.astro
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
import ModeToggle from "@/components/mode-toggle.astro";
|
||||||
|
import NavLinks from "./nav-links.astro";
|
||||||
|
import { PROFILE_INFO } from "@/lib/constants/profile";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<nav
|
||||||
|
class:list={[
|
||||||
|
"screen-line-after sticky top-0 z-50 w-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60",
|
||||||
|
className,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx-auto flex max-w-3xl items-center justify-between px-4 py-1 md:border-x"
|
||||||
|
>
|
||||||
|
<a href="/">
|
||||||
|
<h1 class="text-xl font-bold">{PROFILE_INFO.logo}</h1>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-4">
|
||||||
|
<NavLinks />
|
||||||
|
<ModeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
42
src/components/navbar/nav-links.astro
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
import { NAV_LINKS } from "@/lib/constants/index";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const pathname = Astro.url.pathname;
|
||||||
|
|
||||||
|
// Remove trailing slash from current pathname if exists
|
||||||
|
const currentPath =
|
||||||
|
pathname.endsWith("/") && pathname !== "/" ? pathname.slice(0, -1) : pathname;
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
const currentPathArray = currentPath.split("/").filter(p => p.trim());
|
||||||
|
const pathArray = path.split("/").filter(p => p.trim());
|
||||||
|
|
||||||
|
return currentPath === path || currentPathArray[0] === pathArray[0];
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-4">
|
||||||
|
{
|
||||||
|
NAV_LINKS.map(link => (
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
class={cn(
|
||||||
|
"group relative flex flex-col items-center justify-center text-xs font-medium text-foreground/70 duration-150 hover:text-primary lg:text-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span class={cn("mb", isActive(link.href) && "text-primary")}>
|
||||||
|
{link.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class={cn(
|
||||||
|
"absolute -bottom-px h-[1px] rounded-full bg-primary transition-all duration-150",
|
||||||
|
isActive(link.href) ? "w-full" : "w-0",
|
||||||
|
"group-hover:w-full"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
40
src/components/pagination.astro
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
import type { Page } from "astro";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import LinkButton from "@/components/ui/link-btn.astro";
|
||||||
|
|
||||||
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
page: Page<CollectionEntry<"blog">>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
page.lastPage > 1 && (
|
||||||
|
<nav
|
||||||
|
class="flex items-center justify-center gap-3 text-sm"
|
||||||
|
aria-label="Pagination"
|
||||||
|
>
|
||||||
|
<LinkButton
|
||||||
|
disabled={!page.url.prev}
|
||||||
|
href={page.url.prev as string}
|
||||||
|
ariaLabel="Previous"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="inline-block" />
|
||||||
|
Previous
|
||||||
|
</LinkButton>
|
||||||
|
<span class="font-bold">{page.currentPage} </span>/ {page.lastPage}
|
||||||
|
<LinkButton
|
||||||
|
disabled={!page.url.next}
|
||||||
|
href={page.url.next as string}
|
||||||
|
ariaLabel="Next"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ArrowRight className="inline-block" />
|
||||||
|
</LinkButton>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
141
src/components/profile.astro
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
import BoxContent from "@/components/box/content.astro";
|
||||||
|
import Box from "@/components/box/index.astro";
|
||||||
|
import Avatar from "@/components/ui/avatar.astro";
|
||||||
|
|
||||||
|
import { PROFILE_INFO } from "@/lib/constants/profile";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="screen-line-after relative flex items-center p-4 md:border-x">
|
||||||
|
<div class="shrink-0">
|
||||||
|
<div class="relative z-1 mx-0.5 my-1">
|
||||||
|
<Avatar
|
||||||
|
className="size-20 rounded-full ring-1 ring-border ring-offset-2 ring-offset-background sm:size-28"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-1 flex-col">
|
||||||
|
<div class="flex grow items-end pb-1 pl-4">
|
||||||
|
<div class="line-clamp-1 font-mono text-xs text-muted-foreground">
|
||||||
|
{PROFILE_INFO.slogan}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<h1 class="flex items-center gap-x-2 pl-4 text-2xl font-black">
|
||||||
|
{PROFILE_INFO.displayName}
|
||||||
|
<Icon name="lucide:badge-check" class="size-4 text-blue-500" />
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="py-1 pl-4">
|
||||||
|
<p class="text-sm !font-normal">
|
||||||
|
{PROFILE_INFO.role}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
(
|
||||||
|
<div class="screen-line-after inline-flex w-full items-center gap-2 border-x bg-yellow-100 px-3 py-2 text-sm font-semibold dark:bg-yellow-500/10">
|
||||||
|
<span class="animate-bounce text-base">👋</span> Let's build something
|
||||||
|
great together —
|
||||||
|
<a
|
||||||
|
href={`mailto:${PROFILE_INFO.email}`}
|
||||||
|
class="underline underline-offset-4"
|
||||||
|
>
|
||||||
|
let's talk
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<BoxContent className="space-y-3">
|
||||||
|
<span class="flex items-center justify-start gap-2">
|
||||||
|
<Icon name="lucide:briefcase" class="size-4" />
|
||||||
|
I build software at
|
||||||
|
<a
|
||||||
|
href="https://pratamatechsolution.co.id"
|
||||||
|
target="_blank"
|
||||||
|
class="underline underline-offset-4"
|
||||||
|
>
|
||||||
|
PST
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center justify-start gap-2">
|
||||||
|
<Icon name="lucide:file-text" class="size-4" />
|
||||||
|
Take a look at my
|
||||||
|
<a
|
||||||
|
href="https://drive.google.com/drive/folders/1AzP0gexDJrz9_T42TZkCMPn1MJCEvfP4?usp=sharing"
|
||||||
|
target="_blank"
|
||||||
|
class="underline underline-offset-4"
|
||||||
|
>
|
||||||
|
Resume
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center justify-start gap-2">
|
||||||
|
<Icon name="skill-icons:instagram" class="size-4" />
|
||||||
|
Follow me on
|
||||||
|
<a
|
||||||
|
href="https://www.instagram.com/ygapangestuuu_/"
|
||||||
|
target="_blank"
|
||||||
|
class="underline underline-offset-4"
|
||||||
|
>
|
||||||
|
Instagram
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center justify-start gap-2">
|
||||||
|
<Icon name="logos:threads-icon" class="size-4" />
|
||||||
|
Follow me on
|
||||||
|
<a
|
||||||
|
href="https://www.threads.com/@ygapangestuuu_"
|
||||||
|
target="_blank"
|
||||||
|
class="underline underline-offset-4"
|
||||||
|
>
|
||||||
|
Threads
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center justify-start gap-2">
|
||||||
|
<Icon name="logos:facebook" class="size-4" />
|
||||||
|
Follow me on
|
||||||
|
<a
|
||||||
|
href="https://www.facebook.com/profile.php?id=100092236272919"
|
||||||
|
target="_blank"
|
||||||
|
class="underline underline-offset-4"
|
||||||
|
>
|
||||||
|
Facebook
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center justify-start gap-2">
|
||||||
|
<Icon name="skill-icons:linkedin" class="size-4" />
|
||||||
|
Let's connect on
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/pangestuu/"
|
||||||
|
class="underline underline-offset-4"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
LinkedIn
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center justify-start gap-2">
|
||||||
|
<Icon name="devicon:github" class="size-4" />
|
||||||
|
Check out my repos on
|
||||||
|
<a
|
||||||
|
href="https://github.com/pangestuyoga"
|
||||||
|
class="underline underline-offset-4"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</BoxContent>
|
||||||
|
</Box>
|
||||||
21
src/components/sections/about.astro
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
import { PROFILE_INFO } from "@/lib/constants/profile";
|
||||||
|
import MarkdownRenderer from "@/components/ui/markdown-renderer.astro";
|
||||||
|
|
||||||
|
import Box from "@/components/box/index.astro";
|
||||||
|
import BoxHeader from "@/components/box/header.astro";
|
||||||
|
import BoxTitle from "@/components/box/title.astro";
|
||||||
|
import BoxContent from "@/components/box/content.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<BoxHeader>
|
||||||
|
<BoxTitle> About </BoxTitle>
|
||||||
|
</BoxHeader>
|
||||||
|
<BoxContent>
|
||||||
|
<MarkdownRenderer
|
||||||
|
className="text-sm leading-7 font-medium"
|
||||||
|
content={PROFILE_INFO.about}
|
||||||
|
/>
|
||||||
|
</BoxContent>
|
||||||
|
</Box>
|
||||||
30
src/components/sections/experience/experience-item.astro
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import type { Experience } from "@/lib/types";
|
||||||
|
import ExperiencePositionItem from "./position-item.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
experience: Experience;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { experience } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="space-y-4 p-4 not-last:border-b">
|
||||||
|
<div class="flex items-center space-x-4 px-2">
|
||||||
|
<span class="size-2 rounded-full bg-foreground/60"></span>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-black">
|
||||||
|
{experience.company}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative space-y-4 pl-1 before:absolute before:left-3 before:h-full before:w-px before:bg-border"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
experience.positions.map((position, index) => {
|
||||||
|
return <ExperiencePositionItem position={position} />;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
22
src/components/sections/experience/experience.astro
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
import Box from "@/components/box/index.astro";
|
||||||
|
import BoxHeader from "@/components/box/header.astro";
|
||||||
|
import BoxTitle from "@/components/box/title.astro";
|
||||||
|
import BoxContent from "@/components/box/content.astro";
|
||||||
|
|
||||||
|
import { EXPERIENCES } from "@/lib/constants/experience";
|
||||||
|
import ExperienceItem from "./experience-item.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<BoxHeader>
|
||||||
|
<BoxTitle> Experience </BoxTitle>
|
||||||
|
</BoxHeader>
|
||||||
|
<BoxContent className="px-0">
|
||||||
|
{
|
||||||
|
EXPERIENCES.map(experience => {
|
||||||
|
return <ExperienceItem experience={experience} />;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</BoxContent>
|
||||||
|
</Box>
|
||||||
118
src/components/sections/experience/position-item.astro
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
import { Code2, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
|
import type { ExperiencePosition } from "@/lib/types";
|
||||||
|
import Badge from "@/components/ui/badge.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
position: ExperiencePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { position } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="block w-full text-left">
|
||||||
|
<div class="relative z-1 mb-1 flex items-center space-x-3">
|
||||||
|
<Code2 className="size-4 bg-background" />
|
||||||
|
<h3 class="text-base font-black underline-offset-4">
|
||||||
|
{position.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="flex items-center gap-2 pl-7 font-mono text-sm text-foreground"
|
||||||
|
>
|
||||||
|
{position.year}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description-container my-4 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="description-content max-h-0 overflow-hidden transition-all duration-300"
|
||||||
|
>
|
||||||
|
<ul class="ml-7 list-none space-y-2 text-sm leading-7">
|
||||||
|
{
|
||||||
|
position.description.map((item) => (
|
||||||
|
<li class="description-item text-justify">{item}</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="toggle-btn mt-2 ml-7 flex cursor-pointer items-center gap-1 rounded-md border px-4 py-2 text-xs font-medium text-primary hover:underline focus:outline-none"
|
||||||
|
>
|
||||||
|
<span class="toggle-text">Show more</span>
|
||||||
|
<ChevronDown className="toggle-icon-down size-4" />
|
||||||
|
<ChevronUp className="toggle-icon-up hidden size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.description-content.expanded {
|
||||||
|
max-height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-container .toggle-icon-up.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-container .toggle-icon-down.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-item {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-item::before {
|
||||||
|
content: "♦";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("astro:page-load", () => {
|
||||||
|
const toggleButtons = document.querySelectorAll(".toggle-btn");
|
||||||
|
|
||||||
|
toggleButtons.forEach((button, index) => {
|
||||||
|
const container = button.closest(".description-container");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const content = container.querySelector(".description-content");
|
||||||
|
const toggleText = button.querySelector(".toggle-text");
|
||||||
|
const iconDown = button.querySelector(".toggle-icon-down");
|
||||||
|
const iconUp = button.querySelector(".toggle-icon-up");
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
if (content) content.classList.add("expanded");
|
||||||
|
if (toggleText) toggleText.textContent = "Show less";
|
||||||
|
if (iconDown) iconDown.classList.add("hidden");
|
||||||
|
if (iconUp) iconUp.classList.remove("hidden");
|
||||||
|
|
||||||
|
iconUp?.classList.add("visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
if (content) {
|
||||||
|
content.classList.toggle("expanded");
|
||||||
|
|
||||||
|
if (content.classList.contains("expanded")) {
|
||||||
|
if (toggleText) toggleText.textContent = "Show less";
|
||||||
|
if (iconDown) iconDown.classList.add("hidden");
|
||||||
|
if (iconUp) iconUp.classList.remove("hidden");
|
||||||
|
iconUp?.classList.add("visible");
|
||||||
|
} else {
|
||||||
|
if (toggleText) toggleText.textContent = "Show more";
|
||||||
|
if (iconDown) iconDown.classList.remove("hidden");
|
||||||
|
if (iconUp) iconUp.classList.add("hidden");
|
||||||
|
iconUp?.classList.remove("visible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
49
src/components/sections/projects/project-item.astro
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
import { ChevronRightIcon, FolderCodeIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import MarkdownRenderer from "@/components/ui/markdown-renderer.astro";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import Badge from "@/components/ui/badge.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
project: CollectionEntry<"projects">;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { project } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={`/projects/${project.id}`}
|
||||||
|
class="flex cursor-pointer items-center not-last:border-b"
|
||||||
|
>
|
||||||
|
<FolderCodeIcon className="mx-4.5 size-5 shrink-0 text-muted-foreground" />
|
||||||
|
|
||||||
|
<div class="group flex w-full cursor-pointer flex-col gap-y-3 border-l p-4">
|
||||||
|
<div class="flex w-full items-center justify-between text-left select-none">
|
||||||
|
<h3
|
||||||
|
class="text-lg font-semibold underline-offset-6 group-hover:underline"
|
||||||
|
>
|
||||||
|
{project.data.name}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ChevronRightIcon
|
||||||
|
className="size-4 shrink-0 text-muted-foreground transition-transform duration-150 group-hover:translate-x-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MarkdownRenderer
|
||||||
|
className="text-xs leading-5 font-medium md:text-sm"
|
||||||
|
content={project.data.description}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
project.data.technologies.length > 0 && (
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
{project.data.technologies.map(technology => {
|
||||||
|
return <Badge>{technology}</Badge>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
41
src/components/tech-stack.astro
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
import Box from "@/components/box/index.astro";
|
||||||
|
import BoxHeader from "@/components/box/header.astro";
|
||||||
|
import BoxTitle from "@/components/box/title.astro";
|
||||||
|
import BoxContent from "@/components/box/content.astro";
|
||||||
|
import { TECH_STACK } from "@/lib/constants/tech-stack";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<BoxHeader>
|
||||||
|
<BoxTitle>Tech Stack</BoxTitle>
|
||||||
|
</BoxHeader>
|
||||||
|
<BoxContent>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-3"
|
||||||
|
style="content-visibility: auto; contain-intrinsic-size: auto 48px;"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
TECH_STACK.map(item => (
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class:list={[
|
||||||
|
"flex h-10 min-w-[120px] items-center justify-center gap-2 rounded-md border border-solid bg-blue-50/10 p-2",
|
||||||
|
"transition-all duration-200 hover:bg-blue-50/20",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.icon && (
|
||||||
|
<span class="flex size-5 items-center justify-center">
|
||||||
|
<Icon name={item.icon} class="size-5" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span class="truncate text-sm">{item.title}</span>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</BoxContent>
|
||||||
|
</Box>
|
||||||
29
src/components/toc.astro
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
import type { MarkdownHeading } from "astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
headings: MarkdownHeading[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { headings } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="sticky top-18 mt-4 ml-4 hidden max-h-fit min-w-fit flex-col gap-y-3 lg:flex"
|
||||||
|
>
|
||||||
|
<h3 class:list="text-xl">On This Page</h3>
|
||||||
|
{
|
||||||
|
headings
|
||||||
|
.filter(({ depth }) => depth < 3)
|
||||||
|
.map(({ slug, text }) => (
|
||||||
|
<a
|
||||||
|
href={`#${slug}`}
|
||||||
|
class:list={[
|
||||||
|
"text-sm font-normal text-foreground/70 no-underline duration-75 hover:text-foreground",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
• {text}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
32
src/components/ui/avatar.astro
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
import { Picture } from "astro:assets";
|
||||||
|
import ImgAvatar from "@/assets/avatar.png";
|
||||||
|
import { PROFILE_INFO } from "@/lib/constants/profile";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
size?: number;
|
||||||
|
priority?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
size = 100, // Default size if not provided
|
||||||
|
priority = false,
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
// Determine image formats for fallback and webp
|
||||||
|
const formats = ["avif", "webp"];
|
||||||
|
---
|
||||||
|
|
||||||
|
<Picture
|
||||||
|
class:list={[className]}
|
||||||
|
src={ImgAvatar}
|
||||||
|
widths={[size, size * 2]}
|
||||||
|
sizes={`(max-width: ${size}px) 100vw, ${size}px`}
|
||||||
|
formats={formats}
|
||||||
|
alt={`${PROFILE_INFO.displayName}'s avatar`}
|
||||||
|
quality="mid"
|
||||||
|
loading={priority ? "eager" : "lazy"}
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
5
src/components/ui/badge.astro
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<span
|
||||||
|
class="inline-flex items-center rounded-md border !border-solid px-2 py-0.5 text-xs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
17
src/components/ui/icons/alpine.astro
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...Astro.props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2.4c5.302 0 9.6 4.298 9.6 9.6S17.302 21.6 12 21.6 2.4 17.302 2.4 12 6.698 2.4 12 2.4z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M8.4 6h7.2l-3.6 10.8L8.4 6zm1.2 2.4l2.4 7.2 2.4-7.2H9.6z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 452 B |
21
src/components/ui/icons/bootstrap.astro
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...Astro.props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2.4c5.302 0 9.6 4.298 9.6 9.6S17.302 21.6 12 21.6 2.4 17.302 2.4 12 6.698 2.4 12 2.4z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M7.2 6h9.6v12H7.2V6zm1.2 1.2v9.6h7.2V7.2H8.4z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M9.6 9.6h4.8v4.8H9.6V9.6zm1.2 1.2v2.4h2.4v-2.4h-2.4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 545 B |
13
src/components/ui/icons/facebook.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...Astro.props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 471 B |
21
src/components/ui/icons/filament.astro
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...Astro.props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2.4c5.302 0 9.6 4.298 9.6 9.6S17.302 21.6 12 21.6 2.4 17.302 2.4 12 6.698 2.4 12 2.4z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 6l-6 6 6 6 6-6-6-6zm0 2.4L16.8 12 12 15.6 7.2 12 12 8.4z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M10.8 10.8h2.4v2.4h-2.4v-2.4z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 537 B |
21
src/components/ui/icons/flux.astro
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...Astro.props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2.4c5.302 0 9.6 4.298 9.6 9.6S17.302 21.6 12 21.6 2.4 17.302 2.4 12 6.698 2.4 12 2.4z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M6 8h12v8H6V8zm1.2 1.2v5.6h9.6V9.2H7.2z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M9.6 12h4.8v1.2H9.6V12z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 510 B |
6
src/components/ui/icons/github.astro
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg viewBox="0 0 438.549 438.549" {...Astro.props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
13
src/components/ui/icons/instagram.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...Astro.props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
13
src/components/ui/icons/linkedin.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
width="800px"
|
||||||
|
height="800px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...Astro.props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M22 3.47059V20.5294C22 20.9194 21.8451 21.2935 21.5693 21.5693C21.2935 21.8451 20.9194 22 20.5294 22H3.47059C3.08056 22 2.70651 21.8451 2.43073 21.5693C2.15494 21.2935 2 20.9194 2 20.5294V3.47059C2 3.08056 2.15494 2.70651 2.43073 2.43073C2.70651 2.15494 3.08056 2 3.47059 2H20.5294C20.9194 2 21.2935 2.15494 21.5693 2.43073C21.8451 2.70651 22 3.08056 22 3.47059ZM7.88235 9.64706H4.94118V19.0588H7.88235V9.64706ZM8.14706 6.41177C8.14861 6.18929 8.10632 5.96869 8.02261 5.76255C7.93891 5.55642 7.81542 5.36879 7.65919 5.21039C7.50297 5.05198 7.31708 4.92589 7.11213 4.83933C6.90718 4.75277 6.68718 4.70742 6.46471 4.70588H6.41177C5.95934 4.70588 5.52544 4.88561 5.20552 5.20552C4.88561 5.52544 4.70588 5.95934 4.70588 6.41177C4.70588 6.86419 4.88561 7.29809 5.20552 7.61801C5.52544 7.93792 5.95934 8.11765 6.41177 8.11765C6.63426 8.12312 6.85565 8.0847 7.06328 8.00458C7.27092 7.92447 7.46074 7.80422 7.62189 7.65072C7.78304 7.49722 7.91237 7.31346 8.00248 7.10996C8.09259 6.90646 8.14172 6.6872 8.14706 6.46471V6.41177ZM19.0588 13.3412C19.0588 10.5118 17.2588 9.41177 15.4706 9.41177C14.8851 9.38245 14.3021 9.50715 13.7799 9.77345C13.2576 10.0397 12.8143 10.4383 12.4941 10.9294H12.4118V9.64706H9.64706V19.0588H12.5882V14.0529C12.5457 13.5403 12.7072 13.0315 13.0376 12.6372C13.3681 12.2429 13.8407 11.9949 14.3529 11.9471H14.4647C15.4 11.9471 16.0941 12.5353 16.0941 14.0176V19.0588H19.0353L19.0588 13.3412Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
21
src/components/ui/icons/livewire.astro
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...Astro.props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2.4c5.302 0 9.6 4.298 9.6 9.6S17.302 21.6 12 21.6 2.4 17.302 2.4 12 6.698 2.4 12 2.4z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 6c-3.314 0-6 2.686-6 6s2.686 6 6 6 6-2.686 6-6-2.686-6-6-6zm0 2.4c1.988 0 3.6 1.612 3.6 3.6S13.988 16.8 12 16.8 8.4 15.188 8.4 13.2 10.012 9.6 12 9.6z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M8.4 10.8h7.2v2.4H8.4v-2.4zm-1.2-1.2h9.6v4.8H7.2V9.6z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 655 B |
18
src/components/ui/icons/threads.astro
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...Astro.props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2.4c5.302 0 9.6 4.298 9.6 9.6S17.302 21.6 12 21.6 2.4 17.302 2.4 12 6.698 2.4 12 2.4z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 6c-3.314 0-6 2.686-6 6s2.686 6 6 6 6-2.686 6-6-2.686-6-6-6zm0 2.4c1.988 0 3.6 1.612 3.6 3.6S13.988 16.8 12 16.8 8.4 15.188 8.4 13.2 10.012 9.6 12 9.6z"
|
||||||
|
></path>
|
||||||
|
<circle fill="currentColor" cx="16.8" cy="7.2" r="1.2"></circle>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 617 B |
12
src/components/ui/icons/verified.astro
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...Astro.props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M13.3393 0.582135C12.6142 -0.194045 11.3836 -0.194045 10.6584 0.582135L8.88012 2.48429C8.51756 2.8711 8.00564 3.0843 7.47584 3.06515L4.87538 2.97706C3.81324 2.94132 2.94259 3.81197 2.97834 4.87411L3.06642 7.47712C3.0843 8.00691 2.87238 8.51884 2.48429 8.88139L0.582135 10.6584C-0.194045 11.3836 -0.194045 12.6155 0.582135 13.3406L2.48429 15.1189C2.87238 15.4815 3.0843 15.9921 3.06642 16.5232L2.97706 19.1249C2.94259 20.1871 3.81324 21.0577 4.87538 21.022L7.47712 20.9339C8.00691 20.916 8.51884 21.1279 8.88139 21.5148L10.6584 23.4169C11.3848 24.1944 12.6155 24.1944 13.3419 23.4169L15.1202 21.5148C15.4815 21.1279 15.9934 20.9147 16.5232 20.9339L19.1249 21.022C20.1871 21.0577 21.059 20.1871 21.022 19.1249L20.9352 16.5219C20.916 15.9921 21.1292 15.4815 21.516 15.1189L23.4182 13.3406C24.1944 12.6155 24.1944 11.3836 23.4182 10.6584L21.516 8.88012C21.1292 8.51884 20.916 8.00691 20.9352 7.47584L21.022 4.87411C21.059 3.81197 20.1871 2.94132 19.1249 2.97706L16.5232 3.06642C15.9934 3.08302 15.4815 2.8711 15.1189 2.48429L13.3393 0.582135ZM5.91327 12.5402L10.2908 16.9164L17.5458 8.99374L15.8262 7.4018L10.2091 13.5232L7.56393 10.878L5.91327 12.5402Z"
|
||||||
|
fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
49
src/components/ui/link-btn.astro
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
import { buttonVariants } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
href: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
target?: string;
|
||||||
|
variant?: "default" | "ghost" | "secondary";
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
href,
|
||||||
|
ariaLabel,
|
||||||
|
target,
|
||||||
|
className,
|
||||||
|
disabled = false,
|
||||||
|
variant = "ghost",
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
disabled ? (
|
||||||
|
<span
|
||||||
|
class:list={[
|
||||||
|
buttonVariants({ size: "sm", variant }),
|
||||||
|
"rounded-full text-muted-foreground hover:bg-transparent hover:text-muted-foreground [&_svg]:size-4",
|
||||||
|
className,
|
||||||
|
]}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
{href}
|
||||||
|
{target}
|
||||||
|
class:list={[
|
||||||
|
buttonVariants({ size: "sm", variant }),
|
||||||
|
"!rounded-full",
|
||||||
|
className,
|
||||||
|
]}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
src/components/ui/link-tag.astro
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
import { capitalizeFirstLetter } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
href: string;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { href, tag } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
class="group flex items-center gap-x-1 rounded-sm decoration-muted-foreground underline-offset-4 duration-150 hover:underline"
|
||||||
|
>
|
||||||
|
<span>#{tag}</span>
|
||||||
|
<ArrowRight
|
||||||
|
className="size-3.5 scale-0 -rotate-45 duration-75 group-hover:scale-100"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
12
src/components/ui/markdown-renderer.astro
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
import { markdownToHtml } from "@/lib/markdown";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { className, content } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div set:html={markdownToHtml(content, className)} />
|
||||||
20
src/components/ui/pattern.astro
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
"screen-line-before screen-line-after relative flex h-4 w-full md:border-x",
|
||||||
|
"before:absolute before:-left-[100vw] before:h-full before:w-[200vw]",
|
||||||
|
// Replace the striped background with dotted pattern
|
||||||
|
"before:bg-[image:radial-gradient(var(--pattern-foreground)_2px,_transparent_2px)] before:bg-[size:15px_15px]",
|
||||||
|
"before:[--pattern-foreground:var(--color-black)]/5 dark:before:[--pattern-foreground:var(--color-white)]/5",
|
||||||
|
className,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
36
src/content.config.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { defineCollection, z } from "astro:content";
|
||||||
|
import { glob } from "astro/loaders";
|
||||||
|
|
||||||
|
const blog = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/data/blog" }),
|
||||||
|
schema: () =>
|
||||||
|
z.object({
|
||||||
|
pubDatetime: z.date(),
|
||||||
|
series: z.string().optional(),
|
||||||
|
image: z.string().optional(),
|
||||||
|
title: z.string(),
|
||||||
|
draft: z.boolean().optional(),
|
||||||
|
tags: z.array(z.string()).default(["others"]),
|
||||||
|
category: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const projects = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/data/projects" }),
|
||||||
|
schema: () =>
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
technologies: z.array(z.string()),
|
||||||
|
description: z.string(),
|
||||||
|
image: z.string().optional(),
|
||||||
|
sourceCode: z.string().optional(),
|
||||||
|
preview: z.string().optional(),
|
||||||
|
type: z.union([
|
||||||
|
z.literal("core"),
|
||||||
|
z.literal("side"),
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { blog, projects };
|
||||||
36
src/data/projects/csr-purwakarta.mdx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: CSR Purwakarta 🏙️🤝
|
||||||
|
type: core
|
||||||
|
preview: https://csr.purwakarta.go.id
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- Livewire
|
||||||
|
- Filament
|
||||||
|
- AlpineJS
|
||||||
|
- TailwindCSS
|
||||||
|
- MySQL
|
||||||
|
description: A centralized platform for managing Corporate Social Responsibility (CSR) initiatives in Purwakarta, fostering transparency and collaboration between the government and partners. 🏙️🤝
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
**CSR Purwakarta** is the digital heart of social responsibility in the region! ❤️ This platform is built to streamline and showcase the amazing Corporate Social Responsibility (CSR) contributions from companies to the community of Purwakarta.
|
||||||
|
|
||||||
|
It serves as a transparent ecosystem where the government, companies (partners), and the public can see exactly how funds are being used to build a better future! Key stakeholders can propose projects, report completed works, and track the overall impact through data-rich statistics. 📊✨
|
||||||
|
|
||||||
|
## 🚀 Why It Matters
|
||||||
|
* **Transparency First**: Openly displays how CSR funds are allocated and realized, building public trust. 🔍
|
||||||
|
* **Collaboration Hub**: Connects government agencies (Dinas/SKPD) directly with corporate partners for impactful projects. 🔗
|
||||||
|
* **Data-Driven Impact**: Visual statistics show the real-world difference being made across different sectors. 📈
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 🏢 **Partner Management**: A comprehensive database of companies and partners contributing to the region's development.
|
||||||
|
* 📝 **Proposal System**: Dinas and SKPD can submit proposals for CSR funding directly through the system.
|
||||||
|
* 📢 **Activity Reporting**: Partners can easily report and showcase the CSR activities they have completed.
|
||||||
|
* 📊 **Live Statistics**: Real-time charts and graphs displaying fund realization, project counts, and sector distribution.
|
||||||
|
* 📰 **News & Updates**: A dedicated section to highlight success stories and ongoing CSR initiatives.
|
||||||
|
* 🗺️ **Project Tracking**: Monitor the progress of approved CSR programs from start to finish!
|
||||||
|
|
||||||
|
---
|
||||||
|
*Building a better Purwakarta, together!* 🌏👷♂️
|
||||||
33
src/data/projects/dinkes-puskesmas.mdx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: Dinkes - Management of 3 Flagship Community Health Centers 🏥💉
|
||||||
|
type: core
|
||||||
|
preview: https://puskesmas-tegalwaru.pratama.live
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- Livewire
|
||||||
|
- Filament
|
||||||
|
- AlpineJS
|
||||||
|
- TailwindCSS
|
||||||
|
- MySQL
|
||||||
|
description: A comprehensive management system for public health centers (Puskesmas) under the Health Office, featuring service information, blog, and internal management. 🏥💉
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This comprehensive system is the digital backbone for operational needs across **three flagship Puskesmas (Public Health Centers)** under the Health Office! 🚑 It goes beyond simple administration, serving as a dynamic platform for both **internal management** and **public service information**.
|
||||||
|
|
||||||
|
A true health-tech solution, it allows the Health Office (Dinas Kesehatan) to monitor activities, track performance, and ensure that healthcare services are delivered efficiently to the community! 💊✨
|
||||||
|
|
||||||
|
## 🚀 Key Objectives
|
||||||
|
* **Centralized Health Monitoring**: A unified view for the Health Office to oversee multiple health centers. 🧐
|
||||||
|
* **Public Awareness**: Keeping the community informed about health services, schedules, and important news. 📢
|
||||||
|
* **Operational Efficiency**: Streamlining the daily workflow of healthcare providers. ⚡
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 🏥 **Service Management**: Detailed information on available health services and schedules across centers.
|
||||||
|
* 📝 **Integrated Blog & News**: A platform to share health tips, community updates, and official announcements.
|
||||||
|
* 📊 **Activity Monitoring**: Tracking daily operational activities and healthcare service delivery.
|
||||||
|
* 👥 **Personnel Data**: Managing staff information and rosters for each health center.
|
||||||
|
* 📈 **Performance Dashboard**: Analyzing key health metrics and operational data for better decision-making.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Empowering community health through digital management!* 💚🤝
|
||||||
32
src/data/projects/dst-collection.mdx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: DST Collection – Garment/Convection Management System 🧵👕
|
||||||
|
type: core
|
||||||
|
preview: https://konveksi.dstpabuaran.com
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- Javascript
|
||||||
|
- Bootstrap
|
||||||
|
- MySQL
|
||||||
|
description: An application for managing garment production workflows, from pattern design to final delivery. 🧵👕
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
**DST Collection** is a dedicated management system for **garment production (convection)**! 🏭 This powerful tool is designed to optimize every step of the garment manufacturing lifecycle.
|
||||||
|
|
||||||
|
From tracking raw materials and fabrics 🧵 to monitoring each stage of production (cutting, sewing, finishing) ✂️, it ensures that every piece of clothing meets quality standards and deadlines! It's the digital partner for a clothing manufacturing business. 👔
|
||||||
|
|
||||||
|
## 🚀 Key Objectives
|
||||||
|
* **Production Excellence**: Ensuring high-quality garment output by tracking each process step. 📉
|
||||||
|
* **Material Efficiency**: Minimizing waste by managing fabric inventory accurately. 📦
|
||||||
|
* **Order Fulfillment**: Delivering customer orders on time, every time. 🚚
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 📏 **Production Workflow**: Digitalized tracking of the garment manufacturing lifecycle (Design, Pattern, Cut, Sew).
|
||||||
|
* 🧵 **Raw Material Management**: Inventory for fabrics, threads, buttons, and other production needs.
|
||||||
|
* 📦 **Process Monitoring**: Real-time insights into the progress of each batch or order.
|
||||||
|
* 👥 **Employee Tracking**: Assigning tasks and monitoring the productivity of production staff.
|
||||||
|
* 🛒 **Order Management**: Tracking incoming orders and their current status in the pipeline.
|
||||||
|
* 💼 **Finished Goods**: Inventory management for completed garments ready for delivery.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Stitching efficiency into every garment!* 👗✨
|
||||||
37
src/data/projects/jelita-florist.mdx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: Jelita Florist 🌸🌷
|
||||||
|
type: core
|
||||||
|
preview: https://tokobungapurwakarta.com
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- Livewire
|
||||||
|
- Filament
|
||||||
|
- AlpineJS
|
||||||
|
- TailwindCSS
|
||||||
|
- MySQL
|
||||||
|
- WhatsApp Integration
|
||||||
|
description: A beautiful and integrated flower shop management system featuring a stunning landing page, article management, stock control, and powerful analytics. 🌸🌷
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
**Jelita Florist** isn't just a website; it's a flourishing digital garden for business! 🌿 This platform combines a visually captivating **Landing Page** to attract customers with a powerful backend to manage the entire floral business. From tracking every petal in stock to analyzing sales trends, this system helps the business bloom! 🌺
|
||||||
|
|
||||||
|
It's designed to be **user-friendly yet powerful**, giving the owner complete control over their products, content, and customer relationships. 🤝
|
||||||
|
|
||||||
|
## 🚀 Why It Stands Out
|
||||||
|
* **Stunning First Impression**: A beautiful landing page that showcases floral arrangements in all their glory. ✨
|
||||||
|
* **Content is King**: An integrated article/blog section to share flower care tips, news, and promotions! 📝
|
||||||
|
* **Business Intelligence**: A dashboard that turns data into actionable insights with easy-to-read charts and reports. 📊
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 🌐 **Captivating Landing Page**: Designed to wow visitors and convert them into customers.
|
||||||
|
* 🛒 **Stock & Sales Management**: Keep track of inventory and sales seamlessly—no more guessing games!
|
||||||
|
* 👥 **Customer Management**: Build lasting relationships by managing customer data effectively.
|
||||||
|
* 📰 **Article & Blog System**: Drive engagement and SEO with a built-in content management system.
|
||||||
|
* 📈 **Smart Dashboard**: A central hub for easy analysis and comprehensive reports on business performance.
|
||||||
|
* 📱 **WhatsApp Integration**: Direct connection for instant customer inquiries and orders! 💬
|
||||||
|
|
||||||
|
---
|
||||||
|
*Helping the floral business grow, one blossom at a time!* 🌻💼
|
||||||
33
src/data/projects/katalis-indonesia.mdx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: Catalyst Indonesia 🏢✨
|
||||||
|
type: core
|
||||||
|
preview: https://katalis-indonesia.com
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- Javascript
|
||||||
|
- Bootstrap
|
||||||
|
- MySQL
|
||||||
|
description: A clean and professional company profile website developed to present corporate identity and services. 🏢✨
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
**Catalyst Indonesia** is the digital face of the company! 🌐💼 This application is a sleek and professional **Company Profile** website designed to make a strong corporate statement.
|
||||||
|
|
||||||
|
Beyond a simple web presence, it's a strategic platform for communicating the company's **Mission, Vision, and Values** to potential clients and partners! 🤝 With detailed service pages and a clean aesthetic, it establishes credibility and trust instantly. ⭐
|
||||||
|
|
||||||
|
## 🚀 Key Objectives
|
||||||
|
* **Professional Branding**: Reflecting the company's identity and values through a cohesive design. 🎨
|
||||||
|
* **Service Showcase**: clearly detailing the products and services offered to the market. 🛠️
|
||||||
|
* **Customer Trust**: Building confidence through a transparent and professional online presence. ✅
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 🏢 **Corporate Identity**: Dedicated sections for Mission, Vision, and Leadership Team information.
|
||||||
|
* 🛍️ **Services Portfolio**: Detailed catalog of services and offerings with descriptions.
|
||||||
|
* 📝 **News & Updates**: A blog function to share the latest company news and industry insights.
|
||||||
|
* 📧 **Contact Form**: An easy way for potential clients to get in touch. ✉️
|
||||||
|
* 📱 **Responsive Design**: Optimized for mobile and desktop for a professional look on any device.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Elevating corporate presence with digital excellence!* 🌟📈
|
||||||
34
src/data/projects/ppid.mdx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: PPID Purwakarta 🏛️📂
|
||||||
|
type: core
|
||||||
|
preview: https://ppid.purwakarta.go.id
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- PHP
|
||||||
|
- MySQL
|
||||||
|
- Bootstrap
|
||||||
|
- Javascript
|
||||||
|
description: A digital transparency platform for the Regional Information Management Officer (PPID), facilitating public access to government documents and information requests. 🏛️📂
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
**PPID Purwakarta** is the digital gateway to transparency for the Regional Government! 🚪✨ It serves as the official platform for the **Pejabat Pengelola Informasi dan Dokumentasi (PPID)**, ensuring that the public has easy access to open government information.
|
||||||
|
|
||||||
|
This application is all about **accountability and openness**. It allows citizens to submit information requests online and browse through a vast repository of public documents, making government data accessible to everyone with just a few clicks! 🖱️✅
|
||||||
|
|
||||||
|
## 🚀 Key Objectives
|
||||||
|
* **Public Transparency**: Fulfilling the right of the public to access information in a transparent manner. 🔍
|
||||||
|
* **Efficient Service**: Streamlining the process of information requests from manual to digital. ⚡
|
||||||
|
* **Structured Archiving**: Organizing official documents and regulations for easy retrieval. 🗂️
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 📝 **Online Information Requests**: Citizens can easily submit and track requests for specific information.
|
||||||
|
* 📂 **Document Repository**: A structured library of public documents, regulations, and official reports.
|
||||||
|
* ⚖️ **Objection Submission**: A formal channel for the public to file objections regarding information services.
|
||||||
|
* 📊 **Service Statistics**: Real-time data on the number of requests and their status.
|
||||||
|
* 📰 **News & Announcement**: official updates and news from the information officer.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Opening the doors to government transparency!* 🇮🇩📜
|
||||||
33
src/data/projects/sibadeksa.mdx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: Sibadeksa 🏞️📊
|
||||||
|
type: core
|
||||||
|
preview: https://sibadeksa.purwakarta.go.id
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- Javascript
|
||||||
|
- Bootstrap
|
||||||
|
- MySQL
|
||||||
|
description: An Integrated Database Information System for Economic Sector Development and Natural Resources, centralizing regional data. 🏞️📊
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
**Sibadeksa** is the digital cornerstone for regional planning! 🏛️ It stands for **Sistem Informasi Basis Data Terpadu Pembangunan Sektor Perekonomian dan Sumber Daya Alam**. This powerful platform centralizes crucial datasets related to the **Economy** 💰 and **Natural Resources** 🌳 of the region.
|
||||||
|
|
||||||
|
Designed for government planners and analysts, it serves as a unified repository for data across various sectors – from agriculture 🌾 to industry 🏭! With visual analytics, it empowers decision-makers to craft informed policies for sustainable development. 📈🌍
|
||||||
|
|
||||||
|
## 🚀 Key Objectives
|
||||||
|
* **Data Integration**: Breaking down silos by centralizing data from various economic and resource sectors. 🔗
|
||||||
|
* **Informed Policy**: Enabling evidence-based decision-making for regional development plans. 💡
|
||||||
|
* **Monitoring Progress**: Tracking the growth and utilization of resources over time. ⏳
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 🗄️ **Centralized Dataset**: A unified database for economic indicators and natural resource management.
|
||||||
|
* 🌾 **Sector Data Management**: Dedicated modules for Agriculture, Industry, Trade, and Natural Resources.
|
||||||
|
* 📊 **Visual Analytics**: Interactive charts and graphs to visualize development trends and performance.
|
||||||
|
* 🗺️ **Resource Mapping**: Geospatial insights into the distribution of natural resources.
|
||||||
|
* 📑 **Report Generation**: Automated reports for government planning meetings and evaluations.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Data-driven development for a prosperous region!* 🇮🇩💪
|
||||||
36
src/data/projects/simekdom.mdx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: Simekdom 📰🤝
|
||||||
|
type: core
|
||||||
|
preview: https://simedkom.pratama.live
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- Livewire
|
||||||
|
- Filament
|
||||||
|
- AlpineJS
|
||||||
|
- TailwindCSS
|
||||||
|
- MySQL
|
||||||
|
description: An integrated media communication system facilitating seamless collaboration and monitoring between local government and media partners. 📰🤝
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
**Simekdom** is the ultimate bridge between the local government and the media world! 🌉 This powerful platform is designed to manage and monitor the dynamic relationship between government institutions and their media partners.
|
||||||
|
|
||||||
|
It’s not just about tracking news; it’s about **fostering collaboration**! From managing partnerships to monitoring media coverage, Simekdom ensures that communication is transparent, efficient, and well-documented. Plus, with built-in analytics, the government can see exactly how their message is reaching the public! 📢📊
|
||||||
|
|
||||||
|
## 🚀 Why It's Impactful
|
||||||
|
* **Government-Media Synergy**: Creates a structured environment for cooperation, making it easier to work together. 🤝
|
||||||
|
* **Data-Driven Decisions**: The dashboard provides real-time insights into media performance and coverage. 💡
|
||||||
|
* **Seamless Communication**: Integrated email messaging ensures that no important update gets lost in the shuffle! 📧
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 📺 **Media Monitoring**: Keep a close eye on news coverage and media activities related to the government.
|
||||||
|
* 🤝 **Partnership Management**: efficiently manage relationships with various media outlets and partners.
|
||||||
|
* 📝 **Collaboration Hub**: A dedicated space to handle agreements, contracts, and joint activities.
|
||||||
|
* 📊 **Dashboard & Analytics**: Visualize data with intuitive charts to evaluate media impact and reach.
|
||||||
|
* 📰 **Integrated Blog**: Publish official updates, press releases, and news directly from the system.
|
||||||
|
* 📧 **Contact & Email Messaging**: sophisticated 2-way communication system with email reply capabilities! 📬
|
||||||
|
|
||||||
|
---
|
||||||
|
*Empowering transparent and effective government communication!* 🏛️📡
|
||||||
33
src/data/projects/vngrup-multi-bisnis.mdx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: VNGrup Multi-Business 🧸👗
|
||||||
|
type: core
|
||||||
|
preview: https://vngrup.com
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- Javascript
|
||||||
|
- Bootstrap
|
||||||
|
- WhatsApp Integration
|
||||||
|
- MySQL
|
||||||
|
description: A suite of applications for managing different business lines of VNGrup, including toys and clothing, featuring internal management and WhatsApp purchase integration.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
**VNGrup** is a multi-faceted business ecosystem that powers the operations of various sectors under the **VN** brand! 🚀 This suite of applications is tailored to handle the unique needs of **Toy Stores** and **Clothing Retail**.
|
||||||
|
|
||||||
|
While the system is robust for **internal management**—tracking stock, sales, and employee activities—it also features a **Customer-Facing Landing Page** that showcases products. When a customer is ready to buy, the system seamlessly redirects them to **WhatsApp** for a personalized purchase experience! 📲💬
|
||||||
|
|
||||||
|
## 🚀 Key Objectives
|
||||||
|
* **Diverse Business Management**: Unified platform for managing disparate business lines (Toys & Fashion). 🎭
|
||||||
|
* **Internal Efficiency**: Streamlining the backend operations for inventory and staff management. ⚙️
|
||||||
|
* **Direct-to-Customer Sales**: Bridging the gap between browsing and buying through direct WhatsApp communication. 🛍️
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 🧸 **Toy Business Management**: Specialized tools for managing toy inventory and sales.
|
||||||
|
* 👗 **Clothing Retail System**: Dedicated features for tracking garment stock and fashion trends.
|
||||||
|
* 🌐 **Product Landing Page**: A showcase for the business's best products to attract customers.
|
||||||
|
* 📱 **WhatsApp Purchase Flow**: Redirects interested buyers directly to WhatsApp for a smooth transaction.
|
||||||
|
* 📦 **Inventory Control**: Real-time tracking of stock levels across different business units.
|
||||||
|
* 👥 **Employee Management**: Tools for monitoring staff shifts and performance.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Managing diverse businesses with unified digital power!* 🌟💼
|
||||||
38
src/data/projects/webdesaku.mdx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Webdesaku 🏘️🛒
|
||||||
|
type: core
|
||||||
|
preview: https://cadassari.desa.id
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- Livewire
|
||||||
|
- Javascript
|
||||||
|
- Bootstrap
|
||||||
|
- MySQL
|
||||||
|
- RajaOngkir
|
||||||
|
- Xendit
|
||||||
|
description: A powerful e-commerce platform for Village-Owned Enterprises (BUMDes) and a hub for public information transparency, integrated with logistics and payment gateways. 🏘️🛒
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
**Webdesaku** is a revolutionary digital platform that empowers villages! 🚀 It serves a dual purpose: driving economic growth through **Village-Owned Enterprises (BUMDes)** e-commerce and ensuring **public information transparency** for the community.
|
||||||
|
|
||||||
|
This system is a game-changer for local economies, providing a professional online marketplace for village products while also serving as a trusted source of official village news and data. Plus, it seamlessly integrates with **Pusdatin** (Village Data Center) for unified data management! 🌐📊
|
||||||
|
|
||||||
|
## 🚀 Why It's Essential
|
||||||
|
* **Economic Empowerment**: transform local village businesses into digital powerhouses, reaching a wider market! 💰
|
||||||
|
* **Modern Convenience**: Integrated with **RajaOngkir** for automatic shipping calculations and **Xendit** for secure digital payments. 🚚💳
|
||||||
|
* **Transparent Governance**: Keeps the community informed with open access to village data, news, and official announcements. 📢
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 🛒 **BUMDes E-Commerce**: A full-featured online store for managing products, sales, and orders efficiently.
|
||||||
|
* 📦 **Logistics Integration**: Real-time shipping cost calculation with **RajaOngkir** API.
|
||||||
|
* 💳 **Secure Payments**: Frictionless transactions with **Xendit** payment gateway integration.
|
||||||
|
* 📊 **Smart Dashboard**: Comprehensive analytics on sales, visitor stats, and village data.
|
||||||
|
* 📰 **Village Information Hub**: Publish news, announcements, and public documents for transparency.
|
||||||
|
* 🔗 **Pusdatin Integration**: Seamlessly syncs with the central village data application for accurate records.
|
||||||
|
* 📝 **Content Management**: Easy-to-use tools for managing blogs, products, and village profiles.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Digitalizing village economies and connecting communities!* 🌍🤝
|
||||||
50
src/data/projects/yadi-parfum.mdx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: Yadi Parfum 🌸✨
|
||||||
|
type: core
|
||||||
|
preview: https://yadiparfum.com
|
||||||
|
technologies:
|
||||||
|
- Laravel
|
||||||
|
- Livewire
|
||||||
|
- AlpineJS
|
||||||
|
- TailwindCSS
|
||||||
|
- MySQL
|
||||||
|
- Flux UI
|
||||||
|
description: A comprehensive digital management system for the perfume business, handling sales, inventory, finance, and customer loyalty programs. 🌸✨
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
**Yadi Parfum** is more than just a management system; it's a complete digital upgrade for the perfume business! 🚀 Designed to streamline everything from inventory significantly to sales, this platform ensures that business operations run as smoothly as a fine fragrance.
|
||||||
|
|
||||||
|
We’ve built a robust system that handles the nitty-gritty of **finance, stock, and payroll**, while also spicing things up with **membership and voucher features** to keep customers coming back for more! 😍
|
||||||
|
|
||||||
|
Plus, we're not stopping here! The system is built ready for the future, with plans to evolve into a full-blown **online shopping platform**! 🛒💨
|
||||||
|
|
||||||
|
## 🚀 Digital Transformation Success
|
||||||
|
This project is a shining example of **successful digital transformation**! ✨ By moving away from manual processes to this all-in-one digital solution, Yadi Parfum has leveled up their game.
|
||||||
|
* **No more headaches** with manual tracking. 🤯➡️😌
|
||||||
|
* **Efficiency is through the roof!** 📈
|
||||||
|
* Business owners can now focus on **growing their brand** instead of getting lost in paperwork.
|
||||||
|
|
||||||
|
## 💡 Why Users Love It
|
||||||
|
The impact on the user experience has been huge! Here’s why it’s a game-changer:
|
||||||
|
* ⚡ **Lightning-Fast Analysis**: Real-time data means decisions can be made in a snap.
|
||||||
|
* 📊 **Complete & Clear Reports**: No more guessing games; every penny and perfume bottle is accounted for.
|
||||||
|
* 🧘 **Peace of Mind**: Automated payroll and stock alerts mean fewer worries.
|
||||||
|
* 🤝 **Happier Customers**: The loyalty system makes customers feel special and valued. 💖
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
* 🧃**Product Mastery**: Effortlessly organize your perfume catalog with variants, pricing, and details.
|
||||||
|
* 🏭 **Warehouse Command**: Advanced control over stock movements and multiple storage locations.
|
||||||
|
* 📦 **Smart Inventory**: Track raw materials and stock levels in real-time.
|
||||||
|
* 💰 **Financial Wizardry**: Keep a close eye on income, expenses, and profits without the stress.
|
||||||
|
* 💳 **Seamless Sales (POS)**: A smooth checkout experience supporting multiple payment methods.
|
||||||
|
* 📝 **Integrated Blog**: Share perfume tips and business updates directly with your audience!
|
||||||
|
* 🎁 **Loyalty & Vouchers**: Engage customers with memberships and exciting discounts.
|
||||||
|
* 👥 **Payroll Made Easy**: Manage employee salaries and performance effortlessly.
|
||||||
|
* 📲 **Realtime Notifications**: Stay updated instantly with alerts for sales and stock levels! 📲
|
||||||
|
* 🔜 **Future E-Commerce**: Ready to expand into online sales! 🌐
|
||||||
|
|
||||||
|
---
|
||||||
|
*Bringing the sweet scent of success to business management!* 🌹💼
|
||||||
132
src/layouts/layout.astro
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
import "@fontsource/geist-sans";
|
||||||
|
import "@fontsource/geist-sans/500.css";
|
||||||
|
import "@fontsource/geist-sans/600.css";
|
||||||
|
import "@fontsource/geist-sans/700.css";
|
||||||
|
import "@fontsource/geist-sans/800.css";
|
||||||
|
import "@fontsource/geist-sans/900.css";
|
||||||
|
import "@fontsource/roboto-condensed/600.css";
|
||||||
|
import "@fontsource/roboto-condensed/400.css";
|
||||||
|
import "@fontsource/jetbrains-mono/400.css";
|
||||||
|
|
||||||
|
import { ClientRouter } from "astro:transitions";
|
||||||
|
import Analytics from "@vercel/analytics/astro";
|
||||||
|
|
||||||
|
import { SITE } from "@/lib/config";
|
||||||
|
|
||||||
|
import "@/styles/global.css";
|
||||||
|
import BackToTop from "@/components/back-to-top.astro";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
author?: string;
|
||||||
|
profile?: string;
|
||||||
|
description?: string;
|
||||||
|
ogImage?: string;
|
||||||
|
pageType?: string;
|
||||||
|
canonicalURL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = SITE.title,
|
||||||
|
author = SITE.author,
|
||||||
|
profile = SITE.profile,
|
||||||
|
description = SITE.desc,
|
||||||
|
ogImage = SITE.ogImage,
|
||||||
|
pageType = SITE.pageType,
|
||||||
|
canonicalURL = new URL(Astro.url.pathname, Astro.url),
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const socialImageURL = new URL(ogImage ?? SITE.ogImage, Astro.site).toString();
|
||||||
|
|
||||||
|
const structuredData = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BlogPosting",
|
||||||
|
headline: `${title}`,
|
||||||
|
image: `${socialImageURL}`,
|
||||||
|
author: [
|
||||||
|
{
|
||||||
|
"@type": "Person",
|
||||||
|
name: `${author}`,
|
||||||
|
...(profile && { url: profile }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("astro:after-swap", () => {
|
||||||
|
const getThemePreference = () => {
|
||||||
|
if (
|
||||||
|
typeof localStorage !== "undefined" &&
|
||||||
|
localStorage.getItem("theme")
|
||||||
|
) {
|
||||||
|
return localStorage.getItem("theme");
|
||||||
|
}
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDark = getThemePreference() === "dark";
|
||||||
|
document.documentElement.classList[isDark ? "add" : "remove"]("dark");
|
||||||
|
|
||||||
|
if (typeof localStorage !== "undefined") {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const isDark = document.documentElement.classList.contains("dark");
|
||||||
|
localStorage.setItem("theme", isDark ? "dark" : "light");
|
||||||
|
});
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<html lang="en" class="scroll-smooth">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<!-- <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> -->
|
||||||
|
<link rel="canonical" href={canonicalURL} />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
|
||||||
|
<!-- General Meta Tags -->
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="title" content={title} />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta name="author" content={author} />
|
||||||
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content={pageType} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:image" content={socialImageURL} />
|
||||||
|
<meta property="og:url" content={canonicalURL} />
|
||||||
|
|
||||||
|
<!-- X / Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="twitter:url" content={canonicalURL} />
|
||||||
|
<meta property="twitter:title" content={title} />
|
||||||
|
<meta property="twitter:description" content={description} />
|
||||||
|
<meta property="twitter:image" content={socialImageURL} />
|
||||||
|
|
||||||
|
<ClientRouter />
|
||||||
|
|
||||||
|
<!-- Google JSON-LD Structured data -->
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
is:inline
|
||||||
|
set:html={JSON.stringify(structuredData)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<meta name="theme-color" content="" />
|
||||||
|
</head>
|
||||||
|
<body class="relative min-h-screen overflow-x-hidden">
|
||||||
|
<slot />
|
||||||
|
<BackToTop />
|
||||||
|
<Analytics />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
57
src/layouts/page-layout.astro
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
import { capitalizeFirstLetter } from "@/lib/utils";
|
||||||
|
import Layout from "./layout.astro";
|
||||||
|
|
||||||
|
import Navbar from "@/components/navbar/index.astro";
|
||||||
|
import Footer from "@/components/footer.astro";
|
||||||
|
import LinkBtn from "@/components/ui/link-btn.astro";
|
||||||
|
import Pattern from "@/components/ui/pattern.astro";
|
||||||
|
|
||||||
|
import Box from "@/components/box/index.astro";
|
||||||
|
import BoxTitle from "@/components/box/title.astro";
|
||||||
|
import BoxHeader from "@/components/box/header.astro";
|
||||||
|
import BoxContent from "@/components/box/content.astro";
|
||||||
|
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
backLink?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, backLink } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={`${title}`}>
|
||||||
|
<Navbar />
|
||||||
|
<main class="mx-auto min-h-screen max-w-3xl" aria-label="Main content">
|
||||||
|
<Pattern className="h-14">
|
||||||
|
{
|
||||||
|
backLink && (
|
||||||
|
<LinkBtn
|
||||||
|
ariaLabel="Back link"
|
||||||
|
className="absolute bottom-0 left-2 z-1"
|
||||||
|
href={backLink}
|
||||||
|
>
|
||||||
|
<ArrowLeft />
|
||||||
|
</LinkBtn>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Pattern>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<BoxHeader>
|
||||||
|
<BoxTitle className="text-2xl !font-black"
|
||||||
|
>{capitalizeFirstLetter(title)}</BoxTitle
|
||||||
|
>
|
||||||
|
</BoxHeader>
|
||||||
|
<BoxContent>
|
||||||
|
{description}
|
||||||
|
</BoxContent>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</Layout>
|
||||||
104
src/layouts/project-layout.astro
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
import Footer from "@/components/footer.astro";
|
||||||
|
import Navbar from "@/components/navbar/index.astro";
|
||||||
|
import { capitalizeFirstLetter } from "@/lib/utils";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
import Layout from "./layout.astro";
|
||||||
|
|
||||||
|
import Pattern from "@/components/ui/pattern.astro";
|
||||||
|
|
||||||
|
import BoxContent from "@/components/box/content.astro";
|
||||||
|
import BoxHeader from "@/components/box/header.astro";
|
||||||
|
import Box from "@/components/box/index.astro";
|
||||||
|
import BoxTitle from "@/components/box/title.astro";
|
||||||
|
|
||||||
|
import Badge from "@/components/ui/badge.astro";
|
||||||
|
import LinkBtn from "@/components/ui/link-btn.astro";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
technologies: string[];
|
||||||
|
sourceCode?: string;
|
||||||
|
preview?: string;
|
||||||
|
image?: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, technologies, description, preview, sourceCode, image } =
|
||||||
|
Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={`${title}`} ogImage={image} description={description}>
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main class="mx-auto min-h-screen max-w-3xl" aria-label="Main content">
|
||||||
|
<Pattern className="h-14">
|
||||||
|
<div class="z-1 mx-2 my-auto flex w-full items-center justify-between">
|
||||||
|
<LinkBtn ariaLabel="Back btn" href="/projects">
|
||||||
|
<Icon name="lucide:arrow-left" class="size-4" />
|
||||||
|
</LinkBtn>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
sourceCode && (
|
||||||
|
<LinkBtn
|
||||||
|
ariaLabel="Source code"
|
||||||
|
target="_blank"
|
||||||
|
className="text-xs"
|
||||||
|
variant="default"
|
||||||
|
href={sourceCode}
|
||||||
|
>
|
||||||
|
Source
|
||||||
|
<Icon name="logos:github" class="size-3" />
|
||||||
|
</LinkBtn>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
preview && (
|
||||||
|
<LinkBtn
|
||||||
|
ariaLabel="Deployed App"
|
||||||
|
variant="default"
|
||||||
|
target="_blank"
|
||||||
|
className="text-xs"
|
||||||
|
variant="default"
|
||||||
|
href={preview}
|
||||||
|
>
|
||||||
|
Show
|
||||||
|
<Icon name="lucide:external-link" class="size-3" />
|
||||||
|
</LinkBtn>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Pattern>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<BoxHeader>
|
||||||
|
<BoxTitle className="text-2xl !font-black md:text-3xl"
|
||||||
|
>{capitalizeFirstLetter(title)}</BoxTitle
|
||||||
|
>
|
||||||
|
</BoxHeader>
|
||||||
|
|
||||||
|
<BoxContent className="space-y-4">
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
{
|
||||||
|
technologies.map(technology => {
|
||||||
|
return <Badge>{technology}</Badge>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</BoxContent>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Pattern />
|
||||||
|
|
||||||
|
<Box className="min-w-full">
|
||||||
|
<BoxContent className="pt-8">
|
||||||
|
<article class="prose min-w-full">
|
||||||
|
<slot />
|
||||||
|
</article>
|
||||||
|
</BoxContent>
|
||||||
|
</Box>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
9
src/lib/config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const SITE = {
|
||||||
|
pageType: 'website',
|
||||||
|
author: "Yoga Pangestu",
|
||||||
|
profile: "https://pangestu.vercel.app",
|
||||||
|
desc: "A digital space to share my professional experiences and work.",
|
||||||
|
title: "Yoga Pangestu",
|
||||||
|
ogImage: '/og-image.jpg',
|
||||||
|
postPerPage: 6,
|
||||||
|
} as const;
|
||||||
50
src/lib/constants/experience.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import type { Experience } from "../types";
|
||||||
|
|
||||||
|
export const EXPERIENCES: Experience[] = [
|
||||||
|
{
|
||||||
|
company: "PT Pratama Solusi Teknologi",
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
title: "Full Stack Developer",
|
||||||
|
year: "May 01, 2023 - Present",
|
||||||
|
description: [
|
||||||
|
"Developed and maintained multiple web applications for business clients and local government institutions.",
|
||||||
|
"Implemented RESTful APIs for internal systems and third-party integrations.",
|
||||||
|
"Involved in application architecture design from requirement analysis through deployment.",
|
||||||
|
"Handled deployment and configuration on production servers.",
|
||||||
|
"Collaborated with developers and stakeholders to deliver features aligned with business needs.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
company: "Freelance | Locally",
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
title: "MERN Stack Developer",
|
||||||
|
year: "May 01, 2024 - Present",
|
||||||
|
description: [
|
||||||
|
"Developed custom web applications based on client requirements, translating business needs into scalable technical solutions.",
|
||||||
|
"Assisted local business owners in digitizing their operations through tailored web systems and automation tools.",
|
||||||
|
"Designed and implemented RESTful APIs for frontend-backend communication and third-party integrations.",
|
||||||
|
"Deployed applications to production environments and handled basic server configuration and maintenance.",
|
||||||
|
"Provided ongoing support, feature enhancements, and performance optimization for client projects.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
company: "Yadi Parfum",
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
title: "Tim Leader",
|
||||||
|
year: "April 01, 2023 - May 01, 2025",
|
||||||
|
description: [
|
||||||
|
"Developed a business operation system covering inventory, payroll, transactions, and employee data.",
|
||||||
|
"Assisted in digitizing operational processes previously handled manually.",
|
||||||
|
"Coordinated daily operational needs and supported team collaboration.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
14
src/lib/constants/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export const NAV_LINKS = [
|
||||||
|
{
|
||||||
|
label: "Home",
|
||||||
|
href: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Projects",
|
||||||
|
href: "/projects",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Contact",
|
||||||
|
href: "/contact",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
13
src/lib/constants/profile.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export const PROFILE_INFO = {
|
||||||
|
role: "Full Stack Developer",
|
||||||
|
logo: "Yoga Pangestu",
|
||||||
|
slogan: "I want to be a superman.",
|
||||||
|
displayName: "Yoga Pangestu",
|
||||||
|
email: "info.pangestuyoga@gmail.com",
|
||||||
|
about: `
|
||||||
|
I'm a Full Stack Developer with over 3 years of experience in web application development, covering all stages of the development process, including system design, development, testing, and deployment. I work with a structured approach and pay close attention to code quality, performance, and application security.
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
I have a strong curiosity about emerging technologies, enjoy continuous learning, and adapt quickly to new tools and frameworks. I am comfortable working independently as well as collaborating within teams to deliver effective and sustainable digital solutions.
|
||||||
|
`,
|
||||||
|
} as const;
|
||||||
101
src/lib/constants/tech-stack.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
export const TECH_STACK = [
|
||||||
|
// Backend Framework
|
||||||
|
{
|
||||||
|
title: "Laravel",
|
||||||
|
href: "https://laravel.com/",
|
||||||
|
icon: "logos:laravel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Livewire",
|
||||||
|
href: "https://livewire.laravel.com/",
|
||||||
|
icon: "devicon:livewire",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Filament",
|
||||||
|
href: "https://filamentphp.com/",
|
||||||
|
icon: "fluent-color:lightbulb-filament-16",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Programming Languages
|
||||||
|
{
|
||||||
|
title: "JavaScript",
|
||||||
|
href: "https://developer.mozilla.org/en-US/docs/Web/JavaScript",
|
||||||
|
icon: "logos:javascript",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Python",
|
||||||
|
href: "https://www.python.org/",
|
||||||
|
icon: "logos:python",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Frontend Technologies
|
||||||
|
{
|
||||||
|
title: "HTML5",
|
||||||
|
href: "https://developer.mozilla.org/en-US/docs/Web/HTML",
|
||||||
|
icon: "logos:html-5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "CSS",
|
||||||
|
href: "https://developer.mozilla.org/en-US/docs/Web/CSS",
|
||||||
|
icon: "skill-icons:css",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Astro",
|
||||||
|
href: "https://astro.build/",
|
||||||
|
icon: "devicon:astro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Vue.js",
|
||||||
|
href: "https://vuejs.org/",
|
||||||
|
icon: "logos:vue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Alpine.js",
|
||||||
|
href: "https://alpinejs.dev/",
|
||||||
|
icon: "devicon:alpinejs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tailwind CSS",
|
||||||
|
href: "https://tailwindcss.com/",
|
||||||
|
icon: "devicon:tailwindcss",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bootstrap",
|
||||||
|
href: "https://getbootstrap.com/",
|
||||||
|
icon: "logos:bootstrap",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Flux UI",
|
||||||
|
href: "https://fluxui.dev/",
|
||||||
|
icon: "logos:flux",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Databases
|
||||||
|
{
|
||||||
|
title: "MySQL",
|
||||||
|
href: "https://www.mysql.com/",
|
||||||
|
icon: "logos:mysql",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "PostgreSQL",
|
||||||
|
href: "https://www.postgresql.org/",
|
||||||
|
icon: "logos:postgresql",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Development Tools
|
||||||
|
{
|
||||||
|
title: "Git",
|
||||||
|
href: "https://git-scm.com/",
|
||||||
|
icon: "devicon:git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "VS Code",
|
||||||
|
href: "https://code.visualstudio.com/",
|
||||||
|
icon: "logos:visual-studio-code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "GitHub",
|
||||||
|
href: "https://github.com/",
|
||||||
|
icon: "devicon:github",
|
||||||
|
},
|
||||||
|
];
|
||||||
13
src/lib/markdown.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import MarkdownIt from 'markdown-it';
|
||||||
|
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
html: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function markdownToHtml(markdown: string, className?: string): string {
|
||||||
|
md.renderer.rules.paragraph_open = () => `<p class="${className}">`;
|
||||||
|
|
||||||
|
return md.render(markdown);
|
||||||
|
}
|
||||||
|
|
||||||
10
src/lib/supabase.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
const supabaseUrl = import.meta.env.SUPABASE_URL || process.env.SUPABASE_URL;
|
||||||
|
const supabaseAnonKey = import.meta.env.SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY;
|
||||||
|
|
||||||
|
if (!supabaseUrl || !supabaseAnonKey) {
|
||||||
|
console.warn('Warning: Supabase credentials are not defined in environmental variables.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl || '', supabaseAnonKey || '');
|
||||||
10
src/lib/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export type ExperiencePosition = {
|
||||||
|
title: string;
|
||||||
|
year: string;
|
||||||
|
description: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Experience = {
|
||||||
|
company: string;
|
||||||
|
positions: ExperiencePosition[];
|
||||||
|
};
|
||||||
57
src/lib/utils.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
import calculateReadingTime from 'reading-time';
|
||||||
|
import { fromMarkdown } from 'mdast-util-from-markdown';
|
||||||
|
import { toString } from 'mdast-util-to-string';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function capitalizeFirstLetter(sentence: string): string {
|
||||||
|
return `${sentence.charAt(0).toUpperCase()}${sentence.slice(1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export const getReadingTime = (text: string): string | undefined => {
|
||||||
|
if (!text || !text.length) return undefined;
|
||||||
|
try {
|
||||||
|
const { minutes } = calculateReadingTime(toString(fromMarkdown(text)));
|
||||||
|
if (minutes && minutes > 0) {
|
||||||
|
return `${Math.ceil(minutes)} min read`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
228
src/pages/contact.astro
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@/layouts/page-layout.astro";
|
||||||
|
import Box from "@/components/box/index.astro";
|
||||||
|
import BoxContent from "@/components/box/content.astro";
|
||||||
|
import { buttonVariants } from "@/lib/utils";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout
|
||||||
|
title="Contact Me"
|
||||||
|
description="I’d be glad to connect and discuss how we might work together. Whether you have a project in mind, a question, or just want to say hello, feel free to reach out."
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<BoxContent>
|
||||||
|
<form id="contact-form" class="space-y-6">
|
||||||
|
<!-- Access Key for Web3Forms -->
|
||||||
|
<input type="hidden" name="access_key" value="13de7619-2b61-4a08-ac80-234cb27882dd">
|
||||||
|
<!-- Customize Web3Forms Settings -->
|
||||||
|
<input type="hidden" name="subject" value="Portfolio Inquiry - New Contact Message">
|
||||||
|
<input type="hidden" name="from_name" value="Pangestu Portfolio">
|
||||||
|
<!-- Botspam protection -->
|
||||||
|
<input type="checkbox" name="botcheck" class="hidden" style="display: none;">
|
||||||
|
|
||||||
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="name" class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
|
Name <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
placeholder="Your name"
|
||||||
|
required
|
||||||
|
autocomplete="off"
|
||||||
|
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-destructive hidden" id="name-error">Please enter your name.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="phone" class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
|
Phone Number <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="phone"
|
||||||
|
name="phone"
|
||||||
|
placeholder="+62 821 .... ...."
|
||||||
|
autocomplete="off"
|
||||||
|
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-destructive hidden" id="phone-error">Please enter a valid phone number (min 10 digits).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="email" class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
|
Email <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="johndoe@example.com"
|
||||||
|
required
|
||||||
|
autocomplete="off"
|
||||||
|
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-destructive hidden" id="email-error">Please enter a valid email address.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="message" class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
|
Message <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
name="message"
|
||||||
|
rows="5"
|
||||||
|
placeholder="Tell me about your project..."
|
||||||
|
required
|
||||||
|
autocomplete="off"
|
||||||
|
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
></textarea>
|
||||||
|
<p class="text-xs text-destructive hidden" id="message-error">Please enter a message.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" id="submit-btn" class={buttonVariants({ className: "w-full sm:w-auto" })}>
|
||||||
|
Send Message
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="result" class="mt-4 text-center text-sm font-medium hidden"></div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</BoxContent>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<div class="mt-12 text-center text-sm text-muted-foreground">
|
||||||
|
<p>Prefer chat via WhatsApp?</p>
|
||||||
|
<a href="https://wa.me/6282121495806?text=Hello%20I%20am%20interested%20in%20your%20services"
|
||||||
|
class="text-primary hover:underline">
|
||||||
|
Chat on WhatsApp
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
const form = document.getElementById('contact-form');
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
|
||||||
|
const nameInput = document.getElementById('name');
|
||||||
|
const phoneInput = document.getElementById('phone');
|
||||||
|
const emailInput = document.getElementById('email');
|
||||||
|
const messageInput = document.getElementById('message');
|
||||||
|
|
||||||
|
const nameError = document.getElementById('name-error');
|
||||||
|
const phoneError = document.getElementById('phone-error');
|
||||||
|
const emailError = document.getElementById('email-error');
|
||||||
|
const messageError = document.getElementById('message-error');
|
||||||
|
|
||||||
|
// Regex patterns
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
const phoneRegex = /^\+?[\d\s-]{10,}$/; // Allows +, digits, spaces, hyphens, min 10 chars
|
||||||
|
|
||||||
|
function showError(input, errorElement, show) {
|
||||||
|
if (show) {
|
||||||
|
errorElement.classList.remove('hidden');
|
||||||
|
input.classList.add('border-destructive');
|
||||||
|
input.classList.remove('border-input');
|
||||||
|
} else {
|
||||||
|
errorElement.classList.add('hidden');
|
||||||
|
input.classList.remove('border-destructive');
|
||||||
|
input.classList.add('border-input');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Name Validation
|
||||||
|
if (!nameInput.value.trim()) {
|
||||||
|
showError(nameInput, nameError, true);
|
||||||
|
isValid = false;
|
||||||
|
} else {
|
||||||
|
showError(nameInput, nameError, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email Validation
|
||||||
|
if (!emailRegex.test(emailInput.value.trim())) {
|
||||||
|
showError(emailInput, emailError, true);
|
||||||
|
isValid = false;
|
||||||
|
} else {
|
||||||
|
showError(emailInput, emailError, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone Validation (Optional but basic format if entered)
|
||||||
|
if (phoneInput.value.trim() && !phoneRegex.test(phoneInput.value.trim())) {
|
||||||
|
showError(phoneInput, phoneError, true);
|
||||||
|
isValid = false;
|
||||||
|
} else {
|
||||||
|
showError(phoneInput, phoneError, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message Validation
|
||||||
|
if (!messageInput.value.trim()) {
|
||||||
|
showError(messageInput, messageError, true);
|
||||||
|
isValid = false;
|
||||||
|
} else {
|
||||||
|
showError(messageInput, messageError, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const object = Object.fromEntries(formData);
|
||||||
|
const json = JSON.stringify(object);
|
||||||
|
|
||||||
|
result.innerHTML = "Sending...";
|
||||||
|
result.classList.remove('hidden', 'text-green-500', 'text-red-500');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
|
|
||||||
|
fetch('https://api.web3forms.com/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: json
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
let json = await response.json();
|
||||||
|
if (response.status == 200) {
|
||||||
|
result.innerHTML = "Message sent successfully! I'll get back to you soon.";
|
||||||
|
result.classList.add('text-green-500');
|
||||||
|
form.reset();
|
||||||
|
} else {
|
||||||
|
console.log(response);
|
||||||
|
result.innerHTML = json.message || "Something went wrong. Please try again later.";
|
||||||
|
result.classList.add('text-red-500');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
result.innerHTML = "Something went wrong. Please try again later.";
|
||||||
|
result.classList.add('text-red-500');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
form.reset();
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
setTimeout(() => {
|
||||||
|
result.style.display = "none";
|
||||||
|
result.classList.add('hidden'); // Ensure hidden class is re-added
|
||||||
|
result.style.display = ""; // Clear inline style
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
31
src/pages/index.astro
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
import Layout from "@/layouts/layout.astro";
|
||||||
|
import Navbar from "@/components/navbar/index.astro";
|
||||||
|
import Footer from "@/components/footer.astro";
|
||||||
|
import Pattern from "@/components/ui/pattern.astro";
|
||||||
|
import Profile from "@/components/profile.astro";
|
||||||
|
import About from "@/components/sections/about.astro";
|
||||||
|
import TechStack from "@/components/tech-stack.astro";
|
||||||
|
import Experience from "@/components/sections/experience/experience.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Yoga Pangestu">
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main
|
||||||
|
aria-label="Main content"
|
||||||
|
class="mx-auto min-h-[calc(100vh-120px)] max-w-3xl"
|
||||||
|
>
|
||||||
|
<Pattern className="h-14" />
|
||||||
|
<Profile />
|
||||||
|
<Pattern />
|
||||||
|
<About />
|
||||||
|
<Pattern />
|
||||||
|
<TechStack />
|
||||||
|
<Pattern />
|
||||||
|
<Experience />
|
||||||
|
<Pattern />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</Layout>
|
||||||
35
src/pages/projects/[projectId].astro
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
import ProjectLayout from "@/layouts/project-layout.astro";
|
||||||
|
import { render } from "astro:content";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
project: CollectionEntry<"projects">;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const projects = await getCollection("projects");
|
||||||
|
|
||||||
|
const project = projects.map(project => ({
|
||||||
|
params: { projectId: project.id },
|
||||||
|
props: { project },
|
||||||
|
}));
|
||||||
|
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { project } = Astro.props;
|
||||||
|
|
||||||
|
const rendered = await render(project);
|
||||||
|
---
|
||||||
|
|
||||||
|
<ProjectLayout
|
||||||
|
title={project.data.name}
|
||||||
|
technologies={project.data.technologies}
|
||||||
|
sourceCode={project.data.sourceCode}
|
||||||
|
preview={project.data.preview}
|
||||||
|
image={project.data.image}
|
||||||
|
description={project.data.description}
|
||||||
|
>
|
||||||
|
<rendered.Content />
|
||||||
|
</ProjectLayout>
|
||||||
44
src/pages/projects/index.astro
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
import Pattern from "@/components/ui/pattern.astro";
|
||||||
|
import PageLayout from "@/layouts/page-layout.astro";
|
||||||
|
|
||||||
|
import Box from "@/components/box/index.astro";
|
||||||
|
import BoxHeader from "@/components/box/header.astro";
|
||||||
|
import BoxTitle from "@/components/box/title.astro";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import ProjectItem from "@/components/sections/projects/project-item.astro";
|
||||||
|
|
||||||
|
const projects = await getCollection("projects");
|
||||||
|
|
||||||
|
const coreProjects = projects.filter(project => project.data.type === "core");
|
||||||
|
const sideProjects = projects.filter(project => project.data.type === "side");
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout
|
||||||
|
title="Selected Work"
|
||||||
|
description="A curated showcase of my applications, technical experiments, and production projects."
|
||||||
|
>
|
||||||
|
|
||||||
|
<Pattern />
|
||||||
|
<Box>
|
||||||
|
<BoxHeader>
|
||||||
|
<BoxTitle> Key Project </BoxTitle>
|
||||||
|
</BoxHeader>
|
||||||
|
|
||||||
|
{coreProjects.map(item => <ProjectItem project={item} />)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{
|
||||||
|
sideProjects.length > 0 && (
|
||||||
|
<Pattern />
|
||||||
|
<Box>
|
||||||
|
<BoxHeader>
|
||||||
|
<BoxTitle>Side Hustles</BoxTitle>
|
||||||
|
</BoxHeader>
|
||||||
|
{sideProjects.map(item => <ProjectItem project={item} />)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<Pattern />
|
||||||
|
</PageLayout>
|
||||||
174
src/styles/global.css
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin '@tailwindcss/typography';
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
@import "./mdx.css";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-primary: hsl(var(--primary));
|
||||||
|
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||||
|
--color-secondary: hsl(var(--secondary));
|
||||||
|
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||||
|
--color-muted: hsl(var(--muted));
|
||||||
|
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||||
|
--color-accent: hsl(var(--accent));
|
||||||
|
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||||
|
--color-destructive: hsl(var(--destructive));
|
||||||
|
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||||
|
--color-border: hsl(var(--border));
|
||||||
|
--color-input: hsl(var(--input));
|
||||||
|
--color-ring: hsl(var(--ring));
|
||||||
|
--color-background: hsl(var(--background));
|
||||||
|
--color-foreground: hsl(var(--foreground));
|
||||||
|
--color-card: hsl(var(--card));
|
||||||
|
--color-card-foreground: hsl(var(--card-foreground));
|
||||||
|
--color-popover: hsl(var(--popover));
|
||||||
|
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||||
|
--color-hover: hsl(var(--ring));
|
||||||
|
--color-radius: var(--radius);
|
||||||
|
|
||||||
|
/* Chart Colors */
|
||||||
|
--color-chart-1: hsl(var(--chart-1));
|
||||||
|
--color-chart-2: hsl(var(--chart-2));
|
||||||
|
--color-chart-3: hsl(var(--chart-3));
|
||||||
|
--color-chart-4: hsl(var(--chart-4));
|
||||||
|
--color-chart-5: hsl(var(--chart-5));
|
||||||
|
|
||||||
|
--animate-accordion-down: accordion-down 0.3s ease-out;
|
||||||
|
--animate-accordion-up: accordion-up 0.3s ease-out;
|
||||||
|
|
||||||
|
@keyframes accordion-down {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes accordion-up {
|
||||||
|
from {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary: 240 5.9% 10%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 240 5.9% 10%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 3%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 240 10% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 240 10% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 240 5.9% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 240 3.7% 15.9%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 240 3.7% 15.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 3.7% 19%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 240 4.9% 83.9%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
@apply border-dashed border-border;
|
||||||
|
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: hsl(var(--border)) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
@apply scroll-smooth;
|
||||||
|
font-family: Geist Sans;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply overscroll-none bg-background text-foreground;
|
||||||
|
font-synthesis-weight: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
font-family: Roboto Condensed;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: JetBrains Mono;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar styling */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: hsl(var(--border));
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility screen-line-before {
|
||||||
|
@apply relative before:absolute before:top-0 before:-left-[100vw] before:h-0 before:w-[200vw] before:border-t before:border-dashed before:border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility screen-line-after {
|
||||||
|
@apply relative after:absolute after:bottom-0 after:-left-[100vw] after:h-0 after:w-[200vw] after:border-b after:border-dashed after:border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code {
|
||||||
|
@apply absolute top-2 right-2 cursor-pointer rounded-md border border-transparent p-2 text-xs font-bold duration-150 hover:bg-primary hover:text-primary-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code svg {
|
||||||
|
@apply size-4;
|
||||||
|
}
|
||||||
|
|
||||||
38
src/styles/mdx.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
@layer base {
|
||||||
|
.prose {
|
||||||
|
@apply prose-headings:!mb-2 prose-headings:scroll-mt-20 prose-headings:!text-foreground prose-p:!text-sm prose-p:leading-8 prose-p:!text-foreground lg:prose-p:!text-base dark:prose-p:!text-foreground/80 prose-a:!text-foreground prose-a:underline-offset-4 prose-blockquote:!border-l-accent prose-blockquote:opacity-80 prose-figcaption:!text-foreground prose-figcaption:opacity-70 prose-strong:!text-foreground prose-code:rounded prose-code:bg-muted/75 prose-code:p-1 prose-code:!text-foreground prose-code:before:!content-none prose-code:after:!content-none prose-pre:!max-h-[512px] prose-ol:!text-foreground prose-ul:overflow-x-clip prose-ul:!text-foreground prose-li:!text-sm prose-li:!text-foreground prose-li:marker:!text-foreground/80 lg:prose-li:!text-base dark:prose-li:!text-foreground/80 prose-table:text-foreground prose-th:border prose-th:border-border prose-td:border prose-td:border-border prose-img:!my-2 prose-img:!rounded-lg prose-video:!rounded-lg prose-hr:!border-border;
|
||||||
|
}
|
||||||
|
.prose a {
|
||||||
|
@apply break-words;
|
||||||
|
}
|
||||||
|
.prose thead th:first-child,
|
||||||
|
tbody td:first-child,
|
||||||
|
tfoot td:first-child {
|
||||||
|
padding-inline-start: 0.5714286em !important;
|
||||||
|
}
|
||||||
|
.prose h2#table-of-contents {
|
||||||
|
@apply mb-2;
|
||||||
|
}
|
||||||
|
.prose details {
|
||||||
|
@apply inline-block cursor-pointer text-foreground select-none;
|
||||||
|
}
|
||||||
|
.prose summary {
|
||||||
|
@apply focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent focus-visible:outline-dashed;
|
||||||
|
}
|
||||||
|
.prose h2#table-of-contents + p {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Code Blocks & Syntax Highlighting ===== */
|
||||||
|
pre:has(code) {
|
||||||
|
@apply border border-border;
|
||||||
|
}
|
||||||
|
code,
|
||||||
|
blockquote {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > code {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [
|
||||||
|
".astro/types.d.ts",
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||