Compare commits
5 Commits
6bd1a2d648
...
f5e739494a
| Author | SHA1 | Date | |
|---|---|---|---|
|
f5e739494a
|
|||
|
5f467665bc
|
|||
|
99f3fb5ec8
|
|||
|
5fa9a10203
|
|||
|
f613005a23
|
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import fs from "fs";
|
||||||
|
import https from "https";
|
||||||
|
import http from "http";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const root = path.resolve(__dirname, "..");
|
||||||
|
const jsonPath = path.join(root, "src/data/bookmarks.json");
|
||||||
|
const imgDir = path.join(root, "public/images/bookmarks");
|
||||||
|
|
||||||
|
fs.mkdirSync(imgDir, { recursive: true });
|
||||||
|
|
||||||
|
function slugify(title) {
|
||||||
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function httpGet(url, options = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const proto = url.startsWith("https") ? https : http;
|
||||||
|
proto.get(url, options, resolve).on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function download(url, dest, redirects = 5) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (redirects <= 0) return reject(new Error("Too many redirects"));
|
||||||
|
const proto = url.startsWith("https") ? https : http;
|
||||||
|
proto
|
||||||
|
.get(url, { headers: { "User-Agent": "Mozilla/5.0" } }, (res) => {
|
||||||
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
const next = new URL(res.headers.location, url).href;
|
||||||
|
download(next, dest, redirects - 1).then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
reject(new Error(`HTTP ${res.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ct = res.headers["content-type"] || "";
|
||||||
|
const ext = ct.includes("png") ? ".png" : ct.includes("webp") ? ".webp" : ".jpg";
|
||||||
|
const finalDest = dest + ext;
|
||||||
|
const ws = fs.createWriteStream(finalDest);
|
||||||
|
res.pipe(ws);
|
||||||
|
ws.on("finish", () => {
|
||||||
|
ws.close();
|
||||||
|
resolve("/images/bookmarks/" + path.basename(finalDest));
|
||||||
|
});
|
||||||
|
ws.on("error", reject);
|
||||||
|
})
|
||||||
|
.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPosterFromOMDB(title, year) {
|
||||||
|
const query = encodeURIComponent(title);
|
||||||
|
const url = `https://www.omdbapi.com/?t=${query}&y=${year}&apikey=trilogy`;
|
||||||
|
try {
|
||||||
|
const res = await httpGet(url);
|
||||||
|
let body = "";
|
||||||
|
for await (const chunk of res) body += chunk;
|
||||||
|
const data = JSON.parse(body);
|
||||||
|
if (data.Poster && data.Poster !== "N/A") return data.Poster;
|
||||||
|
} catch {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(ms) {
|
||||||
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const data = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||||
|
let fetched = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
const slug = slugify(item.title);
|
||||||
|
const existing = fs.readdirSync(imgDir).find((f) => f.startsWith(slug + "."));
|
||||||
|
if (existing) {
|
||||||
|
item.image = "/images/bookmarks/" + existing;
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.image && item.image.startsWith("/images/")) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let imageUrl = item.image;
|
||||||
|
if (!imageUrl || imageUrl.startsWith("http")) {
|
||||||
|
const omdbUrl = await fetchPosterFromOMDB(item.title, item.year);
|
||||||
|
if (omdbUrl) imageUrl = omdbUrl;
|
||||||
|
await delay(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageUrl) {
|
||||||
|
console.error(` SKIP ${item.title}: no image URL found`);
|
||||||
|
failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const localPath = await download(imageUrl, path.join(imgDir, slug));
|
||||||
|
console.log(` OK ${item.title} -> ${localPath}`);
|
||||||
|
item.image = localPath;
|
||||||
|
fetched++;
|
||||||
|
} catch (e) {
|
||||||
|
const omdbUrl = await fetchPosterFromOMDB(item.title, item.year);
|
||||||
|
if (omdbUrl) {
|
||||||
|
try {
|
||||||
|
const localPath = await download(omdbUrl, path.join(imgDir, slug));
|
||||||
|
console.log(` OK ${item.title} -> ${localPath} (via OMDB fallback)`);
|
||||||
|
item.image = localPath;
|
||||||
|
fetched++;
|
||||||
|
continue;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
console.error(` FAIL ${item.title}: ${e.message}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2) + "\n");
|
||||||
|
console.log(`\nDone. Fetched: ${fetched}, Skipped: ${skipped}, Failed: ${failed}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -27,17 +27,16 @@ interface Props {
|
|||||||
const { name, role, bio, avatarUrl } = Astro.props;
|
const { name, role, bio, avatarUrl } = Astro.props;
|
||||||
|
|
||||||
const about: Skill[] = [
|
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: "cloud", title: "Cloud Native", desc: "Leading Builds for OpenShift 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: "cpu", title: "Kernel & Toolchain", desc: "Linux kernel, GCC & glibc contributor", svgPath: '<rect x="4" y="4" width="16" height="16" rx="2" ry="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/>' },
|
||||||
{ 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: "code", title: "Open Source", desc: "GSoC alumnus & mentor, Campus Expert", 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"/>' },
|
{ icon: "server", title: "Self-hosting", desc: "Fedora daily driver, homelab everything", 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[] = [
|
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: "terminal", title: "Languages", desc: "C/C++, Go, Bash", 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: "pen-tool", title: "Editor", desc: "Helix, Zellij, lazygit", 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: "settings", title: "Platforms", desc: "Fedora, Git, CMake, GitHub Actions", svgPath: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>' },
|
||||||
{ 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[] = [
|
const links: SocialLink[] = [
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ const navLinks: { href: string; label: string; external?: boolean }[] = [
|
|||||||
{ href: "/posts", label: "Posts" },
|
{ href: "/posts", label: "Posts" },
|
||||||
{ href: "/resume", label: "Resume" },
|
{ href: "/resume", label: "Resume" },
|
||||||
{ href: "/events", label: "Events" },
|
{ href: "/events", label: "Events" },
|
||||||
|
{ href: "/contributions", label: "Contributions" },
|
||||||
|
{ href: "/bookmarks", label: "Bookmarks" },
|
||||||
{ href: "/meeting", label: "Meet" },
|
{ href: "/meeting", label: "Meet" },
|
||||||
{ href: "https://todo.avinal.space/explore", label: "Memos", external: true },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentPath = Astro.url.pathname;
|
const currentPath = Astro.url.pathname;
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
---
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<aside class="related" aria-label="Related posts">
|
||||||
|
<h2 class="related-heading">Related Posts</h2>
|
||||||
|
<ul class="related-list">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<li class="related-item">
|
||||||
|
<a href={`/posts/${post.id}/`} class="related-link">
|
||||||
|
<div class="related-thumb">
|
||||||
|
{post.data.image ? (
|
||||||
|
<img src={post.data.image} alt={post.data.title} class="thumb-img" loading="lazy" decoding="async" />
|
||||||
|
) : (
|
||||||
|
<span class="thumb-placeholder">{post.data.title.charAt(0)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="related-info">
|
||||||
|
<div class="related-meta">
|
||||||
|
<span class="badge">{post.data.category}</span>
|
||||||
|
<span class="text-muted text-xs">{fmtDate(post.data.date)}</span>
|
||||||
|
</div>
|
||||||
|
<strong class="related-title">{post.data.title}</strong>
|
||||||
|
{post.data.description && (
|
||||||
|
<p class="related-desc">{post.data.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.related {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding-top: var(--space-8);
|
||||||
|
margin-top: var(--space-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-heading {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-item {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-item:first-child {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-link {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 140px 1fr;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-3) var(--space-2);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: background-color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-link:hover {
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-thumb {
|
||||||
|
aspect-ratio: 3 / 2;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-info {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-title {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
color: var(--text);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.related-link {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-thumb {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
headings: { depth: number; slug: string; text: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { headings } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<details class="toc">
|
||||||
|
<summary class="toc-toggle">
|
||||||
|
<span class="toc-label">Table of Contents</span>
|
||||||
|
<svg class="toc-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
|
</summary>
|
||||||
|
<nav class="toc-nav" aria-label="Table of contents">
|
||||||
|
<ol class="toc-list">
|
||||||
|
{headings.map((h) => (
|
||||||
|
<li class:list={[`toc-depth-${h.depth}`]}>
|
||||||
|
<a href={`#${h.slug}`} class="toc-link">{h.text}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toc {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: var(--space-4) var(--space-5);
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-toggle::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-chevron {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: transform var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc[open] .toc-chevron {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-nav {
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list li {
|
||||||
|
padding: var(--space-1) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-depth-3 {
|
||||||
|
padding-left: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-link {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "The Pragmatic Programmer",
|
||||||
|
"author": "David Thomas, Andrew Hunt",
|
||||||
|
"type": "book",
|
||||||
|
"year": 2019,
|
||||||
|
"url": "https://www.goodreads.com/book/show/4099.The_Pragmatic_Programmer",
|
||||||
|
"image": "/images/bookmarks/the-pragmatic-programmer.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Designing Data-Intensive Applications",
|
||||||
|
"author": "Martin Kleppmann",
|
||||||
|
"type": "book",
|
||||||
|
"year": 2017,
|
||||||
|
"url": "https://www.goodreads.com/book/show/23463279-designing-data-intensive-applications",
|
||||||
|
"image": "/images/bookmarks/designing-data-intensive-applications.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Clean Code",
|
||||||
|
"author": "Robert C. Martin",
|
||||||
|
"type": "book",
|
||||||
|
"year": 2008,
|
||||||
|
"url": "https://www.goodreads.com/book/show/3735293-clean-code",
|
||||||
|
"image": "/images/bookmarks/clean-code.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Shawshank Redemption",
|
||||||
|
"author": "Frank Darabont",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 1994,
|
||||||
|
"url": "https://www.imdb.com/title/tt0111161/",
|
||||||
|
"image": "/images/bookmarks/the-shawshank-redemption.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Schindler's List",
|
||||||
|
"author": "Steven Spielberg",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 1993,
|
||||||
|
"url": "https://www.imdb.com/title/tt0108052/",
|
||||||
|
"image": "/images/bookmarks/schindlers-list.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Forrest Gump",
|
||||||
|
"author": "Robert Zemeckis",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 1994,
|
||||||
|
"url": "https://www.imdb.com/title/tt0109830/",
|
||||||
|
"image": "/images/bookmarks/forrest-gump.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Green Mile",
|
||||||
|
"author": "Frank Darabont",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 1999,
|
||||||
|
"url": "https://www.imdb.com/title/tt0120689/",
|
||||||
|
"image": "/images/bookmarks/the-green-mile.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Interstellar",
|
||||||
|
"author": "Christopher Nolan",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2014,
|
||||||
|
"url": "https://www.imdb.com/title/tt0816692/",
|
||||||
|
"image": "/images/bookmarks/interstellar.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Oppenheimer",
|
||||||
|
"author": "Christopher Nolan",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2023,
|
||||||
|
"url": "https://www.imdb.com/title/tt15398776/",
|
||||||
|
"image": "/images/bookmarks/oppenheimer.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Social Network",
|
||||||
|
"author": "David Fincher",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2010,
|
||||||
|
"url": "https://www.imdb.com/title/tt1285016/",
|
||||||
|
"image": "/images/bookmarks/the-social-network.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "A Beautiful Mind",
|
||||||
|
"author": "Ron Howard",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2001,
|
||||||
|
"url": "https://www.imdb.com/title/tt0268978/",
|
||||||
|
"image": "/images/bookmarks/a-beautiful-mind.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Truman Show",
|
||||||
|
"author": "Peter Weir",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 1998,
|
||||||
|
"url": "https://www.imdb.com/title/tt0120382/",
|
||||||
|
"image": "/images/bookmarks/the-truman-show.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Grand Budapest Hotel",
|
||||||
|
"author": "Wes Anderson",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2014,
|
||||||
|
"url": "https://www.imdb.com/title/tt2278388/",
|
||||||
|
"image": "/images/bookmarks/the-grand-budapest-hotel.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Joker",
|
||||||
|
"author": "Todd Phillips",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2019,
|
||||||
|
"url": "https://www.imdb.com/title/tt7286456/",
|
||||||
|
"image": "/images/bookmarks/joker.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "WALL-E",
|
||||||
|
"author": "Andrew Stanton",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2008,
|
||||||
|
"url": "https://www.imdb.com/title/tt0910970/",
|
||||||
|
"image": "/images/bookmarks/wall-e.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Aviator",
|
||||||
|
"author": "Martin Scorsese",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2004,
|
||||||
|
"url": "https://www.imdb.com/title/tt0338751/",
|
||||||
|
"image": "/images/bookmarks/the-aviator.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Erin Brockovich",
|
||||||
|
"author": "Steven Soderbergh",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2000,
|
||||||
|
"url": "https://www.imdb.com/title/tt0195685/",
|
||||||
|
"image": "/images/bookmarks/erin-brockovich.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Green Book",
|
||||||
|
"author": "Peter Farrelly",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2018,
|
||||||
|
"url": "https://www.imdb.com/title/tt6966692/",
|
||||||
|
"image": "/images/bookmarks/green-book.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Hachi: A Dog's Tale",
|
||||||
|
"author": "Lasse Hallström",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2009,
|
||||||
|
"url": "https://www.imdb.com/title/tt1028532/",
|
||||||
|
"image": "/images/bookmarks/hachi-a-dog-s-tale.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Léon: The Professional",
|
||||||
|
"author": "Luc Besson",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 1994,
|
||||||
|
"url": "https://www.imdb.com/title/tt0110413/",
|
||||||
|
"image": "/images/bookmarks/l-on-the-professional.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Terminator",
|
||||||
|
"author": "James Cameron",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 1984,
|
||||||
|
"url": "https://www.imdb.com/title/tt0088247/",
|
||||||
|
"image": "/images/bookmarks/the-terminator.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "X-Men: First Class",
|
||||||
|
"author": "Matthew Vaughn",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2011,
|
||||||
|
"url": "https://www.imdb.com/title/tt1270798/",
|
||||||
|
"image": "/images/bookmarks/x-men-first-class.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Now You See Me",
|
||||||
|
"author": "Louis Leterrier",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2013,
|
||||||
|
"url": "https://www.imdb.com/title/tt1670345/",
|
||||||
|
"image": "/images/bookmarks/now-you-see-me.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Special 26",
|
||||||
|
"author": "Neeraj Pandey",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2013,
|
||||||
|
"url": "https://www.imdb.com/title/tt2377938/",
|
||||||
|
"image": "/images/bookmarks/special-26.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Drishyam",
|
||||||
|
"author": "Nishikant Kamat",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2015,
|
||||||
|
"url": "https://www.imdb.com/title/tt4430212/",
|
||||||
|
"image": "/images/bookmarks/drishyam.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Drishyam 2",
|
||||||
|
"author": "Abhishek Pathak",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2022,
|
||||||
|
"url": "https://www.imdb.com/title/tt15501640/",
|
||||||
|
"image": "/images/bookmarks/drishyam-2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Bumblebee",
|
||||||
|
"author": "Travis Knight",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2018,
|
||||||
|
"url": "https://www.imdb.com/title/tt4701182/",
|
||||||
|
"image": "/images/bookmarks/bumblebee.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Ministry of Ungentlemanly Warfare",
|
||||||
|
"author": "Guy Ritchie",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2024,
|
||||||
|
"url": "https://www.imdb.com/title/tt14128670/",
|
||||||
|
"image": "/images/bookmarks/the-ministry-of-ungentlemanly-warfare.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Raid",
|
||||||
|
"author": "Gareth Evans",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2011,
|
||||||
|
"url": "https://www.imdb.com/title/tt1899353/",
|
||||||
|
"image": "/images/bookmarks/the-raid.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Raid 2",
|
||||||
|
"author": "Gareth Evans",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2014,
|
||||||
|
"url": "https://www.imdb.com/title/tt2265171/",
|
||||||
|
"image": "/images/bookmarks/the-raid-2.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Einstein and Eddington",
|
||||||
|
"author": "Philip Martin",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2008,
|
||||||
|
"url": "https://www.imdb.com/title/tt0995036/",
|
||||||
|
"image": "/images/bookmarks/einstein-and-eddington.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Nuremberg",
|
||||||
|
"author": "Yves Simoneau",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2000,
|
||||||
|
"url": "https://www.imdb.com/title/tt0208629/",
|
||||||
|
"image": "/images/bookmarks/nuremberg.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Fast and the Furious",
|
||||||
|
"author": "Rob Cohen",
|
||||||
|
"type": "movie",
|
||||||
|
"year": 2001,
|
||||||
|
"url": "https://www.imdb.com/title/tt0232500/",
|
||||||
|
"image": "/images/bookmarks/the-fast-and-the-furious.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Chainsaw Man the Movie: Reze Arc",
|
||||||
|
"author": "MAPPA",
|
||||||
|
"type": "anime",
|
||||||
|
"year": 2025,
|
||||||
|
"image": "/images/bookmarks/chainsaw-man-the-movie-reze-arc.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Mr. Robot",
|
||||||
|
"author": "Sam Esmail",
|
||||||
|
"type": "show",
|
||||||
|
"year": 2015,
|
||||||
|
"url": "https://www.imdb.com/title/tt4158110/",
|
||||||
|
"image": "/images/bookmarks/mr-robot.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Silicon Valley",
|
||||||
|
"author": "Mike Judge",
|
||||||
|
"type": "show",
|
||||||
|
"year": 2014,
|
||||||
|
"url": "https://www.imdb.com/title/tt2575988/",
|
||||||
|
"image": "/images/bookmarks/silicon-valley.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Masters of the Air",
|
||||||
|
"author": "John Shiban, John Orloff",
|
||||||
|
"type": "show",
|
||||||
|
"year": 2024,
|
||||||
|
"url": "https://www.imdb.com/title/tt2640044/",
|
||||||
|
"image": "/images/bookmarks/masters-of-the-air.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Blue Eye Samurai",
|
||||||
|
"author": "Amber Noizumi, Michael Green",
|
||||||
|
"type": "anime",
|
||||||
|
"year": 2023,
|
||||||
|
"url": "https://www.imdb.com/title/tt13309742/",
|
||||||
|
"image": "/images/bookmarks/blue-eye-samurai.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "My Love from the Star",
|
||||||
|
"author": "Jang Tae-yoo",
|
||||||
|
"type": "show",
|
||||||
|
"year": 2013,
|
||||||
|
"url": "https://www.imdb.com/title/tt3199438/",
|
||||||
|
"image": "/images/bookmarks/my-love-from-the-star.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "When Life Gives You Tangerines",
|
||||||
|
"author": "Im Hyun-wook",
|
||||||
|
"type": "show",
|
||||||
|
"year": 2025,
|
||||||
|
"image": "/images/bookmarks/when-life-gives-you-tangerines.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Midnight Diner",
|
||||||
|
"author": "Joji Matsuoka",
|
||||||
|
"type": "show",
|
||||||
|
"year": 2009,
|
||||||
|
"url": "https://www.imdb.com/title/tt5765544/",
|
||||||
|
"image": "/images/bookmarks/midnight-diner.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Panchayat",
|
||||||
|
"author": "Deepak Kumar Mishra",
|
||||||
|
"type": "show",
|
||||||
|
"year": 2020,
|
||||||
|
"url": "https://www.imdb.com/title/tt11247028/",
|
||||||
|
"image": "/images/bookmarks/panchayat.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Attack on Titan",
|
||||||
|
"author": "Hajime Isayama",
|
||||||
|
"type": "anime",
|
||||||
|
"year": 2013,
|
||||||
|
"url": "https://www.imdb.com/title/tt2560140/",
|
||||||
|
"image": "/images/bookmarks/attack-on-titan.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Death Note",
|
||||||
|
"author": "Tsugumi Ohba, Takeshi Obata",
|
||||||
|
"type": "anime",
|
||||||
|
"year": 2006,
|
||||||
|
"url": "https://www.imdb.com/title/tt0877057/",
|
||||||
|
"image": "/images/bookmarks/death-note.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Demon Slayer",
|
||||||
|
"author": "Koyoharu Gotouge",
|
||||||
|
"type": "anime",
|
||||||
|
"year": 2019,
|
||||||
|
"url": "https://www.imdb.com/title/tt9335498/",
|
||||||
|
"image": "/images/bookmarks/demon-slayer.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Solo Leveling",
|
||||||
|
"author": "Chugong",
|
||||||
|
"type": "anime",
|
||||||
|
"year": 2024,
|
||||||
|
"url": "https://www.imdb.com/title/tt21209876/",
|
||||||
|
"image": "/images/bookmarks/solo-leveling.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Sakamoto Days",
|
||||||
|
"author": "Yuto Suzuki",
|
||||||
|
"type": "anime",
|
||||||
|
"year": 2025,
|
||||||
|
"image": "/images/bookmarks/sakamoto-days.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "One Punch Man",
|
||||||
|
"author": "ONE",
|
||||||
|
"type": "anime",
|
||||||
|
"year": 2015,
|
||||||
|
"url": "https://www.imdb.com/title/tt4508902/",
|
||||||
|
"image": "/images/bookmarks/one-punch-man.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Orb: On the Movements of the Earth",
|
||||||
|
"author": "Uoto",
|
||||||
|
"type": "anime",
|
||||||
|
"year": 2024,
|
||||||
|
"image": "/images/bookmarks/orb-on-the-movements-of-the-earth.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -7,8 +7,14 @@
|
|||||||
"talk": "From First Commit to Mentor: Climbing the Open Source Ladder",
|
"talk": "From First Commit to Mentor: Climbing the Open Source Ladder",
|
||||||
"description": "Presented at the Canonical/Ubuntu conference on the journey from first-time contributor to mentor in open source. Covered practical advice on starting out, staying motivated, transitioning to mentorship, and fostering stronger communities. Included a deep-dive into my GSoC experience at FOSSology.",
|
"description": "Presented at the Canonical/Ubuntu conference on the journey from first-time contributor to mentor in open source. Covered practical advice on starting out, staying motivated, transitioning to mentorship, and fostering stronger communities. Included a deep-dive into my GSoC experience at FOSSology.",
|
||||||
"links": [
|
"links": [
|
||||||
{ "label": "Event Page", "url": "https://events.canonical.com/event/89/contributions/485/" },
|
{
|
||||||
{ "label": "Slides", "url": "https://www.canva.com/design/DAGNthYwrck/Gt7mv99p6zH-aVbx2AtEuQ/view" }
|
"label": "Event Page",
|
||||||
|
"url": "https://events.canonical.com/event/89/contributions/485/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Slides",
|
||||||
|
"url": "https://www.canva.com/design/DAGNthYwrck/Gt7mv99p6zH-aVbx2AtEuQ/view"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -18,8 +24,14 @@
|
|||||||
"role": "mentor",
|
"role": "mentor",
|
||||||
"description": "Represented the FOSSology project at the annual GSoC Mentor Summit hosted at Google's Sunnyvale campus. Participated in unconference sessions on mentoring practices, community building, and project sustainability.",
|
"description": "Represented the FOSSology project at the annual GSoC Mentor Summit hosted at Google's Sunnyvale campus. Participated in unconference sessions on mentoring practices, community building, and project sustainability.",
|
||||||
"links": [
|
"links": [
|
||||||
{ "label": "GSoC", "url": "https://summerofcode.withgoogle.com/" },
|
{
|
||||||
{ "label": "FOSSology", "url": "https://www.fossology.org" }
|
"label": "GSoC",
|
||||||
|
"url": "https://summerofcode.withgoogle.com/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "FOSSology",
|
||||||
|
"url": "https://www.fossology.org"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -29,17 +41,10 @@
|
|||||||
"role": "organizer",
|
"role": "organizer",
|
||||||
"description": "Organized and led a month-long series of talks and workshops as a GitHub Campus Expert. Topics ranged from introduction to open source, Git workflows, and writing GSoC proposals to lightning talks by contributors from projects like Julia and VideoLAN. Over 200 students participated across 7 sessions.",
|
"description": "Organized and led a month-long series of talks and workshops as a GitHub Campus Expert. Topics ranged from introduction to open source, Git workflows, and writing GSoC proposals to lightning talks by contributors from projects like Julia and VideoLAN. Over 200 students participated across 7 sessions.",
|
||||||
"links": [
|
"links": [
|
||||||
{ "label": "Campus Expert Profile", "url": "https://githubcampus.expert/avinal/" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "GitHub Universe 2021",
|
"label": "Campus Expert Profile",
|
||||||
"date": "2021-10-27",
|
"url": "https://githubcampus.expert/avinal/"
|
||||||
"location": "Virtual",
|
}
|
||||||
"role": "attendee",
|
|
||||||
"description": "Attended GitHub's annual developer conference as a GitHub Campus Expert. Explored new features including Copilot, Codespaces, and Actions improvements.",
|
|
||||||
"links": [
|
|
||||||
{ "label": "GitHub Universe", "url": "https://githubuniverse.com/" }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -50,7 +55,10 @@
|
|||||||
"talk": "Campus Experts: Preparation, Application Tips, Perks & Benefits",
|
"talk": "Campus Experts: Preparation, Application Tips, Perks & Benefits",
|
||||||
"description": "Delivered a live session on how to become a GitHub Campus Expert — covering the application process, preparation tips, community benefits, and what to expect from the program.",
|
"description": "Delivered a live session on how to become a GitHub Campus Expert — covering the application process, preparation tips, community benefits, and what to expect from the program.",
|
||||||
"links": [
|
"links": [
|
||||||
{ "label": "Campus Expert Profile", "url": "https://githubcampus.expert/avinal/" }
|
{
|
||||||
|
"label": "Campus Expert Profile",
|
||||||
|
"url": "https://githubcampus.expert/avinal/"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -142,8 +142,9 @@
|
|||||||
"Produced step-by-step tutorials with annotated screenshots, making the project accessible to new contributors and end users alike"
|
"Produced step-by-step tutorials with annotated screenshots, making the project accessible to new contributors and end users alike"
|
||||||
],
|
],
|
||||||
"links": [
|
"links": [
|
||||||
{ "label": "GSoD Case Study", "url": "https://developers.google.com/season-of-docs" },
|
{ "label": "Docs Website", "url": "https://docs.videolan.me/vlc-user/android/3.X/en/index.html" },
|
||||||
{ "label": "VideoLAN", "url": "https://www.videolan.org" }
|
{ "label": "Docs Repository", "url": "https://code.videolan.org/docs/vlc-user/-/tree/android/3.X?ref_type=heads" },
|
||||||
|
{ "label": "GSoD Report", "url": "/posts/blogs/gsod2020-report" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -162,18 +163,34 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "GNU C Library",
|
"name": "GNU Toolchain",
|
||||||
"position": "Contributor",
|
"position": "Contributor",
|
||||||
"url": "https://sourceware.org/glibc/",
|
"url": "https://gcc.gnu.org/",
|
||||||
"startDate": "2024-05-01",
|
"startDate": "2024-05-01",
|
||||||
"summary": "GNU Project",
|
"summary": "GNU Project — GCC & GNU C Library",
|
||||||
"location": "Remote",
|
"location": "Remote",
|
||||||
"highlights": [
|
"highlights": [
|
||||||
"Contributing patches to glibc — one of the most critical pieces of the GNU/Linux ecosystem, used by virtually every Linux distribution",
|
"Contributing patches to GCC and GNU C Library — two of the most critical pieces of the GNU/Linux ecosystem, used by virtually every Linux distribution",
|
||||||
"Working on bug fixes and improvements submitted via the Sourceware mailing list and reviewed by core glibc maintainers"
|
"Working on compiler internals (tree-ssa-strlen optimizations) and C library bug fixes submitted via Sourceware and reviewed by core maintainers"
|
||||||
],
|
],
|
||||||
"links": [
|
"links": [
|
||||||
{ "label": "Patches", "url": "https://sourceware.org/cgit/glibc/log/?qt=author&q=avinal" }
|
{ "label": "GNU C Library Patches", "url": "https://sourceware.org/cgit/glibc/log/?qt=author&q=avinal" },
|
||||||
|
{ "label": "GCC Patches", "url": "https://gitlab.com/gnutools/gcc/-/commits/master?author=Avinal%20Kumar" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Linux Kernel",
|
||||||
|
"position": "Contributor",
|
||||||
|
"url": "https://kernel.org/",
|
||||||
|
"startDate": "2026-04-01",
|
||||||
|
"summary": "DRM Subsystem",
|
||||||
|
"location": "Remote",
|
||||||
|
"highlights": [
|
||||||
|
"Contributing patches to the Linux kernel DRM subsystem — adding new MIPI DSI helper functions and migrating panel drivers to improved APIs",
|
||||||
|
"Patches reviewed and merged via the freedesktop.org GitLab DRM tree"
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{ "label": "Patches", "url": "https://gitlab.freedesktop.org/drm/misc/kernel/-/commits/drm-misc-next?author=Avinal%20Kumar" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from "./BaseLayout.astro";
|
import BaseLayout from "./BaseLayout.astro";
|
||||||
|
import TableOfContents from "@/components/TableOfContents.astro";
|
||||||
|
import RelatedPosts from "@/components/RelatedPosts.astro";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -10,6 +13,8 @@ interface Props {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
image?: string;
|
image?: string;
|
||||||
readingTime: string;
|
readingTime: string;
|
||||||
|
headings?: { depth: number; slug: string; text: string }[];
|
||||||
|
relatedPosts?: CollectionEntry<"posts">[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -21,6 +26,8 @@ const {
|
|||||||
tags = [],
|
tags = [],
|
||||||
image,
|
image,
|
||||||
readingTime,
|
readingTime,
|
||||||
|
headings = [],
|
||||||
|
relatedPosts = [],
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const fmtDate = (d: Date) =>
|
const fmtDate = (d: Date) =>
|
||||||
@@ -49,6 +56,7 @@ const blogPostingLd = {
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={title} description={description} ogImage={image || undefined} jsonLd={blogPostingLd}>
|
<BaseLayout title={title} description={description} ogImage={image || undefined} jsonLd={blogPostingLd}>
|
||||||
|
<div class="reading-progress" aria-hidden="true"></div>
|
||||||
<article class="post-page">
|
<article class="post-page">
|
||||||
{image && (
|
{image && (
|
||||||
<div class="post-hero-img">
|
<div class="post-hero-img">
|
||||||
@@ -83,9 +91,13 @@ const blogPostingLd = {
|
|||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{headings.length > 0 && <TableOfContents headings={headings} />}
|
||||||
|
|
||||||
<div class="prose">
|
<div class="prose">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{relatedPosts.length > 0 && <RelatedPosts posts={relatedPosts} />}
|
||||||
</article>
|
</article>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
@@ -114,6 +126,7 @@ const blogPostingLd = {
|
|||||||
margin-bottom: var(--space-10);
|
margin-bottom: var(--space-10);
|
||||||
padding-bottom: var(--space-6);
|
padding-bottom: var(--space-6);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-category {
|
.post-category {
|
||||||
@@ -136,6 +149,7 @@ const blogPostingLd = {
|
|||||||
.post-meta-row {
|
.post-meta-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
@@ -153,6 +167,7 @@ const blogPostingLd = {
|
|||||||
.post-tags {
|
.post-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
margin-top: var(--space-3);
|
margin-top: var(--space-3);
|
||||||
}
|
}
|
||||||
@@ -165,4 +180,30 @@ const blogPostingLd = {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reading-progress {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 3px;
|
||||||
|
width: 0%;
|
||||||
|
background-color: var(--accent);
|
||||||
|
z-index: 200;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const bar = document.querySelector<HTMLElement>('.reading-progress');
|
||||||
|
const article = document.querySelector('.post-page');
|
||||||
|
if (bar && article) {
|
||||||
|
const update = () => {
|
||||||
|
const total = article.scrollHeight - window.innerHeight;
|
||||||
|
const scrolled = window.scrollY - (article as HTMLElement).offsetTop;
|
||||||
|
const pct = Math.min(100, Math.max(0, (scrolled / total) * 100));
|
||||||
|
bar.style.width = `${pct}%`;
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', update, { passive: true });
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||||
|
import bookmarksData from "@/data/bookmarks.json";
|
||||||
|
|
||||||
|
interface Bookmark {
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
type: "book" | "movie" | "show" | "anime";
|
||||||
|
year: number;
|
||||||
|
url?: string;
|
||||||
|
image?: string;
|
||||||
|
note?: string;
|
||||||
|
favorite?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookmarks = (bookmarksData as Bookmark[]).sort((a, b) => b.year - a.year);
|
||||||
|
|
||||||
|
const typeLabels: Record<string, string> = {
|
||||||
|
book: "Books",
|
||||||
|
movie: "Movies",
|
||||||
|
show: "Shows",
|
||||||
|
anime: "Anime",
|
||||||
|
};
|
||||||
|
|
||||||
|
const types = ["book", "movie", "show", "anime"] as const;
|
||||||
|
|
||||||
|
const bookCount = bookmarks.filter((b) => b.type === "book").length;
|
||||||
|
const movieCount = bookmarks.filter((b) => b.type === "movie").length;
|
||||||
|
const showCount = bookmarks.filter((b) => b.type === "show").length;
|
||||||
|
const animeCount = bookmarks.filter((b) => b.type === "anime").length;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="Bookmarks" description="Books, movies, and shows I've enjoyed">
|
||||||
|
<div class="bookmarks-page">
|
||||||
|
<header class="bookmarks-header">
|
||||||
|
<h1>Bookmarks</h1>
|
||||||
|
<p class="bookmarks-desc">
|
||||||
|
Books, movies, shows, and anime I think are worth your time. Starred entries are personal favorites.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="stats-bar">
|
||||||
|
<button class="stat stat-total active" data-filter="all">
|
||||||
|
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
||||||
|
<span class="stat-value">{bookmarks.length}</span>
|
||||||
|
<span class="stat-label">all</span>
|
||||||
|
</button>
|
||||||
|
<button class="stat stat-books" data-filter="book">
|
||||||
|
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
||||||
|
<span class="stat-value">{bookCount}</span>
|
||||||
|
<span class="stat-label">books</span>
|
||||||
|
</button>
|
||||||
|
<button class="stat stat-movies" data-filter="movie">
|
||||||
|
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/><line x1="17" y1="17" x2="22" y2="17"/></svg>
|
||||||
|
<span class="stat-value">{movieCount}</span>
|
||||||
|
<span class="stat-label">movies</span>
|
||||||
|
</button>
|
||||||
|
<button class="stat stat-shows" data-filter="show">
|
||||||
|
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"/><polyline points="17 2 12 7 7 2"/></svg>
|
||||||
|
<span class="stat-value">{showCount}</span>
|
||||||
|
<span class="stat-label">shows</span>
|
||||||
|
</button>
|
||||||
|
<button class="stat stat-anime" data-filter="anime">
|
||||||
|
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||||
|
<span class="stat-value">{animeCount}</span>
|
||||||
|
<span class="stat-label">anime</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bookmarks-grid">
|
||||||
|
{bookmarks.map((b) => (
|
||||||
|
<a
|
||||||
|
class="bookmark-card"
|
||||||
|
data-type={b.type}
|
||||||
|
href={b.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{b.image && (
|
||||||
|
<div class="bookmark-cover">
|
||||||
|
<img src={b.image} alt={b.title} loading="lazy" decoding="async" />
|
||||||
|
{b.favorite && (
|
||||||
|
<span class="bookmark-fav" title="Personal favorite">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5"><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>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="bookmark-body">
|
||||||
|
<div class="bookmark-top">
|
||||||
|
<span class="badge bookmark-type">{b.type}</span>
|
||||||
|
<span class="bookmark-year">{b.year}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="bookmark-title">{b.title}</h3>
|
||||||
|
<p class="bookmark-author">{b.author}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bookmarks-page {
|
||||||
|
max-width: var(--max-w-page);
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-header {
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-header h1 {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-desc {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
max-width: var(--max-w-prose);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-4) var(--space-3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color var(--duration-fast) var(--ease-out),
|
||||||
|
box-shadow var(--duration-fast) var(--ease-out),
|
||||||
|
background var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat:hover {
|
||||||
|
border-color: var(--stat-color);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: opacity var(--duration-fast) var(--ease-out),
|
||||||
|
color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat:hover .stat-icon,
|
||||||
|
.stat.active .stat-icon {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--stat-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-total { --stat-color: var(--accent); }
|
||||||
|
.stat-books { --stat-color: #10b981; }
|
||||||
|
.stat-movies { --stat-color: #f59e0b; }
|
||||||
|
.stat-shows { --stat-color: #8b5cf6; }
|
||||||
|
.stat-anime { --stat-color: #ef4444; }
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: 800;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat:hover .stat-value,
|
||||||
|
.stat.active .stat-value {
|
||||||
|
color: var(--stat-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat.active {
|
||||||
|
border-color: var(--stat-color);
|
||||||
|
background: color-mix(in srgb, var(--stat-color) 8%, var(--bg-surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.stats-bar {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.bookmarks-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.bookmarks-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color var(--duration-fast) var(--ease-out),
|
||||||
|
box-shadow var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-card:hover {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-card[data-hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-cover {
|
||||||
|
aspect-ratio: 2 / 3;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
filter: grayscale(100%);
|
||||||
|
transition: filter var(--duration-normal) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-card:hover .bookmark-cover img {
|
||||||
|
filter: grayscale(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-fav {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-2);
|
||||||
|
right: var(--space-2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: #f59e0b;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-body {
|
||||||
|
padding: var(--space-4) var(--space-5) var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-type {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-year {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-title {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-author {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const stats = document.querySelectorAll<HTMLButtonElement>('.stat[data-filter]');
|
||||||
|
const cards = document.querySelectorAll<HTMLAnchorElement>('.bookmark-card');
|
||||||
|
|
||||||
|
stats.forEach((stat) => {
|
||||||
|
stat.addEventListener('click', () => {
|
||||||
|
stats.forEach((s) => s.classList.remove('active'));
|
||||||
|
stat.classList.add('active');
|
||||||
|
|
||||||
|
const filter = stat.dataset.filter;
|
||||||
|
cards.forEach((card) => {
|
||||||
|
if (filter === 'all' || card.dataset.type === filter) {
|
||||||
|
card.removeAttribute('data-hidden');
|
||||||
|
} else {
|
||||||
|
card.setAttribute('data-hidden', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||||
|
import contribData from "@/data/contributions.json";
|
||||||
|
|
||||||
|
interface Contribution {
|
||||||
|
project: string;
|
||||||
|
projectUrl: string;
|
||||||
|
platform: string;
|
||||||
|
type: string;
|
||||||
|
kind: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
date: string;
|
||||||
|
status?: string;
|
||||||
|
description?: string;
|
||||||
|
relatedIssue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contributions = (contribData as Contribution[]).sort(
|
||||||
|
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalCount = contributions.length;
|
||||||
|
const projects = [...new Set(contributions.map((c) => c.project))];
|
||||||
|
const types = [...new Set(contributions.map((c) => c.type))];
|
||||||
|
const kinds = [...new Set(contributions.map((c) => c.kind))];
|
||||||
|
const platforms = [...new Set(contributions.map((c) => c.platform))];
|
||||||
|
const mergedCount = contributions.filter((c) => c.status === "merged").length;
|
||||||
|
|
||||||
|
function fmtDate(iso: string) {
|
||||||
|
const d = new Date(iso);
|
||||||
|
return `${d.getDate()} ${d.toLocaleString("en-US", { month: "short" })} ${d.getFullYear()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeLabels: Record<string, string> = {
|
||||||
|
code: "Code",
|
||||||
|
docs: "Docs",
|
||||||
|
infra: "Infra",
|
||||||
|
};
|
||||||
|
|
||||||
|
const kindLabels: Record<string, string> = {
|
||||||
|
pr: "PR",
|
||||||
|
issue: "Issue",
|
||||||
|
commit: "Commit",
|
||||||
|
patch: "Patch",
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusColors: Record<string, string> = {
|
||||||
|
merged: "badge-merged",
|
||||||
|
open: "badge-open",
|
||||||
|
closed: "badge-closed",
|
||||||
|
};
|
||||||
|
|
||||||
|
const platformIcons: Record<string, string> = {
|
||||||
|
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"/>`,
|
||||||
|
gitlab: `<path d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"/>`,
|
||||||
|
sourceware: `<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>`,
|
||||||
|
gerrit: `<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>`,
|
||||||
|
other: `<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>`,
|
||||||
|
};
|
||||||
|
|
||||||
|
function platformIcon(p: string): string {
|
||||||
|
return platformIcons[p] ?? platformIcons.other;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "CollectionPage",
|
||||||
|
name: "Open Source Contributions",
|
||||||
|
description: "Open source contributions across GitHub, GitLab, Sourceware, and other platforms.",
|
||||||
|
url: "https://avinal.space/contributions",
|
||||||
|
author: {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Avinal Kumar",
|
||||||
|
url: "https://avinal.space",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="Contributions"
|
||||||
|
description="Open source contributions across GitHub, GitLab, Sourceware, and other platforms"
|
||||||
|
jsonLd={collectionLd}
|
||||||
|
>
|
||||||
|
<div class="contrib-page">
|
||||||
|
<header class="contrib-header">
|
||||||
|
<h1>Contributions</h1>
|
||||||
|
<p class="contrib-desc">
|
||||||
|
A curated selection of my open source contributions across projects and platforms. This list represents only a portion of my overall work — PRs, patches, commits, and issues that I consider meaningful.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="stats-bar" id="stats-bar">
|
||||||
|
<div class="stat stat-contributions">
|
||||||
|
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><line x1="1.05" y1="12" x2="7" y2="12"/><line x1="17.01" y1="12" x2="22.96" y2="12"/></svg>
|
||||||
|
<span class="stat-value" id="stat-showing">{totalCount}</span>
|
||||||
|
<span class="stat-label">contributions</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat stat-projects">
|
||||||
|
<svg class="stat-icon" width="20" height="20" 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>
|
||||||
|
<span class="stat-value">{projects.length}</span>
|
||||||
|
<span class="stat-label">projects</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat stat-merged">
|
||||||
|
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/></svg>
|
||||||
|
<span class="stat-value">{mergedCount}</span>
|
||||||
|
<span class="stat-label">merged</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat stat-platforms">
|
||||||
|
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||||
|
<span class="stat-value">{platforms.length}</span>
|
||||||
|
<span class="stat-label">platforms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-bar" id="filter-bar">
|
||||||
|
<select class="filter-select" data-filter="project" aria-label="Filter by project">
|
||||||
|
<option value="">All projects</option>
|
||||||
|
{projects.sort().map((p) => (
|
||||||
|
<option value={p}>{p}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select class="filter-select" data-filter="type" aria-label="Filter by type">
|
||||||
|
<option value="">All types</option>
|
||||||
|
{types.map((t) => (
|
||||||
|
<option value={t}>{typeLabels[t] ?? t}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select class="filter-select" data-filter="kind" aria-label="Filter by kind">
|
||||||
|
<option value="">All kinds</option>
|
||||||
|
{kinds.map((k) => (
|
||||||
|
<option value={k}>{kindLabels[k] ?? k}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select class="filter-select" data-filter="platform" aria-label="Filter by platform">
|
||||||
|
<option value="">All platforms</option>
|
||||||
|
{platforms.map((p) => (
|
||||||
|
<option value={p}>{p}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button class="filter-clear" id="clear-filters" style="display:none;">Clear</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline" id="timeline">
|
||||||
|
{contributions.map((c) => (
|
||||||
|
<div
|
||||||
|
class={`tl-entry ct-${c.type}`}
|
||||||
|
data-project={c.project}
|
||||||
|
data-type={c.type}
|
||||||
|
data-kind={c.kind}
|
||||||
|
data-platform={c.platform}
|
||||||
|
>
|
||||||
|
<div class="tl-label-cell">
|
||||||
|
<span class="tl-type-label">{typeLabels[c.type] ?? c.type}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tl-rail-cell">
|
||||||
|
<div class="tl-dot" />
|
||||||
|
</div>
|
||||||
|
<div class="tl-card">
|
||||||
|
<div class="tl-card-header">
|
||||||
|
<div class="tl-dates">
|
||||||
|
{fmtDate(c.date)}
|
||||||
|
<span class="tl-platform">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><Fragment set:html={platformIcon(c.platform)} /></svg>
|
||||||
|
<a href={c.projectUrl} class="project-link" target="_blank" rel="noopener noreferrer">{c.project}</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="tl-title">
|
||||||
|
<a href={c.url} class="contrib-link" target="_blank" rel="noopener noreferrer">{c.title}</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badge-row">
|
||||||
|
{c.status && (
|
||||||
|
<span class={`badge ${statusColors[c.status] ?? ""}`}>{c.status}</span>
|
||||||
|
)}
|
||||||
|
<span class="badge badge-kind">{kindLabels[c.kind] ?? c.kind}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{c.description && <p class="contrib-description">{c.description}</p>}
|
||||||
|
{c.relatedIssue && (
|
||||||
|
<a href={c.relatedIssue} class="related-issue-link" target="_blank" rel="noopener noreferrer">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||||
|
Related issue
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="empty-state" id="empty-state" style="display:none;">
|
||||||
|
No contributions match the current filters.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.contrib-page {
|
||||||
|
max-width: var(--max-w-page);
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrib-header {
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrib-header h1 {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrib-desc {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
max-width: var(--max-w-prose);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Stats ---- */
|
||||||
|
.stats-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-4) var(--space-3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
transition: border-color var(--duration-fast) var(--ease-out),
|
||||||
|
box-shadow var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat:hover {
|
||||||
|
border-color: var(--stat-color);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: opacity var(--duration-fast) var(--ease-out),
|
||||||
|
color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat:hover .stat-icon {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--stat-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-contributions { --stat-color: var(--accent); }
|
||||||
|
.stat-projects { --stat-color: #8b5cf6; }
|
||||||
|
.stat-merged { --stat-color: #10b981; }
|
||||||
|
.stat-platforms { --stat-color: #f59e0b; }
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: 800;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat:hover .stat-value {
|
||||||
|
color: var(--stat-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Filters ---- */
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
padding-right: var(--space-6);
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23737373' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 8px center;
|
||||||
|
transition: border-color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select:hover,
|
||||||
|
.filter-select:focus-visible {
|
||||||
|
border-color: var(--accent);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select.has-value {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-clear {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-clear:hover {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Timeline ---- */
|
||||||
|
.timeline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-entry {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 64px 24px 1fr;
|
||||||
|
gap: 0 var(--space-2);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-entry[data-hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-label-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: var(--space-5);
|
||||||
|
padding-right: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-type-label {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-code .tl-type-label { color: var(--accent); }
|
||||||
|
.ct-docs .tl-type-label { color: #10b981; }
|
||||||
|
.ct-infra .tl-type-label { color: #f59e0b; }
|
||||||
|
|
||||||
|
.tl-rail-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--accent);
|
||||||
|
background: var(--bg);
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
margin-top: var(--space-5);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-code .tl-dot { border-color: var(--accent); }
|
||||||
|
.ct-docs .tl-dot { border-color: #10b981; }
|
||||||
|
.ct-infra .tl-dot { border-color: #f59e0b; }
|
||||||
|
|
||||||
|
.tl-rail-cell::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--space-5) + 12px);
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--border);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-entry:last-child .tl-rail-cell::after { display: none; }
|
||||||
|
|
||||||
|
.tl-card {
|
||||||
|
padding: var(--space-4) var(--space-5);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
transition: border-color var(--duration-fast) var(--ease-out),
|
||||||
|
box-shadow var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-card:hover {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-dates {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-platform {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-link {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-title {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrib-link {
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrib-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-row {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 2px var(--space-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-merged {
|
||||||
|
color: #10b981;
|
||||||
|
border-color: #10b981;
|
||||||
|
background: color-mix(in srgb, #10b981 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-open {
|
||||||
|
color: #f59e0b;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
background: color-mix(in srgb, #f59e0b 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-closed {
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-color: var(--border);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-kind {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrib-description {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-issue-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--duration-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-issue-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-10) 0;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Responsive ---- */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-bar {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-entry {
|
||||||
|
grid-template-columns: 48px 16px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-type-label { font-size: 8px; }
|
||||||
|
|
||||||
|
.tl-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-dates {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const selects = document.querySelectorAll<HTMLSelectElement>(".filter-select");
|
||||||
|
const entries = document.querySelectorAll<HTMLElement>(".tl-entry[data-project]");
|
||||||
|
const clearBtn = document.getElementById("clear-filters") as HTMLButtonElement;
|
||||||
|
const showingStat = document.getElementById("stat-showing");
|
||||||
|
const emptyState = document.getElementById("empty-state");
|
||||||
|
const timeline = document.getElementById("timeline");
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
const filters: Record<string, string> = {};
|
||||||
|
selects.forEach((s) => {
|
||||||
|
if (s.value) filters[s.dataset.filter!] = s.value;
|
||||||
|
s.classList.toggle("has-value", !!s.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
let visible = 0;
|
||||||
|
entries.forEach((el) => {
|
||||||
|
const match = Object.entries(filters).every(
|
||||||
|
([dim, val]) => el.dataset[dim] === val,
|
||||||
|
);
|
||||||
|
if (match) {
|
||||||
|
el.removeAttribute("data-hidden");
|
||||||
|
visible++;
|
||||||
|
} else {
|
||||||
|
el.setAttribute("data-hidden", "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (showingStat) showingStat.textContent = String(visible);
|
||||||
|
if (emptyState) emptyState.style.display = visible === 0 ? "" : "none";
|
||||||
|
if (timeline) timeline.style.display = visible === 0 ? "none" : "";
|
||||||
|
if (clearBtn) clearBtn.style.display = Object.keys(filters).length > 0 ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
selects.forEach((s) => s.addEventListener("change", applyFilters));
|
||||||
|
|
||||||
|
if (clearBtn) {
|
||||||
|
clearBtn.addEventListener("click", () => {
|
||||||
|
selects.forEach((s) => { s.value = ""; s.classList.remove("has-value"); });
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -50,7 +50,7 @@ const personLd = {
|
|||||||
<HeroCard
|
<HeroCard
|
||||||
name="Avinal Kumar"
|
name="Avinal Kumar"
|
||||||
role="Software Engineer II at Red Hat"
|
role="Software Engineer II at Red Hat"
|
||||||
bio="I build things for hybrid cloud, contribute to open source, and self-host everything I can. GNU/Linux and free software are two of my favorite things."
|
bio="Leading OpenShift Builds at Red Hat by day, contributing to the Linux kernel and GNU toolchain by night. Free software advocate, self-hoster, and GSoC mentor."
|
||||||
avatarUrl={user?.avatar_url}
|
avatarUrl={user?.avatar_url}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,11 +11,26 @@ export async function getStaticPaths() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { post } = Astro.props;
|
const { post } = Astro.props;
|
||||||
const { Content } = await render(post);
|
const { Content, headings } = await render(post);
|
||||||
|
const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
|
||||||
|
|
||||||
const wordCount = post.body?.split(/\s+/).length ?? 0;
|
const wordCount = post.body?.split(/\s+/).length ?? 0;
|
||||||
const minutes = Math.max(1, Math.round(wordCount / 220));
|
const minutes = Math.max(1, Math.round(wordCount / 220));
|
||||||
const readingTime = `${minutes} min read`;
|
const readingTime = `${minutes} min read`;
|
||||||
|
|
||||||
|
const allPosts = await getCollection("posts", ({ data }) => !data.draft);
|
||||||
|
const relatedPosts = allPosts
|
||||||
|
.filter((p) => p.id !== post.id)
|
||||||
|
.map((p) => {
|
||||||
|
let score = 0;
|
||||||
|
if (p.data.category === post.data.category) score += 10;
|
||||||
|
score += p.data.tags.filter((t) => post.data.tags.includes(t)).length * 3;
|
||||||
|
return { post: p, score };
|
||||||
|
})
|
||||||
|
.filter((s) => s.score > 0)
|
||||||
|
.sort((a, b) => b.score - a.score || b.post.data.date.getTime() - a.post.data.date.getTime())
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((s) => s.post);
|
||||||
---
|
---
|
||||||
|
|
||||||
<PostLayout
|
<PostLayout
|
||||||
@@ -27,6 +42,8 @@ const readingTime = `${minutes} min read`;
|
|||||||
tags={post.data.tags}
|
tags={post.data.tags}
|
||||||
image={post.data.image}
|
image={post.data.image}
|
||||||
readingTime={readingTime}
|
readingTime={readingTime}
|
||||||
|
headings={tocHeadings}
|
||||||
|
relatedPosts={relatedPosts}
|
||||||
>
|
>
|
||||||
<Content />
|
<Content />
|
||||||
</PostLayout>
|
</PostLayout>
|
||||||
|
|||||||