Skip to content

Commit e8cc7a6

Browse files
committed
feat(playground): update UI to match apps/desktop with shadcn-ui
- Add types and store (same as apps/desktop) - Update Home page with series and tutorial cards - Add series detail page - Add tutorial detail page with terminal execution - Add terminal panel component - Update app shell to support terminal positioning
1 parent 58a5890 commit e8cc7a6

File tree

10 files changed

+1326
-226
lines changed

10 files changed

+1326
-226
lines changed

playground/apps/desktop/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"react-dom": "19.2.4",
2222
"react-markdown": "^10.1.0",
2323
"rehype-highlight": "^7.0.2",
24-
"remark-gfm": "^4.0.1"
24+
"remark-gfm": "^4.0.1",
25+
"zustand": "^5.0.12"
2526
},
2627
"devDependencies": {
2728
"@innate/tsconfig": "workspace:*",

playground/apps/desktop/src/app/page.tsx

Lines changed: 309 additions & 95 deletions
Large diffs are not rendered by default.
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
"use client";
2+
3+
import { useParams, useRouter } from "next/navigation";
4+
import { useAppStore } from "@/store/useAppStore";
5+
import {
6+
Card,
7+
CardContent,
8+
CardHeader,
9+
CardTitle,
10+
Button,
11+
Badge,
12+
} from "@innate/ui";
13+
import {
14+
ArrowLeft,
15+
BookOpen,
16+
Clock,
17+
BarChart3,
18+
Trophy,
19+
Play,
20+
Sparkles,
21+
CheckCircle,
22+
Circle,
23+
} from "lucide-react";
24+
25+
export default function SeriesDetail() {
26+
const params = useParams();
27+
const router = useRouter();
28+
const seriesId = params.id as string;
29+
30+
const { series, getTutorialsBySeries, progress } = useAppStore();
31+
32+
const currentSeries = series.find((s) => s.id === seriesId);
33+
const tutorials = getTutorialsBySeries(seriesId);
34+
35+
if (!currentSeries) {
36+
return (
37+
<div className="flex items-center justify-center h-full">
38+
<div className="text-muted-foreground">系列不存在</div>
39+
</div>
40+
);
41+
}
42+
43+
const completedCount = tutorials.filter((t) => progress[t.id]?.completed).length;
44+
const progressPercent = tutorials.length > 0 ? (completedCount / tutorials.length) * 100 : 0;
45+
const totalDuration = tutorials.reduce((sum, t) => sum + t.duration, 0);
46+
const nextTutorial = tutorials.find((t) => !progress[t.id]?.completed);
47+
48+
const getDifficultyColor = (difficulty: string) => {
49+
switch (difficulty) {
50+
case "beginner":
51+
return "from-emerald-500 to-teal-500";
52+
case "intermediate":
53+
return "from-amber-500 to-orange-500";
54+
case "advanced":
55+
return "from-rose-500 to-pink-500";
56+
default:
57+
return "from-primary to-secondary";
58+
}
59+
};
60+
61+
const getDifficultyText = (difficulty: string) => {
62+
switch (difficulty) {
63+
case "beginner":
64+
return "入门";
65+
case "intermediate":
66+
return "进阶";
67+
case "advanced":
68+
return "高级";
69+
default:
70+
return "入门";
71+
}
72+
};
73+
74+
return (
75+
<div className="flex flex-col h-full overflow-auto">
76+
{/* Hero Header */}
77+
<div className="relative overflow-hidden">
78+
<div
79+
className="absolute inset-0 opacity-20"
80+
style={{
81+
background: `linear-gradient(135deg, ${currentSeries.color}40 0%, transparent 60%)`,
82+
}}
83+
/>
84+
<div
85+
className="absolute top-0 right-0 w-96 h-96 rounded-full blur-3xl -translate-y-1/2 translate-x-1/4"
86+
style={{ background: `${currentSeries.color}30` }}
87+
/>
88+
89+
<div className="relative px-8 py-8">
90+
<Button
91+
variant="ghost"
92+
onClick={() => router.push("/")}
93+
className="mb-4"
94+
>
95+
<ArrowLeft className="mr-2" size={16} />
96+
返回系列列表
97+
</Button>
98+
99+
<div className="flex flex-col md:flex-row md:items-start gap-6">
100+
<div
101+
className="w-20 h-20 rounded-2xl flex items-center justify-center text-4xl shrink-0"
102+
style={{
103+
background: `linear-gradient(135deg, ${currentSeries.color}30 0%, ${currentSeries.color}50 100%)`,
104+
}}
105+
>
106+
{currentSeries.icon || "📚"}
107+
</div>
108+
109+
<div className="flex-1">
110+
<div className="flex flex-wrap items-center gap-3 mb-3">
111+
<Badge
112+
className={`text-white bg-gradient-to-r ${getDifficultyColor(
113+
currentSeries.difficulty
114+
)}`}
115+
>
116+
{getDifficultyText(currentSeries.difficulty)}
117+
</Badge>
118+
<span className="flex items-center gap-1.5 text-sm text-muted-foreground">
119+
<BookOpen size={14} />
120+
{tutorials.length} 个教程
121+
</span>
122+
<span className="flex items-center gap-1.5 text-sm text-muted-foreground">
123+
<Clock size={14} />
124+
{totalDuration} 分钟
125+
</span>
126+
</div>
127+
128+
<h1 className="text-3xl font-bold mb-2">{currentSeries.title}</h1>
129+
<p className="text-muted-foreground text-lg max-w-2xl">
130+
{currentSeries.description}
131+
</p>
132+
133+
{/* Progress Section */}
134+
<div className="mt-6 p-4 bg-card/50 backdrop-blur-sm border rounded-xl">
135+
<div className="flex items-center justify-between mb-3">
136+
<div className="flex items-center gap-2">
137+
<BarChart3 className="text-primary" size={18} />
138+
<span className="font-medium">学习进度</span>
139+
</div>
140+
<div className="flex items-center gap-2">
141+
<span className="text-2xl font-bold text-primary">{Math.round(progressPercent)}%</span>
142+
<span className="text-muted-foreground">
143+
({completedCount}/{tutorials.length})
144+
</span>
145+
</div>
146+
</div>
147+
<div className="h-3 bg-muted rounded-full overflow-hidden">
148+
<div
149+
className="h-full bg-gradient-to-r from-primary to-secondary rounded-full transition-all"
150+
style={{ width: `${progressPercent}%` }}
151+
/>
152+
</div>
153+
154+
{nextTutorial && progressPercent < 100 && (
155+
<Button
156+
onClick={() => router.push(`/tutorial/${nextTutorial.id}`)}
157+
className="mt-4 bg-gradient-to-r from-primary to-secondary"
158+
>
159+
<Play className="mr-2 fill-current" size={16} />
160+
继续学习: {nextTutorial.title}
161+
</Button>
162+
)}
163+
164+
{progressPercent === 100 && (
165+
<div className="mt-4 flex items-center gap-2 px-4 py-2 bg-emerald-500/10 border border-emerald-500/30 text-emerald-500 rounded-xl">
166+
<Trophy size={18} />
167+
<span className="font-medium">恭喜!你已完成本系列所有教程</span>
168+
</div>
169+
)}
170+
</div>
171+
</div>
172+
</div>
173+
</div>
174+
</div>
175+
176+
{/* Tutorials List */}
177+
<div className="px-8 pb-8">
178+
<div className="flex items-center gap-3 mb-6">
179+
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
180+
<Sparkles className="text-primary-foreground" size={20} />
181+
</div>
182+
<div>
183+
<h2 className="text-xl font-bold">教程列表</h2>
184+
<p className="text-sm text-muted-foreground">按顺序完成所有教程</p>
185+
</div>
186+
</div>
187+
188+
{tutorials.length > 0 ? (
189+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
190+
{tutorials.map((tutorial) => {
191+
const isCompleted = progress[tutorial.id]?.completed;
192+
return (
193+
<Card
194+
key={tutorial.id}
195+
className="group cursor-pointer transition-all hover:shadow-lg hover:border-primary/50"
196+
onClick={() => router.push(`/tutorial/${tutorial.id}`)}
197+
>
198+
<CardHeader className="pb-2">
199+
<div className="flex items-start justify-between gap-3">
200+
<div className="flex-1 min-w-0">
201+
<div className="flex items-center gap-2 mb-1">
202+
<Badge
203+
className={`text-xs ${
204+
tutorial.difficulty === "beginner"
205+
? "bg-emerald-500/10 text-emerald-500"
206+
: tutorial.difficulty === "intermediate"
207+
? "bg-amber-500/10 text-amber-500"
208+
: "bg-rose-500/10 text-rose-500"
209+
}`}
210+
>
211+
{getDifficultyText(tutorial.difficulty)}
212+
</Badge>
213+
{isCompleted && (
214+
<Badge
215+
variant="outline"
216+
className="text-xs text-emerald-500 border-emerald-500/20"
217+
>
218+
<CheckCircle className="w-3 h-3 mr-1" />
219+
已完成
220+
</Badge>
221+
)}
222+
</div>
223+
<CardTitle className="text-base group-hover:text-primary transition-colors">
224+
{tutorial.title}
225+
</CardTitle>
226+
</div>
227+
<div
228+
className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${
229+
isCompleted
230+
? "bg-emerald-500/10 text-emerald-500"
231+
: "bg-muted text-muted-foreground"
232+
}`}
233+
>
234+
{isCompleted ? <CheckCircle size={18} /> : <Circle size={18} />}
235+
</div>
236+
</div>
237+
</CardHeader>
238+
<CardContent>
239+
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">
240+
{tutorial.description}
241+
</p>
242+
<div className="flex items-center justify-between">
243+
<div className="flex items-center gap-3 text-sm text-muted-foreground">
244+
<div className="flex items-center gap-1">
245+
<Clock size={14} />
246+
<span>{tutorial.duration} 分钟</span>
247+
</div>
248+
</div>
249+
<Button
250+
size="sm"
251+
variant={isCompleted ? "outline" : "default"}
252+
className={
253+
isCompleted
254+
? "text-emerald-500 border-emerald-500/20"
255+
: "bg-gradient-to-r from-primary to-secondary opacity-0 group-hover:opacity-100 transition-opacity"
256+
}
257+
>
258+
<Play className="w-3 h-3 mr-1 fill-current" />
259+
{isCompleted ? "复习" : "开始"}
260+
</Button>
261+
</div>
262+
</CardContent>
263+
</Card>
264+
);
265+
})}
266+
</div>
267+
) : (
268+
<div className="text-center py-16 bg-card rounded-2xl border border-dashed">
269+
<BookOpen size={64} className="mx-auto mb-4 text-muted-foreground opacity-30" />
270+
<p className="text-muted-foreground text-lg">该系列暂无教程</p>
271+
</div>
272+
)}
273+
</div>
274+
</div>
275+
);
276+
}

0 commit comments

Comments
 (0)