feat: homepage personal portfolio (Phase 4)

- data/profile.yaml schema: name, handle, tagline, avatar, bio, links
- data/projects.yaml schema: featured projects with title, tagline, repo, demo, image, tags
- _partials/home/hero.html: avatar + name + tagline + bio + social links
- _partials/home/projects.html: data-driven project grid
- _partials/home/recent-posts.html: configurable count, view-all link
- home.html composes hero → projects → recent-posts
- assets/icons: github, mail, rss (Lucide-style)
- exampleSite fixtures: profile, 4 projects, 5 SVG placeholder images
This commit is contained in:
tiennm99 committed 2026-05-07 20:53:06 +07:00
1 parent da7a834b8f
commit 7366d05e24
14 files changed
+192 -5

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.56v-2c-3.2.7-3.88-1.36-3.88-1.36-.52-1.32-1.27-1.67-1.27-1.67-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.76 2.69 1.25 3.34.95.1-.74.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.7 0-1.26.45-2.29 1.18-3.1-.12-.29-.51-1.46.11-3.05 0 0 .96-.31 3.15 1.18a10.94 10.94 0 0 1 5.74 0c2.19-1.49 3.15-1.18 3.15-1.18.62 1.59.23 2.76.11 3.05.74.81 1.18 1.84 1.18 3.1 0 4.43-2.69 5.4-5.26 5.69.41.36.78 1.06.78 2.14v3.18c0 .31.21.68.8.56C20.21 21.39 23.5 17.08 23.5 12 23.5 5.65 18.35.5 12 .5z"/>
</svg>

After

Width:  |  Height:  |  Size: 704 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="2" y="4" width="20" height="16" rx="2"/>
<path d="m22 6-10 7L2 6"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 11a9 9 0 0 1 9 9"/>
<path d="M4 4a16 16 0 0 1 16 16"/>
<circle cx="5" cy="19" r="1"/>
</svg>

After

Width:  |  Height:  |  Size: 312 B

@@ -0,0 +1,18 @@
name: "Tien Nguyen"
handle: "tiennm99"
tagline: "Building tools, breaking things, writing it down."
avatar: "img/avatar.svg"
bio: |
Software engineer based in Ho Chi Minh City. I build for the web,
experiment with AI tooling, and write down what I learn.
links:
- icon: github
label: GitHub
url: https://github.com/tiennm99
- icon: mail
label: Email
url: mailto:hi@miti99.com
- icon: rss
label: RSS
url: /index.xml
@@ -0,0 +1,25 @@
featured:
- title: "bonsai"
tagline: "Minimalist Hugo theme for link-in-bio. Inspired by Linktree and Japanese bonsai aesthetics."
repo: https://github.com/tiennm99/bonsai
demo: https://tiennm99.github.io/bonsai/
image: "img/projects/bonsai.svg"
tags: [hugo, theme, minimal]
- title: "miti99bot"
tagline: "Plug-n-play Telegram bot framework on Cloudflare Workers with grammY."
repo: https://github.com/tiennm99/miti99bot
image: "img/projects/miti99bot.svg"
tags: [telegram, cloudflare, framework]
- title: "rplace"
tagline: "Collaborative pixel art canvas inspired by Reddit r/place. Real-time WebSocket sync."
repo: https://github.com/tiennm99/rplace
image: "img/projects/rplace.svg"
tags: [svelte, hono, durable-objects]
- title: "vngeoguessr"
tagline: "GeoGuessr clone for Vietnamese locations. Mapillary street view + Redis leaderboards."
repo: https://github.com/tiennm99/vngeoguessr
image: "img/projects/vngeoguessr.svg"
tags: [geo, mapillary, redis]
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#1a1a2e"/>
<stop offset="100%" stop-color="#4a4a7e"/>
</linearGradient>
</defs>
<circle cx="60" cy="60" r="60" fill="url(#g)"/>
<text x="60" y="74" text-anchor="middle" font-family="system-ui,sans-serif" font-size="48" fill="#f5f5f5" font-weight="300">月</text>
</svg>

After

Width:  |  Height:  |  Size: 475 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 200" width="320" height="200">
<rect width="320" height="200" fill="#0d1f17"/>
<text x="160" y="110" text-anchor="middle" font-family="serif" font-size="64" fill="#7fb8a4">盆栽</text>
</svg>

After

Width:  |  Height:  |  Size: 254 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 200" width="320" height="200">
<rect width="320" height="200" fill="#1f2937"/>
<text x="160" y="118" text-anchor="middle" font-family="ui-monospace,monospace" font-size="40" fill="#60a5fa">miti99bot</text>
</svg>

After

Width:  |  Height:  |  Size: 274 B

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 200" width="320" height="200">
<rect width="320" height="200" fill="#fef3c7"/>
<g>
<rect x="40" y="40" width="40" height="40" fill="#ef4444"/>
<rect x="80" y="40" width="40" height="40" fill="#22c55e"/>
<rect x="120" y="40" width="40" height="40" fill="#3b82f6"/>
<rect x="160" y="40" width="40" height="40" fill="#f59e0b"/>
<rect x="200" y="40" width="40" height="40" fill="#a855f7"/>
<rect x="240" y="40" width="40" height="40" fill="#06b6d4"/>
<rect x="40" y="80" width="40" height="40" fill="#22c55e"/>
<rect x="80" y="80" width="40" height="40" fill="#3b82f6"/>
<rect x="120" y="80" width="40" height="40" fill="#ef4444"/>
<rect x="200" y="80" width="40" height="40" fill="#06b6d4"/>
<rect x="240" y="80" width="40" height="40" fill="#f59e0b"/>
</g>
<text x="160" y="160" text-anchor="middle" font-family="ui-monospace,monospace" font-size="22" fill="#1f2937">r/place</text>
</svg>

After

Width:  |  Height:  |  Size: 996 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 200" width="320" height="200">
<rect width="320" height="200" fill="#064e3b"/>
<circle cx="160" cy="100" r="50" fill="none" stroke="#34d399" stroke-width="3"/>
<line x1="110" y1="100" x2="210" y2="100" stroke="#34d399" stroke-width="2"/>
<line x1="160" y1="50" x2="160" y2="150" stroke="#34d399" stroke-width="2"/>
<text x="160" y="180" text-anchor="middle" font-family="ui-monospace,monospace" font-size="18" fill="#a7f3d0">VN GeoGuessr</text>
</svg>

After

Width:  |  Height:  |  Size: 519 B

@@ -0,0 +1,31 @@
{{- $profile := site.Data.profile -}}
{{- with $profile -}}
<section class="home-hero" aria-labelledby="home-hero-name">
{{- with .avatar }}
<img class="home-hero-avatar" src="{{ . | relURL }}" alt="{{ $profile.name | default site.Title }}" width="120" height="120" loading="eager" decoding="async">
{{- end }}
<h1 id="home-hero-name" class="home-hero-name">{{ .name | default site.Title }}</h1>
{{- with .tagline }}
<p class="home-hero-tagline">{{ . }}</p>
{{- end }}
{{- with .bio }}
<div class="home-hero-bio">{{ . | markdownify }}</div>
{{- end }}
{{- with .links }}
<ul class="home-hero-links">
{{- range . }}
<li>
<a href="{{ .url }}" rel="me noopener" aria-label="{{ .label | default .icon }}">
{{- with .icon }}{{ partial "icon.html" . }}{{ end }}
<span>{{ .label | default .icon }}</span>
</a>
</li>
{{- end }}
</ul>
{{- end }}
</section>
{{- end -}}
@@ -0,0 +1,40 @@
{{- with site.Data.projects.featured -}}
<section class="home-projects" aria-labelledby="home-projects-heading">
<h2 id="home-projects-heading" class="home-projects-heading">
{{ i18n "featuredProjects" | default "Featured Projects" }}
</h2>
<ul class="project-grid">
{{- range . }}
{{- $project := . }}
<li class="project-card">
{{- with $project.image }}
<div class="project-card-image">
<img src="{{ . | relURL }}" alt="{{ $project.title }}" loading="lazy" decoding="async">
</div>
{{- end }}
<h3 class="project-card-title">{{ $project.title }}</h3>
{{- with $project.tagline }}
<p class="project-card-tagline">{{ . }}</p>
{{- end }}
{{- with $project.tags }}
<ul class="project-card-tags">
{{- range . }}<li>{{ . }}</li>{{ end }}
</ul>
{{- end }}
<ul class="project-card-links">
{{- with $project.repo }}
<li><a href="{{ . }}" rel="noopener">repo →</a></li>
{{- end }}
{{- with $project.demo }}
<li><a href="{{ . }}" rel="noopener">demo →</a></li>
{{- end }}
</ul>
</li>
{{- end }}
</ul>
</section>
{{- end -}}
@@ -0,0 +1,21 @@
{{- $count := site.Params.home.recentPostsCount | default 5 -}}
{{- $posts := first $count (where (where site.RegularPages "Type" "post") "Draft" false) -}}
{{- with $posts -}}
<section class="home-recent-posts" aria-labelledby="home-recent-heading">
<h2 id="home-recent-heading" class="home-recent-heading">
{{ i18n "recentPosts" | default "Recent Posts" }}
</h2>
<ul class="post-list">
{{- range . }}
<li>{{ partial "post-card.html" . }}</li>
{{- end }}
</ul>
{{- if gt (len (where (where site.RegularPages "Type" "post") "Draft" false)) $count }}
<p class="home-recent-more">
<a href="{{ "/post/" | relURL }}">{{ i18n "viewAll" | default "View all" }} →</a>
</p>
{{- end }}
</section>
{{- end -}}
+3 -5
View File
@@ -1,9 +1,7 @@
{{ define "body_class" }}home{{ end }}
{{ define "main" }}
{{/* Phase 4 fills hero/projects/recent-posts here. Placeholder for now. */}}
<section class="home-placeholder">
<h1>{{ site.Title }}</h1>
{{- with site.Params.description }}<p>{{ . }}</p>{{ end }}
</section>
{{ partial "home/hero.html" . }}
{{ partial "home/projects.html" . }}
{{ partial "home/recent-posts.html" . }}
{{ end }}