feat(projects): update Elynd details and add Linky project

- Revise Elynd from experimental AI workspace to AI English learning platform
- Add Linky as new indie project (self-hosted bookmark manager)
- Add status badges (completed/in-progress) to project cards
- Add Preview and GitHub action buttons to project cards
- Improve card layout with flexbox for better content alignment
This commit is contained in:
zguiyang
2026-03-17 11:05:43 +08:00
parent 123b3edc64
commit 014430e1b7
3 changed files with 266 additions and 60 deletions

View File

@@ -159,32 +159,105 @@ const sharedCases: Project[] = [
}, },
{ {
id: 'elynd', id: 'elynd',
featured: false, featured: true,
type: 'experiment', type: 'product',
status: 'building', status: 'building',
role: 'Founder & Developer', role: 'Founder & Developer',
impact: 'Exploring AI-assisted development workflows in production-like scenarios', impact: 'Built an AI-powered English reading and learning platform for language learners',
systemType: 'AI-assisted workspace', systemType: 'AI-powered English reading & learning platform',
context: 'An open workspace product exploring practical AI collaboration in software development.', context: 'An AI-assisted English reading learning tool that combines reading, listening, word lookup, and AI Q&A to help users practice low-barrier language input.',
title: 'Elynd', title: 'Elynd - AI English Learning Platform',
icon: 'Zap', icon: 'BookOpen',
color: 'purple', color: 'purple',
image: { image: {
bg: 'from-purple-500/20 to-blue-500/20', bg: 'from-purple-500/20 to-blue-500/20',
hover: 'from-purple-500/30 to-blue-500/30', hover: 'from-purple-500/30 to-blue-500/30',
text: 'text-purple-500', text: 'text-purple-500',
}, },
challenges: ['Designing useful AI workflows without adding process burden'], challenges: [
responsibilities: ['Product exploration and end-to-end implementation'], 'Designing a smooth graded reading experience with content difficulty adaptation',
outcomes: ['Validated several practical AI-assisted development patterns'], 'Implementing real-time TTS audio playback with Azure Speech SDK',
description: [ 'Building an efficient word lookup system with instant definitions',
'An open AI workspace for builders.', 'Creating AI-powered Q&A that understands reading context',
'Used as an R&D track, not the primary professional narrative.',
], ],
coverImage: '', responsibilities: [
coverImageAlt: 'Elynd project cover', 'End-to-end product development from concept to launch',
tech: ['TypeScript', 'React', 'AI Workflow', 'Open Source'], 'Designed and implemented backend API with AdonisJS v6',
link: '#', 'Built frontend with Vue 3, TypeScript, and modern UI components',
'Integrated OpenAI SDK for AI conversation features',
'Implemented EPUB import and content parsing pipeline',
],
outcomes: [
'Launched a complete learning platform with 4 core features',
'Achieved smooth reading experience with progress tracking',
'Integrated Azure TTS for immersive listening practice',
'Enabled AI-powered interactive learning with context awareness',
],
description: [
'AI-powered English reading and learning platform.',
'Combines reading, listening, word lookup, and AI Q&A for comprehensive language learning.',
],
tech: [
'AdonisJS v6',
'PostgreSQL',
'Redis',
'Vue 3',
'TypeScript',
'Pinia',
'Reka UI',
'shadcn-vue',
'Tailwind CSS 4',
'OpenAI SDK',
'Azure Speech SDK',
'pnpm workspace',
],
link: 'https://github.com/zguiyang/elynd',
links: {
github: 'https://github.com/zguiyang/elynd',
demo: 'https://elynd.zhaoguiyang.com/',
},
},
{
id: 'linky',
featured: true,
type: 'product',
status: 'completed',
role: 'Founder & Developer',
impact: 'Built a self-hosted knowledge management tool for bookmarks and notes',
systemType: 'Personal knowledge management system',
context: 'A simple, self-hosted tool for organizing bookmarks and writing notes with full user data control.',
title: 'Linky',
icon: 'Link',
color: 'cyan',
image: {
bg: 'from-cyan-500/20 to-blue-500/20',
hover: 'from-cyan-500/30 to-blue-500/30',
text: 'text-cyan-500',
},
challenges: [
'Designing intuitive bookmark organization with tags',
'Implementing AI-powered auto-tagging feature',
'Building self-hosted authentication system',
],
responsibilities: [
'End-to-end product development',
'Backend API design with AdonisJS',
'Frontend implementation with Nuxt',
],
outcomes: [
'Launched self-hosted version with full user data ownership',
'Integrated AI auto-tagging with OpenAI compatible APIs',
],
description: [
'A self-hosted bookmark manager and note-taking tool.',
'Focus on privacy, simplicity, and AI-assisted organization.',
],
tech: ['AdonisJS', 'Nuxt', 'PostgreSQL', 'Vue', 'Pinia', 'Tailwind CSS'],
link: 'https://github.com/zguiyang/linky',
links: {
github: 'https://github.com/zguiyang/linky',
demo: 'https://linky.zhaoguiyang.com/',
},
}, },
]; ];
@@ -246,15 +319,74 @@ export const projects = {
{ {
...sharedCases[4], ...sharedCases[4],
role: '创始人 & 开发者', role: '创始人 & 开发者',
impact: '在真实场景中探索 AI 协作开发工作流', impact: '打造 AI 辅助英语阅读学习平台,帮助语言学习者提升阅读能力',
systemType: 'AI 协作工作空间', systemType: 'AI 驱动英语阅读学习平台',
context: '探索 AI 在软件工程中可落地协作方式的开放产品。', context: 'AI 辅助的英语阅读学习工具,通过「阅读 + 听读 + 查词 + AI 提问」四位一体帮助用户完成低门槛的语言输入练习。',
title: 'Elynd', title: 'Elynd - AI 英语学习平台',
challenges: ['在不增加流程负担前提下设计有效 AI 工作流'], challenges: [
responsibilities: ['产品探索与端到端实现'], '设计流畅的分级阅读体验,实现内容难度自适应',
outcomes: ['验证多种可实践的 AI 协作开发模式'], '集成 Azure Speech SDK 实现实时 TTS 音频播放',
description: ['一个面向构建者的开放 AI 工作空间。', '作为研发探索方向,不作为职业主叙事。'], '构建高效的即点查词系统,支持即时释义显示',
tech: ['TypeScript', 'React', 'AI 协作', '开源'], '打造理解阅读上下文的 AI 智能问答功能',
],
responsibilities: [
'从概念到上线的端到端产品开发',
'使用 AdonisJS v6 设计与实现后端 API',
'使用 Vue 3、TypeScript 和现代 UI 组件构建前端',
'集成 OpenAI SDK 实现 AI 对话功能',
'实现 EPUB 导入与内容解析流程',
],
outcomes: [
'上线具备 4 大核心功能的完整学习平台',
'实现流畅阅读体验与学习进度跟踪',
'集成 Azure TTS 实现沉浸式听读练习',
'支持上下文感知的 AI 智能交互学习',
],
description: [
'AI 驱动的英语阅读学习平台。',
'融合阅读、听读、查词、AI 问答,一站式语言学习体验。',
],
tech: [
'AdonisJS v6',
'PostgreSQL',
'Redis',
'Vue 3',
'TypeScript',
'Pinia',
'Reka UI',
'shadcn-vue',
'Tailwind CSS 4',
'OpenAI SDK',
'Azure Speech SDK',
'pnpm workspace',
],
},
{
...sharedCases[5],
role: '创始人 & 开发者',
impact: '打造自托管的书签管理与笔记工具,让用户完全掌控自己的数据',
systemType: '个人知识管理系统',
context: '简单、自托管的工具,用于组织书签和写笔记,数据完全由用户自己掌控。',
title: 'Linky',
challenges: [
'设计直观的书签组织系统,支持标签化管理',
'实现 AI 自动标签功能',
'构建自托管认证系统',
],
responsibilities: [
'端到端产品开发',
'使用 AdonisJS 设计后端 API',
'使用 Nuxt 实现前端',
],
outcomes: [
'上线自托管版本,用户完全拥有数据主权',
'集成 AI 自动标签功能,支持 OpenAI 兼容 API',
],
description: [
'自托管的书签管理器和笔记工具。',
'注重隐私、简洁和 AI 辅助整理。',
],
tech: ['AdonisJS', 'Nuxt', 'PostgreSQL', 'Vue', 'Pinia', 'Tailwind CSS'],
}, },
], ],
}; };

View File

@@ -11,7 +11,7 @@ const lang = (Astro.currentLocale as Lang) || defaultLang;
const isZh = lang === 'zh'; const isZh = lang === 'zh';
const pageProjects = projects[lang].map((project) => ({ const pageProjects = projects[lang].map((project) => ({
...project, ...project,
category: project.id === 'elynd' ? 'indie' : 'enterprise', category: project.id === 'elynd' || project.id === 'linky' ? 'indie' : 'enterprise',
})); }));
const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise'); const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise');
const indieProjects = pageProjects.filter((project) => project.category === 'indie'); const indieProjects = pageProjects.filter((project) => project.category === 'indie');
@@ -66,7 +66,7 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
</div> </div>
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3"> <div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
{indieProjects.map((project) => ( {indieProjects.map((project) => (
<article class="page-surface overflow-hidden"> <article class="page-surface h-full overflow-hidden flex flex-col">
<div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30"> <div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30">
{project.coverImage ? ( {project.coverImage ? (
<img <img
@@ -83,22 +83,59 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
<p class="mt-1 text-sm text-foreground/80">{project.systemType}</p> <p class="mt-1 text-sm text-foreground/80">{project.systemType}</p>
</div> </div>
)} )}
</div> {/* Status Badge */}
<div class="p-5"> <div class="absolute right-3 top-3">
<p class="text-xs font-semibold uppercase tracking-wider text-primary/80">{isZh ? '独立项目' : 'Independent'}</p> <span
<h3 class="mt-2 text-lg font-bold">{project.title}</h3> class={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
<p class="text-sm text-muted-foreground">{project.context}</p> project.status === 'completed'
<div class="mt-4 flex flex-wrap gap-2"> ? 'bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30'
{project.tech.slice(0, 5).map((tech) => ( : 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border border-amber-500/30'
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span> }`}
))} >
{project.status === 'completed' ? (isZh ? '已完成' : 'Completed') : (isZh ? '开发中' : 'In Progress')}
</span>
</div>
</div>
<div class="flex flex-1 flex-col p-5">
<div class="flex flex-1 flex-col pb-4">
<h3 class="text-lg font-bold">{project.title}</h3>
{project.context && <p class="mt-2 text-sm text-muted-foreground">{project.context}</p>}
<div class="mt-4 flex flex-wrap gap-2">
{project.tech.slice(0, 5).map((tech) => (
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
))}
</div>
</div>
{/* Action Buttons */}
<div class="mt-auto flex gap-3 border-t border-border/50 pt-5">
{project.links?.demo && (
<a
href={project.links.demo}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3.5 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
<path d="M12.232 4.232a2.5 2.5 0 0 1 3.536 3.536l-1.225 1.224a.75.75 0 0 0 1.061 1.06l1.224-1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0 .225 5.865.75.75 0 0 0 .977-1.139 2.5 2.5 0 0 1-.142-3.635l3-3Z" />
<path d="M11.603 7.963a.75.75 0 0 0-.977 1.139 2.5 2.5 0 0 1 .142 3.635l-3 3a4 4 0 0 0 5.656 5.656l1.224-1.224a.75.75 0 0 0-1.06-1.06l-1.224 1.224a2.5 2.5 0 0 1-3.536-3.536l1.225-1.224a.75.75 0 0 0-1.061-1.06l-1.224 1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0-.225 5.865Z" />
</svg>
{isZh ? '预览' : 'Preview'}
</a>
)}
{project.links?.github && (
<a
href={project.links.github}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3.5 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-muted"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
GitHub
</a>
)}
</div> </div>
<p class="mt-4 text-sm font-semibold text-foreground/90">{isZh ? '结果:' : 'Outcome:'} {project.outcomes?.[0] ?? project.impact}</p>
{project.link !== '#' && (
<a href={project.link} target="_blank" rel="noopener noreferrer" class="mt-3 inline-flex text-sm font-semibold text-primary hover:text-primary/80">
{isZh ? '查看项目' : 'Open Project'}
</a>
)}
</div> </div>
</article> </article>
))} ))}

View File

@@ -11,7 +11,7 @@ const lang = (Astro.currentLocale as Lang) || defaultLang;
const isZh = lang === 'zh'; const isZh = lang === 'zh';
const pageProjects = projects[lang].map((project) => ({ const pageProjects = projects[lang].map((project) => ({
...project, ...project,
category: project.id === 'elynd' ? 'indie' : 'enterprise', category: project.id === 'elynd' || project.id === 'linky' ? 'indie' : 'enterprise',
})); }));
const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise'); const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise');
const indieProjects = pageProjects.filter((project) => project.category === 'indie'); const indieProjects = pageProjects.filter((project) => project.category === 'indie');
@@ -66,7 +66,7 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
</div> </div>
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3"> <div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
{indieProjects.map((project) => ( {indieProjects.map((project) => (
<article class="page-surface overflow-hidden"> <article class="page-surface h-full overflow-hidden flex flex-col">
<div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30"> <div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30">
{project.coverImage ? ( {project.coverImage ? (
<img <img
@@ -83,22 +83,59 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
<p class="mt-1 text-sm text-foreground/80">{project.systemType}</p> <p class="mt-1 text-sm text-foreground/80">{project.systemType}</p>
</div> </div>
)} )}
</div> {/* Status Badge */}
<div class="p-5"> <div class="absolute right-3 top-3">
<p class="text-xs font-semibold uppercase tracking-wider text-primary/80">{isZh ? '独立项目' : 'Independent'}</p> <span
<h3 class="mt-2 text-lg font-bold">{project.title}</h3> class={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
<p class="text-sm text-muted-foreground">{project.context}</p> project.status === 'completed'
<div class="mt-4 flex flex-wrap gap-2"> ? 'bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30'
{project.tech.slice(0, 5).map((tech) => ( : 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border border-amber-500/30'
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span> }`}
))} >
{project.status === 'completed' ? (isZh ? '已完成' : 'Completed') : (isZh ? '开发中' : 'In Progress')}
</span>
</div>
</div>
<div class="flex flex-1 flex-col p-5">
<div class="flex flex-1 flex-col pb-4">
<h3 class="text-lg font-bold">{project.title}</h3>
{project.context && <p class="mt-2 text-sm text-muted-foreground">{project.context}</p>}
<div class="mt-4 flex flex-wrap gap-2">
{project.tech.slice(0, 5).map((tech) => (
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
))}
</div>
</div>
{/* Action Buttons */}
<div class="mt-auto flex gap-3 border-t border-border/50 pt-5">
{project.links?.demo && (
<a
href={project.links.demo}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3.5 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
<path d="M12.232 4.232a2.5 2.5 0 0 1 3.536 3.536l-1.225 1.224a.75.75 0 0 0 1.061 1.06l1.224-1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0 .225 5.865.75.75 0 0 0 .977-1.139 2.5 2.5 0 0 1-.142-3.635l3-3Z" />
<path d="M11.603 7.963a.75.75 0 0 0-.977 1.139 2.5 2.5 0 0 1 .142 3.635l-3 3a4 4 0 0 0 5.656 5.656l1.224-1.224a.75.75 0 0 0-1.06-1.06l-1.224 1.224a2.5 2.5 0 0 1-3.536-3.536l1.225-1.224a.75.75 0 0 0-1.061-1.06l-1.224 1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0-.225 5.865Z" />
</svg>
{isZh ? '预览' : 'Preview'}
</a>
)}
{project.links?.github && (
<a
href={project.links.github}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3.5 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-muted"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
GitHub
</a>
)}
</div> </div>
<p class="mt-4 text-sm font-semibold text-foreground/90">{isZh ? '结果:' : 'Outcome:'} {project.outcomes?.[0] ?? project.impact}</p>
{project.link !== '#' && (
<a href={project.link} target="_blank" rel="noopener noreferrer" class="mt-3 inline-flex text-sm font-semibold text-primary hover:text-primary/80">
{isZh ? '查看项目' : 'Open Project'}
</a>
)}
</div> </div>
</article> </article>
))} ))}