diff --git a/.github/scripts/lib/github-api.mjs b/.github/scripts/lib/github-api.mjs new file mode 100644 index 0000000..9b7fe65 --- /dev/null +++ b/.github/scripts/lib/github-api.mjs @@ -0,0 +1,235 @@ +// GitHub API access for the projects list: owned namespaces, their repos, and +// the repos the user contributed merged pull requests to. + +const API = "https://api.github.com"; +const GRAPHQL = `${API}/graphql`; + +/** + * @typedef {object} Repo + * @property {string} nameWithOwner + * @property {string} name + * @property {string} owner + * @property {string} htmlUrl + * @property {string} description + * @property {string} homepage + * @property {boolean} hasPages + * @property {number} stars + * @property {boolean} isFork + * @property {boolean} isPrivate + * @property {boolean} isArchived + * @property {string} pushedAt Last push timestamp, ISO 8601. + */ + +/** + * @typedef {object} Contribution + * @property {Repo} repo + * @property {number} mergedPrs + */ + +/** Minimal GitHub client bound to one token and one user. */ +export class GitHubClient { + /** + * @param {string} token Personal access token with `repo` and `read:org`. + * @param {string} user Login whose projects are listed. + */ + constructor(token, user) { + this.token = token; + this.user = user; + } + + /** @returns {Record} */ + get restHeaders() { + return { + Authorization: `Bearer ${this.token}`, + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": `${this.user}-projects-updater`, + }; + } + + /** + * Fetch every page of a REST collection endpoint. + * @param {string} pathAndQuery Path starting with `/`, query without `page`. + * @returns {Promise} + */ + async restPaged(pathAndQuery) { + const all = []; + const sep = pathAndQuery.includes("?") ? "&" : "?"; + for (let page = 1; page <= 20; page += 1) { + const res = await fetch(`${API}${pathAndQuery}${sep}page=${page}`, { + headers: this.restHeaders, + }); + if (!res.ok) { + throw new Error(`REST ${pathAndQuery} ${res.status}: ${await res.text()}`); + } + const batch = await res.json(); + if (!Array.isArray(batch) || batch.length === 0) break; + all.push(...batch); + if (batch.length < 100) break; + } + return all; + } + + /** + * @param {string} query + * @param {Record} variables + * @returns {Promise} + */ + async graphql(query, variables) { + const res = await fetch(GRAPHQL, { + method: "POST", + headers: { + Authorization: `Bearer ${this.token}`, + "Content-Type": "application/json", + "User-Agent": `${this.user}-projects-updater`, + }, + body: JSON.stringify({ query, variables }), + }); + if (!res.ok) throw new Error(`GraphQL ${res.status}: ${await res.text()}`); + const body = await res.json(); + if (body.errors) throw new Error(`GraphQL errors: ${JSON.stringify(body.errors)}`); + return body.data; + } + + /** + * Logins the user controls: their own account plus every organization where + * their membership role is `admin`. + * @returns {Promise>} + */ + async fetchOwnedLogins() { + let memberships; + try { + memberships = await this.restPaged("/user/memberships/orgs?state=active&per_page=100"); + } catch (err) { + throw new Error( + `Cannot read organization memberships (the token needs the read:org scope): ${err.message}`, + ); + } + const logins = new Set([this.user.toLowerCase()]); + for (const m of memberships) { + if (m.role === "admin" && m.organization?.login) { + logins.add(m.organization.login.toLowerCase()); + } + } + return logins; + } + + /** + * Public repos across every owned login, deduplicated by full name. + * @param {Set} ownedLogins + * @returns {Promise} + */ + async fetchOwnedRepos(ownedLogins) { + const byName = new Map(); + for (const login of ownedLogins) { + const endpoint = + login === this.user.toLowerCase() + ? `/users/${login}/repos?type=owner&sort=updated&per_page=100` + : `/orgs/${login}/repos?type=all&sort=updated&per_page=100`; + for (const r of await this.restPaged(endpoint)) { + if (r.private) continue; + if (!byName.has(r.full_name)) byName.set(r.full_name, fromRest(r)); + } + } + return [...byName.values()]; + } + + /** + * Repos the user has merged pull requests in, with the per-repo count. + * Uses the search API, which caps at 1000 results. + * @returns {Promise} + */ + async fetchMergedPrContributions() { + const counts = new Map(); + const repos = new Map(); + let cursor = null; + for (let i = 0; i < 10; i += 1) { + const data = await this.graphql(MERGED_PR_QUERY, { + q: `is:pr is:merged is:public author:${this.user}`, + cursor, + }); + const search = data?.search; + if (!search) break; + for (const node of search.nodes) { + const repo = node?.repository; + if (!repo || repo.isPrivate) continue; + const key = repo.nameWithOwner; + counts.set(key, (counts.get(key) || 0) + 1); + if (!repos.has(key)) repos.set(key, fromGraphql(repo)); + } + if (!search.pageInfo.hasNextPage) break; + cursor = search.pageInfo.endCursor; + } + return [...counts.entries()].map(([key, mergedPrs]) => ({ + repo: repos.get(key), + mergedPrs, + })); + } +} + +const MERGED_PR_QUERY = ` +query($q: String!, $cursor: String) { + search(query: $q, type: ISSUE, first: 100, after: $cursor) { + pageInfo { hasNextPage endCursor } + nodes { + ... on PullRequest { + repository { + nameWithOwner + name + url + description + homepageUrl + stargazerCount + pushedAt + isPrivate + isFork + isArchived + owner { login } + } + } + } + } +}`; + +/** + * @param {object} r REST repository payload. + * @returns {Repo} + */ +function fromRest(r) { + return { + nameWithOwner: r.full_name, + name: r.name, + owner: r.owner?.login || "", + htmlUrl: r.html_url, + description: r.description || "", + homepage: r.homepage || "", + hasPages: !!r.has_pages, + stars: r.stargazers_count || 0, + isFork: !!r.fork, + isPrivate: !!r.private, + isArchived: !!r.archived, + pushedAt: r.pushed_at || "", + }; +} + +/** + * @param {object} r GraphQL repository node. + * @returns {Repo} + */ +function fromGraphql(r) { + return { + nameWithOwner: r.nameWithOwner, + name: r.name, + owner: r.owner?.login || "", + htmlUrl: r.url, + description: r.description || "", + homepage: r.homepageUrl || "", + // has_pages is not exposed by GraphQL; contributed repos rely on homepage. + hasPages: false, + stars: r.stargazerCount || 0, + isFork: !!r.isFork, + isPrivate: !!r.isPrivate, + isArchived: !!r.isArchived, + pushedAt: r.pushedAt || "", + }; +} diff --git a/.github/scripts/lib/render-projects.mjs b/.github/scripts/lib/render-projects.mjs new file mode 100644 index 0000000..e80385b --- /dev/null +++ b/.github/scripts/lib/render-projects.mjs @@ -0,0 +1,109 @@ +// Markdown rendering and marker injection for the projects page. + +/** + * Public URL of a repo: its declared homepage, else its GitHub Pages URL when + * Pages is enabled. Returns an empty string when the repo has neither. + * @param {import("./github-api.mjs").Repo} repo + * @returns {string} + */ +export function resolveUrl(repo) { + const home = (repo.homepage || "").trim(); + if (home) return home; + if (repo.hasPages) return `https://${repo.owner}.github.io/${repo.name}/`; + return ""; +} + +/** + * @param {string} text + * @returns {string} Cell-safe single-line text. + */ +function cell(text) { + return (text || "") + .replace(/\|/g, "\\|") + .replace(/\r?\n/g, " ") + .trim(); +} + +/** + * @param {string} url + * @returns {string} + */ +function linkOrDash(url) { + return url ? `[${url}](${url})` : "—"; +} + +/** + * @param {string[]} headers + * @param {string[][]} rows + * @returns {string[]} Markdown table lines, or an empty-state line. + */ +function table(headers, rows) { + if (rows.length === 0) return ["_Chưa có project nào._"]; + return [ + `| ${headers.join(" | ")} |`, + `| ${headers.map(() => "---").join(" | ")} |`, + ...rows.map((r) => `| ${r.join(" | ")} |`), + ]; +} + +/** + * Repos with at least one star, most-starred first. + * @param {import("./github-api.mjs").Repo[]} repos + * @returns {string[]} + */ +export function renderStarred(repos) { + const rows = repos.map((r) => [ + `[${r.nameWithOwner}](${r.htmlUrl})`, + cell(r.description), + String(r.stars), + linkOrDash(resolveUrl(r)), + ]); + return table(["Name", "Description", "Stars", "URL"], rows); +} + +/** + * Repos that have a clickable URL, by name. + * @param {import("./github-api.mjs").Repo[]} repos + * @returns {string[]} + */ +export function renderLinked(repos) { + const rows = repos.map((r) => [ + `[${r.nameWithOwner}](${r.htmlUrl})`, + cell(r.description), + linkOrDash(resolveUrl(r)), + ]); + return table(["Name", "Description", "URL"], rows); +} + +/** + * Repos owned by others, by merged pull request count. + * @param {import("./github-api.mjs").Contribution[]} contributions + * @returns {string[]} + */ +export function renderContributed(contributions) { + const rows = contributions.map((c) => [ + `[${c.repo.nameWithOwner}](${c.repo.htmlUrl})`, + cell(c.repo.description), + String(c.mergedPrs), + String(c.repo.stars), + ]); + return table(["Name", "Description", "Merged PRs", "Stars"], rows); +} + +/** + * Replace the content between a marker pair. Throws when the pair is missing, + * so a renamed marker fails loudly instead of silently dropping a list. + * @param {string} source + * @param {string} marker Marker base name, e.g. `PROJECTS_STARRED`. + * @param {string[]} lines + * @returns {string} + */ +export function injectBlock(source, marker, lines) { + const start = ``; + const end = ``; + const pattern = new RegExp(`${start}[\\s\\S]*?${end}`); + if (!pattern.test(source)) { + throw new Error(`Marker pair ${marker} not found in projects page`); + } + return source.replace(pattern, [start, "", ...lines, "", end].join("\n")); +} diff --git a/.github/scripts/update-projects-list.mjs b/.github/scripts/update-projects-list.mjs index c7b0aca..5fd20ca 100644 --- a/.github/scripts/update-projects-list.mjs +++ b/.github/scripts/update-projects-list.mjs @@ -1,189 +1,79 @@ #!/usr/bin/env node -// Build a projects list (owned + contributed) and inject it into -// content/page/projects/index.md between the PROJECTS_LIST markers. +// Build the three project lists and inject them into the projects page: // -// A project is included if it has a URL, meaning either: -// - repo.homepage is set, OR -// - repo.has_pages is true (owned repos only — GraphQL does not expose -// has_pages for contributed repos, so those must have homepage set). +// 1. Starred — public repos I own (personal account or an org I administer) +// with at least one star, by stars then most recent push. +// 2. Linked — public repos I own that have a clickable URL (homepage or +// GitHub Pages), no star requirement, by most recent push. +// May overlap list 1. +// 3. Contributed — public repos owned by someone else, by how many of my pull +// requests were merged there, then most recent push. +// +// Forks are excluded from lists 1 and 2: their homepage belongs to upstream, +// not to me. Private repos are excluded everywhere — the page is public. import fs from "node:fs/promises"; import path from "node:path"; -const token = process.env.GH_TOKEN; -const user = process.env.GH_USER; +import { GitHubClient } from "./lib/github-api.mjs"; +import { + injectBlock, + renderContributed, + renderLinked, + renderStarred, + resolveUrl, +} from "./lib/render-projects.mjs"; -if (!token || !user) { - console.error("GH_TOKEN and GH_USER env vars are required"); - process.exit(1); -} +const PAGE = "content/page/projects/index.md"; -const restHeaders = { - Authorization: `Bearer ${token}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - "User-Agent": `${user}-projects-updater`, -}; - -async function fetchOwnedRepos() { - const all = []; - for (let page = 1; page <= 20; page += 1) { - const url = `https://api.github.com/users/${user}/repos?type=all&sort=updated&per_page=100&page=${page}`; - const res = await fetch(url, { headers: restHeaders }); - if (!res.ok) { - throw new Error(`REST /users/${user}/repos ${res.status}: ${await res.text()}`); - } - const batch = await res.json(); - if (!Array.isArray(batch) || batch.length === 0) break; - all.push(...batch); - if (batch.length < 100) break; - } - return all.map((r) => ({ - nameWithOwner: r.full_name, - name: r.name, - htmlUrl: r.html_url, - description: r.description, - homepage: r.homepage, - hasPages: !!r.has_pages, - owner: r.owner?.login, - isOwned: true, - })); -} - -const CONTRIB_QUERY = ` -query($login: String!, $cursor: String) { - user(login: $login) { - repositoriesContributedTo( - first: 100 - after: $cursor - includeUserRepositories: false - privacy: PUBLIC - contributionTypes: [COMMIT, PULL_REQUEST, PULL_REQUEST_REVIEW] - orderBy: { field: UPDATED_AT, direction: DESC } - ) { - pageInfo { hasNextPage endCursor } - nodes { - nameWithOwner - name - url - description - homepageUrl - owner { login } - } - } - } -}`; - -async function fetchContributedRepos() { - const all = []; - let cursor = null; - for (let i = 0; i < 20; i += 1) { - const res = await fetch("https://api.github.com/graphql", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - "User-Agent": `${user}-projects-updater`, - }, - body: JSON.stringify({ - query: CONTRIB_QUERY, - variables: { login: user, cursor }, - }), - }); - if (!res.ok) { - throw new Error(`GraphQL ${res.status}: ${await res.text()}`); - } - const body = await res.json(); - if (body.errors) { - throw new Error(`GraphQL errors: ${JSON.stringify(body.errors)}`); - } - const node = body.data?.user?.repositoriesContributedTo; - if (!node) break; - for (const r of node.nodes) { - all.push({ - nameWithOwner: r.nameWithOwner, - name: r.name, - htmlUrl: r.url, - description: r.description, - homepage: r.homepageUrl, - hasPages: false, // not exposed via GraphQL; rely on homepage - owner: r.owner.login, - isOwned: false, - }); - } - if (!node.pageInfo.hasNextPage) break; - cursor = node.pageInfo.endCursor; - } - return all; -} - -function resolveUrl(repo) { - const home = (repo.homepage || "").trim(); - if (home) return home; - if (repo.hasPages && repo.isOwned) { - return `https://${repo.owner}.github.io/${repo.name}`; - } - return null; -} - -function toRow(repo, url) { - const desc = (repo.description || "") - .replace(/\|/g, "\\|") - .replace(/\r?\n/g, " ") - .trim(); - return `| [${repo.nameWithOwner}](${repo.htmlUrl}) | ${desc} | [${url}](${url}) |`; -} - -const START = ""; -const END = ""; - -function buildBlock(rows) { - return [ - START, - "", - "| Name | Description | URL |", - "| --- | --- | --- |", - ...rows, - "", - END, - ].join("\n"); -} +/** + * Most recently pushed first; a repo with no timestamp sorts last. + * @param {import("./lib/github-api.mjs").Repo} a + * @param {import("./lib/github-api.mjs").Repo} b + * @returns {number} + */ +const byRecentPush = (a, b) => (b.pushedAt || "").localeCompare(a.pushedAt || ""); async function main() { - const [owned, contributed] = await Promise.all([ - fetchOwnedRepos(), - fetchContributedRepos(), + const token = process.env.GH_TOKEN; + const user = process.env.GH_USER; + if (!token || !user) { + console.error("GH_TOKEN and GH_USER env vars are required"); + process.exit(1); + } + + const gh = new GitHubClient(token, user); + const ownedLogins = await gh.fetchOwnedLogins(); + const [ownedRepos, contributions] = await Promise.all([ + gh.fetchOwnedRepos(ownedLogins), + gh.fetchMergedPrContributions(), ]); - const merged = new Map(); - for (const r of [...owned, ...contributed]) { - if (!merged.has(r.nameWithOwner)) merged.set(r.nameWithOwner, r); - } + const mine = ownedRepos.filter((r) => !r.isFork); - const rows = []; - const sorted = [...merged.values()].sort((a, b) => - a.nameWithOwner.localeCompare(b.nameWithOwner), - ); - for (const r of sorted) { - const url = resolveUrl(r); - if (!url) continue; - rows.push(toRow(r, url)); - } + const starred = mine + .filter((r) => r.stars > 0) + .sort((a, b) => b.stars - a.stars || byRecentPush(a, b)); - const block = buildBlock(rows); - const file = path.resolve("content/page/projects/index.md"); + const linked = mine.filter((r) => resolveUrl(r) !== "").sort(byRecentPush); + + const contributed = contributions + .filter((c) => !ownedLogins.has(c.repo.owner.toLowerCase())) + .sort((a, b) => b.mergedPrs - a.mergedPrs || byRecentPush(a.repo, b.repo)); + + const file = path.resolve(PAGE); const current = await fs.readFile(file, "utf8"); - const marker = /[\s\S]*?/; - const next = marker.test(current) - ? current.replace(marker, block) - : `${current.trimEnd()}\n\n${block}\n`; + let next = injectBlock(current, "PROJECTS_STARRED", renderStarred(starred)); + next = injectBlock(next, "PROJECTS_LINKED", renderLinked(linked)); + next = injectBlock(next, "PROJECTS_CONTRIBUTED", renderContributed(contributed)); + const summary = `${starred.length} starred, ${linked.length} linked, ${contributed.length} contributed`; if (next === current) { - console.log(`No changes (${rows.length} projects).`); + console.log(`No changes (${summary}).`); return; } await fs.writeFile(file, next); - console.log(`Updated ${file} with ${rows.length} projects.`); + console.log(`Updated ${PAGE} (${summary}).`); } main().catch((err) => { diff --git a/content/page/projects/index.md b/content/page/projects/index.md index 0a2fa24..67c628d 100644 --- a/content/page/projects/index.md +++ b/content/page/projects/index.md @@ -10,105 +10,136 @@ menu: icon: list --- -Danh sách những projects mình đã làm hoặc có đóng góp, có URL demo / homepage. URL có thể là: +Projects nổi bật của mình, chia thành ba danh sách. Tất cả đều là repo public, và +đều được **tạo tự động bằng script** (`.github/scripts/update-projects-list.mjs`, +chạy mỗi ngày qua GitHub Actions) — không sửa tay phần trong bảng. + +## 1. Projects có star + +Repo mình sở hữu — trên tài khoản cá nhân hoặc trong org do mình làm owner — có ít +nhất 1 star. Sắp xếp theo số star giảm dần, cùng số star thì repo cập nhật gần +đây hơn lên trước. Fork không được tính. + + + +| Name | Description | Stars | URL | +| --- | --- | --- | --- | +| [tiennm99/awesome-ai-dev-tools](https://github.com/tiennm99/awesome-ai-dev-tools) | Daily-refreshed ranking of open-source AI developer tools — coding agents, ADEs, AI editors and code-review tools — by GitHub stars, with 7-day deltas, star-history charts and a tag-filtered dashboard. | 3 | [https://awesome-ai-dev-tools.pages.dev/](https://awesome-ai-dev-tools.pages.dev/) | +| [tiennm99/penny-pincher-provider](https://github.com/tiennm99/penny-pincher-provider) | Curated list of affordable + free LLM API providers — Claude Code passes, coding-plan subs, free-tier APIs. | 1 | [https://tiennm99.github.io/penny-pincher-provider/](https://tiennm99.github.io/penny-pincher-provider/) | +| [tiennm99/ai-labs](https://github.com/tiennm99/ai-labs) | Experiments with AI-assisted coding workflows — ClaudeKit, Superpowers, BMAD, oh-my-claudecode, gstack, and course work, merged with full history | 1 | [https://tiennm99.github.io/ai-labs/](https://tiennm99.github.io/ai-labs/) | +| [tiennm99/export-telegram-group-members](https://github.com/tiennm99/export-telegram-group-members) | Export Telegram group members to CSV using Telethon — admin auth required for full member visibility. | 1 | — | +| [tiennm99/time-mocker](https://github.com/tiennm99/time-mocker) | Windows tool that injects fake time into running processes by hooking Win32 time APIs. Rust. | 1 | — | +| [tiennm99/lottery-generator](https://github.com/tiennm99/lottery-generator) | Go port of my lottery number generator — useful for Vietlott and similar lotteries. | 1 | — | +| [tiennm99/arduino](https://github.com/tiennm99/arduino) | Collection of small Arduino projects and sketches. | 1 | — | +| [tiennm99/adventofcode](https://github.com/tiennm99/adventofcode) | My Advent of Code 2025 solutions | 1 | — | + + + +## 2. Projects có link + +Repo mình sở hữu và có URL để bấm vào xem trực tiếp, không cần star. Danh sách này +có thể trùng với danh sách trên. URL có thể là: - Website tĩnh / app host trên [GitHub Pages](https://pages.github.com/), [Cloudflare Pages](https://pages.cloudflare.com/), [Vercel](https://vercel.com/), [Netlify](https://www.netlify.com/)... - [Cloudflare Workers](https://workers.cloudflare.com/) hoặc serverless endpoint khác - Telegram bot ([t.me/...](https://t.me/)) - Custom domain, hoặc bất kỳ hình thức deploy nào khác -_(Danh sách này được tạo tự động, sắp xếp theo tên repo)_ +Sắp xếp theo lần cập nhật gần nhất. - + | Name | Description | URL | | --- | --- | --- | -| [abhisheknaiidu/awesome-github-profile-readme](https://github.com/abhisheknaiidu/awesome-github-profile-readme) | 😎 A curated list of awesome GitHub Profile which updates in real time | [https://bit.ly/awesome-gh](https://bit.ly/awesome-gh) | -| [BerriAI/litellm](https://github.com/BerriAI/litellm) | The fastest, litest AI Gateway. Rust core with Python SDK. Call 100+ LLM APIs in OpenAI (or native) format with cost tracking, guardrails, load balancing, and logging [Bedrock, Azure, OpenAI, Anthropic, OpenAI, VertexAI, vLLM, Nvidia NIM] | [https://docs.litellm.ai/docs/](https://docs.litellm.ai/docs/) | -| [bkfcduchoa/bkfcduchoa.github.io](https://github.com/bkfcduchoa/bkfcduchoa.github.io) | | [https://bkfcduchoa.github.io/](https://bkfcduchoa.github.io/) | -| [coollabsio/coolify](https://github.com/coollabsio/coolify) | An open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers. | [https://coolify.io](https://coolify.io) | -| [gohugoio/hugoThemesSiteBuilder](https://github.com/gohugoio/hugoThemesSiteBuilder) | The source for https://themes.gohugo.io | [https://themes.gohugo.io](https://themes.gohugo.io) | -| [kaitranntt/ccs](https://github.com/kaitranntt/ccs) | Switch between Claude accounts, Gemini, Copilot, OpenRouter (300+ models) via CLIProxyAPI OAuth proxy. Visual dashboard, remote proxy support, WebSearch fallback. Zero-config to production-ready. | [https://ccs.kaitran.ca/](https://ccs.kaitran.ca/) | -| [ngamtheproject/ngamtheproject.github.io](https://github.com/ngamtheproject/ngamtheproject.github.io) | Website for Ngăm - a charity organization founded by high school students. | [https://ngamtheproject.github.io/](https://ngamtheproject.github.io/) | -| [ripienaar/free-for-dev](https://github.com/ripienaar/free-for-dev) | A list of SaaS, PaaS and IaaS offerings that have free tiers of interest to devops and infradev | [https://free-for.dev/](https://free-for.dev/) | -| [survival2d/survival2d.github.io](https://github.com/survival2d/survival2d.github.io) | Repo to deploy client to GitHub Pages | [https://survival2d.github.io/](https://survival2d.github.io/) | -| [tiennm99/20200411](https://github.com/tiennm99/20200411) | An old gift website. Archived 2021-02-06. | [https://tiennm99.github.io/20200411/](https://tiennm99.github.io/20200411/) | -| [tiennm99/20221225](https://github.com/tiennm99/20221225) | A small just-for-fun website built on Christmas Day 2022. | [https://tiennm99.github.io/20221225/](https://tiennm99.github.io/20221225/) | -| [tiennm99/ai-labs](https://github.com/tiennm99/ai-labs) | Experiments with AI-assisted coding workflows — ClaudeKit, Superpowers, BMAD, oh-my-claudecode, gstack, and course work, merged with full history | [https://tiennm99.github.io/ai-labs/](https://tiennm99.github.io/ai-labs/) | -| [tiennm99/apart](https://github.com/tiennm99/apart) | Small personal website with photos and music for someone far away. | [https://tiennm99.github.io/apart/](https://tiennm99.github.io/apart/) | | [tiennm99/awesome-ai-dev-tools](https://github.com/tiennm99/awesome-ai-dev-tools) | Daily-refreshed ranking of open-source AI developer tools — coding agents, ADEs, AI editors and code-review tools — by GitHub stars, with 7-day deltas, star-history charts and a tag-filtered dashboard. | [https://awesome-ai-dev-tools.pages.dev/](https://awesome-ai-dev-tools.pages.dev/) | -| [tiennm99/awesome-github-profile-readme](https://github.com/tiennm99/awesome-github-profile-readme) | 😎 A curated list of awesome GitHub Profile which updates in real time | [https://bit.ly/awesome-gh](https://bit.ly/awesome-gh) | -| [tiennm99/beentogether](https://github.com/tiennm99/beentogether) | Been Together — a small website celebrating time spent together with music and photos. | [https://tiennm99.github.io/beentogether/](https://tiennm99.github.io/beentogether/) | -| [tiennm99/blog](https://github.com/tiennm99/blog) | Source of miti99.com — personal blog (VN/EN) built with Hugo + Stack theme. | [https://miti99.com](https://miti99.com) | -| [tiennm99/ccs](https://github.com/tiennm99/ccs) | Switch between Claude accounts, Gemini, Copilot, OpenRouter (300+ models) via CLIProxyAPI OAuth proxy. Visual dashboard, remote proxy support, WebSearch fallback. Zero-config to production-ready. | [https://ccs.kaitran.ca/](https://ccs.kaitran.ca/) | -| [tiennm99/cf-worker-telegram](https://github.com/tiennm99/cf-worker-telegram) | A Cloudflare Worker that acts as a transparent proxy for the Telegram Bot API. Useful for bypassing network restrictions or as a middleware for your Telegram bot. | [https://telegram.miti99.workers.dev](https://telegram.miti99.workers.dev) | -| [tiennm99/claude-status-webhook](https://github.com/tiennm99/claude-status-webhook) | Telegram bot forwarding Claude Status updates — alerts for incidents and resolutions. Serverless on Cloudflare Workers. | [https://t.me/MiTiClaudeStatusBot](https://t.me/MiTiClaudeStatusBot) | -| [tiennm99/cloudflare-proxy](https://github.com/tiennm99/cloudflare-proxy) | Inspired by tuanpb99/cf-worker-telegram, this Cloudflare Worker script has been adapted to act as a transparent proxy for any HTTP or HTTPS URL, powered by Cloudflare’s global edge network. | [https://cloudflare-proxy.miti99.workers.dev/](https://cloudflare-proxy.miti99.workers.dev/) | -| [tiennm99/coolify](https://github.com/tiennm99/coolify) | An open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers. | [https://coolify.io](https://coolify.io) | -| [tiennm99/cv](https://github.com/tiennm99/cv) | My personal CV written in Typst with RenderCV. | [https://tiennm99.github.io/cv/](https://tiennm99.github.io/cv/) | -| [tiennm99/demngayxaem](https://github.com/tiennm99/demngayxaem) | Đếm ngày xa em — a small relationship 'days-apart' counter website. | [https://tiennm99.github.io/demngayxaem/](https://tiennm99.github.io/demngayxaem/) | -| [tiennm99/dental-visit](https://github.com/tiennm99/dental-visit) | Small website created for a dental visit appointment. | [https://tiennm99.github.io/dental-visit/](https://tiennm99.github.io/dental-visit/) | -| [tiennm99/DocsGPT](https://github.com/tiennm99/DocsGPT) | Private AI platform for agents, assistants and enterprise search. Built-in Agent Builder, Deep research, Document analysis, Multi-model support, and API connectivity for agents. | [https://app.docsgpt.cloud/](https://app.docsgpt.cloud/) | -| [tiennm99/double-click-test](https://github.com/tiennm99/double-click-test) | Website to quickly check a mouse for double-click issues. | [https://tiennm99.github.io/double-click-test/](https://tiennm99.github.io/double-click-test/) | -| [tiennm99/exchange-rate-export](https://github.com/tiennm99/exchange-rate-export) | Export BIDV and Techcombank FX rates to Excel — Next.js web app with date range selection | [https://exchange-rate-export.vercel.app/](https://exchange-rate-export.vercel.app/) | -| [tiennm99/free-domains](https://github.com/tiennm99/free-domains) | Free subdomains for personal sites, open-source projects, and more. | [https://freesubdomains.org](https://freesubdomains.org) | -| [tiennm99/free-for-dev](https://github.com/tiennm99/free-for-dev) | A list of SaaS, PaaS and IaaS offerings that have free tiers of interest to devops and infradev | [https://free-for.dev/](https://free-for.dev/) | -| [tiennm99/goclaw](https://github.com/tiennm99/goclaw) | GoClaw - GoClaw is OpenClaw rebuilt in Go — with multi-tenant isolation, 5-layer security, and native concurrency. Deploy AI agent teams at scale without compromising on safety. | [https://goclaw.sh](https://goclaw.sh) | -| [tiennm99/godot](https://github.com/tiennm99/godot) | Godot Engine – Multi-platform 2D and 3D game engine | [https://godotengine.org](https://godotengine.org) | -| [tiennm99/hugo-blox-builder](https://github.com/tiennm99/hugo-blox-builder) | 🧱 Describe your site, AI builds it, you own it as Markdown. Snap together Tailwind blocks like Lego — landing pages, blogs, portfolios, docs & more. No AI slop. Free to deploy anywhere 👇 | [https://wowchemy.com/templates/](https://wowchemy.com/templates/) | -| [tiennm99/hugoThemesSiteBuilder](https://github.com/tiennm99/hugoThemesSiteBuilder) | The source for https://themes.gohugo.io | [https://themes.gohugo.io](https://themes.gohugo.io) | -| [tiennm99/hurt](https://github.com/tiennm99/hurt) | Small website inspired by the film 'Thắp sáng anh, sưởi ấm em' — a little unusual, just for fun. | [https://tiennm99.github.io/hurt/](https://tiennm99.github.io/hurt/) | -| [tiennm99/is-a-dev](https://github.com/tiennm99/is-a-dev) | Grab your own sweet-looking '.is-a.dev' subdomain. | [https://is-a.dev](https://is-a.dev) | -| [tiennm99/is-a-good-dev](https://github.com/tiennm99/is-a-good-dev) | Register for your is-a-good.dev domain! | [https://is-a-good.dev](https://is-a-good.dev) | -| [tiennm99/java-design-patterns](https://github.com/tiennm99/java-design-patterns) | Design patterns implemented in Java | [https://java-design-patterns.com](https://java-design-patterns.com) | -| [tiennm99/litellm](https://github.com/tiennm99/litellm) | Python SDK, Proxy Server (AI Gateway) to call 100+ LLM APIs in OpenAI (or native) format, with cost tracking, guardrails, loadbalancing and logging. [Bedrock, Azure, OpenAI, VertexAI, Cohere, Anthropic, Sagemaker, HuggingFace, VLLM, NVIDIA NIM] | [https://docs.litellm.ai/docs/](https://docs.litellm.ai/docs/) | -| [tiennm99/llmapikey](https://github.com/tiennm99/llmapikey) | Pending/archived: free capped OpenRouter API key giveaway paused until a suitable LLM gateway/provider is found. | [https://llmapikey.vercel.app](https://llmapikey.vercel.app) | -| [tiennm99/loldle](https://github.com/tiennm99/loldle) | LoLdle-style League of Legends daily champion guessing game — SvelteKit web app, Go data scraper, and Android stub in one repo; champion data auto-updates weekly | [https://tiennm99.github.io/loldle/](https://tiennm99.github.io/loldle/) | -| [tiennm99/lombok](https://github.com/tiennm99/lombok) | Very spicy additions to the Java programming language. | [https://projectlombok.org/](https://projectlombok.org/) | -| [tiennm99/loto](https://github.com/tiennm99/loto) | Lô Tô (Vietnamese lottery bingo) number board — Next.js web app | [https://tiennm99.github.io/loto/](https://tiennm99.github.io/loto/) | -| [tiennm99/mai-birthday-2021](https://github.com/tiennm99/mai-birthday-2021) | Birthday page for Trúc Mai (2021) | [https://tiennm99.github.io/mai-birthday-2021/](https://tiennm99.github.io/mai-birthday-2021/) | -| [tiennm99/miti-loki](https://github.com/tiennm99/miti-loki) | Cloudflare Worker that forwards log messages to Grafana Cloud Loki. | [https://miti-loki.miti99.workers.dev/](https://miti-loki.miti99.workers.dev/) | -| [tiennm99/miti-telegram](https://github.com/tiennm99/miti-telegram) | Cloudflare Worker that forwards incoming messages to my Telegram account | [https://miti-telegram.miti99.workers.dev](https://miti-telegram.miti99.workers.dev) | -| [tiennm99/miti99bot](https://github.com/tiennm99/miti99bot) | Plug-n-play Telegram bot framework in Go, self-hosted on Coolify + MongoDB Atlas with cron, games, schedules, and paper trading modules. | [https://t.me/miti99bot](https://t.me/miti99bot) | -| [tiennm99/nntv](https://github.com/tiennm99/nntv) | Night Ninja: Twilight Voyage — turn-based stealth puzzle. Ninja rabbit rescues carrot princess from veggie guards. | [https://tiennm99.github.io/nntv/](https://tiennm99.github.io/nntv/) | -| [tiennm99/ocrot](https://github.com/tiennm99/ocrot) | Website mừng bạn Thân trở thành ông chủ quán ăn Ốc Rốt | [https://tiennm99.github.io/ocrot/](https://tiennm99.github.io/ocrot/) | -| [tiennm99/open-domains](https://github.com/tiennm99/open-domains) | Free subdomains for personal sites, open-source projects, and more. | [https://open-domains.net](https://open-domains.net) | -| [tiennm99/penny-pincher-provider](https://github.com/tiennm99/penny-pincher-provider) | Curated list of affordable + free LLM API providers — Claude Code passes, coding-plan subs, free-tier APIs. | [https://tiennm99.github.io/penny-pincher-provider/](https://tiennm99.github.io/penny-pincher-provider/) | -| [tiennm99/rplace](https://github.com/tiennm99/rplace) | Collaborative pixel-art canvas (r/place clone). Svelte 5 + Hono on Cloudflare Workers + Durable Object SQLite. | [https://rplace.miti99.workers.dev/](https://rplace.miti99.workers.dev/) | -| [tiennm99/sokoban](https://github.com/tiennm99/sokoban) | From-scratch Sokoban puzzle game — Svelte 5 + Vite, all 155 Microban levels, undo, progress tracking | [https://tiennm99.github.io/sokoban/](https://tiennm99.github.io/sokoban/) | -| [tiennm99/store-scraper-api](https://github.com/tiennm99/store-scraper-api) | HTTP wrapper hosting google-play-scraper and app-store-scraper — POST to /google/:method or /apple/:method | [https://store-scraper-api.vercel.app](https://store-scraper-api.vercel.app) | -| [tiennm99/store-scraper-bot](https://github.com/tiennm99/store-scraper-bot) | Telegram bot tracking Apple App Store + Google Play app updates. JS port of store-scraper-bot-java. | [https://store-scraper-bot.vercel.app](https://store-scraper-bot.vercel.app) | -| [tiennm99/styleguide](https://github.com/tiennm99/styleguide) | Style guides for Google-originated open-source projects | [https://google.github.io/styleguide/](https://google.github.io/styleguide/) | -| [tiennm99/sudoku-solver](https://github.com/tiennm99/sudoku-solver) | 9×9 Sudoku solver in the browser using recursive backtracking — plain JavaScript, Svelte 5 + Vite, on GitHub Pages. | [https://tiennm99.github.io/sudoku-solver/](https://tiennm99.github.io/sudoku-solver/) | -| [tiennm99/test-deeplink-sms](https://github.com/tiennm99/test-deeplink-sms) | Small test page for verifying SMS deeplink behavior on mobile devices. | [https://tiennm99.github.io/test-deeplink-sms/](https://tiennm99.github.io/test-deeplink-sms/) | -| [tiennm99/thedev.id](https://github.com/tiennm99/thedev.id) | Identity for developers on the web. | [https://thedev.id](https://thedev.id) | -| [tiennm99/thptqg](https://github.com/tiennm99/thptqg) | Tra cứu điểm thi THPT Quốc gia 2016–2017 — 1,7 triệu thí sinh · Truy vấn SQL client-side với sql.js | [https://tiennm99.github.io/thptqg/](https://tiennm99.github.io/thptqg/) | | [tiennm99/tiennm99](https://github.com/tiennm99/tiennm99) | Config files for my GitHub profile. | [https://github.com/tiennm99](https://github.com/tiennm99) | -| [tiennm99/tiennm99.github.io](https://github.com/tiennm99/tiennm99.github.io) | Redirect page for my personal website | [https://tiennm99.github.io/](https://tiennm99.github.io/) | -| [tiennm99/tn1](https://github.com/tiennm99/tn1) | Hugo photo gallery site built on the Gallery Deluxe module | [https://tiennm99.github.io/tn1/](https://tiennm99.github.io/tn1/) | -| [tiennm99/Valentine-Vue-Tailwind](https://github.com/tiennm99/Valentine-Vue-Tailwind) | Support @lds217 for Valentine 2025 | [https://tiennm99.github.io/Valentine-Vue-Tailwind/](https://tiennm99.github.io/Valentine-Vue-Tailwind/) | -| [tiennm99/valentine2025](https://github.com/tiennm99/valentine2025) | A fun Valentine's Day 2025 website. | [https://tiennm99.github.io/valentine2025/](https://tiennm99.github.io/valentine2025/) | -| [tiennm99/vigeoguessr](https://github.com/tiennm99/vigeoguessr) | Guess place on map from street view image | [https://vigeoguessr.netlify.app/](https://vigeoguessr.netlify.app/) | -| [tiennm99/vngeoguessr](https://github.com/tiennm99/vngeoguessr) | GeoGuessr clone for Vietnamese locations — Mapillary panoramas, Turf.js boundaries, Upstash REST leaderboards | [https://vngeoguessr.vercel.app](https://vngeoguessr.vercel.app) | -| [tiennm99/webgl-heart](https://github.com/tiennm99/webgl-heart) | | [https://tiennm99.github.io/webgl-heart/](https://tiennm99.github.io/webgl-heart/) | -| [tiennm99dev/artemis-demo-agent](https://github.com/tiennm99dev/artemis-demo-agent) | Vietnamese VNG hackathon demo agent for lost-and-found matching and an internal marketplace | [https://artemis-lime.vercel.app](https://artemis-lime.vercel.app) | -| [tiennm99dev/bonsai](https://github.com/tiennm99dev/bonsai) | Minimalist Hugo theme for link-in-bio pages — Linktree alternative with Japanese bonsai aesthetics. | [https://tiennm99dev.github.io/bonsai/](https://tiennm99dev.github.io/bonsai/) | -| [tiennm99dev/chudaibi](https://github.com/tiennm99dev/chudaibi) | Chú đại bi, từ bộ Kinh Nhật Tụng của chùa Hoằng Pháp | [https://thuvienhoangphap.github.io/chudaibi/](https://thuvienhoangphap.github.io/chudaibi/) | -| [tiennm99dev/colordle](https://github.com/tiennm99dev/colordle) | | [https://tiennm99dev.github.io/colordle/](https://tiennm99dev.github.io/colordle/) | -| [tiennm99dev/dating](https://github.com/tiennm99dev/dating) | Warm-witty dating JD and CV site built with SvelteKit | [https://tiennm99dev.github.io/dating/](https://tiennm99dev.github.io/dating/) | -| [tiennm99dev/github-readme-stats](https://github.com/tiennm99dev/github-readme-stats) | :zap: Dynamically generated stats for your github readmes | [https://github-readme-stats.vercel.app](https://github-readme-stats.vercel.app) | -| [tiennm99dev/iammiti99](https://github.com/tiennm99dev/iammiti99) | Personal link-in-bio page — a self-hosted Linktree alternative for my own use. | [https://tiennm99dev.github.io/iammiti99/](https://tiennm99dev.github.io/iammiti99/) | +| [tiennm99/blog](https://github.com/tiennm99/blog) | Source of miti99.com — personal blog (VN/EN) built with Hugo + Stack theme. | [https://miti99.com](https://miti99.com) | +| [tiennm99/miti99bot](https://github.com/tiennm99/miti99bot) | Plug-n-play Telegram bot framework in Go, self-hosted on Coolify + MongoDB Atlas with cron, games, schedules, and paper trading modules. | [https://t.me/miti99bot](https://t.me/miti99bot) | | [tiennm99dev/mathmax](https://github.com/tiennm99dev/mathmax) | MathMax — Toán tương tác cho học sinh THCS Việt Nam (lớp 6-9). Static site. | [https://tiennm99dev.github.io/mathmax/](https://tiennm99dev.github.io/mathmax/) | +| [tiennm99/penny-pincher-provider](https://github.com/tiennm99/penny-pincher-provider) | Curated list of affordable + free LLM API providers — Claude Code passes, coding-plan subs, free-tier APIs. | [https://tiennm99.github.io/penny-pincher-provider/](https://tiennm99.github.io/penny-pincher-provider/) | +| [tiennm99/loldle](https://github.com/tiennm99/loldle) | LoLdle-style League of Legends daily champion guessing game — SvelteKit web app, Go data scraper, and Android stub in one repo; champion data auto-updates weekly | [https://tiennm99.github.io/loldle/](https://tiennm99.github.io/loldle/) | +| [tiennm99/vngeoguessr](https://github.com/tiennm99/vngeoguessr) | GeoGuessr clone for Vietnamese locations — Mapillary panoramas, Turf.js boundaries, Upstash REST leaderboards | [https://vngeoguessr.vercel.app](https://vngeoguessr.vercel.app) | +| [bkfcduchoa/bkfcduchoa.github.io](https://github.com/bkfcduchoa/bkfcduchoa.github.io) | | [https://bkfcduchoa.github.io/](https://bkfcduchoa.github.io/) | +| [tiennm99/cv](https://github.com/tiennm99/cv) | My personal CV written in Typst with RenderCV. | [https://tiennm99.github.io/cv/](https://tiennm99.github.io/cv/) | +| [tiennm99/ai-labs](https://github.com/tiennm99/ai-labs) | Experiments with AI-assisted coding workflows — ClaudeKit, Superpowers, BMAD, oh-my-claudecode, gstack, and course work, merged with full history | [https://tiennm99.github.io/ai-labs/](https://tiennm99.github.io/ai-labs/) | +| [tiennm99/sokoban](https://github.com/tiennm99/sokoban) | From-scratch Sokoban puzzle game — Svelte 5 + Vite, all 155 Microban levels, undo, progress tracking | [https://tiennm99.github.io/sokoban/](https://tiennm99.github.io/sokoban/) | +| [tiennm99/loto](https://github.com/tiennm99/loto) | Lô Tô (Vietnamese lottery bingo) number board — Next.js web app | [https://tiennm99.github.io/loto/](https://tiennm99.github.io/loto/) | +| [tiennm99/sudoku-solver](https://github.com/tiennm99/sudoku-solver) | 9×9 Sudoku solver in the browser using recursive backtracking — plain JavaScript, Svelte 5 + Vite, on GitHub Pages. | [https://tiennm99.github.io/sudoku-solver/](https://tiennm99.github.io/sudoku-solver/) | +| [tiennm99dev/timshow](https://github.com/tiennm99dev/timshow) | Tìm show — find live shows and events in Vietnam. | [https://tiennm99dev.github.io/timshow/](https://tiennm99dev.github.io/timshow/) | | [tiennm99dev/pikachu](https://github.com/tiennm99dev/pikachu) | Pikachu tile-matching puzzle game — Phaser 3 + Next.js + TypeScript | [https://tiennm99dev.github.io/pikachu/](https://tiennm99dev.github.io/pikachu/) | -| [tiennm99dev/programming-fengshui](https://github.com/tiennm99dev/programming-fengshui) | Map programming languages to Ngũ Hành (Five Elements) by Linguist colors. Classic and HSL-classifier modes. | [https://tiennm99dev.github.io/programming-fengshui/](https://tiennm99dev.github.io/programming-fengshui/) | -| [tiennm99dev/quycongdongvi](https://github.com/tiennm99dev/quycongdongvi) | Website của Quỹ cộng đồng Vị | [https://tiennm99dev.github.io/quycongdongvi/](https://tiennm99dev.github.io/quycongdongvi/) | -| [tiennm99dev/reigen](https://github.com/tiennm99dev/reigen) | reigen | [https://tiennm99dev.github.io/reigen/](https://tiennm99dev.github.io/reigen/) | | [tiennm99dev/rubik](https://github.com/tiennm99dev/rubik) | Interactive 3x3 Rubik's cube in the browser — drag faces, scramble, timer, undo. Three.js + Svelte 5 + Vite. | [https://tiennm99dev.github.io/rubik/](https://tiennm99dev.github.io/rubik/) | -| [tiennm99dev/thuvienhoangphap](https://github.com/tiennm99dev/thuvienhoangphap) | Thư Viện Hoằng Pháp — Vietnamese Buddhist dharma library website. | [https://thuvienhoangphap.netlify.app/](https://thuvienhoangphap.netlify.app/) | -| [tiennm99dev/timshow](https://github.com/tiennm99dev/timshow) | Tìm show — find live shows and events in Vietnam. | [https://tiennm99dev.github.io/timshow](https://tiennm99dev.github.io/timshow) | -| [tiennm99dev/tsuki](https://github.com/tiennm99dev/tsuki) | 月 — Hugo blog & portfolio theme. Vietnamese-first typography, View Transitions, Pagefind search, Giscus comments. | [https://tiennm99dev.github.io/tsuki/](https://tiennm99dev.github.io/tsuki/) | +| [tiennm99/miti-telegram](https://github.com/tiennm99/miti-telegram) | Cloudflare Worker that forwards incoming messages to my Telegram account | [https://miti-telegram.miti99.workers.dev](https://miti-telegram.miti99.workers.dev) | +| [tiennm99/claude-status-webhook](https://github.com/tiennm99/claude-status-webhook) | Telegram bot forwarding Claude Status updates — alerts for incidents and resolutions. Serverless on Cloudflare Workers. | [https://t.me/MiTiClaudeStatusBot](https://t.me/MiTiClaudeStatusBot) | +| [tiennm99/hurt](https://github.com/tiennm99/hurt) | Small website inspired by the film 'Thắp sáng anh, sưởi ấm em' — a little unusual, just for fun. | [https://tiennm99.github.io/hurt/](https://tiennm99.github.io/hurt/) | +| [tiennm99/rplace](https://github.com/tiennm99/rplace) | Collaborative pixel-art canvas (r/place clone). Svelte 5 + Hono on Cloudflare Workers + Durable Object SQLite. | [https://rplace.miti99.workers.dev/](https://rplace.miti99.workers.dev/) | +| [tiennm99/store-scraper-bot](https://github.com/tiennm99/store-scraper-bot) | Telegram bot tracking Apple App Store + Google Play app updates. JS port of store-scraper-bot-java. | [https://store-scraper-bot.vercel.app](https://store-scraper-bot.vercel.app) | +| [tiennm99/nntv](https://github.com/tiennm99/nntv) | Night Ninja: Twilight Voyage — turn-based stealth puzzle. Ninja rabbit rescues carrot princess from veggie guards. | [https://tiennm99.github.io/nntv/](https://tiennm99.github.io/nntv/) | +| [tiennm99/miti-loki](https://github.com/tiennm99/miti-loki) | Cloudflare Worker that forwards log messages to Grafana Cloud Loki. | [https://miti-loki.miti99.workers.dev/](https://miti-loki.miti99.workers.dev/) | +| [tiennm99/store-scraper-api](https://github.com/tiennm99/store-scraper-api) | HTTP wrapper hosting google-play-scraper and app-store-scraper — POST to /google/:method or /apple/:method | [https://store-scraper-api.vercel.app](https://store-scraper-api.vercel.app) | +| [tiennm99/exchange-rate-export](https://github.com/tiennm99/exchange-rate-export) | Export BIDV and Techcombank FX rates to Excel — Next.js web app with date range selection | [https://exchange-rate-export.vercel.app/](https://exchange-rate-export.vercel.app/) | +| [tiennm99/thptqg](https://github.com/tiennm99/thptqg) | Tra cứu điểm thi THPT Quốc gia 2016–2017 — 1,7 triệu thí sinh · Truy vấn SQL client-side với sql.js | [https://tiennm99.github.io/thptqg/](https://tiennm99.github.io/thptqg/) | +| [tiennm99/ocrot](https://github.com/tiennm99/ocrot) | Website mừng bạn Thân trở thành ông chủ quán ăn Ốc Rốt | [https://tiennm99.github.io/ocrot/](https://tiennm99.github.io/ocrot/) | +| [tiennm99dev/reigen](https://github.com/tiennm99dev/reigen) | reigen | [https://tiennm99dev.github.io/reigen/](https://tiennm99dev.github.io/reigen/) | | [tiennm99dev/wolzard](https://github.com/tiennm99dev/wolzard) | | [https://tiennm99dev.github.io/wolzard/](https://tiennm99dev.github.io/wolzard/) | -| [tiennm99dev/xr2s25fe](https://github.com/tiennm99dev/xr2s25fe) | Personal device-upgrade datasheet — iPhone XR (2018) to Galaxy S25 FE (2025), rendered six ways: blueprint + five Apple-style formats. | [https://tiennm99dev.github.io/xr2s25fe/](https://tiennm99dev.github.io/xr2s25fe/) | +| [tiennm99dev/dating](https://github.com/tiennm99dev/dating) | Warm-witty dating JD and CV site built with SvelteKit | [https://tiennm99dev.github.io/dating/](https://tiennm99dev.github.io/dating/) | +| [tiennm99dev/tsuki](https://github.com/tiennm99dev/tsuki) | 月 — Hugo blog & portfolio theme. Vietnamese-first typography, View Transitions, Pagefind search, Giscus comments. | [https://tiennm99dev.github.io/tsuki/](https://tiennm99dev.github.io/tsuki/) | +| [tiennm99dev/bonsai](https://github.com/tiennm99dev/bonsai) | Minimalist Hugo theme for link-in-bio pages — Linktree alternative with Japanese bonsai aesthetics. | [https://tiennm99dev.github.io/bonsai/](https://tiennm99dev.github.io/bonsai/) | +| [ngamtheproject/ngamtheproject.github.io](https://github.com/ngamtheproject/ngamtheproject.github.io) | Website for Ngăm - a charity organization founded by high school students. | [https://ngamtheproject.github.io/](https://ngamtheproject.github.io/) | +| [tiennm99/llmapikey](https://github.com/tiennm99/llmapikey) | Pending/archived: free capped OpenRouter API key giveaway paused until a suitable LLM gateway/provider is found. | [https://llmapikey.vercel.app](https://llmapikey.vercel.app) | | [tienthieusac/tienthieusac.github.io](https://github.com/tienthieusac/tienthieusac.github.io) | Tiên Thiếu Sắc | [https://tienthieusac.github.io/](https://tienthieusac.github.io/) | +| [tiennm99dev/chudaibi](https://github.com/tiennm99dev/chudaibi) | Chú đại bi, từ bộ Kinh Nhật Tụng của chùa Hoằng Pháp | [https://thuvienhoangphap.github.io/chudaibi/](https://thuvienhoangphap.github.io/chudaibi/) | +| [tiennm99/dental-visit](https://github.com/tiennm99/dental-visit) | Small website created for a dental visit appointment. | [https://tiennm99.github.io/dental-visit/](https://tiennm99.github.io/dental-visit/) | +| [tiennm99/mai-birthday-2021](https://github.com/tiennm99/mai-birthday-2021) | Birthday page for Trúc Mai (2021) | [https://tiennm99.github.io/mai-birthday-2021/](https://tiennm99.github.io/mai-birthday-2021/) | +| [tiennm99/apart](https://github.com/tiennm99/apart) | Small personal website with photos and music for someone far away. | [https://tiennm99.github.io/apart/](https://tiennm99.github.io/apart/) | +| [tiennm99dev/xr2s25fe](https://github.com/tiennm99dev/xr2s25fe) | Personal device-upgrade datasheet — iPhone XR (2018) to Galaxy S25 FE (2025), rendered six ways: blueprint + five Apple-style formats. | [https://tiennm99dev.github.io/xr2s25fe/](https://tiennm99dev.github.io/xr2s25fe/) | +| [tiennm99/tiennm99.github.io](https://github.com/tiennm99/tiennm99.github.io) | Redirect page for my personal website | [https://tiennm99.github.io/](https://tiennm99.github.io/) | +| [tiennm99dev/iammiti99](https://github.com/tiennm99dev/iammiti99) | Personal link-in-bio page — a self-hosted Linktree alternative for my own use. | [https://tiennm99dev.github.io/iammiti99/](https://tiennm99dev.github.io/iammiti99/) | +| [tiennm99/demngayxaem](https://github.com/tiennm99/demngayxaem) | Đếm ngày xa em — a small relationship 'days-apart' counter website. | [https://tiennm99.github.io/demngayxaem/](https://tiennm99.github.io/demngayxaem/) | +| [tiennm99/beentogether](https://github.com/tiennm99/beentogether) | Been Together — a small website celebrating time spent together with music and photos. | [https://tiennm99.github.io/beentogether/](https://tiennm99.github.io/beentogether/) | +| [tiennm99/20221225](https://github.com/tiennm99/20221225) | A small just-for-fun website built on Christmas Day 2022. | [https://tiennm99.github.io/20221225/](https://tiennm99.github.io/20221225/) | +| [tiennm99dev/programming-fengshui](https://github.com/tiennm99dev/programming-fengshui) | Map programming languages to Ngũ Hành (Five Elements) by Linguist colors. Classic and HSL-classifier modes. | [https://tiennm99dev.github.io/programming-fengshui/](https://tiennm99dev.github.io/programming-fengshui/) | +| [tiennm99/20200411](https://github.com/tiennm99/20200411) | An old gift website. Archived 2021-02-06. | [https://tiennm99.github.io/20200411/](https://tiennm99.github.io/20200411/) | +| [tiennm99dev/thuvienhoangphap](https://github.com/tiennm99dev/thuvienhoangphap) | Thư Viện Hoằng Pháp — Vietnamese Buddhist dharma library website. | [https://thuvienhoangphap.netlify.app/](https://thuvienhoangphap.netlify.app/) | +| [tiennm99/test-deeplink-sms](https://github.com/tiennm99/test-deeplink-sms) | Small test page for verifying SMS deeplink behavior on mobile devices. | [https://tiennm99.github.io/test-deeplink-sms/](https://tiennm99.github.io/test-deeplink-sms/) | +| [tiennm99dev/quycongdongvi](https://github.com/tiennm99dev/quycongdongvi) | Website của Quỹ cộng đồng Vị | [https://tiennm99dev.github.io/quycongdongvi/](https://tiennm99dev.github.io/quycongdongvi/) | +| [tiennm99/valentine2025](https://github.com/tiennm99/valentine2025) | A fun Valentine's Day 2025 website. | [https://tiennm99.github.io/valentine2025/](https://tiennm99.github.io/valentine2025/) | +| [tiennm99/double-click-test](https://github.com/tiennm99/double-click-test) | Website to quickly check a mouse for double-click issues. | [https://tiennm99.github.io/double-click-test/](https://tiennm99.github.io/double-click-test/) | +| [ngamtheproject/ngamtheproject](https://github.com/ngamtheproject/ngamtheproject) | Redirect to Ngăm's website | [https://ngamtheproject.github.io/ngamtheproject/](https://ngamtheproject.github.io/ngamtheproject/) | +| [tiennm99/tn1](https://github.com/tiennm99/tn1) | Hugo photo gallery site built on the Gallery Deluxe module | [https://tiennm99.github.io/tn1/](https://tiennm99.github.io/tn1/) | +| [survival2d/survival2d.github.io](https://github.com/survival2d/survival2d.github.io) | Repo to deploy client to GitHub Pages | [https://survival2d.github.io/](https://survival2d.github.io/) | - \ No newline at end of file + + +## 3. Projects mình đóng góp + +Repo của người khác (không phải của mình, cũng không thuộc org mình làm owner) mà +mình có pull request đã được merge. Sắp xếp theo số PR đã merge giảm dần, cùng số +PR thì repo cập nhật gần đây hơn lên trước. + + + +| Name | Description | Merged PRs | Stars | +| --- | --- | --- | --- | +| [iluwatar/java-design-patterns](https://github.com/iluwatar/java-design-patterns) | Design patterns implemented in Java | 6 | 94706 | +| [is-a-dev/register](https://github.com/is-a-dev/register) | Grab your own sweet-looking '.is-a.dev' subdomain. | 4 | 11323 | +| [is-a-good-dev/register](https://github.com/is-a-good-dev/register) | Register for your is-a-good.dev domain! | 4 | 620 | +| [open-domains/register](https://github.com/open-domains/register) | Free subdomains for personal sites, open-source projects, and more. | 4 | 2601 | +| [coollabsio/coolify](https://github.com/coollabsio/coolify) | An open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers. | 3 | 61962 | +| [tarampampam/domains](https://github.com/tarampampam/domains) | 🌐 DNS configuration for some of my domains | 3 | 807 | +| [zfoo-project/zfoo](https://github.com/zfoo-project/zfoo) | 💡Extremely fast enterprise server framework, can be used in RPC, game server, web server. | 3 | 2014 | +| [old-free-domains/register](https://github.com/old-free-domains/register) | Free subdomains for personal sites, open-source projects, and more. | 2 | 598 | +| [onyx-dot-app/documentation](https://github.com/onyx-dot-app/documentation) | Documentation for the Onyx Software | 1 | 33 | +| [oraios/serena](https://github.com/oraios/serena) | A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent | 1 | 29543 | +| [sirmalloc/ccstatusline](https://github.com/sirmalloc/ccstatusline) | 🚀 Beautiful highly customizable statusline for Claude Code CLI with powerline support, themes, and more. | 1 | 12930 | +| [ripienaar/free-for-dev](https://github.com/ripienaar/free-for-dev) | A list of SaaS, PaaS and IaaS offerings that have free tiers of interest to devops and infradev | 1 | 137712 | +| [Bogdanp/awesome-advent-of-code](https://github.com/Bogdanp/awesome-advent-of-code) | A collection of awesome resources related to the yearly Advent of Code challenge. | 1 | 3225 | +| [kaitranntt/ccs](https://github.com/kaitranntt/ccs) | Switch between Claude accounts, Gemini, Copilot, OpenRouter (300+ models) via CLIProxyAPI OAuth proxy. Visual dashboard, remote proxy support, WebSearch fallback. Zero-config to production-ready. | 1 | 2856 | +| [projectlombok/lombok](https://github.com/projectlombok/lombok) | Very spicy additions to the Java programming language. | 1 | 13484 | +| [HugoBlox/kit](https://github.com/HugoBlox/kit) | 🧱 Describe your site, AI builds it, you own it as Markdown. Snap together Tailwind blocks like Lego — landing pages, blogs, portfolios, docs & more. No AI slop. Free to deploy anywhere 👇 | 1 | 9722 | +| [tkowalcz/tjahzi](https://github.com/tkowalcz/tjahzi) | Java clients, log4j2 and logback appenders for Grafana Loki | 1 | 139 | +| [dhyeythumar/awesome-readme-tools](https://github.com/dhyeythumar/awesome-readme-tools) | A curated list of awesome readme tools. ✌ | 1 | 265 | +| [thedev-id/thedev.id](https://github.com/thedev-id/thedev.id) | Identity for developers on the web. | 1 | 1728 | +| [is-amazing/register](https://github.com/is-amazing/register) | Register your is-amaz.ing project domain here! | 1 | 149 | +| [DavidNiessen/SimpleGuicePlugin](https://github.com/DavidNiessen/SimpleGuicePlugin) | An example for dependency injection (Google Guice) in a Spigot plugin | 1 | 2 | + +