diff options
| -rw-r--r-- | public/kheardle.png | bin | 0 -> 17100 bytes | |||
| -rw-r--r-- | public/typemixx.png | bin | 0 -> 226438 bytes | |||
| -rw-r--r-- | src/components/Card.astro | 469 | ||||
| -rw-r--r-- | src/pages/projects.astro | 43 |
4 files changed, 237 insertions, 275 deletions
diff --git a/public/kheardle.png b/public/kheardle.png Binary files differnew file mode 100644 index 0000000..2dffae8 --- /dev/null +++ b/public/kheardle.png diff --git a/public/typemixx.png b/public/typemixx.png Binary files differnew file mode 100644 index 0000000..85ab581 --- /dev/null +++ b/public/typemixx.png diff --git a/src/components/Card.astro b/src/components/Card.astro index 6ffef91..326bd1d 100644 --- a/src/components/Card.astro +++ b/src/components/Card.astro @@ -3,6 +3,7 @@ interface Props { title: string; body: string; href: string; + project?: string; year?: string; language: string; languageColor: string; @@ -14,6 +15,7 @@ interface Props { const { href, + project, title, body, language, @@ -27,55 +29,90 @@ const { --- <li class="link-card"> - <a href={href}> - <div class="card-layout"> - <div class="content-container"> - <div class="header-section"> - <h2>{title} {year ? `(${year})` : ""}</h2> - <div class="meta-info"> - <div class="language-container"> - <span class="dot" style={{ backgroundColor: languageColor }}></span> - <span class="language">{language}</span> - </div> - { - tags && tags.length > 0 && ( - <div class="tags-container"> - {tags.map((tag) => ( - <span class="tag">{tag}</span> - ))} - </div> - ) - } + <div class="card-layout"> + + <div class="content-container"> + + <div class="header-section"> + <h2>{title} {year ? `(${year})` : ""}</h2> + + <div class="meta-info"> + <div class="language-container"> + <span class="dot" style={{ backgroundColor: languageColor }}></span> + <span class="language">{language}</span> </div> + + { + tags && tags.length > 0 && ( + <div class="tags-container"> + {tags.map((tag) => ( + <span class="tag">{tag}</span> + ))} + </div> + ) + } </div> - <div class="description"> - <div class="bullet-list" set:html={body.split('\n').map(line => - line.trim().startsWith('•') ? - `<div class="bullet-item"><span class="bullet">•</span>${line.substring(1)}</div>` : - line - ).join('\n')} /> - {contribution && <p class="contribution">{contribution}</p>} - </div> </div> - {image ? ( - <div class="image-wrapper" onclick="event.stopPropagation();"> - <div class="image-container"> - <img src={image} alt={imageAlt || title} /> - <div class="image-overlay"> - <span class="zoom-icon">🔍</span> - </div> + + <div class="description"> + <div + class="bullet-list" + set:html={body + .split("\n") + .map(line => + line.trim().startsWith("•") + ? `<div class="bullet-item"><span class="bullet">•</span>${line.substring(1)}</div>` + : line + ) + .join("\n")} + /> + + {contribution && <p class="contribution">{contribution}</p>} + </div> + + <div class="actions"> + <a + class="btn" + href={href} + target="_blank" + rel="noopener noreferrer" + > + Source Code + </a> + + {project && ( + <a + class="btn primary" + href={project} + target="_blank" + rel="noopener noreferrer" + > + Live Demo + </a> + )} + </div> + + </div> + + {image ? ( + <div class="image-wrapper" onclick="event.stopPropagation();"> + <div class="image-container"> + <img src={image} alt={imageAlt || title} /> + <div class="image-overlay"> + <span class="zoom-icon">🔍</span> </div> </div> - ) : ( - <div class="no-image-placeholder"> - <div class="placeholder-content"> - <span class="placeholder-icon">✨</span> - <span class="placeholder-text">View Project</span> - </div> + </div> + ) : ( + <div class="no-image-placeholder"> + <div class="placeholder-content"> + <span class="placeholder-icon">✨</span> + <span class="placeholder-text">View Project</span> </div> - )} - </div> - </a> + </div> + )} + + </div> </li> <style> @@ -83,126 +120,22 @@ const { list-style: none; display: flex; background-color: #23262d; - background-image: none; - background-size: 400%; border-radius: 12px; - background-position: 100%; transition: all 0.3s ease; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1), 0 4px 10px rgba(0, 0, 0, 0.1); - position: relative; + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.1), + 0 4px 10px rgba(0, 0, 0, 0.1); overflow: hidden; min-height: 200px; height: 100%; } - .link-card > a { - width: 100%; - text-decoration: none; - line-height: 1.6; - color: white; - background-color: #23262d; - opacity: 0.95; - display: block; - position: relative; - box-sizing: border-box; - height: 100%; - padding: 0; - } - .card-layout { display: grid; grid-template-columns: 1fr; gap: 1.5rem; padding: 1.75rem; - min-height: 220px; - height: 100%; - } - - .image-wrapper { - width: 100%; - display: flex; - justify-content: center; - align-items: center; - z-index: 5; - } - - .image-container { - width: 100%; - max-width: 350px; - border-radius: 8px; - overflow: hidden; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); - background-color: #1a1c21; - padding: 0.5rem; - cursor: pointer; - position: relative; - transition: all 0.3s ease; - } - - .no-image-placeholder { - width: 100%; - height: 180px; - border-radius: 8px; - display: flex; - justify-content: center; - align-items: center; - background-color: #1d1f25; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); - transition: all 0.2s ease; - } - - .placeholder-content { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0.8rem; - color: rgba(255, 255, 255, 0.6); - transition: all 0.2s ease; - } - - .placeholder-icon { - font-size: 2rem; - } - - .placeholder-text { - font-size: 1rem; - font-weight: 500; - } - - .image-container img { - width: 100%; - object-fit: contain; - border-radius: 6px; - transition: transform 0.3s ease; - max-height: 200px; - } - - .image-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.5); - opacity: 0; - display: flex; - justify-content: center; - align-items: center; - transition: opacity 0.3s ease; - } - - .zoom-icon { - color: white; - font-size: 1.5rem; - } - - .image-container:hover .image-overlay { - opacity: 1; - } - - .image-container:hover img { - transform: scale(1.05); } .content-container { @@ -217,6 +150,13 @@ const { gap: 0.75rem; } + h2 { + margin: 0; + font-weight: bold; + font-size: 1.5rem; + line-height: 1.3; + } + .meta-info { display: flex; flex-direction: column; @@ -233,7 +173,6 @@ const { width: 10px; height: 10px; border-radius: 50%; - display: inline-block; } .language { @@ -246,60 +185,33 @@ const { display: flex; flex-wrap: wrap; gap: 0.5rem; - margin-top: 0.25rem; } .tag { background-color: #4d4e4e; - color: #ffffff; + color: #fff; padding: 5px 10px; font-size: 0.7rem; font-weight: 600; border-radius: 20px; - display: inline-block; - transition: background-color 0.2s ease; } .description { - flex: 1; display: flex; flex-direction: column; gap: 0.75rem; } - h2 { - margin: 0; - font-weight: bold; - font-size: 1.5rem; - transition: color 0.3s ease; - line-height: 1.3; - } - - p, .bullet-list { - margin: 0; - font-size: 0.95rem; - line-height: 1.5; - color: rgba(255, 255, 255, 0.85); - max-height: none; - overflow: visible; - text-overflow: ellipsis; - } - - .bullet-list { - white-space: pre-line; - } - .bullet-list { display: flex; flex-direction: column; gap: 0.35rem; + white-space: pre-line; } - /* Style for bullet points in descriptions */ .bullet-item { position: relative; padding-left: 1.25rem; - margin-bottom: 0.1rem; line-height: 1.4; } @@ -311,128 +223,141 @@ const { } .contribution { - margin-top: 0.5rem; font-style: italic; color: rgba(255, 255, 255, 0.7); } - .link-card:is(:hover, :focus-within) { - background-position: 0; - background-image: var(--accent-gradient); - transform: translateY(-5px); - box-shadow: 0 12px 25px rgba(0, 0, 0, 0.25); + /* ACTION BUTTONS */ + .actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + margin-top: 0.5rem; } - .link-card:is(:hover, :focus-within) .image-container:not(:hover) img { - transform: scale(1.02); + .btn { + padding: 0.4rem 0.75rem; + border-radius: 6px; + font-size: 0.85rem; + text-decoration: none; + color: white; + background: #3a3d46; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: 0.2s ease; } - .link-card:is(:hover, :focus-within) .no-image-placeholder .placeholder-content { - color: rgba(255, 255, 255, 0.9); + .btn:hover { + transform: translateY(-2px); + background: #4a4d57; } - .link-card:is(:hover, :focus-within) .tag { - background-color: #5d5e5e; + .btn.primary { + background: rgb(var(--accent-light)); + color: black; + font-weight: 600; } - .link-card:is(:hover, :focus-within) h2 { - color: rgb(var(--accent-light)); + .image-wrapper { + display: flex; + justify-content: center; + align-items: center; } - @media (min-width: 768px) { - .card-layout { - grid-template-columns: 3fr 2fr; - align-items: center; - gap: 2rem; - } + .image-container { + max-width: 350px; + border-radius: 8px; + overflow: hidden; + position: relative; + background: #1a1c21; + padding: 0.5rem; + } - .card-layout:has(.image-wrapper) .content-container { - grid-column: 1; - } + .image-container img { + width: 100%; + max-height: 200px; + object-fit: contain; + border-radius: 6px; + transition: transform 0.3s ease; + } - .card-layout .image-wrapper, - .card-layout .no-image-placeholder { - grid-column: 2; - grid-row: 1; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - } - - .card-layout .image-container { - max-width: 100%; - height: auto; - } - - .card-layout .no-image-placeholder { - min-height: 200px; - width: 100%; - } + .image-container:hover img { + transform: scale(1.05); } - @media (max-width: 767px) { - .card-layout { - gap: 1.25rem; - padding: 1.25rem; - grid-template-columns: 1fr; - } + .image-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.5); + opacity: 0; + display: flex; + justify-content: center; + align-items: center; + transition: opacity 0.3s ease; + } - .meta-info { - flex-direction: row; - justify-content: space-between; - align-items: flex-start; - flex-wrap: wrap; - } + .image-container:hover .image-overlay { + opacity: 1; + } - .tags-container { - margin-top: 0.5rem; - } + .zoom-icon { + font-size: 1.5rem; + color: white; + } - h2 { - font-size: 1.25rem; - } + .no-image-placeholder { + width: 100%; + height: 180px; + border-radius: 8px; + display: flex; + justify-content: center; + align-items: center; + background-color: #1d1f25; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); + } - .image-wrapper, - .no-image-placeholder { - order: -1; - width: 100%; - margin-bottom: 1rem; - } - - .image-container { - max-width: 100%; - margin: 0 auto; - } - - .no-image-placeholder { - height: 160px; - } + .placeholder-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.8rem; + color: rgba(255, 255, 255, 0.6); } - @media (max-width: 480px) { - .meta-info { - flex-direction: column; - align-items: flex-start; - } + .placeholder-icon { + font-size: 2rem; + } - .card-layout { - padding: 1.1rem; - gap: 1rem; - } + .placeholder-text { + font-size: 1rem; + font-weight: 500; + } + + /* HOVER */ + .link-card:hover { + transform: translateY(-5px); + box-shadow: 0 12px 25px rgba(0, 0, 0, 0.25); + } - p { - font-size: 0.9rem; - line-height: 1.6; + .link-card:hover h2 { + color: rgb(var(--accent-light)); + } + + /* RESPONSIVE */ + @media (min-width: 768px) { + .card-layout { + grid-template-columns: 3fr 2fr; + align-items: center; + gap: 2rem; } + } - .tags-container { - margin-top: 0.25rem; + @media (max-width: 767px) { + .card-layout { + padding: 1.25rem; } - .tag { - padding: 4px 8px; - font-size: 0.65rem; + h2 { + font-size: 1.25rem; } } </style> diff --git a/src/pages/projects.astro b/src/pages/projects.astro index 1cb84ba..8c2bb48 100644 --- a/src/pages/projects.astro +++ b/src/pages/projects.astro @@ -24,7 +24,37 @@ import Card from "../components/Card.astro"; </div> <ul role="list" class="project-grid"> - <!-- 2026 --> + <Card + href="https://github.com/pinapelz/k-heardle" + project="https://k-heardle.moekyun.me/" + title="K-Heardle" + body={`• Daily K-pop music guessing game inspired by Heardle +• Automated Python and OpenCV based daily game content generation +• Stateless backend using signed game state to prevent tampering +• Tracks player statistics, streaks, and shareable daily results locally`} + language="TypeScript" + languageColor="#3178c6" + image="/kheardle.png" + imageAlt="K-Heardle gameplay" + year="2026" + tags={["React", "TypeScript", ,"Python", "OpenCV","Express"]} + /> + <Card + href="https://github.com/pinapelz/mixx-type" + project="https://type.moekyun.me" + title="mixx-type" + body={`• Fast-paced typing game built around typing to the lyrics of songs +• Tracks accuracy, WPM, and streak performance in real time +• Portable chart format built based on .lrc files +• Responsive UI with smooth input handling and instant feedback +• Lightweight full-stack architecture optimized for low latency gameplay`} + language="TypeScript" + languageColor="#3178c6" + image="/typemixx.png" + imageAlt="Mixx Type typing interface" + year="2026" + tags={["React", "TypeScript", "Pocketbase", "Next.js"]} + /> <Card href="https://github.com/pinapelz/auto-live-tl" title="auto-live-tl" @@ -39,6 +69,7 @@ import Card from "../components/Card.astro"; year="2026" tags={[ "Python", + "QT6 (Pyside)", "OpenAI-Whisper", "ASR", "LLM" @@ -46,6 +77,7 @@ import Card from "../components/Card.astro"; /> <Card href="https://github.com/pinapelz/lrc-karaoke-player" + project="https://lrc.moekyun.me" title="lrc-karaoke-player" body={`• Client-side web app for LRC-based karaoke playback and tooling • LRC-Player: synchronized scrolling lyrics with karaoke-style highlighting, with dual audio mixing @@ -100,6 +132,7 @@ import Card from "../components/Card.astro"; <Card href="https://github.com/pinapelz/573-updates" + project="https://arcade.moekyun.me" title="573-UPDATES" body={`• Web scraper for arcade gaming news and updates • Tracks patch notes, announcements, and community posts @@ -117,6 +150,7 @@ import Card from "../components/Card.astro"; <!-- 2024 --> <Card href="https://github.com/pinapelz/Mirage" + project="https://mirage.pinapelz.com" title="Mirage" body={`• Tracks rhythm game scores across multiple titles • Supports niche games without official metadata @@ -155,7 +189,8 @@ import Card from "../components/Card.astro"; <!-- 2023 --> <Card - href="https://patchwork.moekyun.me" + href="https://github.com/Patchwork-Archive" + project="https://patchwork.moekyun.me" title="Patchwork Archive" body={`• Scalable system for archiving YouTube videos • React frontend for managing requests and archives @@ -172,6 +207,7 @@ import Card from "../components/Card.astro"; <Card href="https://github.com/pinapelz/NijiTrack" + project="https://phase-tracker.com/" title="Nijitrack" body={`• Analytics platform for YouTube channel metrics • Tracks subscribers, views, and historical trends @@ -210,7 +246,8 @@ import Card from "../components/Card.astro"; /> <Card - href="https://blog.pinapelz.com" + href="https://git.moekyun.me/blog.pinapelz.com" + project="https://blog.pinapelz.com" title="Personal Blog" body={`• Performance-focused blog built with Astro • Combines MDX with React components for richer posts |
