Skip to content

Commit 9c90ef4

Browse files
committed
feat: basic show solutions feature
1 parent b323b4a commit 9c90ef4

File tree

6 files changed

+103
-56
lines changed

6 files changed

+103
-56
lines changed

Diff for: README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,16 @@ The development server will be running at [http://localhost:3000](http://localho
4141
## Todolist
4242

4343
- [ ] Content
44-
- [x] Switch playgrounds on different guides
45-
- [x] Allow each guide to toggle features
4644
- [ ] Allow each guide to configure file filter
47-
- [ ] Solution for each guide
48-
- [x] A button of "Edit this page"
45+
- [ ] Persist user changes when toggling solutions
46+
- [ ] Only make necessary changes when navigating between guides
4947
- [ ] Verification for tutorial tasks
5048
- [ ] Search feature
5149
- [ ] Embedded Nuxt Docs (update CORS headers)
50+
- [x] Switch playgrounds on different guides
51+
- [x] Allow each guide to toggle features
52+
- [x] Solution for each guide
53+
- [x] A button of "Edit this page"
5254
- [ ] Command K System
5355
- [ ] About Page
5456
- [ ] Welcome Screen

Diff for: components/PanelEditor.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ watch(
1818
)
1919
2020
watch(
21-
() => play.mountedGuide,
21+
() => [play.mountedGuide, play.showingSolution],
2222
() => {
2323
input.value = play.fileSelected?.read() || ''
2424
},

Diff for: components/TheNav.vue

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ const timeAgo = useTimeAgo(buildTime)
1515
</NuxtLink>
1616

1717
<div flex-auto />
18+
<button
19+
v-if="play.mountedGuide?.solutions"
20+
@click="play.mountGuide(play.mountedGuide, !play.showingSolution)"
21+
>
22+
Toggle Solution
23+
</button>
1824
<button
1925
v-if="play.status === 'ready' && play.features.download !== false"
2026
rounded p2

Diff for: content/.template/solutions/app.vue

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script setup lang="ts">
2+
const msg = 'This is the solution!'
3+
</script>
4+
5+
<template>
6+
<div>{{ msg.toUpperCase() }}</div>
7+
</template>

Diff for: modules/template-loader.ts

+39-20
Original file line numberDiff line numberDiff line change
@@ -45,29 +45,48 @@ export default defineNuxtModule({
4545
if (!id.match(/\/\.template\/index\.ts/))
4646
return
4747

48-
const filesDirs = resolve(id, '../files')
49-
const files = await fg('**/*.*', {
50-
ignore: [
51-
'**/node_modules/**',
52-
'**/.git/**',
53-
'**/.nuxt/**',
54-
],
55-
dot: true,
56-
cwd: filesDirs,
57-
onlyFiles: true,
58-
absolute: false,
59-
})
48+
async function getFileMap(dir: string) {
49+
const files = await fg('**/*.*', {
50+
ignore: [
51+
'**/node_modules/**',
52+
'**/.git/**',
53+
'**/.nuxt/**',
54+
],
55+
dot: true,
56+
cwd: dir,
57+
onlyFiles: true,
58+
absolute: false,
59+
})
6060

61-
const filesMap: Record<string, string> = {}
61+
if (!files.length)
62+
return undefined
6263

63-
await Promise.all(
64-
files.sort().map(async (filename) => {
65-
const content = await fs.readFile(resolve(filesDirs, filename), 'utf-8')
66-
filesMap[filename] = content
67-
}),
68-
)
64+
const filesMap: Record<string, string> = {}
65+
66+
await Promise.all(
67+
files.sort().map(async (filename) => {
68+
const content = await fs.readFile(resolve(dir, filename), 'utf-8')
69+
filesMap[filename] = content
70+
}),
71+
)
72+
73+
return filesMap
74+
}
75+
76+
const [
77+
files,
78+
solutions,
79+
] = await Promise.all([
80+
getFileMap(resolve(id, '../files')),
81+
getFileMap(resolve(id, '../solutions')),
82+
])
6983

70-
return `${code}\nmeta.files = ${JSON.stringify(filesMap)}\n`
84+
return [
85+
code,
86+
`meta.files = ${JSON.stringify(files)}`,
87+
`meta.solutions = ${JSON.stringify(solutions)}`,
88+
'',
89+
].join('\n')
7190
},
7291
})
7392
},

Diff for: stores/playground.ts

+44-31
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const usePlaygroundStore = defineStore('playground', () => {
3030
const clientInfo = ref<ClientInfo>()
3131
const fileSelected = shallowRef<Raw<VirtualFile>>()
3232
const mountedGuide = shallowRef<Raw<GuideMeta>>()
33+
const showingSolution = ref(false)
3334
const features = ref<PlaygroundFeatures>({})
3435

3536
const previewLocation = ref({
@@ -248,9 +249,43 @@ export const usePlaygroundStore = defineStore('playground', () => {
248249

249250
const guideDispose: (() => void | Promise<void>)[] = []
250251

251-
async function mountGuide(guide?: GuideMeta) {
252+
async function _mountFiles(overrides: Record<string, string>) {
253+
await Promise.all(
254+
Object.entries(overrides)
255+
.map(async ([filepath, content]) => {
256+
await webcontainer.value?.fs.mkdir(dirname(filepath), { recursive: true })
257+
await updateOrCreateFile(filepath, content)
258+
}),
259+
)
260+
261+
async function updateOrCreateFile(filepath: string, content: string) {
262+
const file = files.get(filepath)
263+
if (file) {
264+
const oldContent = file.read()
265+
await file.write(content)
266+
guideDispose.push(async () => {
267+
await file.write(oldContent)
268+
})
269+
return file
270+
}
271+
else {
272+
const newFile = new VirtualFile(filepath, content)
273+
newFile.wc = webcontainer.value
274+
await newFile.write(content)
275+
files.set(filepath, newFile)
276+
guideDispose.push(async () => {
277+
files.delete(filepath)
278+
await webcontainer.value!.fs.rm(filepath)
279+
})
280+
return newFile
281+
}
282+
}
283+
}
284+
285+
async function mountGuide(guide?: GuideMeta, withSolution = false) {
252286
await mountPromise
253287

288+
// TODO: only make necessary changes
254289
// Unmount the previous guide
255290
await Promise.all(guideDispose.map(dispose => dispose()))
256291
guideDispose.length = 0
@@ -259,13 +294,12 @@ export const usePlaygroundStore = defineStore('playground', () => {
259294
// eslint-disable-next-line no-console
260295
console.log('mounting guide', guide)
261296

262-
await Promise.all(
263-
Object.entries(guide?.files || {})
264-
.map(async ([filepath, content]) => {
265-
await webcontainer.value?.fs.mkdir(dirname(filepath), { recursive: true })
266-
await updateOrCreateFile(filepath, content)
267-
}),
268-
)
297+
const overrides = {
298+
...guide.files,
299+
...withSolution ? guide.solutions : {},
300+
}
301+
302+
await _mountFiles(overrides)
269303

270304
features.value = guide?.features || {}
271305
}
@@ -278,31 +312,9 @@ export const usePlaygroundStore = defineStore('playground', () => {
278312
updatePreviewUrl()
279313

280314
mountedGuide.value = guide
315+
showingSolution.value = withSolution
281316

282317
return undefined
283-
284-
async function updateOrCreateFile(filepath: string, content: string) {
285-
const file = files.get(filepath)
286-
if (file) {
287-
const oldContent = file.read()
288-
await file.write(content)
289-
guideDispose.push(async () => {
290-
await file.write(oldContent)
291-
})
292-
return file
293-
}
294-
else {
295-
const newFile = new VirtualFile(filepath, content)
296-
newFile.wc = webcontainer.value
297-
await newFile.write(content)
298-
files.set(filepath, newFile)
299-
guideDispose.push(async () => {
300-
files.delete(filepath)
301-
await webcontainer.value!.fs.rm(filepath)
302-
})
303-
return newFile
304-
}
305-
}
306318
}
307319

308320
return {
@@ -314,6 +326,7 @@ export const usePlaygroundStore = defineStore('playground', () => {
314326
fileSelected,
315327
mountedGuide,
316328
mountGuide,
329+
showingSolution,
317330
previewLocation,
318331
previewUrl,
319332
features,

0 commit comments

Comments
 (0)