aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--public/kheardle.pngbin0 -> 17100 bytes
-rw-r--r--public/typemixx.pngbin0 -> 226438 bytes
-rw-r--r--src/components/Card.astro469
-rw-r--r--src/pages/projects.astro43
4 files changed, 237 insertions, 275 deletions
diff --git a/public/kheardle.png b/public/kheardle.png
new file mode 100644
index 0000000..2dffae8
--- /dev/null
+++ b/public/kheardle.png
Binary files differ
diff --git a/public/typemixx.png b/public/typemixx.png
new file mode 100644
index 0000000..85ab581
--- /dev/null
+++ b/public/typemixx.png
Binary files differ
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
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage