feat: redesign my webiste from scratch
- remove hugo and paper box theme - inspiration https://jay.fish - use astro based system Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
@@ -0,0 +1,3 @@
|
|||||||
|
# WakaTime API key — used to fetch coding activity stats for the homepage
|
||||||
|
# Get yours at https://wakatime.com/settings/api-key
|
||||||
|
WAKATIME_API_KEY=
|
||||||
@@ -1,37 +1,28 @@
|
|||||||
# Sample workflow for building and deploying a Hugo site to GitHub Pages
|
|
||||||
name: Check build
|
name: Check build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Runs on pushes targeting the default branch
|
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
# Default to bash
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Build job
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 22
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: ./package-lock.json
|
|
||||||
- name: Setup Hugo
|
- name: Install dependencies
|
||||||
uses: peaceiris/actions-hugo@v2
|
run: npm ci
|
||||||
with:
|
|
||||||
hugo-version: '0.134.3'
|
- name: Check types
|
||||||
extended: true
|
run: npm run check
|
||||||
- name: Install elm-land and node packages
|
|
||||||
run: npm install
|
- name: Build site
|
||||||
- name: build
|
run: npm run build
|
||||||
run: make
|
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
/dist
|
# Dependencies
|
||||||
/.elm-land
|
node_modules/
|
||||||
/.env
|
|
||||||
/elm-stuff
|
# Build output
|
||||||
/node_modules
|
dist/
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
|
||||||
/static/main.css
|
# Legacy Hugo
|
||||||
/temp
|
public/
|
||||||
/blog/public
|
|
||||||
/blog/node_modules
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
public
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
[submodule "themes/box-box"]
|
|
||||||
path = themes/box-box
|
|
||||||
url = https://github.com/avinal/box-box.git
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
.PHONY: dev build preview clean install check fmt lint new-post help
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
help: ## Show this help
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||||
|
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
install: ## Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
dev: ## Start dev server with hot reload
|
||||||
|
npx astro dev
|
||||||
|
|
||||||
|
build: ## Build for production
|
||||||
|
npx astro build
|
||||||
|
|
||||||
|
preview: ## Preview production build locally
|
||||||
|
npx astro preview
|
||||||
|
|
||||||
|
check: ## Run Astro type checking
|
||||||
|
npx astro check
|
||||||
|
|
||||||
|
clean: ## Remove build artifacts
|
||||||
|
rm -rf dist .astro node_modules/.astro
|
||||||
|
|
||||||
|
nuke: ## Full clean (includes node_modules)
|
||||||
|
rm -rf dist .astro node_modules
|
||||||
|
|
||||||
|
fresh: nuke install ## Clean install from scratch
|
||||||
@@ -1,2 +1,87 @@
|
|||||||
# My Personal Website and Blog
|
# avinal.space
|
||||||
|
|
||||||
|
Personal website and blog built with [Astro](https://astro.build). Minimal, fast, and almost entirely HTML & CSS with zero-JS by default.
|
||||||
|
|
||||||
|
**Live:** [avinal.space](https://avinal.space)
|
||||||
|
|
||||||
|
## Design Inspiration
|
||||||
|
|
||||||
|
- [jay.fish](https://jay.fish/) — homepage layout, activity graph, bento grid
|
||||||
|
- [usememos.com](https://usememos.com/) — clean typography, color palette, overall theme
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
| Route | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `/` | Homepage with hero card, GitHub/WakaTime activity graph, Game of Life widget, recent posts, and pinned repos |
|
||||||
|
| `/posts/` | Blog index with category filters and featured images |
|
||||||
|
| `/posts/<category>/` | Category-filtered post listings |
|
||||||
|
| `/posts/<category>/<slug>/` | Individual blog posts |
|
||||||
|
| `/resume/` | Resume page (data driven from `src/data/resume.json`) |
|
||||||
|
| `/meeting/` | Book a meeting via [Cal.com](https://cal.com) embed |
|
||||||
|
| `/setup/` | Hardware and software setup |
|
||||||
|
| `/rss.xml` | RSS feed |
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) 22+
|
||||||
|
- npm
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/avinal/avinal.github.io.git
|
||||||
|
cd avinal.github.io
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the example env file and add your keys:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `WAKATIME_API_KEY` | No | Enables WakaTime coding stats on the homepage activity graph. Get yours at [wakatime.com/settings/api-key](https://wakatime.com/settings/api-key) |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev # Start dev server with hot reload
|
||||||
|
make build # Build for production
|
||||||
|
make preview # Preview production build locally
|
||||||
|
make check # Run Astro type checking
|
||||||
|
make clean # Remove build artifacts
|
||||||
|
make nuke # Full clean (includes node_modules)
|
||||||
|
make fresh # Clean install from scratch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/ # Reusable Astro components
|
||||||
|
├── config/ # Theme tokens and site config
|
||||||
|
├── content/posts/ # Blog posts (Markdown)
|
||||||
|
├── data/ # JSON data (resume, repos)
|
||||||
|
├── layouts/ # Page layouts
|
||||||
|
├── lib/ # Utilities and rehype plugins
|
||||||
|
├── pages/ # Route pages
|
||||||
|
└── styles/ # Global CSS
|
||||||
|
public/ # Static assets (images, favicons)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- **Theme & colors:** `src/config/theme.ts` — single file for all design tokens, easily swap the entire color palette
|
||||||
|
- **Repos:** `src/data/repos.json` — pinned repositories shown on the homepage
|
||||||
|
- **Resume:** `src/data/resume.json` — JSON Resume format, drives the `/resume/` page
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
The site is deployed via [Netlify](https://netlify.com). Any push to `main` triggers a build automatically. See `netlify.toml` for the build config.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See [LICENSE](LICENSE) for details.
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
import sitemap from "@astrojs/sitemap";
|
||||||
|
import rehypeImageAlign from "./src/lib/rehype-image-align.ts";
|
||||||
|
import rehypeSlug from "rehype-slug";
|
||||||
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
site: "https://avinal.space",
|
||||||
|
output: "static",
|
||||||
|
integrations: [sitemap()],
|
||||||
|
markdown: {
|
||||||
|
shikiConfig: {
|
||||||
|
theme: "github-dark-default",
|
||||||
|
},
|
||||||
|
rehypePlugins: [
|
||||||
|
rehypeImageAlign,
|
||||||
|
rehypeSlug,
|
||||||
|
[
|
||||||
|
rehypeAutolinkHeadings,
|
||||||
|
{
|
||||||
|
behavior: "append",
|
||||||
|
properties: { className: ["heading-anchor"], ariaLabel: "Link to this section" },
|
||||||
|
content: { type: "text", value: "#" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
date: "2023-01-01T08:00:00-07:00"
|
|
||||||
draft: false
|
|
||||||
title: Home
|
|
||||||
---
|
|
||||||
|
|
||||||
I am a Software Engineer II at Red Hat, specializing in hybrid cloud engineering.
|
|
||||||
I have been involved with Google's Summer of Code and Google Season of Docs
|
|
||||||
programs as a mentor and contributor to Open Source for many years. For fun,
|
|
||||||
I like to play around with cutting-edge areas of computer science; at the
|
|
||||||
moment, I'm learning about Elm. GNU/Linux and free/open-source software are
|
|
||||||
two of my favorite things
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
date: "2023-01-01T08:30:00-07:00"
|
|
||||||
draft: false
|
|
||||||
title: Posts
|
|
||||||
---
|
|
||||||
|
|
||||||
All the posts.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
baseURL: 'https://avinal.space/'
|
|
||||||
languageCode: 'en-us'
|
|
||||||
title: "Avinal's Website"
|
|
||||||
theme: 'box-box'
|
|
||||||
|
|
||||||
menus:
|
|
||||||
main:
|
|
||||||
- name: Home
|
|
||||||
pageRef: /
|
|
||||||
weight: 10
|
|
||||||
- name: Posts
|
|
||||||
pageRef: /posts
|
|
||||||
weight: 20
|
|
||||||
|
|
||||||
disableKinds: ["taxonomy"]
|
|
||||||
|
|
||||||
taxonomies:
|
|
||||||
category: category
|
|
||||||
|
|
||||||
permalinks:
|
|
||||||
category: "/posts/category/:slug"
|
|
||||||
|
|
||||||
ignoreLogs: 'warning-goldmark-raw-html'
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[build]
|
[build]
|
||||||
command = "hugo"
|
command = "npm run build"
|
||||||
publish = "public"
|
publish = "dist"
|
||||||
|
|
||||||
[build.environment]
|
[build.environment]
|
||||||
HUGO_VERSION = "0.140.1"
|
NODE_VERSION = "22"
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "avinal.github.io",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"check": "astro check"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/avinal/avinal.github.io.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/avinal/avinal.github.io/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/avinal/avinal.github.io#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/rss": "^4.0.15",
|
||||||
|
"@astrojs/sitemap": "^3.7.0",
|
||||||
|
"astro": "^5.17.3",
|
||||||
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
|
"rehype-slug": "^6.0.0",
|
||||||
|
"unist-util-visit": "^5.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 923 B After Width: | Height: | Size: 923 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 938 KiB After Width: | Height: | Size: 938 KiB |
|
Before Width: | Height: | Size: 6.6 MiB After Width: | Height: | Size: 6.6 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 721 KiB After Width: | Height: | Size: 721 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 521 KiB After Width: | Height: | Size: 521 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 598 KiB After Width: | Height: | Size: 598 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 617 KiB After Width: | Height: | Size: 617 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 395 KiB After Width: | Height: | Size: 395 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 847 KiB After Width: | Height: | Size: 847 KiB |
|
Before Width: | Height: | Size: 657 KiB After Width: | Height: | Size: 657 KiB |
|
Before Width: | Height: | Size: 645 KiB After Width: | Height: | Size: 645 KiB |
|
Before Width: | Height: | Size: 1001 KiB After Width: | Height: | Size: 1001 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 613 KiB After Width: | Height: | Size: 613 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 540 KiB After Width: | Height: | Size: 540 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 505 B After Width: | Height: | Size: 505 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,232 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Combined activity card: graph (left) + stats (right) in one row.
|
||||||
|
* Mirrors jay.fish's "Commit Carnage" + "Kill Count" layout.
|
||||||
|
*/
|
||||||
|
import type { ActivityData } from "@/lib/activity";
|
||||||
|
import type { GitHubUser } from "@/lib/github";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
activity: ActivityData;
|
||||||
|
user: GitHubUser | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { activity, user } = Astro.props;
|
||||||
|
const hasWaka = activity.wakatime.available;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="activity-card card">
|
||||||
|
<div class="activity-header">
|
||||||
|
<h3 class="widget-title">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
||||||
|
Activity
|
||||||
|
</h3>
|
||||||
|
<span class="text-muted text-xs">past year</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activity.weeks.length > 0 ? (
|
||||||
|
<div class="graph-scroll">
|
||||||
|
<div class="graph-grid" role="img" aria-label="Activity graph">
|
||||||
|
{activity.weeks.map((week) => (
|
||||||
|
<div class="graph-col">
|
||||||
|
{week.map((day) => {
|
||||||
|
const ghLevel = day.githubLevel;
|
||||||
|
const wakaLvl = hasWaka && day.wakaSeconds > 0
|
||||||
|
? Math.min(Math.max(1, Math.ceil(day.wakaSeconds / 1800)), 4)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const d = new Date(day.date);
|
||||||
|
const fmtDate = `${String(d.getDate()).padStart(2, "0")} ${d.toLocaleString("en-US", { month: "short" })} ${d.getFullYear()}`;
|
||||||
|
const tipParts = [fmtDate, `${day.githubCount} contribution${day.githubCount !== 1 ? "s" : ""}`];
|
||||||
|
if (hasWaka && day.wakaSeconds > 0) tipParts.push(day.wakaText);
|
||||||
|
const tip = tipParts.join(" · ");
|
||||||
|
|
||||||
|
const hasGh = ghLevel > 0;
|
||||||
|
const hasWk = wakaLvl > 0;
|
||||||
|
const isSplit = hasGh && hasWk;
|
||||||
|
|
||||||
|
if (isSplit) {
|
||||||
|
return (
|
||||||
|
<div class="graph-cell split-cell" title={tip}>
|
||||||
|
<div class="cell-gh" style={`background-color: var(--graph-${ghLevel})`}></div>
|
||||||
|
<div class="cell-waka" style={`background-color: var(--waka-${wakaLvl})`}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const color = hasWk ? `var(--waka-${wakaLvl})` : `var(--graph-${ghLevel})`;
|
||||||
|
return <div class="graph-cell" style={`background-color: ${color}`} title={tip}></div>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="graph-legend">
|
||||||
|
<span class="legend-group">
|
||||||
|
<span class="text-xs text-muted">GitHub</span>
|
||||||
|
<div class="legend-cell" style="background-color: var(--graph-1)"></div>
|
||||||
|
<div class="legend-cell" style="background-color: var(--graph-2)"></div>
|
||||||
|
<div class="legend-cell" style="background-color: var(--graph-3)"></div>
|
||||||
|
<div class="legend-cell" style="background-color: var(--graph-4)"></div>
|
||||||
|
</span>
|
||||||
|
{hasWaka && (
|
||||||
|
<span class="legend-group">
|
||||||
|
<div class="legend-cell" style="background-color: var(--waka-1)"></div>
|
||||||
|
<div class="legend-cell" style="background-color: var(--waka-2)"></div>
|
||||||
|
<div class="legend-cell" style="background-color: var(--waka-3)"></div>
|
||||||
|
<div class="legend-cell" style="background-color: var(--waka-4)"></div>
|
||||||
|
<span class="text-xs text-muted">WakaTime</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p class="text-muted text-sm">Activity data unavailable.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Stats below the graph -->
|
||||||
|
<div class="activity-stats">
|
||||||
|
<dl class="stats-row">
|
||||||
|
<div class="stat-item gh">
|
||||||
|
<dt>Contributions</dt>
|
||||||
|
<dd>{activity.github.total.toLocaleString()}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item gh">
|
||||||
|
<dt>Public Repos</dt>
|
||||||
|
<dd>{user?.public_repos ?? "—"}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item gh">
|
||||||
|
<dt>Followers</dt>
|
||||||
|
<dd>{user?.followers ?? "—"}</dd>
|
||||||
|
</div>
|
||||||
|
{hasWaka && (
|
||||||
|
<Fragment>
|
||||||
|
<div class="stat-item waka">
|
||||||
|
<dt>Coded (year)</dt>
|
||||||
|
<dd>{activity.wakatime.totalText}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item waka">
|
||||||
|
<dt>Daily Avg</dt>
|
||||||
|
<dd>{activity.wakatime.dailyAvgText}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item waka">
|
||||||
|
<dt>Best Day</dt>
|
||||||
|
<dd>{activity.wakatime.bestDayText}</dd>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.activity-card {
|
||||||
|
padding: var(--space-5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-scroll {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.graph-scroll {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-cell {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-cell {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-gh { width: 50%; height: 11px; border-radius: 2px 0 0 2px; }
|
||||||
|
.cell-waka { width: 50%; height: 11px; border-radius: 0 2px 2px 0; }
|
||||||
|
|
||||||
|
.graph-legend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-cell {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats — horizontal row below the graph */
|
||||||
|
.activity-stats {
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-4) var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item dt {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item dd {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item.gh dd {
|
||||||
|
color: var(--graph-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item.waka dd {
|
||||||
|
color: var(--waka-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="footer-inner">
|
||||||
|
<p class="footer-copy">
|
||||||
|
© {year} Avinal Kumar
|
||||||
|
</p>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="https://github.com/avinal" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||||
|
<span class="footer-sep" aria-hidden="true">·</span>
|
||||||
|
<a href="https://linkedin.com/in/avinal" target="_blank" rel="noopener noreferrer">LinkedIn</a>
|
||||||
|
<span class="footer-sep" aria-hidden="true">·</span>
|
||||||
|
<a href="/posts">Posts</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.footer {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding-block: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-4);
|
||||||
|
max-width: var(--max-w-page);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-copy {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-sep {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Conway's Game of Life widget.
|
||||||
|
* Runs continuously; hovering over the canvas brings cells to life.
|
||||||
|
* Resumes simulation after hover interaction.
|
||||||
|
*/
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="gol-card card">
|
||||||
|
<h3 class="widget-title">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
||||||
|
Game of Life
|
||||||
|
</h3>
|
||||||
|
<canvas id="gol-canvas"></canvas>
|
||||||
|
<p class="gol-hint text-muted text-xs">hover to bring cells alive</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.gol-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
height: 100%;
|
||||||
|
min-height: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gol-canvas {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 180px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: crosshair;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.gol-card {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gol-hint {
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById("gol-canvas") as HTMLCanvasElement;
|
||||||
|
if (canvas) {
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
const CELL = 8;
|
||||||
|
const GAP = 1;
|
||||||
|
const STEP = CELL + GAP;
|
||||||
|
|
||||||
|
let cols = 0;
|
||||||
|
let rows = 0;
|
||||||
|
let grid: Uint8Array;
|
||||||
|
let next: Uint8Array;
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
canvas.width = rect.width * dpr;
|
||||||
|
canvas.height = rect.height * dpr;
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
|
const newCols = Math.floor(rect.width / STEP);
|
||||||
|
const newRows = Math.floor(rect.height / STEP);
|
||||||
|
|
||||||
|
if (newCols !== cols || newRows !== rows) {
|
||||||
|
const oldGrid = grid;
|
||||||
|
const oldCols = cols;
|
||||||
|
const oldRows = rows;
|
||||||
|
cols = newCols;
|
||||||
|
rows = newRows;
|
||||||
|
grid = new Uint8Array(cols * rows);
|
||||||
|
next = new Uint8Array(cols * rows);
|
||||||
|
|
||||||
|
if (oldGrid) {
|
||||||
|
const mc = Math.min(oldCols, cols);
|
||||||
|
const mr = Math.min(oldRows, rows);
|
||||||
|
for (let r = 0; r < mr; r++) {
|
||||||
|
for (let c = 0; c < mc; c++) {
|
||||||
|
grid[r * cols + c] = oldGrid[r * oldCols + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function seed() {
|
||||||
|
for (let i = 0; i < grid.length; i++) {
|
||||||
|
grid[i] = Math.random() < 0.3 ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function idx(r: number, c: number) {
|
||||||
|
return ((r + rows) % rows) * cols + ((c + cols) % cols);
|
||||||
|
}
|
||||||
|
|
||||||
|
function step() {
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
let neighbors = 0;
|
||||||
|
for (let dr = -1; dr <= 1; dr++) {
|
||||||
|
for (let dc = -1; dc <= 1; dc++) {
|
||||||
|
if (dr === 0 && dc === 0) continue;
|
||||||
|
neighbors += grid[idx(r + dr, c + dc)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const alive = grid[r * cols + c];
|
||||||
|
next[r * cols + c] =
|
||||||
|
alive ? (neighbors === 2 || neighbors === 3 ? 1 : 0) : (neighbors === 3 ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[grid, next] = [next, grid];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColors() {
|
||||||
|
const style = getComputedStyle(document.documentElement);
|
||||||
|
return {
|
||||||
|
bg: style.getPropertyValue("--bg-surface").trim() || "#f8f9fa",
|
||||||
|
alive: style.getPropertyValue("--accent").trim() || "#2563eb",
|
||||||
|
dim: style.getPropertyValue("--border").trim() || "#e5e7eb",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
const colors = getColors();
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
ctx.clearRect(0, 0, rect.width, rect.height);
|
||||||
|
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
const x = c * STEP;
|
||||||
|
const y = r * STEP;
|
||||||
|
ctx.fillStyle = grid[r * cols + c] ? colors.alive : colors.dim;
|
||||||
|
ctx.globalAlpha = grid[r * cols + c] ? 0.85 : 0.15;
|
||||||
|
ctx.fillRect(x, y, CELL, CELL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let animId: number;
|
||||||
|
let lastTick = 0;
|
||||||
|
const TICK_MS = 300;
|
||||||
|
|
||||||
|
function loop(ts: number) {
|
||||||
|
if (ts - lastTick >= TICK_MS) {
|
||||||
|
step();
|
||||||
|
draw();
|
||||||
|
lastTick = ts;
|
||||||
|
}
|
||||||
|
animId = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintCells(e: MouseEvent | TouchEvent) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
let clientX: number, clientY: number;
|
||||||
|
if ("touches" in e) {
|
||||||
|
clientX = e.touches[0].clientX;
|
||||||
|
clientY = e.touches[0].clientY;
|
||||||
|
} else {
|
||||||
|
clientX = e.clientX;
|
||||||
|
clientY = e.clientY;
|
||||||
|
}
|
||||||
|
const x = clientX - rect.left;
|
||||||
|
const y = clientY - rect.top;
|
||||||
|
const c = Math.floor(x / STEP);
|
||||||
|
const r = Math.floor(y / STEP);
|
||||||
|
|
||||||
|
for (let dr = -1; dr <= 1; dr++) {
|
||||||
|
for (let dc = -1; dc <= 1; dc++) {
|
||||||
|
const nr = r + dr;
|
||||||
|
const nc = c + dc;
|
||||||
|
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
|
||||||
|
grid[nr * cols + nc] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isDrawing = false;
|
||||||
|
canvas.addEventListener("mouseenter", () => { isDrawing = true; });
|
||||||
|
canvas.addEventListener("mouseleave", () => { isDrawing = false; });
|
||||||
|
canvas.addEventListener("mousemove", (e) => {
|
||||||
|
if (isDrawing) paintCells(e);
|
||||||
|
});
|
||||||
|
canvas.addEventListener("touchmove", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
paintCells(e);
|
||||||
|
}, { passive: false });
|
||||||
|
canvas.addEventListener("click", (e) => paintCells(e));
|
||||||
|
|
||||||
|
const resizeObs = new ResizeObserver(() => {
|
||||||
|
resize();
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
resizeObs.observe(canvas);
|
||||||
|
|
||||||
|
resize();
|
||||||
|
animId = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Combined hero card: profile + about/skills + social links.
|
||||||
|
* Mirrors jay.fish's left hero section — everything about the person in one card.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Skill {
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
svgPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SocialLink {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
bio: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, role, bio, avatarUrl } = Astro.props;
|
||||||
|
|
||||||
|
const about: Skill[] = [
|
||||||
|
{ icon: "cloud", title: "Hybrid Cloud", desc: "Building infrastructure at Red Hat", svgPath: '<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>' },
|
||||||
|
{ icon: "monitor", title: "Linux Enthusiast", desc: "Fedora daily driver, Arch tinkerer", svgPath: '<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>' },
|
||||||
|
{ icon: "code", title: "Open Source", desc: "GSoC/GSoD mentor and contributor", svgPath: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>' },
|
||||||
|
{ icon: "server", title: "Homelab", desc: "Self-hosting on Raspberry Pi", svgPath: '<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const tools: Skill[] = [
|
||||||
|
{ icon: "terminal", title: "Languages", desc: "Go, Python, Elm, C/C++", svgPath: '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>' },
|
||||||
|
{ icon: "pen-tool", title: "Editors", desc: "Neovim, VS Code", svgPath: '<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/>' },
|
||||||
|
{ icon: "database", title: "Infra", desc: "Kubernetes, OpenShift, Tekton", svgPath: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>' },
|
||||||
|
{ icon: "flask", title: "Learning", desc: "Elm, functional programming", svgPath: '<path d="M9 3h6v7l5 8a2 2 0 0 1-1.7 3H5.7a2 2 0 0 1-1.7-3l5-8V3z"/><line x1="8" y1="3" x2="16" y2="3"/>' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const links: SocialLink[] = [
|
||||||
|
{ label: "GitHub", href: "https://github.com/avinal", icon: "github" },
|
||||||
|
{ label: "LinkedIn", href: "https://linkedin.com/in/avinal", icon: "linkedin" },
|
||||||
|
{ label: "Twitter", href: "https://twitter.com/Avinal_", icon: "twitter" },
|
||||||
|
{ label: "WakaTime", href: "https://wakatime.com/@avinal", icon: "wakatime" },
|
||||||
|
{ label: "RSS", href: "/rss.xml", icon: "rss" },
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="hero-card card">
|
||||||
|
<!-- Identity -->
|
||||||
|
<div class="hero-identity">
|
||||||
|
{avatarUrl ? (
|
||||||
|
<img src={avatarUrl} alt={name} class="hero-avatar" width="64" height="64" />
|
||||||
|
) : (
|
||||||
|
<div class="hero-avatar hero-avatar-fallback" aria-hidden="true">{name.charAt(0)}</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h1 class="hero-name">{name}</h1>
|
||||||
|
<p class="hero-role">{role}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="hero-bio">{bio}</p>
|
||||||
|
|
||||||
|
<!-- Skills columns -->
|
||||||
|
<div class="hero-skills">
|
||||||
|
<div class="skills-col">
|
||||||
|
<h3 class="col-heading">About</h3>
|
||||||
|
<ul class="skill-list">
|
||||||
|
{about.map(({ svgPath, title, desc }) => (
|
||||||
|
<li class="skill-item">
|
||||||
|
<svg class="skill-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={svgPath} /></svg>
|
||||||
|
<div>
|
||||||
|
<strong>{title}</strong>
|
||||||
|
<span class="skill-desc">{desc}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="skills-col">
|
||||||
|
<h3 class="col-heading">Tools & Stack</h3>
|
||||||
|
<ul class="skill-list">
|
||||||
|
{tools.map(({ svgPath, title, desc }) => (
|
||||||
|
<li class="skill-item">
|
||||||
|
<svg class="skill-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={svgPath} /></svg>
|
||||||
|
<div>
|
||||||
|
<strong>{title}</strong>
|
||||||
|
<span class="skill-desc">{desc}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Social links -->
|
||||||
|
<div class="hero-links">
|
||||||
|
{links.map(({ label, href, icon }) => (
|
||||||
|
<a href={href} class="link-btn" target={href.startsWith("http") ? "_blank" : undefined} rel={href.startsWith("http") ? "noopener noreferrer" : undefined} aria-label={label}>
|
||||||
|
<svg class="link-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
{icon === "github" && <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>}
|
||||||
|
{icon === "linkedin" && <Fragment><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></Fragment>}
|
||||||
|
{icon === "twitter" && <path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"/>}
|
||||||
|
{icon === "wakatime" && <Fragment><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" fill="none"/><path d="M7.5 14.5l2-4 2 3 2-5 2.5 6" stroke-linejoin="round"/></Fragment>}
|
||||||
|
{icon === "rss" && <Fragment><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></Fragment>}
|
||||||
|
</svg>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-identity {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-avatar {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
object-fit: cover;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-avatar-fallback {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: white;
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-name {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-role {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-bio {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-skills {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.hero-skills { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-heading {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-item strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin-right: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding-top: var(--space-2);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn:hover {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-icon { flex-shrink: 0; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
---
|
||||||
|
const navLinks: { href: string; label: string; external?: boolean }[] = [
|
||||||
|
{ href: "/", label: "Home" },
|
||||||
|
{ href: "/posts", label: "Posts" },
|
||||||
|
{ href: "/resume", label: "Resume" },
|
||||||
|
{ href: "/meeting", label: "Meet" },
|
||||||
|
{ href: "https://todo.avinal.space/explore", label: "Memos", external: true },
|
||||||
|
{ href: "/setup", label: "Setup" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentPath = Astro.url.pathname;
|
||||||
|
|
||||||
|
function isActive(href: string): boolean {
|
||||||
|
if (href === "/") return currentPath === "/";
|
||||||
|
return currentPath.startsWith(href);
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="nav-header">
|
||||||
|
<nav class="nav" aria-label="Main navigation">
|
||||||
|
<a href="/" class="nav-logo" aria-label="avinal.space home">
|
||||||
|
avinal<span class="nav-logo-dot">.</span>space
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="nav-toggle"
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="nav-menu"
|
||||||
|
>
|
||||||
|
<span class="nav-toggle-bar"></span>
|
||||||
|
<span class="nav-toggle-bar"></span>
|
||||||
|
<span class="nav-toggle-bar"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul id="nav-menu" class="nav-links" role="list">
|
||||||
|
{navLinks.map(({ href, label, external }) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
class:list={["nav-link", { active: !external && isActive(href) }]}
|
||||||
|
aria-current={!external && isActive(href) ? "page" : undefined}
|
||||||
|
target={external ? "_blank" : undefined}
|
||||||
|
rel={external ? "noopener noreferrer" : undefined}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li>
|
||||||
|
<button class="theme-toggle" aria-label="Toggle dark mode" type="button">
|
||||||
|
<svg class="icon-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||||
|
<svg class="icon-moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: color-mix(in srgb, var(--bg) 85%, transparent);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: var(--max-w-page);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--space-6);
|
||||||
|
height: var(--nav-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logo {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logo-dot {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: block;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out),
|
||||||
|
background-color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
color: var(--text);
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out),
|
||||||
|
background-color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-moon { display: none; }
|
||||||
|
|
||||||
|
:global([data-theme="dark"]) .icon-sun { display: none; }
|
||||||
|
:global([data-theme="dark"]) .icon-moon { display: block; }
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:global(:root:not([data-theme="light"])) .icon-sun { display: none; }
|
||||||
|
:global(:root:not([data-theme="light"])) .icon-moon { display: block; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile hamburger */
|
||||||
|
.nav-toggle {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-toggle-bar {
|
||||||
|
display: block;
|
||||||
|
width: 18px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--text);
|
||||||
|
border-radius: 1px;
|
||||||
|
transition: transform var(--duration-fast) var(--ease-out),
|
||||||
|
opacity var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-toggle {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--nav-height);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: var(--space-4) var(--space-6);
|
||||||
|
background-color: var(--bg);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links.open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const toggle = document.querySelector(".nav-toggle");
|
||||||
|
const menu = document.getElementById("nav-menu");
|
||||||
|
if (toggle && menu) {
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
const expanded = toggle.getAttribute("aria-expanded") === "true";
|
||||||
|
toggle.setAttribute("aria-expanded", String(!expanded));
|
||||||
|
menu.classList.toggle("open");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeToggle = document.querySelector(".theme-toggle");
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener("click", () => {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const current = html.getAttribute("data-theme");
|
||||||
|
const isDark =
|
||||||
|
current === "dark" ||
|
||||||
|
(!current && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||||
|
const next = isDark ? "light" : "dark";
|
||||||
|
html.setAttribute("data-theme", next);
|
||||||
|
localStorage.setItem("theme", next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
posts: CollectionEntry<"posts">[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts } = Astro.props;
|
||||||
|
|
||||||
|
const fmtDate = (d: Date) =>
|
||||||
|
d.toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="posts-card card">
|
||||||
|
<div class="posts-header">
|
||||||
|
<h3 class="widget-title">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
||||||
|
Recent Posts
|
||||||
|
</h3>
|
||||||
|
<a href="/posts" class="posts-view-all">View all →</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{posts.length > 0 ? (
|
||||||
|
<ul class="posts-list">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<li class="post-item">
|
||||||
|
<a href={`/posts/${post.id}/`} class="post-link">
|
||||||
|
<div class="post-thumb">
|
||||||
|
{post.data.image ? (
|
||||||
|
<img src={post.data.image} alt="" class="thumb-img" loading="lazy" />
|
||||||
|
) : (
|
||||||
|
<span class="thumb-placeholder">{post.data.title.charAt(0)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="post-info">
|
||||||
|
<div class="post-meta">
|
||||||
|
<span class="badge">{post.data.category}</span>
|
||||||
|
<span class="text-muted text-xs">{fmtDate(post.data.date)}</span>
|
||||||
|
</div>
|
||||||
|
<strong class="post-title">{post.data.title}</strong>
|
||||||
|
{post.data.description && (
|
||||||
|
<p class="post-desc">{post.data.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p class="text-muted text-sm">No posts yet.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.posts-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-view-all {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-view-all:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-item {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-item:first-child {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-link {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 140px 1fr;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-3) var(--space-2);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: background-color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-link:hover {
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-thumb {
|
||||||
|
aspect-ratio: 3 / 2;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
filter: grayscale(100%);
|
||||||
|
transition: filter var(--duration-normal) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-link:hover .thumb-img {
|
||||||
|
filter: grayscale(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
color: var(--text);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-desc {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
---
|
||||||
|
import type { GitHubRepo } from "@/lib/github";
|
||||||
|
import configRepos from "@/data/repos.json";
|
||||||
|
|
||||||
|
interface ConfigRepo {
|
||||||
|
name: string;
|
||||||
|
owner?: string;
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
stars: number;
|
||||||
|
forks: number;
|
||||||
|
languages: { name: string; color: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
repos: GitHubRepo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { repos: apiRepos } = Astro.props;
|
||||||
|
|
||||||
|
const useConfig = configRepos.length > 0;
|
||||||
|
const items: ConfigRepo[] = useConfig
|
||||||
|
? (configRepos as ConfigRepo[])
|
||||||
|
: apiRepos.map((r) => ({
|
||||||
|
name: r.name,
|
||||||
|
owner: "avinal",
|
||||||
|
description: r.description || "",
|
||||||
|
url: r.html_url,
|
||||||
|
stars: r.stargazers_count,
|
||||||
|
forks: 0,
|
||||||
|
languages: r.language ? [{ name: r.language, color: "" }] : [],
|
||||||
|
}));
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="repos-section">
|
||||||
|
<h3 class="widget-title">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||||
|
Repositories
|
||||||
|
</h3>
|
||||||
|
<div class="repo-grid">
|
||||||
|
{items.map((repo) => (
|
||||||
|
<a href={repo.url} class="repo-card card" target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="repo-top">
|
||||||
|
<div class="repo-name-row">
|
||||||
|
{repo.owner && <span class="repo-owner">{repo.owner}/</span>}
|
||||||
|
<span class="repo-name">{repo.name}</span>
|
||||||
|
</div>
|
||||||
|
<p class="repo-desc">{repo.description}</p>
|
||||||
|
</div>
|
||||||
|
<div class="repo-bottom">
|
||||||
|
<div class="repo-meta">
|
||||||
|
{repo.stars > 0 && (
|
||||||
|
<span class="meta-badge">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||||
|
{repo.stars.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{repo.forks > 0 && (
|
||||||
|
<span class="meta-badge">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v1a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V9"/><path d="M12 12v3"/></svg>
|
||||||
|
{repo.forks.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="repo-langs">
|
||||||
|
{repo.languages.slice(0, 2).map((lang) => (
|
||||||
|
<span class="lang-tag">
|
||||||
|
<span class="lang-dot" style={lang.color ? `background:${lang.color}` : undefined} />
|
||||||
|
{lang.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.repos-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.repo-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--space-4) var(--space-5);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
transition: border-color var(--duration-fast) var(--ease-out),
|
||||||
|
box-shadow var(--duration-fast) var(--ease-out),
|
||||||
|
transform var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-card:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 10%, transparent);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-top {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-owner {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-desc {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-bottom {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-badge svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-langs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
display: inline-block;
|
||||||
|
background-color: var(--text-muted);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
/**
|
||||||
|
* Single source of truth for all design tokens.
|
||||||
|
*
|
||||||
|
* To change the entire look of the site, edit this file.
|
||||||
|
* To create a new theme, duplicate this file (e.g. theme-nord.ts)
|
||||||
|
* and update the import in BaseLayout.astro.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ThemePalette {
|
||||||
|
bg: string;
|
||||||
|
bgSurface: string;
|
||||||
|
bgSurfaceHover: string;
|
||||||
|
text: string;
|
||||||
|
textSecondary: string;
|
||||||
|
textMuted: string;
|
||||||
|
accent: string;
|
||||||
|
accentHover: string;
|
||||||
|
accentSubtle: string;
|
||||||
|
border: string;
|
||||||
|
borderStrong: string;
|
||||||
|
shadow: string;
|
||||||
|
shadowMd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphColors {
|
||||||
|
level0: string;
|
||||||
|
level1: string;
|
||||||
|
level2: string;
|
||||||
|
level3: string;
|
||||||
|
level4: string;
|
||||||
|
wakaLevel1: string;
|
||||||
|
wakaLevel2: string;
|
||||||
|
wakaLevel3: string;
|
||||||
|
wakaLevel4: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemeConfig {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
site: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
author: string;
|
||||||
|
logoText: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
fonts: {
|
||||||
|
sans: string;
|
||||||
|
mono: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
colors: {
|
||||||
|
light: ThemePalette;
|
||||||
|
dark: ThemePalette;
|
||||||
|
graph: {
|
||||||
|
light: GraphColors;
|
||||||
|
dark: GraphColors;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
spacing: {
|
||||||
|
base: string;
|
||||||
|
navHeight: string;
|
||||||
|
maxProse: string;
|
||||||
|
maxPage: string;
|
||||||
|
sectionGap: string;
|
||||||
|
cardPadding: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
radius: {
|
||||||
|
sm: string;
|
||||||
|
md: string;
|
||||||
|
lg: string;
|
||||||
|
full: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
typography: {
|
||||||
|
lineHeight: string;
|
||||||
|
lineHeightTight: string;
|
||||||
|
lineHeightRelaxed: string;
|
||||||
|
trackingTight: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
transitions: {
|
||||||
|
fast: string;
|
||||||
|
normal: string;
|
||||||
|
ease: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Default theme — usememos-inspired clean palette, jay.fish structure
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const theme: ThemeConfig = {
|
||||||
|
name: "default",
|
||||||
|
|
||||||
|
site: {
|
||||||
|
title: "avinal.space",
|
||||||
|
description: "Avinal Kumar — Software Engineer, Open Source Contributor",
|
||||||
|
url: "https://avinal.space",
|
||||||
|
author: "Avinal Kumar",
|
||||||
|
logoText: "avinal.space",
|
||||||
|
},
|
||||||
|
|
||||||
|
fonts: {
|
||||||
|
sans: '"Inter", "Segoe UI", system-ui, -apple-system, sans-serif',
|
||||||
|
mono: '"JetBrains Mono", "Fira Code", ui-monospace, monospace',
|
||||||
|
},
|
||||||
|
|
||||||
|
colors: {
|
||||||
|
light: {
|
||||||
|
bg: "#fafafa",
|
||||||
|
bgSurface: "#ffffff",
|
||||||
|
bgSurfaceHover: "#f5f5f5",
|
||||||
|
text: "#1a1a1a",
|
||||||
|
textSecondary: "#525252",
|
||||||
|
textMuted: "#737373",
|
||||||
|
accent: "#2563eb",
|
||||||
|
accentHover: "#1d4ed8",
|
||||||
|
accentSubtle: "#eff6ff",
|
||||||
|
border: "#e5e5e5",
|
||||||
|
borderStrong: "#d4d4d4",
|
||||||
|
shadow: "0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)",
|
||||||
|
shadowMd: "0 4px 6px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.04)",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
bg: "#111111",
|
||||||
|
bgSurface: "#1a1a1a",
|
||||||
|
bgSurfaceHover: "#262626",
|
||||||
|
text: "#e5e5e5",
|
||||||
|
textSecondary: "#a3a3a3",
|
||||||
|
textMuted: "#737373",
|
||||||
|
accent: "#60a5fa",
|
||||||
|
accentHover: "#93bbfd",
|
||||||
|
accentSubtle: "#172554",
|
||||||
|
border: "#2e2e2e",
|
||||||
|
borderStrong: "#404040",
|
||||||
|
shadow: "0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2)",
|
||||||
|
shadowMd: "0 4px 6px rgba(0,0,0,0.25), 0 2px 4px rgba(0,0,0,0.15)",
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
light: {
|
||||||
|
level0: "#ebedf0",
|
||||||
|
level1: "#9be9a8",
|
||||||
|
level2: "#40c463",
|
||||||
|
level3: "#30a14e",
|
||||||
|
level4: "#216e39",
|
||||||
|
wakaLevel1: "#c4b5fd",
|
||||||
|
wakaLevel2: "#a78bfa",
|
||||||
|
wakaLevel3: "#8b5cf6",
|
||||||
|
wakaLevel4: "#7c3aed",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
level0: "#1e1e1e",
|
||||||
|
level1: "#0e4429",
|
||||||
|
level2: "#006d32",
|
||||||
|
level3: "#26a641",
|
||||||
|
level4: "#39d353",
|
||||||
|
wakaLevel1: "#2e1065",
|
||||||
|
wakaLevel2: "#4c1d95",
|
||||||
|
wakaLevel3: "#7c3aed",
|
||||||
|
wakaLevel4: "#a78bfa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
spacing: {
|
||||||
|
base: "0.25rem",
|
||||||
|
navHeight: "3.5rem",
|
||||||
|
maxProse: "42rem",
|
||||||
|
maxPage: "64rem",
|
||||||
|
sectionGap: "4rem",
|
||||||
|
cardPadding: "1.5rem",
|
||||||
|
},
|
||||||
|
|
||||||
|
radius: {
|
||||||
|
sm: "0.375rem",
|
||||||
|
md: "0.5rem",
|
||||||
|
lg: "0.75rem",
|
||||||
|
full: "9999px",
|
||||||
|
},
|
||||||
|
|
||||||
|
typography: {
|
||||||
|
lineHeight: "1.6",
|
||||||
|
lineHeightTight: "1.2",
|
||||||
|
lineHeightRelaxed: "1.75",
|
||||||
|
trackingTight: "-0.02em",
|
||||||
|
},
|
||||||
|
|
||||||
|
transitions: {
|
||||||
|
fast: "150ms",
|
||||||
|
normal: "250ms",
|
||||||
|
ease: "cubic-bezier(0.16, 1, 0.3, 1)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default theme;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Utility: convert the theme config to CSS custom properties
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function paletteToVars(p: ThemePalette): string {
|
||||||
|
return `
|
||||||
|
--bg: ${p.bg};
|
||||||
|
--bg-surface: ${p.bgSurface};
|
||||||
|
--bg-surface-hover: ${p.bgSurfaceHover};
|
||||||
|
--text: ${p.text};
|
||||||
|
--text-secondary: ${p.textSecondary};
|
||||||
|
--text-muted: ${p.textMuted};
|
||||||
|
--accent: ${p.accent};
|
||||||
|
--accent-hover: ${p.accentHover};
|
||||||
|
--accent-subtle: ${p.accentSubtle};
|
||||||
|
--border: ${p.border};
|
||||||
|
--border-strong: ${p.borderStrong};
|
||||||
|
--shadow: ${p.shadow};
|
||||||
|
--shadow-md: ${p.shadowMd};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphToVars(g: GraphColors): string {
|
||||||
|
return `
|
||||||
|
--graph-0: ${g.level0};
|
||||||
|
--graph-1: ${g.level1};
|
||||||
|
--graph-2: ${g.level2};
|
||||||
|
--graph-3: ${g.level3};
|
||||||
|
--graph-4: ${g.level4};
|
||||||
|
--waka-1: ${g.wakaLevel1};
|
||||||
|
--waka-2: ${g.wakaLevel2};
|
||||||
|
--waka-3: ${g.wakaLevel3};
|
||||||
|
--waka-4: ${g.wakaLevel4};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateThemeCSS(t: ThemeConfig): string {
|
||||||
|
return `
|
||||||
|
:root {
|
||||||
|
--font-sans: ${t.fonts.sans};
|
||||||
|
--font-mono: ${t.fonts.mono};
|
||||||
|
|
||||||
|
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.8125rem);
|
||||||
|
--text-sm: clamp(0.8125rem, 0.78rem + 0.2vw, 0.875rem);
|
||||||
|
--text-base: clamp(0.9375rem, 0.9rem + 0.2vw, 1rem);
|
||||||
|
--text-lg: clamp(1.125rem, 1.05rem + 0.4vw, 1.25rem);
|
||||||
|
--text-xl: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
|
||||||
|
--text-2xl: clamp(1.5rem, 1.2rem + 1.5vw, 2rem);
|
||||||
|
--text-3xl: clamp(1.875rem, 1.4rem + 2.4vw, 2.5rem);
|
||||||
|
--text-4xl: clamp(2.25rem, 1.6rem + 3.2vw, 3.25rem);
|
||||||
|
|
||||||
|
--leading-tight: ${t.typography.lineHeightTight};
|
||||||
|
--leading-normal: ${t.typography.lineHeight};
|
||||||
|
--leading-relaxed: ${t.typography.lineHeightRelaxed};
|
||||||
|
--tracking-tight: ${t.typography.trackingTight};
|
||||||
|
--tracking-normal: 0;
|
||||||
|
|
||||||
|
--space-1: ${t.spacing.base};
|
||||||
|
--space-2: calc(${t.spacing.base} * 2);
|
||||||
|
--space-3: calc(${t.spacing.base} * 3);
|
||||||
|
--space-4: calc(${t.spacing.base} * 4);
|
||||||
|
--space-5: calc(${t.spacing.base} * 5);
|
||||||
|
--space-6: calc(${t.spacing.base} * 6);
|
||||||
|
--space-8: calc(${t.spacing.base} * 8);
|
||||||
|
--space-10: calc(${t.spacing.base} * 10);
|
||||||
|
--space-12: calc(${t.spacing.base} * 12);
|
||||||
|
--space-16: calc(${t.spacing.base} * 16);
|
||||||
|
--space-20: calc(${t.spacing.base} * 20);
|
||||||
|
--space-24: calc(${t.spacing.base} * 24);
|
||||||
|
|
||||||
|
--max-w-prose: ${t.spacing.maxProse};
|
||||||
|
--max-w-page: ${t.spacing.maxPage};
|
||||||
|
--nav-height: ${t.spacing.navHeight};
|
||||||
|
|
||||||
|
--radius-sm: ${t.radius.sm};
|
||||||
|
--radius-md: ${t.radius.md};
|
||||||
|
--radius-lg: ${t.radius.lg};
|
||||||
|
--radius-full: ${t.radius.full};
|
||||||
|
|
||||||
|
--duration-fast: ${t.transitions.fast};
|
||||||
|
--duration-normal: ${t.transitions.normal};
|
||||||
|
--ease-out: ${t.transitions.ease};
|
||||||
|
|
||||||
|
${paletteToVars(t.colors.light)}
|
||||||
|
${graphToVars(t.colors.graph.light)}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
${paletteToVars(t.colors.dark)}
|
||||||
|
${graphToVars(t.colors.graph.dark)}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
${paletteToVars(t.colors.dark)}
|
||||||
|
${graphToVars(t.colors.graph.dark)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineCollection, z } from "astro:content";
|
||||||
|
import { glob } from "astro/loaders";
|
||||||
|
|
||||||
|
const posts = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/*.md", base: "./src/content/posts" }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string().optional().default("Untitled"),
|
||||||
|
date: z.coerce.date().optional().default(new Date("2000-01-01")),
|
||||||
|
description: z.string().optional().default(""),
|
||||||
|
category: z.string().optional().default("uncategorized"),
|
||||||
|
tags: z.array(z.string()).optional().default([]),
|
||||||
|
image: z.string().optional().default(""),
|
||||||
|
draft: z.boolean().optional().default(false),
|
||||||
|
modified: z.coerce.date().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { posts };
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Installing Fedora with automatic and custom partioning
|
title: Installing Fedora with automatic and custom partioning
|
||||||
date: 2023-01-19T23:02:00
|
date: 2023-01-19T23:02:00
|
||||||
category: blog
|
category: blogs
|
||||||
tags: [fedora, partioning]
|
tags: [fedora, partioning]
|
||||||
image: ""
|
image: ""
|
||||||
draft: true
|
draft: true
|
||||||