1
0
mirror of https://github.com/avinal/avinal.github.io.git synced 2026-07-04 07:40:09 +05:30
Files
avinal.github.io/src/pages/resume.astro
T
avinal 924b449301 fix: add fallback for music album art
- remove setup page and fix album art

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-03-05 20:00:58 +05:30

855 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import resume from "@/data/resume.json";
const { basics, work, volunteer, education, skills, projects } = resume as any;
function fmtDate(iso: string) {
const d = new Date(iso);
return d.toLocaleDateString("en-US", { month: "short", year: "numeric" });
}
type ExtLink = { label: string; url: string };
type Role = {
title: string;
startDate: string;
endDate?: string;
summary?: string;
highlights?: string[];
links?: ExtLink[];
};
type TimelineEntry = {
type: "work" | "education" | "volunteer";
title: string;
subtitle: string;
url?: string;
location?: string;
startDate: string;
endDate?: string;
summary?: string;
highlights?: string[];
links?: ExtLink[];
roles?: Role[];
};
type FlatEntry = {
key: string;
type: TimelineEntry["type"];
title: string;
subtitle: string;
url?: string;
location?: string;
startDate: string;
endDate?: string;
summary?: string;
highlights?: string[];
links?: ExtLink[];
};
/**
* Groups flat entries by their `key` (organization/company name).
* Single-entry orgs stay flat; multi-entry orgs become grouped cards
* with nested roles sorted reverse-chronologically.
*/
function groupToTimeline(entries: FlatEntry[]): TimelineEntry[] {
const byKey = new Map<string, FlatEntry[]>();
for (const e of entries) {
if (!byKey.has(e.key)) byKey.set(e.key, []);
byKey.get(e.key)!.push(e);
}
const result: TimelineEntry[] = [];
for (const [, items] of byKey) {
const sorted = [...items].sort(
(a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime(),
);
const latest = sorted[0];
if (sorted.length === 1) {
result.push({
type: latest.type,
title: latest.title,
subtitle: latest.subtitle,
url: latest.url,
location: latest.location,
startDate: latest.startDate,
endDate: latest.endDate,
summary: latest.summary,
highlights: latest.highlights,
links: latest.links,
});
} else {
result.push({
type: latest.type,
title: latest.subtitle,
subtitle: latest.subtitle,
url: latest.url,
location: latest.location,
startDate: latest.startDate,
endDate: latest.endDate,
roles: sorted.map((e) => ({
title: e.title,
startDate: e.startDate,
endDate: e.endDate,
summary: e.summary,
highlights: e.highlights,
links: e.links,
})),
});
}
}
return result;
}
const workFlat: FlatEntry[] = work.map((w: any) => ({
key: w.name,
type: "work" as const,
title: w.position,
subtitle: w.name,
url: w.url || undefined,
location: w.location,
startDate: w.startDate,
endDate: w.endDate,
summary: w.summary,
highlights: w.highlights,
links: w.links,
}));
const volunteerFlat: FlatEntry[] = volunteer.map((v: any) => ({
key: v.organization,
type: "volunteer" as const,
title: v.position,
subtitle: v.organization,
url: v.url || undefined,
location: undefined,
startDate: v.startDate,
endDate: v.endDate,
summary: v.summary,
highlights: undefined as string[] | undefined,
}));
const timeline: TimelineEntry[] = [
...groupToTimeline(workFlat),
...groupToTimeline(volunteerFlat),
...education.map((e: any) => ({
type: "education" as const,
title: e.area ? `${e.studyType} in ${e.area}` : e.studyType,
subtitle: e.institution,
url: e.url || undefined,
location: e.location,
startDate: e.startDate,
endDate: e.endDate,
summary: e.score ? `CGPA: ${e.score}` : undefined,
highlights: undefined as string[] | undefined,
})),
].sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime());
const typeLabels: Record<string, string> = {
work: "Work",
education: "Education",
volunteer: "Community",
};
---
<BaseLayout title="Resume" description={`${basics.name} — ${basics.label}`}>
<div class="resume-page">
<header class="resume-hero">
<div class="hero-text">
<h1>{basics.name}</h1>
<p class="hero-label">{basics.label}</p>
<p class="hero-summary">{basics.summary}</p>
<div class="hero-links">
{basics.profiles.map((p) => (
<a href={p.url} class="profile-link">{p.network}</a>
))}
<a href={basics.url} class="profile-link">Website</a>
</div>
</div>
<div class="hero-photo">
{basics.image ? (
<img src={basics.image} alt={basics.name} />
) : (
<div class="photo-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
)}
</div>
</header>
<div class="resume-body">
<section class="timeline-section">
<h2>Experience & Education</h2>
<div class="timeline">
{timeline.map((entry) => {
const isActive = !entry.endDate;
return (
<div class={`tl-entry tl-${entry.type}`}>
<div class="tl-label-cell">
<span class="tl-type-label">{typeLabels[entry.type]}</span>
</div>
<div class="tl-rail-cell">
<div class:list={["tl-dot", { "tl-dot-active": isActive }]} />
</div>
<div class="tl-card">
{entry.roles ? (
/* Grouped card: multiple roles at the same org */
<Fragment>
<div class="tl-card-header">
<div class="tl-dates">
{fmtDate(entry.roles[entry.roles.length - 1].startDate)} — {entry.endDate ? fmtDate(entry.endDate) : "Present"}
</div>
<h3 class="tl-title tl-org-title">
{entry.url ? <a href={entry.url}>{entry.subtitle}</a> : entry.subtitle}
</h3>
{entry.location && <p class="tl-subtitle"><span class="tl-location">{entry.location}</span></p>}
</div>
<div class="tl-roles">
{entry.roles.map((role, i) => (
<div class:list={["tl-role", { "tl-role-current": i === 0 }]}>
<div class="tl-role-header">
<h4 class="tl-role-title">{role.title}</h4>
<span class="tl-role-dates">
{fmtDate(role.startDate)} — {role.endDate ? fmtDate(role.endDate) : "Present"}
</span>
</div>
{role.summary && <p class="tl-summary">{role.summary}</p>}
{role.highlights && role.highlights.length > 0 && (
<ul class="tl-highlights">
{role.highlights.map((h) => <li>{h}</li>)}
</ul>
)}
{role.links && role.links.length > 0 && (
<div class="tl-links">
{role.links.map((lnk) => (
<a href={lnk.url} class="tl-ext-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"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
{lnk.label}
</a>
))}
</div>
)}
</div>
))}
</div>
</Fragment>
) : (
/* Single card */
<Fragment>
<div class="tl-card-header">
<div class="tl-dates">
{fmtDate(entry.startDate)} — {entry.endDate ? fmtDate(entry.endDate) : "Present"}
</div>
<h3 class="tl-title">{entry.title}</h3>
<p class="tl-subtitle">
{entry.url ? <a href={entry.url}>{entry.subtitle}</a> : entry.subtitle}
{entry.location && <span class="tl-location"> · {entry.location}</span>}
</p>
</div>
{entry.summary && <p class="tl-summary">{entry.summary}</p>}
{entry.highlights && entry.highlights.length > 0 && (
<ul class="tl-highlights">
{entry.highlights.map((h) => <li>{h}</li>)}
</ul>
)}
{entry.links && entry.links.length > 0 && (
<div class="tl-links">
{entry.links.map((lnk) => (
<a href={lnk.url} class="tl-ext-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"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
{lnk.label}
</a>
))}
</div>
)}
</Fragment>
)}
</div>
</div>
)})}
</div>
</section>
<div class="two-col">
<section class="skills-section">
<h2>Skills</h2>
<div class="skill-groups">
{skills.map((group) => (
<div class="skill-group">
<h3>{group.name}</h3>
<div class="skill-pills">
{group.keywords.map((kw) => (
<span class="pill">{kw}</span>
))}
</div>
</div>
))}
</div>
</section>
<section class="projects-section">
<h2>Projects</h2>
<div class="project-list">
{projects.map((p) => (
<div class="project-card">
<h3>{p.url ? <a href={p.url}>{p.name}</a> : p.name}</h3>
<p>{p.description}</p>
{p.highlights && p.highlights.length > 0 && (
<ul class="project-highlights">
{p.highlights.map((h) => <li>{h}</li>)}
</ul>
)}
</div>
))}
</div>
</section>
</div>
</div>
<footer class="resume-footer">
<p>
This resume follows the <a href="https://jsonresume.org">JSON Resume</a> standard.
<a href="/resume" onclick="window.print(); return false;">Print / save as PDF</a> ·
<a href="/data/resume.json" download>Download JSON</a>
</p>
</footer>
</div>
</BaseLayout>
<style>
.resume-page {
max-width: var(--max-w-page);
margin-inline: auto;
}
/* ---- Hero ---- */
.resume-hero {
display: grid;
grid-template-columns: 1fr 200px;
gap: var(--space-8);
align-items: center;
padding: var(--space-10) 0 var(--space-8);
border-bottom: 1px solid var(--border);
margin-bottom: var(--space-10);
}
.hero-photo {
width: 200px;
height: 200px;
border-radius: var(--radius-lg);
overflow: hidden;
border: 2px solid var(--border);
flex-shrink: 0;
background: var(--bg-surface);
}
.hero-photo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.photo-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
opacity: 0.4;
}
.resume-hero h1 {
font-size: clamp(2rem, 5vw, 2.75rem);
margin-bottom: var(--space-2);
letter-spacing: -0.02em;
}
.hero-label {
font-size: var(--text-lg);
color: var(--accent);
font-weight: 500;
margin-bottom: var(--space-4);
}
.hero-summary {
color: var(--text-secondary);
line-height: var(--leading-relaxed);
max-width: 60ch;
margin-bottom: var(--space-4);
}
.meta-item a:hover { color: var(--accent); }
.hero-links {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.profile-link {
display: inline-block;
padding: var(--space-1) var(--space-3);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid var(--border);
border-radius: var(--radius-full);
color: var(--text);
text-decoration: none;
transition: all var(--duration-fast) var(--ease-out);
}
.profile-link:hover {
border-color: var(--accent);
color: var(--accent);
background-color: var(--accent-subtle);
}
/* ---- Timeline ---- */
.timeline-section { margin-bottom: var(--space-12); }
.timeline-section > h2,
.skills-section > h2,
.projects-section > h2 {
font-size: var(--text-xl);
margin-bottom: var(--space-6);
}
.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-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;
}
.tl-work .tl-type-label { color: var(--accent); }
.tl-education .tl-type-label { color: #10b981; }
.tl-volunteer .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;
}
.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-work .tl-dot { border-color: var(--accent); }
.tl-education .tl-dot { border-color: #10b981; background: #10b981; }
.tl-volunteer .tl-dot { border-color: #f59e0b; }
.tl-dot-active {
animation: pulse-dot 2s ease-out infinite;
}
.tl-work .tl-dot-active {
background: var(--accent);
border-color: var(--accent);
--pulse-color: var(--accent);
}
.tl-education .tl-dot-active {
background: #10b981;
border-color: #10b981;
--pulse-color: #10b981;
}
.tl-volunteer .tl-dot-active {
background: #f59e0b;
border-color: #f59e0b;
--pulse-color: #f59e0b;
}
@keyframes pulse-dot {
0% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--pulse-color) 50%, transparent);
}
70% {
box-shadow: 0 0 0 8px color-mix(in srgb, var(--pulse-color) 0%, transparent);
}
100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--pulse-color) 0%, transparent);
}
}
.tl-card {
padding: var(--space-4) var(--space-5);
border: 1px solid var(--border);
border-radius: var(--radius-md);
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 {
font-size: var(--text-xs);
font-weight: 500;
color: var(--text-muted);
margin-bottom: var(--space-1);
font-variant-numeric: tabular-nums;
}
.tl-title {
font-size: var(--text-base);
font-weight: 600;
margin-bottom: var(--space-1);
}
.tl-subtitle {
font-size: var(--text-sm);
color: var(--text-secondary);
}
.tl-subtitle a {
color: var(--accent);
text-decoration: none;
}
.tl-subtitle a:hover { text-decoration: underline; }
.tl-location {
color: var(--text-muted);
font-size: var(--text-xs);
}
.tl-summary {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-top: var(--space-2);
font-style: italic;
}
.tl-highlights {
list-style: none;
padding: 0;
margin-top: var(--space-3);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.tl-highlights li {
font-size: var(--text-sm);
color: var(--text-secondary);
padding-left: 1.2em;
position: relative;
line-height: var(--leading-relaxed);
}
.tl-highlights li::before {
content: "";
position: absolute;
left: 0;
color: var(--accent);
font-weight: 700;
}
/* ---- External links ---- */
.tl-links {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-top: var(--space-3);
}
.tl-ext-link {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--text-xs);
font-weight: 500;
color: var(--accent);
text-decoration: none;
padding: 2px var(--space-2);
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
border-radius: var(--radius-full);
transition: all var(--duration-fast) var(--ease-out);
}
.tl-ext-link:hover {
background: var(--accent-subtle);
border-color: var(--accent);
}
.tl-ext-link svg {
flex-shrink: 0;
opacity: 0.7;
}
/* ---- Grouped roles ---- */
.tl-org-title {
font-size: var(--text-lg);
}
.tl-org-title a {
color: var(--accent);
text-decoration: none;
}
.tl-org-title a:hover {
text-decoration: underline;
}
.tl-roles {
display: flex;
flex-direction: column;
gap: 0;
margin-top: var(--space-4);
}
.tl-role {
padding: var(--space-3) 0 var(--space-3) var(--space-4);
border-left: 2px solid var(--border);
position: relative;
}
.tl-role::before {
content: "";
position: absolute;
left: -5px;
top: calc(var(--space-3) + 6px);
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--border);
}
.tl-role-current::before {
background: var(--accent);
}
.tl-role-current {
border-left-color: var(--accent);
}
.tl-role-header {
display: flex;
align-items: baseline;
gap: var(--space-3);
flex-wrap: wrap;
}
.tl-role-title {
font-size: var(--text-sm);
font-weight: 600;
}
.tl-role-current .tl-role-title {
font-size: var(--text-base);
color: var(--text);
}
.tl-role-dates {
font-size: var(--text-xs);
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
/* ---- Two column: Skills + Projects ---- */
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-10);
margin-bottom: var(--space-10);
}
.skill-groups {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.skill-group h3 {
font-size: var(--text-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-bottom: var(--space-2);
}
.skill-pills {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.pill {
display: inline-block;
padding: var(--space-1) var(--space-3);
font-size: var(--text-xs);
font-weight: 500;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-full);
color: var(--text);
transition: all var(--duration-fast) var(--ease-out);
}
.pill:hover {
border-color: var(--accent);
color: var(--accent);
}
.project-list {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.project-card {
padding: var(--space-4);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-surface);
transition: border-color var(--duration-fast) var(--ease-out);
}
.project-card:hover { border-color: var(--border-strong); }
.project-card h3 {
font-size: var(--text-sm);
font-weight: 600;
margin-bottom: var(--space-1);
}
.project-card h3 a {
color: var(--accent);
text-decoration: none;
}
.project-card h3 a:hover { text-decoration: underline; }
.project-card > p {
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: var(--leading-relaxed);
}
.project-highlights {
list-style: disc;
padding-left: 1.2em;
margin-top: var(--space-2);
font-size: var(--text-xs);
color: var(--text-muted);
}
/* ---- Footer ---- */
.resume-footer {
padding-top: var(--space-6);
border-top: 1px solid var(--border);
text-align: center;
font-size: var(--text-sm);
color: var(--text-muted);
}
.resume-footer a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
}
/* ---- Responsive ---- */
@media (max-width: 768px) {
.two-col {
grid-template-columns: 1fr;
gap: var(--space-8);
}
.resume-hero {
grid-template-columns: 1fr;
}
.hero-photo {
width: 140px;
height: 140px;
justify-self: center;
order: -1;
}
.tl-entry {
grid-template-columns: 48px 16px 1fr;
}
.tl-type-label { font-size: 8px; }
.tl-dot {
width: 10px;
height: 10px;
}
}
/* ---- Print ---- */
@media print {
.resume-page { max-width: 100%; padding: 0; }
.resume-hero { padding: 0 0 1rem; }
.resume-footer { display: none; }
.tl-card { border: 1px solid #ddd; box-shadow: none; }
.tl-card:hover { box-shadow: none; }
.profile-link { border-color: #999; }
.pill { border-color: #999; }
a { color: inherit !important; }
.hero-label { color: #333; }
.tl-dot::before { border-color: #333; }
.tl-education .tl-dot::before { background: #333; border-color: #333; }
}
</style>