1
0
mirror of https://github.com/avinal/avinal.github.io.git synced 2026-07-03 23:30:09 +05:30

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>
This commit is contained in:
2026-02-25 19:46:43 +05:30
committed by Morumotto
parent 62efd95607
commit 6b07ea345f
145 changed files with 10397 additions and 90 deletions
+3
View File
@@ -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=
+13 -22
View File
@@ -1,37 +1,28 @@
# Sample workflow for building and deploying a Hugo site to GitHub Pages
name: Check build
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
pull_request:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Default to bash
defaults:
run:
shell: bash
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version: 22
cache: "npm"
cache-dependency-path: ./package-lock.json
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.134.3'
extended: true
- name: Install elm-land and node packages
run: npm install
- name: build
run: make
- name: Install dependencies
run: npm ci
- name: Check types
run: npm run check
- name: Build site
run: npm run build
+20 -13
View File
@@ -1,14 +1,21 @@
/dist
/.elm-land
/.env
/elm-stuff
/node_modules
# Dependencies
node_modules/
# Build output
dist/
.astro/
# Environment
.env
.env.local
.env.production
# IDE
.idea/
.vscode/
# OS
.DS_Store
*.pem
/static/main.css
/temp
/blog/public
/blog/node_modules
.idea
.vscode
public
# Legacy Hugo
public/
-3
View File
@@ -1,3 +0,0 @@
[submodule "themes/box-box"]
path = themes/box-box
url = https://github.com/avinal/box-box.git
+29
View File
@@ -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
+86 -1
View File
@@ -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.
+28
View File
@@ -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: "#" },
},
],
],
},
});
-12
View File
@@ -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
-7
View File
@@ -1,7 +0,0 @@
---
date: "2023-01-01T08:30:00-07:00"
draft: false
title: Posts
---
All the posts.
-23
View File
@@ -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'
+3 -3
View File
@@ -1,6 +1,6 @@
[build]
command = "hugo"
publish = "public"
command = "npm run build"
publish = "dist"
[build.environment]
HUGO_VERSION = "0.140.1"
NODE_VERSION = "22"
+5634
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -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

+232
View File
@@ -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>
+62
View File
@@ -0,0 +1,62 @@
---
const year = new Date().getFullYear();
---
<footer class="footer">
<div class="footer-inner">
<p class="footer-copy">
&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">&middot;</span>
<a href="https://linkedin.com/in/avinal" target="_blank" rel="noopener noreferrer">LinkedIn</a>
<span class="footer-sep" aria-hidden="true">&middot;</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>
+209
View File
@@ -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>
+242
View File
@@ -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>
+227
View File
@@ -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>
+172
View File
@@ -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 &rarr;</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>
+199
View File
@@ -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>
+300
View File
@@ -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)}
}
}
`;
}
+18
View File
@@ -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
date: 2023-01-19T23:02:00
category: blog
category: blogs
tags: [fedora, partioning]
image: ""
draft: true

Some files were not shown because too many files have changed in this diff Show More