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',
featured: false,
type: 'experiment',
featured: true,
type: 'product',
status: 'building',
role: 'Founder & Developer',
impact: 'Exploring AI-assisted development workflows in production-like scenarios',
systemType: 'AI-assisted workspace',
context: 'An open workspace product exploring practical AI collaboration in software development.',
title: 'Elynd',
icon: 'Zap',
impact: 'Built an AI-powered English reading and learning platform for language learners',
systemType: 'AI-powered English reading & learning platform',
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 - AI English Learning Platform',
icon: 'BookOpen',
color: 'purple',
image: {
bg: 'from-purple-500/20 to-blue-500/20',
hover: 'from-purple-500/30 to-blue-500/30',
text: 'text-purple-500',
},
challenges: ['Designing useful AI workflows without adding process burden'],
responsibilities: ['Product exploration and end-to-end implementation'],
outcomes: ['Validated several practical AI-assisted development patterns'],
description: [
'An open AI workspace for builders.',
'Used as an R&D track, not the primary professional narrative.',
challenges: [
'Designing a smooth graded reading experience with content difficulty adaptation',
'Implementing real-time TTS audio playback with Azure Speech SDK',
'Building an efficient word lookup system with instant definitions',
'Creating AI-powered Q&A that understands reading context',
],
coverImage: '',
coverImageAlt: 'Elynd project cover',
tech: ['TypeScript', 'React', 'AI Workflow', 'Open Source'],
link: '#',
responsibilities: [
'End-to-end product development from concept to launch',
'Designed and implemented backend API with AdonisJS v6',
'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],
role: '创始人 & 开发者',
impact: '在真实场景中探索 AI 协作开发工作流',
systemType: 'AI 协作工作空间',
context: '探索 AI 在软件工程中可落地协作方式的开放产品。',
title: 'Elynd',
challenges: ['在不增加流程负担前提下设计有效 AI 工作流'],
responsibilities: ['产品探索与端到端实现'],
outcomes: ['验证多种可实践的 AI 协作开发模式'],
description: ['一个面向构建者的开放 AI 工作空间。', '作为研发探索方向,不作为职业主叙事。'],
tech: ['TypeScript', 'React', 'AI 协作', '开源'],
impact: '打造 AI 辅助英语阅读学习平台,帮助语言学习者提升阅读能力',
systemType: 'AI 驱动英语阅读学习平台',
context: 'AI 辅助的英语阅读学习工具,通过「阅读 + 听读 + 查词 + AI 提问」四位一体帮助用户完成低门槛的语言输入练习。',
title: 'Elynd - AI 英语学习平台',
challenges: [
'设计流畅的分级阅读体验,实现内容难度自适应',
'集成 Azure Speech SDK 实现实时 TTS 音频播放',
'构建高效的即点查词系统,支持即时释义显示',
'打造理解阅读上下文的 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 pageProjects = projects[lang].map((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 indieProjects = pageProjects.filter((project) => project.category === 'indie');
@@ -66,7 +66,7 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
</div>
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
{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">
{project.coverImage ? (
<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>
</div>
)}
{/* Status Badge */}
<div class="absolute right-3 top-3">
<span
class={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
project.status === 'completed'
? 'bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30'
: 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border border-amber-500/30'
}`}
>
{project.status === 'completed' ? (isZh ? '已完成' : 'Completed') : (isZh ? '开发中' : 'In Progress')}
</span>
</div>
<div class="p-5">
<p class="text-xs font-semibold uppercase tracking-wider text-primary/80">{isZh ? '独立项目' : 'Independent'}</p>
<h3 class="mt-2 text-lg font-bold">{project.title}</h3>
<p class="text-sm text-muted-foreground">{project.context}</p>
</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>
<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'}
</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>
</article>
))}

View File

@@ -11,7 +11,7 @@ const lang = (Astro.currentLocale as Lang) || defaultLang;
const isZh = lang === 'zh';
const pageProjects = projects[lang].map((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 indieProjects = pageProjects.filter((project) => project.category === 'indie');
@@ -66,7 +66,7 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
</div>
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
{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">
{project.coverImage ? (
<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>
</div>
)}
{/* Status Badge */}
<div class="absolute right-3 top-3">
<span
class={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
project.status === 'completed'
? 'bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30'
: 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border border-amber-500/30'
}`}
>
{project.status === 'completed' ? (isZh ? '已完成' : 'Completed') : (isZh ? '开发中' : 'In Progress')}
</span>
</div>
<div class="p-5">
<p class="text-xs font-semibold uppercase tracking-wider text-primary/80">{isZh ? '独立项目' : 'Independent'}</p>
<h3 class="mt-2 text-lg font-bold">{project.title}</h3>
<p class="text-sm text-muted-foreground">{project.context}</p>
</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>
<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'}
</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>
</article>
))}