prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

commit dec29a24645c98de6c2132234172f36129997f81
parent 2db510d6f99937a9dd42a9e753655dbd665e1e1d
Author: Damian Myrda <damian@prime8.dev>
Date:   Sat, 31 Jan 2026 14:05:28 -0600

refactor: rename web directory to website

Diffstat:
M.pre-commit-config.yaml | 2+-
Dweb/src/layouts/components/Header.astro | 153-------------------------------------------------------------------------------
Dweb/src/layouts/tabs/About.astro | 428-------------------------------------------------------------------------------
Dweb/src/layouts/tabs/Blog.astro | 232-------------------------------------------------------------------------------
Dweb/src/layouts/tabs/Experience.astro | 484-------------------------------------------------------------------------------
Dweb/src/layouts/tabs/Projects.astro | 413-------------------------------------------------------------------------------
Dweb/src/pages/experience/[id].astro | 374-------------------------------------------------------------------------------
Rweb/.gitignore -> website/.gitignore | 0
Rweb/.prettierignore -> website/.prettierignore | 0
Rweb/.prettierrc -> website/.prettierrc | 0
Rweb/Dockerfile -> website/Dockerfile | 0
Rweb/README.md -> website/README.md | 0
Rweb/astro.config.mjs -> website/astro.config.mjs | 0
Rweb/bun.lock -> website/bun.lock | 0
Rweb/env.example -> website/env.example | 0
Rweb/eslint.config.js -> website/eslint.config.js | 0
Rweb/package.json -> website/package.json | 0
Rweb/public/amplifier/logo.svg -> website/public/amplifier/logo.svg | 0
Rweb/public/favicon.png -> website/public/favicon.png | 0
Rweb/public/mandolin/company.jpeg -> website/public/mandolin/company.jpeg | 0
Rweb/public/mandolin/company_silly.jpeg -> website/public/mandolin/company_silly.jpeg | 0
Rweb/public/mandolin/employee_polaroids.jpeg -> website/public/mandolin/employee_polaroids.jpeg | 0
Rweb/public/mandolin/logo.svg -> website/public/mandolin/logo.svg | 0
Rweb/public/mandolin/moment_polaroids.jpeg -> website/public/mandolin/moment_polaroids.jpeg | 0
Rweb/public/me.jpg -> website/public/me.jpg | 0
Rweb/public/robots.txt -> website/public/robots.txt | 0
Rweb/public/site.webmanifest -> website/public/site.webmanifest | 0
Rweb/src/blogs/building-my-portfolio.md -> website/src/blogs/building-my-portfolio.md | 0
Rweb/src/blogs/index.ts -> website/src/blogs/index.ts | 0
Rweb/src/config/about.ts -> website/src/config/about.ts | 0
Rweb/src/config/contact.ts -> website/src/config/contact.ts | 0
Rweb/src/config/experience.ts -> website/src/config/experience.ts | 0
Rweb/src/config/index.ts -> website/src/config/index.ts | 0
Rweb/src/config/projects.ts -> website/src/config/projects.ts | 0
Rweb/src/config/tabs.ts -> website/src/config/tabs.ts | 0
Rweb/src/constants/calendar.ts -> website/src/constants/calendar.ts | 0
Rweb/src/constants/env.ts -> website/src/constants/env.ts | 0
Rweb/src/constants/external/wakatime.ts -> website/src/constants/external/wakatime.ts | 0
Rweb/src/constants/ui/boot.ts -> website/src/constants/ui/boot.ts | 0
Rweb/src/constants/ui/scroll.ts -> website/src/constants/ui/scroll.ts | 0
Rweb/src/constants/ui/tags.ts -> website/src/constants/ui/tags.ts | 0
Rweb/src/constants/website.ts -> website/src/constants/website.ts | 0
Rweb/src/external/git.ts -> website/src/external/git.ts | 0
Rweb/src/external/wakatime.ts -> website/src/external/wakatime.ts | 0
Rweb/src/layouts/Layout.astro -> website/src/layouts/Layout.astro | 0
Rweb/src/layouts/components/Article.astro -> website/src/layouts/components/Article.astro | 0
Rweb/src/layouts/components/Footer.astro -> website/src/layouts/components/Footer.astro | 0
Awebsite/src/layouts/components/Header.astro | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rweb/src/layouts/components/Lightbox.astro -> website/src/layouts/components/Lightbox.astro | 0
Awebsite/src/layouts/tabs/About.astro | 430+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebsite/src/layouts/tabs/Blog.astro | 230+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebsite/src/layouts/tabs/Experience.astro | 486+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebsite/src/layouts/tabs/Projects.astro | 411+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rweb/src/pages/404.astro -> website/src/pages/404.astro | 0
Rweb/src/pages/blog/[slug].astro -> website/src/pages/blog/[slug].astro | 0
Awebsite/src/pages/experience/[id].astro | 376+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rweb/src/pages/index.astro -> website/src/pages/index.astro | 0
Rweb/src/pages/projects/[id].astro -> website/src/pages/projects/[id].astro | 0
Rweb/src/pages/sitemap.xml.ts -> website/src/pages/sitemap.xml.ts | 0
Rweb/src/schemas/blog.ts -> website/src/schemas/blog.ts | 0
Rweb/src/schemas/experience.ts -> website/src/schemas/experience.ts | 0
Rweb/src/schemas/index.ts -> website/src/schemas/index.ts | 0
Rweb/src/schemas/person.ts -> website/src/schemas/person.ts | 0
Rweb/src/schemas/project.ts -> website/src/schemas/project.ts | 0
Rweb/src/schemas/website.ts -> website/src/schemas/website.ts | 0
Rweb/src/tabs/about.ts -> website/src/tabs/about.ts | 0
Rweb/src/tabs/blog.ts -> website/src/tabs/blog.ts | 0
Rweb/src/tabs/experience.ts -> website/src/tabs/experience.ts | 0
Rweb/src/tabs/projects.ts -> website/src/tabs/projects.ts | 0
Rweb/src/ui/boot.ts -> website/src/ui/boot.ts | 0
Rweb/src/ui/dom.ts -> website/src/ui/dom.ts | 0
Rweb/src/ui/index.ts -> website/src/ui/index.ts | 0
Rweb/src/ui/keyboard.ts -> website/src/ui/keyboard.ts | 0
Rweb/src/ui/router.ts -> website/src/ui/router.ts | 0
Rweb/src/ui/status.ts -> website/src/ui/status.ts | 0
Rweb/src/ui/tabs.ts -> website/src/ui/tabs.ts | 0
Rweb/src/utils/duration.ts -> website/src/utils/duration.ts | 0
Rweb/src/utils/markdown.ts -> website/src/utils/markdown.ts | 0
Rweb/src/utils/period.ts -> website/src/utils/period.ts | 0
Rweb/src/utils/sleep.ts -> website/src/utils/sleep.ts | 0
Rweb/src/utils/tags.ts -> website/src/utils/tags.ts | 0
Rweb/tsconfig.json -> website/tsconfig.json | 0
82 files changed, 2090 insertions(+), 2085 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml @@ -3,6 +3,6 @@ repos: hooks: - id: lint-staged name: lint-staged - entry: sh -c 'cd web && bunx lint-staged' + entry: sh -c 'cd website && bunx lint-staged' language: system pass_filenames: false diff --git a/web/src/layouts/components/Header.astro b/web/src/layouts/components/Header.astro @@ -1,153 +0,0 @@ ---- -import { TABS } from "../../config/tabs"; ---- - -<header id="tabs"> - <nav class="tab-nav" aria-label="Main navigation"> - {TABS.map((tab, i) => ( - <div - class:list={["tab", { active: i === 0 }]} - data-tab={tab.id} - aria-current={i === 0 ? "page" : undefined} - > - <span class="tab-num">[{i + 1}] </span>{tab.label} - </div> - ))} - </nav> - <div class="tab-spacer"></div> - <div class="status" id="header-status"> - <span class="dpad"> - <span class="dpad-row"><span class="dpad-key">↑</span></span> - <span class="dpad-row" - ><span class="dpad-key">←</span><span class="dpad-key">↓</span><span - class="dpad-key">→</span - ></span - > - </span> - <span class="navigate"> navigate · Tab cycle</span> - </div> -</header> - -<style> - #tabs { - display: flex; - align-items: center; - gap: 4px; - background: var(--bg-alt); - border-bottom: 1px solid var(--border); - padding: 0 12px; - height: 36px; - position: sticky; - top: 0; - z-index: 100; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - } - - #tabs * { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - } - - .tab-nav { - display: flex; - align-items: center; - gap: 4px; - } - - .tab { - padding: 6px 12px; - color: var(--fg-dim); - cursor: pointer; - border-bottom: 2px solid transparent; - transition: - color 0.15s, - border-color 0.15s; - } - - .tab:hover { - color: var(--fg); - } - - .tab.active { - color: var(--accent); - border-bottom-color: var(--accent); - } - - .tab-spacer { - flex: 1; - } - - .status { - display: flex; - align-items: center; - gap: 8px; - color: var(--fg-dim); - font-size: 11px; - margin-right: 8px; - } - - .dpad { - display: inline-flex; - flex-direction: column; - align-items: center; - gap: 1px; - font-size: 8px; - line-height: 1; - } - - .dpad-row { - display: flex; - gap: 1px; - } - - .dpad-key { - width: 12px; - height: 10px; - display: flex; - align-items: center; - justify-content: center; - background: var(--border); - border-radius: 2px; - color: var(--fg); - } - - @media (max-width: 768px) { - #tabs { - flex-wrap: wrap; - height: auto; - min-height: 36px; - padding: 8px 12px; - gap: 8px; - } - - .tab-nav { - width: 100%; - justify-content: space-between; - gap: 0; - } - - .tab { - padding: 6px 8px; - font-size: 12px; - flex: 1; - text-align: center; - } - - .tab-spacer { - display: none; - } - - .status { - display: none; - } - - .tab-num { - display: none; - } - } -</style> diff --git a/web/src/layouts/tabs/About.astro b/web/src/layouts/tabs/About.astro @@ -1,428 +0,0 @@ ---- -import type { WakaTimeData } from "../../external/wakatime"; -import { about, contact } from "../../config"; -import { currentJob } from "../../config/experience"; -import { getTagClass } from "../../utils/tags"; - -interface Props { - wakaTimeData: WakaTimeData | null; -} - -const { wakaTimeData } = Astro.props; ---- - -<section id="about" class="page active"> - <div class="about-layout"> - <div class="about-left"> - <img - class="profile-image" - src="/me.jpg" - alt="Damian Myrda - Software Engineer and high school senior from Chicago" - loading="eager" - /> - <div class="contact-box"> - <div class="box-title">─ Contact ─</div> - <div class="contact-item"> - <span class="label">Email</span> - <a href={`mailto:${contact.email}`}>{contact.email}</a> - </div> - <div class="contact-item"> - <span class="label">GitLab</span> - <a href={contact.gitlab} target="_blank" rel="noopener noreferrer me" - >git.prime8.dev/moncheeta</a - > - </div> - <div class="contact-item"> - <span class="label">LinkedIn</span> - <a - href={contact.linkedin} - target="_blank" - rel="noopener noreferrer me">linkedin.com/in/damian-myrda</a - > - </div> - </div> - </div> - <div class="about-right"> - <div class="section"> - <h1> - {about.name} - <span class="dim">@{about.handle}</span> - </h1> - <p class="bio">{about.bio}</p> - </div> - <div class="section"> - <div class="box-title">─ Info ─</div> - <div class="info-columns"> - <div class="info-column"> - <div class="info-row"> - <span class="label">Location</span> - {about.location} - </div> - <div class="info-row"> - <span class="label">School</span> - {about.school} - </div> - <div class="info-row"> - <span class="label">Graduating</span> - {about.graduating} - </div> - </div> - {currentJob && ( - <div class="info-column"> - <div class="info-row"> - <span class="label">Company</span> - <a href={`#experience/${currentJob.id}`}> - {currentJob.company} - </a> - </div> - <div class="info-row"> - <span class="label">Position</span> - {currentJob.role} - </div> - </div> - )} - </div> - </div> - <div class="section"> - <div class="box-title">─ Interests ─</div> - <div class="tags"> - { - about.interests.map((interest) => ( - <span class={`tag ${getTagClass(interest)}`}>{interest}</span> - )) - } - </div> - </div> - <div class="section"> - <div class="box-title">─ Tools ─</div> - <div class="tags"> - { - about.tools.map((tool) => ( - <span class={`tag ${getTagClass(tool)}`}>{tool}</span> - )) - } - </div> - </div> - <div class="section"> - <div class="box-title">─ Languages ─</div> - <div class="tags"> - { - about.languages.map((lang) => ( - <span class={`tag ${getTagClass(lang)}`}>{lang}</span> - )) - } - </div> - </div> - { - wakaTimeData?.weeklyStats && ( - <div class="section"> - <div class="box-title">─ Activity ─</div> - <div class="weekly-stats"> - <div class="weekly-total"> - {wakaTimeData.weeklyStats.totalText} - </div> - <div class="weekly-days"> - {wakaTimeData.weeklyStats.days.map((day) => ( - <div class="weekly-day-row"> - <span class="weekly-day-name">{day.dayText}</span> - <div class="weekly-day-bar-container"> - <div - class="weekly-day-bar" - style={`width: ${day.barWidthPercent}`} - /> - </div> - <span class="weekly-day-time">{day.timeText}</span> - </div> - ))} - </div> - </div> - </div> - ) - } - </div> - </div> -</section> - -<style> - .about-layout { - display: flex; - width: 100%; - height: 100%; - } - - .about-left { - width: 320px; - flex-shrink: 0; - border-right: 1px solid var(--border); - display: flex; - flex-direction: column; - padding: 16px; - gap: 16px; - overflow-y: auto; - } - - .about-right { - flex: 1; - padding: 24px 32px; - overflow-y: auto; - display: flex; - flex-direction: column; - justify-content: space-evenly; - } - - .profile-image { - width: 100%; - max-width: 280px; - height: auto; - border-radius: 4px; - border: 1px solid var(--border); - } - - .contact-box { - border: 1px solid var(--border); - padding: 12px; - max-width: 280px; - } - - .box-title { - color: var(--accent); - font-size: 12px; - margin-bottom: 8px; - text-align: center; - } - - .contact-item { - display: flex; - margin-bottom: 4px; - font-size: 12px; - min-width: 0; - } - - .contact-item .label { - color: var(--fg-dim); - width: 70px; - flex-shrink: 0; - } - - .contact-item a { - color: var(--light-blue); - text-decoration: none; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - direction: rtl; - text-align: left; - } - - .contact-item a:hover { - opacity: 0.8; - } - - .section { - flex-shrink: 0; - } - - .section h1 { - font-size: 18px; - font-weight: 400; - margin-bottom: 4px; - } - - .section h1 .dim { - color: var(--fg-dim); - font-size: 14px; - } - - .section .bio { - color: var(--fg-dim); - font-style: italic; - } - - .info-columns { - display: flex; - gap: 48px; - } - - .info-column { - flex: 1; - } - - .info-row { - display: flex; - font-size: 13px; - margin-bottom: 4px; - } - - .info-row .label { - color: var(--fg-dim); - width: 100px; - flex-shrink: 0; - } - - .info-row a { - color: var(--light-blue); - text-decoration: none; - } - - .info-row a:hover { - opacity: 0.8; - } - - .tags { - display: flex; - flex-wrap: wrap; - gap: 6px; - } - - .weekly-stats { - background: var(--bg-alt); - border: 1px solid var(--border); - padding: 12px; - } - - .weekly-total { - font-size: 16px; - font-weight: 500; - color: var(--accent); - margin-bottom: 12px; - } - - .weekly-days { - display: flex; - flex-direction: column; - gap: 6px; - } - - .weekly-day-row { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; - } - - .weekly-day-name { - width: 32px; - flex-shrink: 0; - color: var(--fg-dim); - } - - .weekly-day-bar-container { - flex: 1; - height: 8px; - background: var(--bg); - border-radius: 2px; - overflow: hidden; - } - - .weekly-day-bar { - height: 100%; - background: var(--accent); - border-radius: 2px; - } - - .weekly-day-time { - min-width: 50px; - flex-shrink: 0; - text-align: right; - color: var(--fg-dim); - white-space: nowrap; - } - - @media (max-width: 768px) { - .about-layout { - flex-direction: column; - } - - .about-left { - display: flex; - flex-direction: row; - width: 100%; - border-right: none; - border-bottom: 1px solid var(--border); - padding: 16px; - gap: 16px; - } - - .profile-image { - width: 120px; - min-width: 60px; - flex-shrink: 1; - height: 120px; - align-self: flex-start; - object-fit: cover; - object-position: center; - } - - .contact-box { - flex: 1; - max-width: none; - min-width: 0; - } - - .contact-item .label { - width: 70px; - } - - .contact-item a { - overflow: hidden; - text-overflow: ellipsis; - } - - .about-right { - padding: 16px; - display: block; - } - - .section { - margin-bottom: 16px; - } - - .section h1 { - font-size: 16px; - } - - .section h1 .dim { - font-size: 12px; - } - - .info-columns { - flex-direction: column; - gap: 12px; - } - - .tags { - gap: 4px; - } - - .tag { - font-size: 11px; - padding: 2px 6px; - } - - .weekly-stats { - padding: 10px; - } - - .weekly-total { - font-size: 14px; - } - - .weekly-day-row { - font-size: 11px; - } - - .weekly-day-name { - width: 28px; - } - - .weekly-day-time { - min-width: 45px; - } - } - - @media (max-width: 480px) { - .contact-item .label { - display: none; - } - } -</style> diff --git a/web/src/layouts/tabs/Blog.astro b/web/src/layouts/tabs/Blog.astro @@ -1,232 +0,0 @@ ---- -import BlogArticle from "../components/Article.astro"; - -import { blogs } from "../../blogs"; ---- - -<section id="blog" class="page"> - { - blogs.length === 0 ? ( - <div class="blog-empty"> - <p class="color-dim">No blog posts yet.</p> - <p class="color-dim">Check back soon!</p> - </div> - ) : ( - <> - <div id="blog-list-view" class="blog-list-view"> - <div class="blog-list-container"> - {blogs.map((blog, i) => ( - <div class="blog-list-item" data-index={i} data-id={blog.id}> - <span class="blog-list-date">{blog.date}</span> - <span class="blog-list-title">{blog.title}</span> - <span class="blog-list-arrow">→</span> - </div> - ))} - </div> - </div> - - <div id="blog-post-view" class="blog-post-view hidden"> - <div class="blog-post-container"> - <button class="blog-back-btn">← Back to posts</button> - {blogs.map((blog, i) => ( - <div - class="blog-post hidden" - data-index={i} - data-id={blog.id} - data-blog-id={blog.id} - > - <BlogArticle blog={blog} /> - </div> - ))} - </div> - </div> - </> - ) - } -</section> - -<style> - .blog-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - width: 100%; - gap: 8px; - } - - .blog-list-view { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - width: 100%; - } - - .blog-list-view.hidden { - display: none; - } - - .blog-list-container { - max-width: 600px; - width: 100%; - padding: 32px; - } - - .blog-list-item { - display: flex; - align-items: center; - gap: 16px; - padding: 16px 20px; - border: 1px solid var(--border); - margin-bottom: 12px; - cursor: pointer; - transition: - background 0.15s ease, - border-color 0.15s ease; - text-decoration: none; - color: inherit; - } - - .blog-list-item:hover { - background: var(--selection); - border-color: var(--accent); - } - - .blog-list-item.selected { - background: var(--bg-alt); - border-color: var(--accent); - } - - .blog-list-item.selected:hover { - background: var(--selection); - } - - .blog-list-item:last-child { - margin-bottom: 0; - } - - .blog-list-date { - color: var(--fg-dim); - font-size: 12px; - flex-shrink: 0; - width: 90px; - } - - .blog-list-title { - flex: 1; - color: var(--fg); - font-weight: 500; - } - - .blog-list-arrow { - color: var(--fg-dim); - font-size: 14px; - transition: - transform 0.15s, - color 0.15s; - } - - .blog-list-item:hover .blog-list-arrow { - color: var(--accent); - transform: translateX(4px); - } - - .blog-post-view { - display: flex; - justify-content: center; - height: 100%; - width: 100%; - overflow-y: auto; - padding: 32px; - } - - .blog-post-view.hidden { - display: none; - } - - .blog-post-container { - max-width: 700px; - width: 100%; - } - - .blog-post.hidden { - display: none; - } - - .blog-back-btn { - display: inline-block; - background: transparent; - border: 1px solid var(--border); - color: var(--fg-dim); - padding: 8px 16px; - cursor: pointer; - font-family: inherit; - font-size: 12px; - margin-bottom: 24px; - transition: all 0.15s; - text-decoration: none; - } - - .blog-back-btn:hover { - background: var(--bg-alt); - color: var(--accent); - border-color: var(--accent); - } - - @media (max-width: 768px) { - .blog-list-container { - padding: 16px; - } - - .blog-list-item { - gap: 12px; - padding: 12px 16px; - margin-bottom: 10px; - } - - .blog-list-date { - font-size: 11px; - width: 80px; - } - - .blog-list-title { - font-size: 14px; - } - - .blog-post-view { - padding: 16px; - } - - .blog-back-btn { - padding: 6px 12px; - font-size: 11px; - margin-bottom: 16px; - } - } - - @media (max-width: 480px) { - .blog-list-item { - flex-wrap: wrap; - gap: 4px; - } - - .blog-list-date { - width: auto; - order: 2; - } - - .blog-list-title { - width: 100%; - order: 1; - } - - .blog-list-arrow { - order: 3; - margin-left: auto; - } - } -</style> - - diff --git a/web/src/layouts/tabs/Experience.astro b/web/src/layouts/tabs/Experience.astro @@ -1,484 +0,0 @@ ---- -import { experience } from "../../config"; -import { calculateMonths } from "../../utils/period"; -import { MONTH_NAMES } from "../../constants/calendar"; -import { getTagClass } from "../../utils/tags"; - -const dates = experience.map((exp) => { - const [year, month] = exp.startDate.split("-").map(Number); - return new Date(year, month - 1, 1); -}); -const endDates = experience.map((exp) => - exp.endDate - ? new Date( - parseInt(exp.endDate.split("-")[0]), - parseInt(exp.endDate.split("-")[1]) - 1, - 28, - ) - : new Date(), -); -const earliestDate = new Date(Math.min(...dates.map((d) => d.getTime()))); -const latestDate = new Date(Math.max(...endDates.map((d) => d.getTime()))); - -const timelineStart = new Date( - earliestDate.getFullYear(), - earliestDate.getMonth(), - 1, -); -const timelineEnd = latestDate; - -const monthTicks: string[] = []; -let current = new Date(timelineStart); -while (current <= timelineEnd) { - monthTicks.push(MONTH_NAMES[current.getMonth()][0]); - current.setMonth(current.getMonth() + 1); -} ---- - -<section id="experience" class="page"> - <article class="experience-layout"> - <div class="experience-detail"> - { - experience.map((exp, index) => { - const months = calculateMonths(exp.startDate, exp.endDate); - const [startYear, startMonth] = exp.startDate.split("-").map(Number); - const startStr = `${MONTH_NAMES[startMonth - 1]} ${startYear}`; - const endStr = exp.endDate - ? `${MONTH_NAMES[parseInt(exp.endDate.split("-")[1]) - 1]} ${exp.endDate.split("-")[0]}` - : "Present"; - - const isLast = index === experience.length - 1; - - return ( - <div class="exp-row" data-index={index} data-id={exp.id}> - <div class="exp-card" data-index={index} data-id={exp.id}> - <div class="exp-card-header"> - <div class="exp-card-title"> - {exp.logo ? ( - <div class="exp-title-with-logo"> - <img - src={exp.logo} - alt={`${exp.company} logo`} - class={`exp-logo ${exp.logoFilter || ""}`} - /> - <span class="exp-role">{exp.role}</span> - </div> - ) : ( - <div class="exp-title-text"> - <h2>{exp.company}</h2> - <span class="exp-role">{exp.role}</span> - </div> - )} - </div> - <div class="exp-card-meta"> - <span class="exp-dates"> - {startStr} → {endStr} - </span> - <span class="exp-duration">{months} months</span> - {exp.url && ( - <a - class="external-link" - href={exp.url} - target="_blank" - rel="noopener noreferrer" - > - {new URL(exp.url).hostname} → - </a> - )} - </div> - </div> - <div class="exp-card-tech"> - {exp.tech.map((tech) => ( - <span class={`tag ${getTagClass(tech)}`}>{tech}</span> - ))} - </div> - <div class="exp-card-body"> - {exp.description.map((description) => ( - <p>{description}</p> - ))} - </div> - {exp.images && exp.images.length > 0 && ( - <div class="exp-card-images"> - <img - src={exp.images[0]} - alt={`${exp.company} workplace, ${exp.role} experience at ${exp.company}`} - class="exp-image-hero" - loading="lazy" - data-idx="0" - /> - {exp.images.length > 1 && ( - <div class="exp-image-grid"> - {exp.images.slice(1).map((image, index) => ( - <img - src={image} - alt={`${exp.company} team and office environment, image ${index + 2} of ${exp.images!.length}`} - class="exp-image-thumb" - loading="lazy" - data-idx={index + 1} - /> - ))} - </div> - )} - </div> - )} - </div> - <div - class={`exp-timeline ${index === 0 ? "active" : ""}`} - data-index={index} - > - <span class="timeline-dot" /> - <span class="timeline-year"> - [{exp.endDate ? exp.endDate.split("-")[0] : "Now"}] - </span> - {!isLast && <div class="timeline-line" />} - </div> - </div> - ); - }) - } - </div> - </article> -</section> - -<style> - .experience-layout { - display: flex; - flex-direction: row; - height: 100%; - width: 100%; - padding: 24px 32px; - gap: 24px; - } - - .experience-detail { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 16px; - padding: 4px; - } - - .exp-card { - background: var(--bg-alt); - border: 2px solid var(--border); - border-radius: 4px; - padding: 24px; - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 0; - scroll-snap-align: start; - transition: border-color 0.2s ease; - } - - .exp-card.selected { - border-color: var(--accent); - } - - .exp-row { - display: flex; - } - - .exp-timeline { - width: 64px; - flex-shrink: 0; - display: flex; - flex-direction: column; - align-items: center; - padding-top: 24px; - position: relative; - } - - .timeline-dot { - width: 12px; - height: 12px; - border: 2px solid var(--fg-dim); - border-radius: 50%; - background: transparent; - transition: all 0.2s ease; - font-size: 0; - } - - .exp-timeline.active .timeline-dot { - border-color: var(--accent); - background: var(--accent); - transform: scale(1.2); - } - - .timeline-year { - font-size: 10px; - color: var(--fg-dim); - font-family: monospace; - margin-top: 4px; - transition: color 0.2s ease; - } - - .exp-timeline.active .timeline-year { - color: var(--accent); - } - - .timeline-line { - position: absolute; - top: 56px; - bottom: -16px; - width: 2px; - left: 50%; - transform: translateX(-50%); - background: repeating-linear-gradient( - to bottom, - var(--border) 0px, - var(--border) 4px, - transparent 4px, - transparent 8px - ); - } - - .exp-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 16px; - gap: 24px; - } - - .exp-card-title { - display: flex; - align-items: center; - gap: 16px; - } - - .exp-logo { - height: 32px; - width: auto; - flex-shrink: 0; - } - - .exp-logo.logo-invert { - filter: brightness(0) invert(1); - } - - .exp-title-with-logo { - display: flex; - flex-direction: column; - gap: 4px; - } - - .exp-title-with-logo .exp-role { - color: #999; - font-size: 14px; - } - - .exp-title-with-logo .exp-duration { - color: #aaa; - font-size: 12px; - } - - .exp-title-text h2 { - font-size: 22px; - font-weight: 500; - color: var(--accent); - margin: 0 0 4px 0; - } - - .exp-title-text .exp-role { - font-size: 14px; - color: #999; - } - - .exp-card-meta { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 4px; - font-size: 13px; - } - - .exp-dates { - color: var(--fg); - } - - .exp-duration { - color: #aaa; - font-size: 12px; - } - - .exp-card-tech { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 20px; - } - - .exp-card-body { - color: var(--fg); - line-height: 1.7; - padding-bottom: 0; - margin-bottom: 0; - } - - .exp-card-body p { - margin-bottom: 12px; - } - - .exp-card-body p:last-child { - margin-bottom: 0; - } - - .exp-card-images { - display: flex; - gap: 12px; - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid var(--border); - max-height: 180px; - overflow: hidden; - } - - .exp-image-hero { - max-width: 280px; - height: 100%; - border-radius: 4px; - border: 1px solid var(--border); - object-fit: cover; - cursor: pointer; - transition: - transform 0.15s, - box-shadow 0.15s; - } - - .exp-image-hero:hover { - opacity: 0.8; - border-color: var(--accent); - } - - .exp-image-grid { - display: flex; - flex-wrap: wrap; - gap: 8px; - max-width: 400px; - } - - .exp-image-thumb { - width: calc(50% - 4px); - height: 86px; - border-radius: 4px; - border: 1px solid var(--border); - object-fit: cover; - cursor: pointer; - transition: - opacity 0.15s, - border-color 0.15s; - } - - .exp-image-thumb:hover { - opacity: 0.8; - border-color: var(--accent); - } - - .external-link { - color: var(--light-blue); - text-decoration: none; - font-size: 12px; - } - - .external-link:hover { - opacity: 0.8; - } - - @media (max-width: 768px) { - .experience-layout { - padding: 16px; - gap: 12px; - } - - .exp-timeline { - display: none; - } - - .exp-card { - padding: 16px; - } - - .exp-card.selected { - border-color: var(--border); - } - - .exp-card-header { - flex-direction: column; - gap: 12px; - } - - .exp-card-meta { - align-items: flex-start; - } - - .exp-logo { - height: 28px; - } - - .exp-title-text h2 { - font-size: 18px; - } - - .exp-card-tech { - gap: 4px; - } - - .exp-card-body { - font-size: 13px; - } - - .exp-card-images { - flex-direction: column; - gap: 12px; - max-height: none; - overflow: visible; - } - - .exp-image-hero { - max-width: 100%; - height: auto; - max-height: 200px; - } - - .exp-image-grid { - width: 100%; - max-width: none; - flex-direction: row; - flex-wrap: wrap; - gap: 8px; - } - - .exp-image-thumb { - width: calc(33.33% - 6px); - height: 80px; - } - } - - @media (max-width: 480px) { - .exp-image-thumb { - width: calc(50% - 4px); - } - } -</style> - -<script> - document.querySelectorAll(".exp-image-hero, .exp-image-thumb").forEach((img) => { - img.addEventListener("click", () => { - const container = (img as HTMLElement).closest(".exp-card-images"); - const allImages = container - ? Array.from(container.querySelectorAll("img")).map( - (i) => (i as HTMLImageElement).src, - ) - : []; - const startIndex = allImages.indexOf((img as HTMLImageElement).src); - ( - window as Window & { - openLightbox?: (images: string[], startIndex?: number) => void; - } - ).openLightbox?.(allImages, startIndex); - }); - }); -</script> diff --git a/web/src/layouts/tabs/Projects.astro b/web/src/layouts/tabs/Projects.astro @@ -1,413 +0,0 @@ ---- -import { projects } from "../../config/projects"; -import { fetchGitMetadataForProject } from "../../external/git"; -import { getTechMeta, getTagClass } from "../../utils/tags"; -import { parseMarkdown, removeFirstH1 } from "../../utils/markdown"; - -const metadata = Object.fromEntries( - ( - await Promise.all( - projects.map(async (project) => [ - project.id, - await fetchGitMetadataForProject(project), - ]), - ) - ).filter(([, meta]) => meta !== null), -); ---- - -<section id="projects" class="page"> - <aside class="list-view" aria-label="Project list"> - { - projects.map((prg, i) => ( - <div - class={`list-item ${i === 0 ? "selected" : ""}`} - data-index={i} - data-id={prg.id} - > - <div class="list-header"> - <span class="list-title">{prg.name}</span> - <span class="list-meta">{getTechMeta(prg.tech)}</span> - </div> - <div class="list-subtitle">{prg.subtitle}</div> - </div> - )) - } - </aside> - <article class="detail-view"> - { - projects.map((prg, index) => ( - <div - class={`detail-content ${index !== 0 ? "hidden" : ""}`} - data-for={index} - data-id={prg.id} - > - <div class="detail-header"> - <h2>{prg.name}</h2> - <a - class="external-link" - href={prg.url} - target="_blank" - rel="noopener noreferrer" - > - {prg.url.includes("github.com") ? "GitHub" : "GitLab"} → - </a> - </div> - <div class="tech-stack"> - {prg.tech.map((tech) => ( - <span class={`tag ${getTagClass(tech)}`}>{tech}</span> - ))} - </div> - <div class="readme-content" data-readme={prg.id}> - {metadata?.[prg.id]?.readme ? ( - <Fragment - set:html={parseMarkdown(removeFirstH1(metadata[prg.id].readme))} - /> - ) : ( - <p class="color-dim">README not available</p> - )} - </div> - </div> - )) - } - </article> -</section> - -<style> - aside.list-view { - width: 300px; - flex-shrink: 0; - border-right: 1px solid var(--border); - overflow-y: auto; - } - - article.detail-view { - flex: 1; - padding: 24px 32px; - overflow: hidden; - display: flex; - flex-direction: column; - } - - .list-item { - padding: 12px 16px; - border-bottom: 1px solid var(--border); - cursor: pointer; - transition: background 0.1s; - } - - .list-item:hover { - background: var(--bg-alt); - } - - .list-item.selected { - background: var(--selection); - border-left: 3px solid var(--accent); - } - - .list-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2px; - } - - .list-title { - font-weight: 600; - color: var(--fg); - } - - .list-meta { - font-size: 11px; - color: var(--fg-dim); - } - - .list-subtitle { - font-size: 12px; - color: var(--fg-dim); - } - - .detail-content { - display: flex; - flex-direction: column; - height: 100%; - } - - .detail-content.hidden { - display: none; - } - - .detail-content h2 { - font-size: 20px; - font-weight: 400; - color: var(--accent); - margin-bottom: 4px; - } - - .detail-content .tech-stack { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 16px; - } - - .detail-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; - } - - .detail-header h2 { - margin-bottom: 0; - } - - .external-link { - color: var(--light-blue); - text-decoration: none; - font-size: 12px; - } - - .external-link:hover { - opacity: 0.8; - } - - .readme-content { - flex: 1; - min-height: 0; - overflow-y: auto; - font-size: 13px; - line-height: 1.6; - color: var(--fg); - margin-top: 16px; - padding-top: 16px; - padding-right: 12px; - border-top: 1px solid var(--border); - } - - .readme-content :global(p) { - margin-bottom: 12px; - } - - .readme-content :global(p:last-child) { - margin-bottom: 0; - } - - .readme-content :global(h2), - .readme-content :global(.readme-h1) { - font-size: 16px; - color: var(--accent); - margin: 20px 0 8px; - font-weight: 600; - } - - .readme-content :global(h2:first-child), - .readme-content :global(.readme-h1:first-child) { - margin-top: 0; - } - - .readme-content :global(h3), - .readme-content :global(.readme-h2) { - font-size: 14px; - color: var(--accent); - margin: 16px 0 8px; - font-weight: 500; - } - - .readme-content :global(h4), - .readme-content :global(.readme-h3) { - font-size: 13px; - color: var(--fg); - margin: 12px 0 6px; - font-weight: 500; - } - - .readme-content :global(h5), - .readme-content :global(h6) { - font-size: 12px; - color: var(--fg-dim); - margin: 10px 0 6px; - font-weight: 500; - } - - .readme-content :global(pre) { - display: block; - background: var(--bg-alt); - border: 1px solid var(--border); - padding: 12px; - margin: 12px 0; - overflow-x: auto; - font-size: 12px; - line-height: 1.4; - } - - .readme-content :global(pre code) { - background: none; - padding: 0; - font-size: inherit; - } - - .readme-content :global(code) { - background: var(--bg-alt); - padding: 2px 6px; - font-size: 12px; - border-radius: 2px; - } - - .readme-content :global(a) { - color: var(--light-blue); - text-decoration: none; - } - - .readme-content :global(a:hover) { - opacity: 0.8; - } - - .readme-content :global(ul), - .readme-content :global(ol) { - margin: 8px 0; - padding-left: 20px; - } - - .readme-content :global(li) { - margin-bottom: 4px; - } - - .readme-content :global(li > ul), - .readme-content :global(li > ol) { - margin: 4px 0; - } - - .readme-content :global(table) { - width: 100%; - border-collapse: collapse; - margin: 12px 0; - font-size: 12px; - } - - .readme-content :global(th), - .readme-content :global(td) { - border: 1px solid var(--border); - padding: 8px 12px; - text-align: left; - } - - .readme-content :global(th) { - background: var(--bg-alt); - color: var(--accent); - font-weight: 500; - } - - .readme-content :global(tr:nth-child(even)) { - background: rgba(255, 255, 255, 0.02); - } - - .readme-content :global(blockquote) { - border-left: 3px solid var(--accent); - margin: 12px 0; - padding: 8px 16px; - color: var(--fg-dim); - background: var(--bg-alt); - } - - .readme-content :global(blockquote p) { - margin-bottom: 0; - } - - .readme-content :global(hr) { - border: none; - border-top: 1px solid var(--border); - margin: 20px 0; - } - - .readme-content :global(img) { - max-width: 100%; - height: auto; - margin: 12px 0; - } - - .readme-content :global(strong) { - color: var(--fg); - font-weight: 600; - } - - .readme-content :global(em) { - font-style: italic; - color: var(--fg-dim); - } - - .readme-content :global(del) { - text-decoration: line-through; - color: var(--fg-dim); - } - - @media (max-width: 768px) { - #projects { - flex-direction: column; - } - - aside.list-view { - width: 100%; - border-right: none; - border-bottom: 1px solid var(--border); - max-height: 200px; - overflow-y: auto; - } - - .list-item { - padding: 10px 14px; - } - - .list-title { - font-size: 13px; - } - - .list-subtitle { - font-size: 11px; - } - - article.detail-view { - padding: 16px; - overflow-y: auto; - } - - .detail-content h2 { - font-size: 18px; - } - - .detail-header { - flex-direction: column; - align-items: flex-start; - gap: 8px; - } - - .readme-content { - font-size: 12px; - } - - .readme-content :global(pre) { - font-size: 11px; - padding: 10px; - } - - .readme-content :global(code) { - font-size: 11px; - } - } - - @media (max-width: 480px) { - aside.list-view { - max-height: 150px; - } - - .list-item { - padding: 8px 12px; - } - - .list-meta { - display: none; - } - } -</style> - - diff --git a/web/src/pages/experience/[id].astro b/web/src/pages/experience/[id].astro @@ -1,374 +0,0 @@ ---- -import Layout from "../../layouts/Layout.astro"; -import Lightbox from "../../layouts/components/Lightbox.astro"; - -import { SITE_URL } from "../../constants/website"; -import { experience as experiences } from "../../config"; -import { calculateMonths } from "../../utils/period"; -import { MONTH_NAMES } from "../../constants/calendar"; -import { getTagClass } from "../../utils/tags"; -import schemas from "../../schemas/index"; - -const exp = experiences.find((e) => e.id === Astro.params.id); -if (!exp) { - return new Response(null, { - status: 404, - statusText: "Not Found", - }); -} - -const months = calculateMonths(exp.startDate, exp.endDate); -const [startYear, startMonth] = exp.startDate.split("-").map(Number); -const startStr = `${MONTH_NAMES[startMonth - 1]} ${startYear}`; -const endStr = exp.endDate - ? `${MONTH_NAMES[parseInt(exp.endDate.split("-")[1]) - 1]} ${exp.endDate.split("-")[0]}` - : "Present"; - -const keywords = [...exp.tech, exp.company, exp.role].join(", "); -const description = `${exp.role} at ${exp.company}. ${exp.description[0]}`; - -const { getSchemaForExperience, getBreadcrumbSchema } = schemas.experience; ---- - -<Layout - title={`${exp.company} - ${exp.role} | Damian Myrda`} - description={description} - type="website" - url={`${SITE_URL}/experience/${exp.id}`} - keywords={keywords} - image={exp.logo} -> - <main id="content"> - <div class="experience-page"> - <a href="/#experience" class="back-btn">← See more experience</a> - - <article class="experience-article"> - <header class="article-header"> - <div class="header-top"> - <div class="title-section"> - <div class="exp-title-with-logo"> - <img - src={exp.logo} - alt={exp.company} - class={`exp-logo ${exp.logoFilter || ""}`} - /> - <span class="exp-role">{exp.role}</span> - </div> - </div> - <div class="exp-card-meta"> - <span class="exp-dates">{startStr} → {endStr}</span> - <span class="exp-duration">{months} months</span> - {exp.url && ( - <a - class="external-link" - href={exp.url} - target="_blank" - rel="noopener noreferrer" - > - {new URL(exp.url).hostname} → - </a> - )} - </div> - </div> - </header> - - <section class="tech-tags"> - { - exp.tech.map((tech) => ( - <span class={`tag ${getTagClass(tech)}`}>{tech}</span> - )) - } - </section> - - <section class="article-content"> - {exp.description.map((paragraph) => <p>{paragraph}</p>)} - </section> - - { - exp.images && exp.images.length > 0 && ( - <section class="images-section"> - <h2>Photos</h2> - <div class="image-grid"> - {exp.images.map((img, i) => ( - <img - src={img} - alt={`${exp.company} photo ${i + 1}`} - class="experience-image" - loading="lazy" - /> - ))} - </div> - </section> - ) - } - </article> - </div> - <script - type="application/ld+json" - set:html={JSON.stringify(getSchemaForExperience(exp))} - /> - <script - type="application/ld+json" - set:html={JSON.stringify(getBreadcrumbSchema(exp))} - /> - </main> - <Lightbox /> -</Layout> - -<script> - document.getElementById("app")?.classList.add("ready"); - - // Lightbox click handlers for images - document.querySelectorAll(".experience-image").forEach((img) => { - img.addEventListener("click", () => { - const container = (img as HTMLElement).closest(".image-grid"); - const allImages = container - ? Array.from(container.querySelectorAll("img")).map( - (i) => (i as HTMLImageElement).src, - ) - : []; - const startIndex = allImages.indexOf((img as HTMLImageElement).src); - ( - window as Window & { - openLightbox?: (images: string[], startIndex?: number) => void; - } - ).openLightbox?.(allImages, startIndex); - }); - }); -</script> - -<style> - :global(body) { - overflow: auto; - } - - :global(#app) { - height: auto; - min-height: 100vh; - } - - :global(#content) { - overflow: visible; - } - - .experience-page { - max-width: 720px; - margin: 0 auto; - padding: 40px 24px 40px; - min-height: 100vh; - } - - .back-btn { - display: inline-block; - background: transparent; - border: 1px solid var(--border); - color: var(--fg-dim); - padding: 8px 16px; - cursor: pointer; - font-family: inherit; - font-size: 12px; - margin-bottom: 24px; - transition: all 0.15s; - text-decoration: none; - } - - .back-btn:hover { - background: var(--bg-alt); - color: var(--accent); - border-color: var(--accent); - } - - .experience-article { - animation: fadeIn 0.2s ease; - } - - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - - .article-header { - margin-bottom: 24px; - padding-bottom: 16px; - border-bottom: 1px solid var(--border); - } - - .header-top { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 24px; - } - - .title-section { - display: flex; - align-items: center; - gap: 16px; - } - - .exp-logo { - height: 32px; - width: auto; - flex-shrink: 0; - } - - .exp-logo.logo-invert { - filter: brightness(0) invert(1); - } - - .exp-title-with-logo { - display: flex; - flex-direction: column; - gap: 4px; - } - - .exp-title-with-logo .exp-role { - color: #999; - font-size: 14px; - } - - .exp-card-meta { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 4px; - font-size: 13px; - } - - .exp-dates { - color: var(--fg); - } - - .exp-duration { - color: #aaa; - font-size: 12px; - } - - .external-link { - color: var(--light-blue); - text-decoration: none; - font-size: 12px; - } - - .external-link:hover { - opacity: 0.8; - } - - .tech-tags { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 24px; - } - - .article-content { - font-size: 14px; - line-height: 1.7; - color: var(--fg); - margin-bottom: 32px; - } - - .article-content p { - margin-bottom: 16px; - } - - .article-content p:last-child { - margin-bottom: 0; - } - - .images-section h2 { - font-size: 16px; - color: var(--accent); - margin-bottom: 16px; - font-weight: 500; - } - - .image-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; - } - - .experience-image { - width: 100%; - height: 180px; - object-fit: cover; - border-radius: 4px; - border: 1px solid var(--border); - cursor: pointer; - transition: - opacity 0.15s, - border-color 0.15s; - } - - .experience-image:hover { - opacity: 0.9; - border-color: var(--accent); - } - - @media (max-width: 768px) { - .experience-page { - padding: 24px 16px 0; - } - - .back-btn { - padding: 6px 12px; - font-size: 11px; - margin-bottom: 16px; - } - - .header-top { - flex-direction: column; - gap: 12px; - } - - .title-section { - gap: 12px; - } - - .company-logo { - height: 36px; - } - - h1 { - font-size: 22px; - } - - .role { - font-size: 14px; - } - - .article-content { - font-size: 13px; - } - - .image-grid { - grid-template-columns: 1fr; - } - - .experience-image { - height: 200px; - } - } -</style> - -<script> - import { initKeyboard, onKey } from "../../ui/keyboard"; - import { SCROLL_AMOUNT } from "../../constants/ui/scroll"; - - initKeyboard(); - - onKey("ArrowUp", (e) => { - e.preventDefault(); - window.scrollBy({ top: -SCROLL_AMOUNT, behavior: "smooth" }); - }); - - onKey("ArrowDown", (e) => { - e.preventDefault(); - window.scrollBy({ top: SCROLL_AMOUNT, behavior: "smooth" }); - }); -</script> diff --git a/web/.gitignore b/website/.gitignore diff --git a/web/.prettierignore b/website/.prettierignore diff --git a/web/.prettierrc b/website/.prettierrc diff --git a/web/Dockerfile b/website/Dockerfile diff --git a/web/README.md b/website/README.md diff --git a/web/astro.config.mjs b/website/astro.config.mjs diff --git a/web/bun.lock b/website/bun.lock diff --git a/web/env.example b/website/env.example diff --git a/web/eslint.config.js b/website/eslint.config.js diff --git a/web/package.json b/website/package.json diff --git a/web/public/amplifier/logo.svg b/website/public/amplifier/logo.svg diff --git a/web/public/favicon.png b/website/public/favicon.png Binary files differ. diff --git a/web/public/mandolin/company.jpeg b/website/public/mandolin/company.jpeg Binary files differ. diff --git a/web/public/mandolin/company_silly.jpeg b/website/public/mandolin/company_silly.jpeg Binary files differ. diff --git a/web/public/mandolin/employee_polaroids.jpeg b/website/public/mandolin/employee_polaroids.jpeg Binary files differ. diff --git a/web/public/mandolin/logo.svg b/website/public/mandolin/logo.svg diff --git a/web/public/mandolin/moment_polaroids.jpeg b/website/public/mandolin/moment_polaroids.jpeg Binary files differ. diff --git a/web/public/me.jpg b/website/public/me.jpg Binary files differ. diff --git a/web/public/robots.txt b/website/public/robots.txt diff --git a/web/public/site.webmanifest b/website/public/site.webmanifest diff --git a/web/src/blogs/building-my-portfolio.md b/website/src/blogs/building-my-portfolio.md diff --git a/web/src/blogs/index.ts b/website/src/blogs/index.ts diff --git a/web/src/config/about.ts b/website/src/config/about.ts diff --git a/web/src/config/contact.ts b/website/src/config/contact.ts diff --git a/web/src/config/experience.ts b/website/src/config/experience.ts diff --git a/web/src/config/index.ts b/website/src/config/index.ts diff --git a/web/src/config/projects.ts b/website/src/config/projects.ts diff --git a/web/src/config/tabs.ts b/website/src/config/tabs.ts diff --git a/web/src/constants/calendar.ts b/website/src/constants/calendar.ts diff --git a/web/src/constants/env.ts b/website/src/constants/env.ts diff --git a/web/src/constants/external/wakatime.ts b/website/src/constants/external/wakatime.ts diff --git a/web/src/constants/ui/boot.ts b/website/src/constants/ui/boot.ts diff --git a/web/src/constants/ui/scroll.ts b/website/src/constants/ui/scroll.ts diff --git a/web/src/constants/ui/tags.ts b/website/src/constants/ui/tags.ts diff --git a/web/src/constants/website.ts b/website/src/constants/website.ts diff --git a/web/src/external/git.ts b/website/src/external/git.ts diff --git a/web/src/external/wakatime.ts b/website/src/external/wakatime.ts diff --git a/web/src/layouts/Layout.astro b/website/src/layouts/Layout.astro diff --git a/web/src/layouts/components/Article.astro b/website/src/layouts/components/Article.astro diff --git a/web/src/layouts/components/Footer.astro b/website/src/layouts/components/Footer.astro diff --git a/website/src/layouts/components/Header.astro b/website/src/layouts/components/Header.astro @@ -0,0 +1,156 @@ +--- +import { TABS } from "../../config/tabs"; +--- + +<header id="tabs"> + <nav class="tab-nav" aria-label="Main navigation"> + { + TABS.map((tab, i) => ( + <div + class:list={["tab", { active: i === 0 }]} + data-tab={tab.id} + aria-current={i === 0 ? "page" : undefined} + > + <span class="tab-num">[{i + 1}] </span> + {tab.label} + </div> + )) + } + </nav> + <div class="tab-spacer"></div> + <div class="status" id="header-status"> + <span class="dpad"> + <span class="dpad-row"><span class="dpad-key">↑</span></span> + <span class="dpad-row" + ><span class="dpad-key">←</span><span class="dpad-key">↓</span><span + class="dpad-key">→</span + ></span + > + </span> + <span class="navigate"> navigate · Tab cycle</span> + </div> +</header> + +<style> + #tabs { + display: flex; + align-items: center; + gap: 4px; + background: var(--bg-alt); + border-bottom: 1px solid var(--border); + padding: 0 12px; + height: 36px; + position: sticky; + top: 0; + z-index: 100; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + #tabs * { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + .tab-nav { + display: flex; + align-items: center; + gap: 4px; + } + + .tab { + padding: 6px 12px; + color: var(--fg-dim); + cursor: pointer; + border-bottom: 2px solid transparent; + transition: + color 0.15s, + border-color 0.15s; + } + + .tab:hover { + color: var(--fg); + } + + .tab.active { + color: var(--accent); + border-bottom-color: var(--accent); + } + + .tab-spacer { + flex: 1; + } + + .status { + display: flex; + align-items: center; + gap: 8px; + color: var(--fg-dim); + font-size: 11px; + margin-right: 8px; + } + + .dpad { + display: inline-flex; + flex-direction: column; + align-items: center; + gap: 1px; + font-size: 8px; + line-height: 1; + } + + .dpad-row { + display: flex; + gap: 1px; + } + + .dpad-key { + width: 12px; + height: 10px; + display: flex; + align-items: center; + justify-content: center; + background: var(--border); + border-radius: 2px; + color: var(--fg); + } + + @media (max-width: 768px) { + #tabs { + flex-wrap: wrap; + height: auto; + min-height: 36px; + padding: 8px 12px; + gap: 8px; + } + + .tab-nav { + width: 100%; + justify-content: space-between; + gap: 0; + } + + .tab { + padding: 6px 8px; + font-size: 12px; + flex: 1; + text-align: center; + } + + .tab-spacer { + display: none; + } + + .status { + display: none; + } + + .tab-num { + display: none; + } + } +</style> diff --git a/web/src/layouts/components/Lightbox.astro b/website/src/layouts/components/Lightbox.astro diff --git a/website/src/layouts/tabs/About.astro b/website/src/layouts/tabs/About.astro @@ -0,0 +1,430 @@ +--- +import type { WakaTimeData } from "../../external/wakatime"; +import { about, contact } from "../../config"; +import { currentJob } from "../../config/experience"; +import { getTagClass } from "../../utils/tags"; + +interface Props { + wakaTimeData: WakaTimeData | null; +} + +const { wakaTimeData } = Astro.props; +--- + +<section id="about" class="page active"> + <div class="about-layout"> + <div class="about-left"> + <img + class="profile-image" + src="/me.jpg" + alt="Damian Myrda - Software Engineer and high school senior from Chicago" + loading="eager" + /> + <div class="contact-box"> + <div class="box-title">─ Contact ─</div> + <div class="contact-item"> + <span class="label">Email</span> + <a href={`mailto:${contact.email}`}>{contact.email}</a> + </div> + <div class="contact-item"> + <span class="label">GitLab</span> + <a href={contact.gitlab} target="_blank" rel="noopener noreferrer me" + >git.prime8.dev/moncheeta</a + > + </div> + <div class="contact-item"> + <span class="label">LinkedIn</span> + <a + href={contact.linkedin} + target="_blank" + rel="noopener noreferrer me">linkedin.com/in/damian-myrda</a + > + </div> + </div> + </div> + <div class="about-right"> + <div class="section"> + <h1> + {about.name} + <span class="dim">@{about.handle}</span> + </h1> + <p class="bio">{about.bio}</p> + </div> + <div class="section"> + <div class="box-title">─ Info ─</div> + <div class="info-columns"> + <div class="info-column"> + <div class="info-row"> + <span class="label">Location</span> + {about.location} + </div> + <div class="info-row"> + <span class="label">School</span> + {about.school} + </div> + <div class="info-row"> + <span class="label">Graduating</span> + {about.graduating} + </div> + </div> + { + currentJob && ( + <div class="info-column"> + <div class="info-row"> + <span class="label">Company</span> + <a href={`#experience/${currentJob.id}`}> + {currentJob.company} + </a> + </div> + <div class="info-row"> + <span class="label">Position</span> + {currentJob.role} + </div> + </div> + ) + } + </div> + </div> + <div class="section"> + <div class="box-title">─ Interests ─</div> + <div class="tags"> + { + about.interests.map((interest) => ( + <span class={`tag ${getTagClass(interest)}`}>{interest}</span> + )) + } + </div> + </div> + <div class="section"> + <div class="box-title">─ Tools ─</div> + <div class="tags"> + { + about.tools.map((tool) => ( + <span class={`tag ${getTagClass(tool)}`}>{tool}</span> + )) + } + </div> + </div> + <div class="section"> + <div class="box-title">─ Languages ─</div> + <div class="tags"> + { + about.languages.map((lang) => ( + <span class={`tag ${getTagClass(lang)}`}>{lang}</span> + )) + } + </div> + </div> + { + wakaTimeData?.weeklyStats && ( + <div class="section"> + <div class="box-title">─ Activity ─</div> + <div class="weekly-stats"> + <div class="weekly-total"> + {wakaTimeData.weeklyStats.totalText} + </div> + <div class="weekly-days"> + {wakaTimeData.weeklyStats.days.map((day) => ( + <div class="weekly-day-row"> + <span class="weekly-day-name">{day.dayText}</span> + <div class="weekly-day-bar-container"> + <div + class="weekly-day-bar" + style={`width: ${day.barWidthPercent}`} + /> + </div> + <span class="weekly-day-time">{day.timeText}</span> + </div> + ))} + </div> + </div> + </div> + ) + } + </div> + </div> +</section> + +<style> + .about-layout { + display: flex; + width: 100%; + height: 100%; + } + + .about-left { + width: 320px; + flex-shrink: 0; + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + padding: 16px; + gap: 16px; + overflow-y: auto; + } + + .about-right { + flex: 1; + padding: 24px 32px; + overflow-y: auto; + display: flex; + flex-direction: column; + justify-content: space-evenly; + } + + .profile-image { + width: 100%; + max-width: 280px; + height: auto; + border-radius: 4px; + border: 1px solid var(--border); + } + + .contact-box { + border: 1px solid var(--border); + padding: 12px; + max-width: 280px; + } + + .box-title { + color: var(--accent); + font-size: 12px; + margin-bottom: 8px; + text-align: center; + } + + .contact-item { + display: flex; + margin-bottom: 4px; + font-size: 12px; + min-width: 0; + } + + .contact-item .label { + color: var(--fg-dim); + width: 70px; + flex-shrink: 0; + } + + .contact-item a { + color: var(--light-blue); + text-decoration: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + direction: rtl; + text-align: left; + } + + .contact-item a:hover { + opacity: 0.8; + } + + .section { + flex-shrink: 0; + } + + .section h1 { + font-size: 18px; + font-weight: 400; + margin-bottom: 4px; + } + + .section h1 .dim { + color: var(--fg-dim); + font-size: 14px; + } + + .section .bio { + color: var(--fg-dim); + font-style: italic; + } + + .info-columns { + display: flex; + gap: 48px; + } + + .info-column { + flex: 1; + } + + .info-row { + display: flex; + font-size: 13px; + margin-bottom: 4px; + } + + .info-row .label { + color: var(--fg-dim); + width: 100px; + flex-shrink: 0; + } + + .info-row a { + color: var(--light-blue); + text-decoration: none; + } + + .info-row a:hover { + opacity: 0.8; + } + + .tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + + .weekly-stats { + background: var(--bg-alt); + border: 1px solid var(--border); + padding: 12px; + } + + .weekly-total { + font-size: 16px; + font-weight: 500; + color: var(--accent); + margin-bottom: 12px; + } + + .weekly-days { + display: flex; + flex-direction: column; + gap: 6px; + } + + .weekly-day-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + } + + .weekly-day-name { + width: 32px; + flex-shrink: 0; + color: var(--fg-dim); + } + + .weekly-day-bar-container { + flex: 1; + height: 8px; + background: var(--bg); + border-radius: 2px; + overflow: hidden; + } + + .weekly-day-bar { + height: 100%; + background: var(--accent); + border-radius: 2px; + } + + .weekly-day-time { + min-width: 50px; + flex-shrink: 0; + text-align: right; + color: var(--fg-dim); + white-space: nowrap; + } + + @media (max-width: 768px) { + .about-layout { + flex-direction: column; + } + + .about-left { + display: flex; + flex-direction: row; + width: 100%; + border-right: none; + border-bottom: 1px solid var(--border); + padding: 16px; + gap: 16px; + } + + .profile-image { + width: 120px; + min-width: 60px; + flex-shrink: 1; + height: 120px; + align-self: flex-start; + object-fit: cover; + object-position: center; + } + + .contact-box { + flex: 1; + max-width: none; + min-width: 0; + } + + .contact-item .label { + width: 70px; + } + + .contact-item a { + overflow: hidden; + text-overflow: ellipsis; + } + + .about-right { + padding: 16px; + display: block; + } + + .section { + margin-bottom: 16px; + } + + .section h1 { + font-size: 16px; + } + + .section h1 .dim { + font-size: 12px; + } + + .info-columns { + flex-direction: column; + gap: 12px; + } + + .tags { + gap: 4px; + } + + .tag { + font-size: 11px; + padding: 2px 6px; + } + + .weekly-stats { + padding: 10px; + } + + .weekly-total { + font-size: 14px; + } + + .weekly-day-row { + font-size: 11px; + } + + .weekly-day-name { + width: 28px; + } + + .weekly-day-time { + min-width: 45px; + } + } + + @media (max-width: 480px) { + .contact-item .label { + display: none; + } + } +</style> diff --git a/website/src/layouts/tabs/Blog.astro b/website/src/layouts/tabs/Blog.astro @@ -0,0 +1,230 @@ +--- +import BlogArticle from "../components/Article.astro"; + +import { blogs } from "../../blogs"; +--- + +<section id="blog" class="page"> + { + blogs.length === 0 ? ( + <div class="blog-empty"> + <p class="color-dim">No blog posts yet.</p> + <p class="color-dim">Check back soon!</p> + </div> + ) : ( + <> + <div id="blog-list-view" class="blog-list-view"> + <div class="blog-list-container"> + {blogs.map((blog, i) => ( + <div class="blog-list-item" data-index={i} data-id={blog.id}> + <span class="blog-list-date">{blog.date}</span> + <span class="blog-list-title">{blog.title}</span> + <span class="blog-list-arrow">→</span> + </div> + ))} + </div> + </div> + + <div id="blog-post-view" class="blog-post-view hidden"> + <div class="blog-post-container"> + <button class="blog-back-btn">← Back to posts</button> + {blogs.map((blog, i) => ( + <div + class="blog-post hidden" + data-index={i} + data-id={blog.id} + data-blog-id={blog.id} + > + <BlogArticle blog={blog} /> + </div> + ))} + </div> + </div> + </> + ) + } +</section> + +<style> + .blog-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + gap: 8px; + } + + .blog-list-view { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + } + + .blog-list-view.hidden { + display: none; + } + + .blog-list-container { + max-width: 600px; + width: 100%; + padding: 32px; + } + + .blog-list-item { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + border: 1px solid var(--border); + margin-bottom: 12px; + cursor: pointer; + transition: + background 0.15s ease, + border-color 0.15s ease; + text-decoration: none; + color: inherit; + } + + .blog-list-item:hover { + background: var(--selection); + border-color: var(--accent); + } + + .blog-list-item.selected { + background: var(--bg-alt); + border-color: var(--accent); + } + + .blog-list-item.selected:hover { + background: var(--selection); + } + + .blog-list-item:last-child { + margin-bottom: 0; + } + + .blog-list-date { + color: var(--fg-dim); + font-size: 12px; + flex-shrink: 0; + width: 90px; + } + + .blog-list-title { + flex: 1; + color: var(--fg); + font-weight: 500; + } + + .blog-list-arrow { + color: var(--fg-dim); + font-size: 14px; + transition: + transform 0.15s, + color 0.15s; + } + + .blog-list-item:hover .blog-list-arrow { + color: var(--accent); + transform: translateX(4px); + } + + .blog-post-view { + display: flex; + justify-content: center; + height: 100%; + width: 100%; + overflow-y: auto; + padding: 32px; + } + + .blog-post-view.hidden { + display: none; + } + + .blog-post-container { + max-width: 700px; + width: 100%; + } + + .blog-post.hidden { + display: none; + } + + .blog-back-btn { + display: inline-block; + background: transparent; + border: 1px solid var(--border); + color: var(--fg-dim); + padding: 8px 16px; + cursor: pointer; + font-family: inherit; + font-size: 12px; + margin-bottom: 24px; + transition: all 0.15s; + text-decoration: none; + } + + .blog-back-btn:hover { + background: var(--bg-alt); + color: var(--accent); + border-color: var(--accent); + } + + @media (max-width: 768px) { + .blog-list-container { + padding: 16px; + } + + .blog-list-item { + gap: 12px; + padding: 12px 16px; + margin-bottom: 10px; + } + + .blog-list-date { + font-size: 11px; + width: 80px; + } + + .blog-list-title { + font-size: 14px; + } + + .blog-post-view { + padding: 16px; + } + + .blog-back-btn { + padding: 6px 12px; + font-size: 11px; + margin-bottom: 16px; + } + } + + @media (max-width: 480px) { + .blog-list-item { + flex-wrap: wrap; + gap: 4px; + } + + .blog-list-date { + width: auto; + order: 2; + } + + .blog-list-title { + width: 100%; + order: 1; + } + + .blog-list-arrow { + order: 3; + margin-left: auto; + } + } +</style> diff --git a/website/src/layouts/tabs/Experience.astro b/website/src/layouts/tabs/Experience.astro @@ -0,0 +1,486 @@ +--- +import { experience } from "../../config"; +import { calculateMonths } from "../../utils/period"; +import { MONTH_NAMES } from "../../constants/calendar"; +import { getTagClass } from "../../utils/tags"; + +const dates = experience.map((exp) => { + const [year, month] = exp.startDate.split("-").map(Number); + return new Date(year, month - 1, 1); +}); +const endDates = experience.map((exp) => + exp.endDate + ? new Date( + parseInt(exp.endDate.split("-")[0]), + parseInt(exp.endDate.split("-")[1]) - 1, + 28, + ) + : new Date(), +); +const earliestDate = new Date(Math.min(...dates.map((d) => d.getTime()))); +const latestDate = new Date(Math.max(...endDates.map((d) => d.getTime()))); + +const timelineStart = new Date( + earliestDate.getFullYear(), + earliestDate.getMonth(), + 1, +); +const timelineEnd = latestDate; + +const monthTicks: string[] = []; +let current = new Date(timelineStart); +while (current <= timelineEnd) { + monthTicks.push(MONTH_NAMES[current.getMonth()][0]); + current.setMonth(current.getMonth() + 1); +} +--- + +<section id="experience" class="page"> + <article class="experience-layout"> + <div class="experience-detail"> + { + experience.map((exp, index) => { + const months = calculateMonths(exp.startDate, exp.endDate); + const [startYear, startMonth] = exp.startDate.split("-").map(Number); + const startStr = `${MONTH_NAMES[startMonth - 1]} ${startYear}`; + const endStr = exp.endDate + ? `${MONTH_NAMES[parseInt(exp.endDate.split("-")[1]) - 1]} ${exp.endDate.split("-")[0]}` + : "Present"; + + const isLast = index === experience.length - 1; + + return ( + <div class="exp-row" data-index={index} data-id={exp.id}> + <div class="exp-card" data-index={index} data-id={exp.id}> + <div class="exp-card-header"> + <div class="exp-card-title"> + {exp.logo ? ( + <div class="exp-title-with-logo"> + <img + src={exp.logo} + alt={`${exp.company} logo`} + class={`exp-logo ${exp.logoFilter || ""}`} + /> + <span class="exp-role">{exp.role}</span> + </div> + ) : ( + <div class="exp-title-text"> + <h2>{exp.company}</h2> + <span class="exp-role">{exp.role}</span> + </div> + )} + </div> + <div class="exp-card-meta"> + <span class="exp-dates"> + {startStr} → {endStr} + </span> + <span class="exp-duration">{months} months</span> + {exp.url && ( + <a + class="external-link" + href={exp.url} + target="_blank" + rel="noopener noreferrer" + > + {new URL(exp.url).hostname} → + </a> + )} + </div> + </div> + <div class="exp-card-tech"> + {exp.tech.map((tech) => ( + <span class={`tag ${getTagClass(tech)}`}>{tech}</span> + ))} + </div> + <div class="exp-card-body"> + {exp.description.map((description) => ( + <p>{description}</p> + ))} + </div> + {exp.images && exp.images.length > 0 && ( + <div class="exp-card-images"> + <img + src={exp.images[0]} + alt={`${exp.company} workplace, ${exp.role} experience at ${exp.company}`} + class="exp-image-hero" + loading="lazy" + data-idx="0" + /> + {exp.images.length > 1 && ( + <div class="exp-image-grid"> + {exp.images.slice(1).map((image, index) => ( + <img + src={image} + alt={`${exp.company} team and office environment, image ${index + 2} of ${exp.images!.length}`} + class="exp-image-thumb" + loading="lazy" + data-idx={index + 1} + /> + ))} + </div> + )} + </div> + )} + </div> + <div + class={`exp-timeline ${index === 0 ? "active" : ""}`} + data-index={index} + > + <span class="timeline-dot" /> + <span class="timeline-year"> + [{exp.endDate ? exp.endDate.split("-")[0] : "Now"}] + </span> + {!isLast && <div class="timeline-line" />} + </div> + </div> + ); + }) + } + </div> + </article> +</section> + +<style> + .experience-layout { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; + padding: 24px 32px; + gap: 24px; + } + + .experience-detail { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 16px; + padding: 4px; + } + + .exp-card { + background: var(--bg-alt); + border: 2px solid var(--border); + border-radius: 4px; + padding: 24px; + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0; + scroll-snap-align: start; + transition: border-color 0.2s ease; + } + + .exp-card.selected { + border-color: var(--accent); + } + + .exp-row { + display: flex; + } + + .exp-timeline { + width: 64px; + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; + padding-top: 24px; + position: relative; + } + + .timeline-dot { + width: 12px; + height: 12px; + border: 2px solid var(--fg-dim); + border-radius: 50%; + background: transparent; + transition: all 0.2s ease; + font-size: 0; + } + + .exp-timeline.active .timeline-dot { + border-color: var(--accent); + background: var(--accent); + transform: scale(1.2); + } + + .timeline-year { + font-size: 10px; + color: var(--fg-dim); + font-family: monospace; + margin-top: 4px; + transition: color 0.2s ease; + } + + .exp-timeline.active .timeline-year { + color: var(--accent); + } + + .timeline-line { + position: absolute; + top: 56px; + bottom: -16px; + width: 2px; + left: 50%; + transform: translateX(-50%); + background: repeating-linear-gradient( + to bottom, + var(--border) 0px, + var(--border) 4px, + transparent 4px, + transparent 8px + ); + } + + .exp-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; + gap: 24px; + } + + .exp-card-title { + display: flex; + align-items: center; + gap: 16px; + } + + .exp-logo { + height: 32px; + width: auto; + flex-shrink: 0; + } + + .exp-logo.logo-invert { + filter: brightness(0) invert(1); + } + + .exp-title-with-logo { + display: flex; + flex-direction: column; + gap: 4px; + } + + .exp-title-with-logo .exp-role { + color: #999; + font-size: 14px; + } + + .exp-title-with-logo .exp-duration { + color: #aaa; + font-size: 12px; + } + + .exp-title-text h2 { + font-size: 22px; + font-weight: 500; + color: var(--accent); + margin: 0 0 4px 0; + } + + .exp-title-text .exp-role { + font-size: 14px; + color: #999; + } + + .exp-card-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + font-size: 13px; + } + + .exp-dates { + color: var(--fg); + } + + .exp-duration { + color: #aaa; + font-size: 12px; + } + + .exp-card-tech { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 20px; + } + + .exp-card-body { + color: var(--fg); + line-height: 1.7; + padding-bottom: 0; + margin-bottom: 0; + } + + .exp-card-body p { + margin-bottom: 12px; + } + + .exp-card-body p:last-child { + margin-bottom: 0; + } + + .exp-card-images { + display: flex; + gap: 12px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--border); + max-height: 180px; + overflow: hidden; + } + + .exp-image-hero { + max-width: 280px; + height: 100%; + border-radius: 4px; + border: 1px solid var(--border); + object-fit: cover; + cursor: pointer; + transition: + transform 0.15s, + box-shadow 0.15s; + } + + .exp-image-hero:hover { + opacity: 0.8; + border-color: var(--accent); + } + + .exp-image-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; + max-width: 400px; + } + + .exp-image-thumb { + width: calc(50% - 4px); + height: 86px; + border-radius: 4px; + border: 1px solid var(--border); + object-fit: cover; + cursor: pointer; + transition: + opacity 0.15s, + border-color 0.15s; + } + + .exp-image-thumb:hover { + opacity: 0.8; + border-color: var(--accent); + } + + .external-link { + color: var(--light-blue); + text-decoration: none; + font-size: 12px; + } + + .external-link:hover { + opacity: 0.8; + } + + @media (max-width: 768px) { + .experience-layout { + padding: 16px; + gap: 12px; + } + + .exp-timeline { + display: none; + } + + .exp-card { + padding: 16px; + } + + .exp-card.selected { + border-color: var(--border); + } + + .exp-card-header { + flex-direction: column; + gap: 12px; + } + + .exp-card-meta { + align-items: flex-start; + } + + .exp-logo { + height: 28px; + } + + .exp-title-text h2 { + font-size: 18px; + } + + .exp-card-tech { + gap: 4px; + } + + .exp-card-body { + font-size: 13px; + } + + .exp-card-images { + flex-direction: column; + gap: 12px; + max-height: none; + overflow: visible; + } + + .exp-image-hero { + max-width: 100%; + height: auto; + max-height: 200px; + } + + .exp-image-grid { + width: 100%; + max-width: none; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; + } + + .exp-image-thumb { + width: calc(33.33% - 6px); + height: 80px; + } + } + + @media (max-width: 480px) { + .exp-image-thumb { + width: calc(50% - 4px); + } + } +</style> + +<script> + document + .querySelectorAll(".exp-image-hero, .exp-image-thumb") + .forEach((img) => { + img.addEventListener("click", () => { + const container = (img as HTMLElement).closest(".exp-card-images"); + const allImages = container + ? Array.from(container.querySelectorAll("img")).map( + (i) => (i as HTMLImageElement).src, + ) + : []; + const startIndex = allImages.indexOf((img as HTMLImageElement).src); + ( + window as Window & { + openLightbox?: (images: string[], startIndex?: number) => void; + } + ).openLightbox?.(allImages, startIndex); + }); + }); +</script> diff --git a/website/src/layouts/tabs/Projects.astro b/website/src/layouts/tabs/Projects.astro @@ -0,0 +1,411 @@ +--- +import { projects } from "../../config/projects"; +import { fetchGitMetadataForProject } from "../../external/git"; +import { getTechMeta, getTagClass } from "../../utils/tags"; +import { parseMarkdown, removeFirstH1 } from "../../utils/markdown"; + +const metadata = Object.fromEntries( + ( + await Promise.all( + projects.map(async (project) => [ + project.id, + await fetchGitMetadataForProject(project), + ]), + ) + ).filter(([, meta]) => meta !== null), +); +--- + +<section id="projects" class="page"> + <aside class="list-view" aria-label="Project list"> + { + projects.map((prg, i) => ( + <div + class={`list-item ${i === 0 ? "selected" : ""}`} + data-index={i} + data-id={prg.id} + > + <div class="list-header"> + <span class="list-title">{prg.name}</span> + <span class="list-meta">{getTechMeta(prg.tech)}</span> + </div> + <div class="list-subtitle">{prg.subtitle}</div> + </div> + )) + } + </aside> + <article class="detail-view"> + { + projects.map((prg, index) => ( + <div + class={`detail-content ${index !== 0 ? "hidden" : ""}`} + data-for={index} + data-id={prg.id} + > + <div class="detail-header"> + <h2>{prg.name}</h2> + <a + class="external-link" + href={prg.url} + target="_blank" + rel="noopener noreferrer" + > + {prg.url.includes("github.com") ? "GitHub" : "GitLab"} → + </a> + </div> + <div class="tech-stack"> + {prg.tech.map((tech) => ( + <span class={`tag ${getTagClass(tech)}`}>{tech}</span> + ))} + </div> + <div class="readme-content" data-readme={prg.id}> + {metadata?.[prg.id]?.readme ? ( + <Fragment + set:html={parseMarkdown(removeFirstH1(metadata[prg.id].readme))} + /> + ) : ( + <p class="color-dim">README not available</p> + )} + </div> + </div> + )) + } + </article> +</section> + +<style> + aside.list-view { + width: 300px; + flex-shrink: 0; + border-right: 1px solid var(--border); + overflow-y: auto; + } + + article.detail-view { + flex: 1; + padding: 24px 32px; + overflow: hidden; + display: flex; + flex-direction: column; + } + + .list-item { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + cursor: pointer; + transition: background 0.1s; + } + + .list-item:hover { + background: var(--bg-alt); + } + + .list-item.selected { + background: var(--selection); + border-left: 3px solid var(--accent); + } + + .list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2px; + } + + .list-title { + font-weight: 600; + color: var(--fg); + } + + .list-meta { + font-size: 11px; + color: var(--fg-dim); + } + + .list-subtitle { + font-size: 12px; + color: var(--fg-dim); + } + + .detail-content { + display: flex; + flex-direction: column; + height: 100%; + } + + .detail-content.hidden { + display: none; + } + + .detail-content h2 { + font-size: 20px; + font-weight: 400; + color: var(--accent); + margin-bottom: 4px; + } + + .detail-content .tech-stack { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 16px; + } + + .detail-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + } + + .detail-header h2 { + margin-bottom: 0; + } + + .external-link { + color: var(--light-blue); + text-decoration: none; + font-size: 12px; + } + + .external-link:hover { + opacity: 0.8; + } + + .readme-content { + flex: 1; + min-height: 0; + overflow-y: auto; + font-size: 13px; + line-height: 1.6; + color: var(--fg); + margin-top: 16px; + padding-top: 16px; + padding-right: 12px; + border-top: 1px solid var(--border); + } + + .readme-content :global(p) { + margin-bottom: 12px; + } + + .readme-content :global(p:last-child) { + margin-bottom: 0; + } + + .readme-content :global(h2), + .readme-content :global(.readme-h1) { + font-size: 16px; + color: var(--accent); + margin: 20px 0 8px; + font-weight: 600; + } + + .readme-content :global(h2:first-child), + .readme-content :global(.readme-h1:first-child) { + margin-top: 0; + } + + .readme-content :global(h3), + .readme-content :global(.readme-h2) { + font-size: 14px; + color: var(--accent); + margin: 16px 0 8px; + font-weight: 500; + } + + .readme-content :global(h4), + .readme-content :global(.readme-h3) { + font-size: 13px; + color: var(--fg); + margin: 12px 0 6px; + font-weight: 500; + } + + .readme-content :global(h5), + .readme-content :global(h6) { + font-size: 12px; + color: var(--fg-dim); + margin: 10px 0 6px; + font-weight: 500; + } + + .readme-content :global(pre) { + display: block; + background: var(--bg-alt); + border: 1px solid var(--border); + padding: 12px; + margin: 12px 0; + overflow-x: auto; + font-size: 12px; + line-height: 1.4; + } + + .readme-content :global(pre code) { + background: none; + padding: 0; + font-size: inherit; + } + + .readme-content :global(code) { + background: var(--bg-alt); + padding: 2px 6px; + font-size: 12px; + border-radius: 2px; + } + + .readme-content :global(a) { + color: var(--light-blue); + text-decoration: none; + } + + .readme-content :global(a:hover) { + opacity: 0.8; + } + + .readme-content :global(ul), + .readme-content :global(ol) { + margin: 8px 0; + padding-left: 20px; + } + + .readme-content :global(li) { + margin-bottom: 4px; + } + + .readme-content :global(li > ul), + .readme-content :global(li > ol) { + margin: 4px 0; + } + + .readme-content :global(table) { + width: 100%; + border-collapse: collapse; + margin: 12px 0; + font-size: 12px; + } + + .readme-content :global(th), + .readme-content :global(td) { + border: 1px solid var(--border); + padding: 8px 12px; + text-align: left; + } + + .readme-content :global(th) { + background: var(--bg-alt); + color: var(--accent); + font-weight: 500; + } + + .readme-content :global(tr:nth-child(even)) { + background: rgba(255, 255, 255, 0.02); + } + + .readme-content :global(blockquote) { + border-left: 3px solid var(--accent); + margin: 12px 0; + padding: 8px 16px; + color: var(--fg-dim); + background: var(--bg-alt); + } + + .readme-content :global(blockquote p) { + margin-bottom: 0; + } + + .readme-content :global(hr) { + border: none; + border-top: 1px solid var(--border); + margin: 20px 0; + } + + .readme-content :global(img) { + max-width: 100%; + height: auto; + margin: 12px 0; + } + + .readme-content :global(strong) { + color: var(--fg); + font-weight: 600; + } + + .readme-content :global(em) { + font-style: italic; + color: var(--fg-dim); + } + + .readme-content :global(del) { + text-decoration: line-through; + color: var(--fg-dim); + } + + @media (max-width: 768px) { + #projects { + flex-direction: column; + } + + aside.list-view { + width: 100%; + border-right: none; + border-bottom: 1px solid var(--border); + max-height: 200px; + overflow-y: auto; + } + + .list-item { + padding: 10px 14px; + } + + .list-title { + font-size: 13px; + } + + .list-subtitle { + font-size: 11px; + } + + article.detail-view { + padding: 16px; + overflow-y: auto; + } + + .detail-content h2 { + font-size: 18px; + } + + .detail-header { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .readme-content { + font-size: 12px; + } + + .readme-content :global(pre) { + font-size: 11px; + padding: 10px; + } + + .readme-content :global(code) { + font-size: 11px; + } + } + + @media (max-width: 480px) { + aside.list-view { + max-height: 150px; + } + + .list-item { + padding: 8px 12px; + } + + .list-meta { + display: none; + } + } +</style> diff --git a/web/src/pages/404.astro b/website/src/pages/404.astro diff --git a/web/src/pages/blog/[slug].astro b/website/src/pages/blog/[slug].astro diff --git a/website/src/pages/experience/[id].astro b/website/src/pages/experience/[id].astro @@ -0,0 +1,376 @@ +--- +import Layout from "../../layouts/Layout.astro"; +import Lightbox from "../../layouts/components/Lightbox.astro"; + +import { SITE_URL } from "../../constants/website"; +import { experience as experiences } from "../../config"; +import { calculateMonths } from "../../utils/period"; +import { MONTH_NAMES } from "../../constants/calendar"; +import { getTagClass } from "../../utils/tags"; +import schemas from "../../schemas/index"; + +const exp = experiences.find((e) => e.id === Astro.params.id); +if (!exp) { + return new Response(null, { + status: 404, + statusText: "Not Found", + }); +} + +const months = calculateMonths(exp.startDate, exp.endDate); +const [startYear, startMonth] = exp.startDate.split("-").map(Number); +const startStr = `${MONTH_NAMES[startMonth - 1]} ${startYear}`; +const endStr = exp.endDate + ? `${MONTH_NAMES[parseInt(exp.endDate.split("-")[1]) - 1]} ${exp.endDate.split("-")[0]}` + : "Present"; + +const keywords = [...exp.tech, exp.company, exp.role].join(", "); +const description = `${exp.role} at ${exp.company}. ${exp.description[0]}`; + +const { getSchemaForExperience, getBreadcrumbSchema } = schemas.experience; +--- + +<Layout + title={`${exp.company} - ${exp.role} | Damian Myrda`} + description={description} + type="website" + url={`${SITE_URL}/experience/${exp.id}`} + keywords={keywords} + image={exp.logo} +> + <main id="content"> + <div class="experience-page"> + <a href="/#experience" class="back-btn">← See more experience</a> + + <article class="experience-article"> + <header class="article-header"> + <div class="header-top"> + <div class="title-section"> + <div class="exp-title-with-logo"> + <img + src={exp.logo} + alt={exp.company} + class={`exp-logo ${exp.logoFilter || ""}`} + /> + <span class="exp-role">{exp.role}</span> + </div> + </div> + <div class="exp-card-meta"> + <span class="exp-dates">{startStr} → {endStr}</span> + <span class="exp-duration">{months} months</span> + { + exp.url && ( + <a + class="external-link" + href={exp.url} + target="_blank" + rel="noopener noreferrer" + > + {new URL(exp.url).hostname} → + </a> + ) + } + </div> + </div> + </header> + + <section class="tech-tags"> + { + exp.tech.map((tech) => ( + <span class={`tag ${getTagClass(tech)}`}>{tech}</span> + )) + } + </section> + + <section class="article-content"> + {exp.description.map((paragraph) => <p>{paragraph}</p>)} + </section> + + { + exp.images && exp.images.length > 0 && ( + <section class="images-section"> + <h2>Photos</h2> + <div class="image-grid"> + {exp.images.map((img, i) => ( + <img + src={img} + alt={`${exp.company} photo ${i + 1}`} + class="experience-image" + loading="lazy" + /> + ))} + </div> + </section> + ) + } + </article> + </div> + <script + type="application/ld+json" + set:html={JSON.stringify(getSchemaForExperience(exp))} + /> + <script + type="application/ld+json" + set:html={JSON.stringify(getBreadcrumbSchema(exp))} + /> + </main> + <Lightbox /> +</Layout> + +<script> + document.getElementById("app")?.classList.add("ready"); + + // Lightbox click handlers for images + document.querySelectorAll(".experience-image").forEach((img) => { + img.addEventListener("click", () => { + const container = (img as HTMLElement).closest(".image-grid"); + const allImages = container + ? Array.from(container.querySelectorAll("img")).map( + (i) => (i as HTMLImageElement).src, + ) + : []; + const startIndex = allImages.indexOf((img as HTMLImageElement).src); + ( + window as Window & { + openLightbox?: (images: string[], startIndex?: number) => void; + } + ).openLightbox?.(allImages, startIndex); + }); + }); +</script> + +<style> + :global(body) { + overflow: auto; + } + + :global(#app) { + height: auto; + min-height: 100vh; + } + + :global(#content) { + overflow: visible; + } + + .experience-page { + max-width: 720px; + margin: 0 auto; + padding: 40px 24px 40px; + min-height: 100vh; + } + + .back-btn { + display: inline-block; + background: transparent; + border: 1px solid var(--border); + color: var(--fg-dim); + padding: 8px 16px; + cursor: pointer; + font-family: inherit; + font-size: 12px; + margin-bottom: 24px; + transition: all 0.15s; + text-decoration: none; + } + + .back-btn:hover { + background: var(--bg-alt); + color: var(--accent); + border-color: var(--accent); + } + + .experience-article { + animation: fadeIn 0.2s ease; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + .article-header { + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border); + } + + .header-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; + } + + .title-section { + display: flex; + align-items: center; + gap: 16px; + } + + .exp-logo { + height: 32px; + width: auto; + flex-shrink: 0; + } + + .exp-logo.logo-invert { + filter: brightness(0) invert(1); + } + + .exp-title-with-logo { + display: flex; + flex-direction: column; + gap: 4px; + } + + .exp-title-with-logo .exp-role { + color: #999; + font-size: 14px; + } + + .exp-card-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + font-size: 13px; + } + + .exp-dates { + color: var(--fg); + } + + .exp-duration { + color: #aaa; + font-size: 12px; + } + + .external-link { + color: var(--light-blue); + text-decoration: none; + font-size: 12px; + } + + .external-link:hover { + opacity: 0.8; + } + + .tech-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 24px; + } + + .article-content { + font-size: 14px; + line-height: 1.7; + color: var(--fg); + margin-bottom: 32px; + } + + .article-content p { + margin-bottom: 16px; + } + + .article-content p:last-child { + margin-bottom: 0; + } + + .images-section h2 { + font-size: 16px; + color: var(--accent); + margin-bottom: 16px; + font-weight: 500; + } + + .image-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + } + + .experience-image { + width: 100%; + height: 180px; + object-fit: cover; + border-radius: 4px; + border: 1px solid var(--border); + cursor: pointer; + transition: + opacity 0.15s, + border-color 0.15s; + } + + .experience-image:hover { + opacity: 0.9; + border-color: var(--accent); + } + + @media (max-width: 768px) { + .experience-page { + padding: 24px 16px 0; + } + + .back-btn { + padding: 6px 12px; + font-size: 11px; + margin-bottom: 16px; + } + + .header-top { + flex-direction: column; + gap: 12px; + } + + .title-section { + gap: 12px; + } + + .company-logo { + height: 36px; + } + + h1 { + font-size: 22px; + } + + .role { + font-size: 14px; + } + + .article-content { + font-size: 13px; + } + + .image-grid { + grid-template-columns: 1fr; + } + + .experience-image { + height: 200px; + } + } +</style> + +<script> + import { initKeyboard, onKey } from "../../ui/keyboard"; + import { SCROLL_AMOUNT } from "../../constants/ui/scroll"; + + initKeyboard(); + + onKey("ArrowUp", (e) => { + e.preventDefault(); + window.scrollBy({ top: -SCROLL_AMOUNT, behavior: "smooth" }); + }); + + onKey("ArrowDown", (e) => { + e.preventDefault(); + window.scrollBy({ top: SCROLL_AMOUNT, behavior: "smooth" }); + }); +</script> diff --git a/web/src/pages/index.astro b/website/src/pages/index.astro diff --git a/web/src/pages/projects/[id].astro b/website/src/pages/projects/[id].astro diff --git a/web/src/pages/sitemap.xml.ts b/website/src/pages/sitemap.xml.ts diff --git a/web/src/schemas/blog.ts b/website/src/schemas/blog.ts diff --git a/web/src/schemas/experience.ts b/website/src/schemas/experience.ts diff --git a/web/src/schemas/index.ts b/website/src/schemas/index.ts diff --git a/web/src/schemas/person.ts b/website/src/schemas/person.ts diff --git a/web/src/schemas/project.ts b/website/src/schemas/project.ts diff --git a/web/src/schemas/website.ts b/website/src/schemas/website.ts diff --git a/web/src/tabs/about.ts b/website/src/tabs/about.ts diff --git a/web/src/tabs/blog.ts b/website/src/tabs/blog.ts diff --git a/web/src/tabs/experience.ts b/website/src/tabs/experience.ts diff --git a/web/src/tabs/projects.ts b/website/src/tabs/projects.ts diff --git a/web/src/ui/boot.ts b/website/src/ui/boot.ts diff --git a/web/src/ui/dom.ts b/website/src/ui/dom.ts diff --git a/web/src/ui/index.ts b/website/src/ui/index.ts diff --git a/web/src/ui/keyboard.ts b/website/src/ui/keyboard.ts diff --git a/web/src/ui/router.ts b/website/src/ui/router.ts diff --git a/web/src/ui/status.ts b/website/src/ui/status.ts diff --git a/web/src/ui/tabs.ts b/website/src/ui/tabs.ts diff --git a/web/src/utils/duration.ts b/website/src/utils/duration.ts diff --git a/web/src/utils/markdown.ts b/website/src/utils/markdown.ts diff --git a/web/src/utils/period.ts b/website/src/utils/period.ts diff --git a/web/src/utils/sleep.ts b/website/src/utils/sleep.ts diff --git a/web/src/utils/tags.ts b/website/src/utils/tags.ts diff --git a/web/tsconfig.json b/website/tsconfig.json