fsdcats 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,762 @@
1
+ const practicalsData = {
2
+ '1': {
3
+ id: '1',
4
+ fileName: 'practical-1.txt',
5
+ title: 'Practical 1: Vue Directives (v-if/else, v-show, v-for, v-on, v-model)',
6
+ setupSteps: `1. Run:
7
+ npm create vue@latest my-vue-p1
8
+
9
+ 2. Terminal Prompts:
10
+ Select No to all options (TypeScript, Router, Pinia, Vitest, ESLint, Prettier).
11
+
12
+ 3. cd my-vue-p1
13
+ npm install
14
+
15
+ 4. npm run dev
16
+ (App runs on http://localhost:5173)
17
+
18
+ 5. Files to create/update:
19
+ - Create src/users.json
20
+ - Replace src/App.vue with the code below.`,
21
+ components: [
22
+ {
23
+ name: 'users.json',
24
+ path: 'src/users.json',
25
+ code: `[
26
+ { "username": "admin", "role": "Admin", "permissions": ["Create", "Edit", "Delete"] },
27
+ { "username": "editor", "role": "Editor", "permissions": ["Create", "Edit"] },
28
+ { "username": "viewer", "role": "Viewer", "permissions": ["Read Only"] }
29
+ ]`
30
+ },
31
+ {
32
+ name: 'App.vue',
33
+ path: 'src/App.vue',
34
+ code: `<script setup>
35
+ import { ref } from 'vue'
36
+ import users from './users.json'
37
+
38
+ const username = ref('')
39
+ const password = ref('')
40
+ const currentUser = ref(null)
41
+ const error = ref(false)
42
+
43
+ function handleLogin() {
44
+ const found = users.find(u => u.username === username.value && password.value === '123')
45
+ if (found) {
46
+ currentUser.value = found
47
+ error.value = false
48
+ username.value = ''
49
+ password.value = ''
50
+ } else {
51
+ error.value = true
52
+ }
53
+ }
54
+
55
+ function handleLogout() {
56
+ currentUser.value = null
57
+ }
58
+ </script>
59
+
60
+ <template>
61
+ <div style="font-family: sans-serif; max-width: 400px; margin: 30px auto; padding: 20px; border: 1px solid #ccc; border-radius: 8px;">
62
+ <!-- 1. v-if / v-else: Toggle between Login and Dashboard -->
63
+ <div v-if="!currentUser">
64
+ <h2>System Login</h2>
65
+ <!-- 2. v-model: Two-way input binding -->
66
+ <p><input v-model="username" placeholder="Username (admin/editor/viewer)" style="width: 100%; padding: 8px;" /></p>
67
+ <p><input v-model="password" type="password" placeholder="Password (123)" style="width: 100%; padding: 8px;" /></p>
68
+
69
+ <!-- 3. v-show: Conditional visibility without DOM removal -->
70
+ <p v-show="error" style="color: red;">Invalid credentials! (Use password: 123)</p>
71
+
72
+ <!-- 4. v-on (@): Event listening -->
73
+ <button @click="handleLogin" style="padding: 8px 16px; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer;">
74
+ Login
75
+ </button>
76
+ </div>
77
+
78
+ <div v-else>
79
+ <h2>Welcome, {{ currentUser.username }}</h2>
80
+ <p>Role: <strong>{{ currentUser.role }}</strong></p>
81
+ <button @click="handleLogout" style="padding: 6px 12px; cursor: pointer;">Logout</button>
82
+
83
+ <h3>Allowed Permissions:</h3>
84
+ <ul>
85
+ <!-- 5. v-for: List iteration with key binding -->
86
+ <li v-for="(perm, index) in currentUser.permissions" :key="index">
87
+ ✅ {{ perm }}
88
+ </li>
89
+ </ul>
90
+ </div>
91
+ </div>
92
+ </template>`
93
+ }
94
+ ]
95
+ },
96
+
97
+ '2': {
98
+ id: '2',
99
+ fileName: 'practical-2.txt',
100
+ title: 'Practical 2: Data Binding (Props and Emits)',
101
+ setupSteps: `1. Run:
102
+ npm create vue@latest my-vue-p2
103
+
104
+ 2. Terminal Prompts:
105
+ Select No to all options.
106
+
107
+ 3. cd my-vue-p2
108
+ npm install
109
+
110
+ 4. npm run dev
111
+
112
+ 5. Files to create/update:
113
+ - Create src/DriverCard.vue (Child component)
114
+ - Replace src/App.vue with the code below (Parent component).`,
115
+ components: [
116
+ {
117
+ name: 'DriverCard.vue',
118
+ path: 'src/DriverCard.vue',
119
+ code: `<script setup>
120
+ // 1. defineProps: Receives data from parent
121
+ defineProps({
122
+ name: { type: String, required: true },
123
+ id: { type: Number, required: true }
124
+ })
125
+
126
+ // 2. defineEmits: Declares event to notify parent
127
+ const emit = defineEmits(['remove'])
128
+ </script>
129
+
130
+ <template>
131
+ <div style="display: flex; justify-content: space-between; align-items: center; padding: 10px; margin: 8px 0; background: #f3f4f6; border-radius: 6px;">
132
+ <span>🏎️ {{ name }} <small>(ID: {{ id }})</small></span>
133
+ <!-- Emits 'remove' event passing driver ID -->
134
+ <button @click="emit('remove', id)" style="background: #ef4444; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
135
+ Remove
136
+ </button>
137
+ </div>
138
+ </template>`
139
+ },
140
+ {
141
+ name: 'App.vue',
142
+ path: 'src/App.vue',
143
+ code: `<script setup>
144
+ import { ref } from 'vue'
145
+ import DriverCard from './DriverCard.vue'
146
+
147
+ const newDriverName = ref('')
148
+ const drivers = ref([
149
+ { id: 1, name: 'Lewis Hamilton' },
150
+ { id: 2, name: 'Charles Leclerc' },
151
+ { id: 3, name: 'Max Verstappen' }
152
+ ])
153
+
154
+ // Function called when child emits 'remove'
155
+ function removeDriver(id) {
156
+ drivers.value = drivers.value.filter(d => d.id !== id)
157
+ }
158
+
159
+ function addDriver() {
160
+ if (!newDriverName.value.trim()) return
161
+ drivers.value.push({
162
+ id: Date.now(),
163
+ name: newDriverName.value.trim()
164
+ })
165
+ newDriverName.value = ''
166
+ }
167
+ </script>
168
+
169
+ <template>
170
+ <div style="font-family: sans-serif; max-width: 450px; margin: 30px auto; padding: 20px; border: 1px solid #ccc; border-radius: 8px;">
171
+ <h2>Driver Dashboard (Props & Emits)</h2>
172
+
173
+ <!-- Add driver form -->
174
+ <div style="display: flex; gap: 8px; margin-bottom: 16px;">
175
+ <input v-model="newDriverName" placeholder="Enter driver name" style="flex: 1; padding: 8px;" />
176
+ <button @click="addDriver" style="padding: 8px 14px; background: #10b981; color: white; border: none; border-radius: 4px; cursor: pointer;">
177
+ Add
178
+ </button>
179
+ </div>
180
+
181
+ <!--
182
+ - Data Binding DOWN via PROPS: :name and :id
183
+ - Event Binding UP via EMIT: @remove
184
+ -->
185
+ <DriverCard
186
+ v-for="driver in drivers"
187
+ :key="driver.id"
188
+ :id="driver.id"
189
+ :name="driver.name"
190
+ @remove="removeDriver"
191
+ />
192
+
193
+ <p v-if="drivers.length === 0" style="color: #666; font-style: italic;">No drivers in the roster.</p>
194
+ </div>
195
+ </template>`
196
+ }
197
+ ]
198
+ },
199
+
200
+ '3': {
201
+ id: '3',
202
+ fileName: 'practical-3.txt',
203
+ title: 'Practical 3: Document Upload & Preview System (Vue 3 + Express + Multer)',
204
+ setupSteps: `Backend Setup:
205
+ 1. mkdir backend && cd backend
206
+ 2. npm init -y
207
+ 3. npm install express multer cors
208
+ 4. Create server.js and paste the server.js code below.
209
+ 5. node server.js
210
+ (Runs on http://localhost:5000)
211
+
212
+ Frontend Setup:
213
+ 1. In a new terminal:
214
+ npm create vue@latest frontend
215
+ (Select No to all options)
216
+ 2. cd frontend && npm install
217
+ 3. npm run dev
218
+ (Runs on http://localhost:5173)
219
+ 4. Replace frontend/src/App.vue with the App.vue code below.`,
220
+ components: [
221
+ {
222
+ name: 'server.js (Backend)',
223
+ path: 'backend/server.js',
224
+ code: `const express = require('express');
225
+ const multer = require('multer');
226
+ const cors = require('cors');
227
+ const path = require('path');
228
+ const fs = require('fs');
229
+
230
+ const app = express();
231
+ app.use(cors());
232
+ app.use(express.json());
233
+
234
+ // Ensure uploads directory exists
235
+ const uploadDir = path.join(__dirname, 'uploads');
236
+ if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);
237
+
238
+ // Serve uploaded files statically for preview/download
239
+ app.use('/uploads', express.static(uploadDir));
240
+
241
+ // Multer Storage Configuration
242
+ const storage = multer.diskStorage({
243
+ destination: (req, file, cb) => cb(null, 'uploads/'),
244
+ filename: (req, file, cb) => cb(null, \`\${Date.now()}-\${file.originalname}\`)
245
+ });
246
+ const upload = multer({ storage });
247
+
248
+ let documents = [];
249
+
250
+ // 1. Upload Route
251
+ app.post('/api/documents/upload', upload.single('file'), (req, res) => {
252
+ if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
253
+ const newDoc = {
254
+ id: Date.now(),
255
+ originalName: req.file.originalname,
256
+ filename: req.file.filename,
257
+ size: req.file.size,
258
+ mimeType: req.file.mimetype
259
+ };
260
+ documents.unshift(newDoc);
261
+ res.status(201).json(newDoc);
262
+ });
263
+
264
+ // 2. Fetch All Documents Route
265
+ app.get('/api/documents', (req, res) => {
266
+ res.json(documents);
267
+ });
268
+
269
+ // 3. Delete Document Route
270
+ app.delete('/api/documents/:id', (req, res) => {
271
+ const id = Number(req.params.id);
272
+ const doc = documents.find(d => d.id === id);
273
+ if (!doc) return res.status(404).json({ error: 'File not found' });
274
+
275
+ // Delete file from disk
276
+ const filePath = path.join(uploadDir, doc.filename);
277
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
278
+
279
+ documents = documents.filter(d => d.id !== id);
280
+ res.json({ message: 'Deleted successfully' });
281
+ });
282
+
283
+ app.listen(5000, () => console.log('Backend running on http://localhost:5000'));`
284
+ },
285
+ {
286
+ name: 'App.vue (Frontend)',
287
+ path: 'frontend/src/App.vue',
288
+ code: `<script setup>
289
+ import { ref, onMounted } from 'vue'
290
+
291
+ const selectedFile = ref(null)
292
+ const documents = ref([])
293
+ const loading = ref(false)
294
+
295
+ async function fetchDocuments() {
296
+ const res = await fetch('http://localhost:5000/api/documents')
297
+ documents.value = await res.json()
298
+ }
299
+
300
+ function handleFileSelect(e) {
301
+ selectedFile.value = e.target.files[0]
302
+ }
303
+
304
+ async function handleUpload() {
305
+ if (!selectedFile.value) return
306
+ loading.value = true
307
+
308
+ const formData = new FormData()
309
+ formData.append('file', selectedFile.value)
310
+
311
+ await fetch('http://localhost:5000/api/documents/upload', {
312
+ method: 'POST',
313
+ body: formData
314
+ })
315
+
316
+ selectedFile.value = null
317
+ document.getElementById('fileInput').value = ''
318
+ loading.value = false
319
+ await fetchDocuments()
320
+ }
321
+
322
+ async function handleDelete(id) {
323
+ await fetch(\`http://localhost:5000/api/documents/\${id}\`, { method: 'DELETE' })
324
+ await fetchDocuments()
325
+ }
326
+
327
+ onMounted(fetchDocuments)
328
+ </script>
329
+
330
+ <template>
331
+ <div style="font-family: sans-serif; max-width: 500px; margin: 30px auto; padding: 20px; border: 1px solid #ccc; border-radius: 8px;">
332
+ <h2>Document Upload & Preview</h2>
333
+
334
+ <!-- File input & upload button -->
335
+ <div style="margin-bottom: 20px;">
336
+ <input id="fileInput" type="file" @change="handleFileSelect" />
337
+ <button :disabled="!selectedFile || loading" @click="handleUpload" style="padding: 6px 12px; margin-left: 8px;">
338
+ {{ loading ? 'Uploading...' : 'Upload' }}
339
+ </button>
340
+ </div>
341
+
342
+ <!-- Document List & Preview Link -->
343
+ <h3>Uploaded Documents ({{ documents.length }})</h3>
344
+ <ul style="padding-left: 20px;">
345
+ <li v-for="doc in documents" :key="doc.id" style="margin-bottom: 10px;">
346
+ <a :href="\`http://localhost:5000/uploads/\${doc.filename}\`" target="_blank" style="color: #2563eb; font-weight: bold;">
347
+ {{ doc.originalName }}
348
+ </a>
349
+ <small style="color: #666; margin-left: 8px;">({{ (doc.size / 1024).toFixed(1) }} KB)</small>
350
+ <button @click="handleDelete(doc.id)" style="margin-left: 10px; color: red;">Delete</button>
351
+ </li>
352
+ </ul>
353
+ </div>
354
+ </template>`
355
+ }
356
+ ]
357
+ },
358
+
359
+ '4': {
360
+ id: '4',
361
+ fileName: 'practical-4.txt',
362
+ title: 'Practical 4: Localized State Management (Pinia Multi-Step Form)',
363
+ setupSteps: `Frontend Setup:
364
+ 1. npm create vue@latest frontend
365
+ (Select YES to Pinia, No to everything else)
366
+ 2. cd frontend && npm install
367
+ 3. npm run dev
368
+ 4. Create src/store.js and replace src/App.vue with the code below.
369
+
370
+ Backend Setup (MongoDB):
371
+ 1. mkdir backend && cd backend
372
+ 2. npm init -y
373
+ 3. npm install express mongoose cors
374
+ 4. Create server.js and paste the server.js code below.
375
+ 5. node server.js`,
376
+ components: [
377
+ {
378
+ name: 'store.js (Pinia Store)',
379
+ path: 'frontend/src/store.js',
380
+ code: `import { defineStore } from 'pinia'
381
+
382
+ export const useApplicationStore = defineStore('application', {
383
+ state: () => ({
384
+ currentStep: 1,
385
+ personal: { name: '', email: '', phone: '' },
386
+ education: { degree: '', college: '', year: '' },
387
+ experience: { company: '', role: '' },
388
+ submitted: false
389
+ }),
390
+ actions: {
391
+ goNext() {
392
+ if (this.currentStep < 4) this.currentStep++
393
+ },
394
+ goBack() {
395
+ if (this.currentStep > 1) this.currentStep--
396
+ },
397
+ async submitApplication() {
398
+ const payload = {
399
+ personal: this.personal,
400
+ education: this.education,
401
+ experience: this.experience
402
+ }
403
+ const res = await fetch('http://localhost:5000/api/applications', {
404
+ method: 'POST',
405
+ headers: { 'Content-Type': 'application/json' },
406
+ body: JSON.stringify(payload)
407
+ })
408
+ if (res.ok) this.submitted = true
409
+ },
410
+ resetForm() {
411
+ this.currentStep = 1
412
+ this.personal = { name: '', email: '', phone: '' }
413
+ this.education = { degree: '', college: '', year: '' }
414
+ this.experience = { company: '', role: '' }
415
+ this.submitted = false
416
+ }
417
+ }
418
+ })`
419
+ },
420
+ {
421
+ name: 'App.vue (Frontend)',
422
+ path: 'frontend/src/App.vue',
423
+ code: `<script setup>
424
+ import { useApplicationStore } from './store.js'
425
+ const store = useApplicationStore()
426
+ </script>
427
+
428
+ <template>
429
+ <div style="font-family: sans-serif; max-width: 450px; margin: 30px auto; padding: 20px; border: 1px solid #ccc; border-radius: 8px;">
430
+ <h2>Job Application Form</h2>
431
+ <p><strong>Step {{ store.currentStep }} of 4</strong></p>
432
+
433
+ <!-- Step 1: Personal Details -->
434
+ <div v-if="store.currentStep === 1">
435
+ <h3>Step 1: Personal Details</h3>
436
+ <p><input v-model="store.personal.name" placeholder="Full Name" style="width: 100%; padding: 8px;" /></p>
437
+ <p><input v-model="store.personal.email" placeholder="Email" style="width: 100%; padding: 8px;" /></p>
438
+ <p><input v-model="store.personal.phone" placeholder="Phone" style="width: 100%; padding: 8px;" /></p>
439
+ </div>
440
+
441
+ <!-- Step 2: Education -->
442
+ <div v-else-if="store.currentStep === 2">
443
+ <h3>Step 2: Education</h3>
444
+ <p><input v-model="store.education.degree" placeholder="Degree (e.g. BSc CS)" style="width: 100%; padding: 8px;" /></p>
445
+ <p><input v-model="store.education.college" placeholder="College Name" style="width: 100%; padding: 8px;" /></p>
446
+ <p><input v-model="store.education.year" placeholder="Passing Year" style="width: 100%; padding: 8px;" /></p>
447
+ </div>
448
+
449
+ <!-- Step 3: Experience -->
450
+ <div v-else-if="store.currentStep === 3">
451
+ <h3>Step 3: Work Experience</h3>
452
+ <p><input v-model="store.experience.company" placeholder="Company Name" style="width: 100%; padding: 8px;" /></p>
453
+ <p><input v-model="store.experience.role" placeholder="Designation / Role" style="width: 100%; padding: 8px;" /></p>
454
+ </div>
455
+
456
+ <!-- Step 4: Review & Submit -->
457
+ <div v-else-if="store.currentStep === 4">
458
+ <h3>Step 4: Review Details</h3>
459
+ <p><strong>Name:</strong> {{ store.personal.name }} | {{ store.personal.email }}</p>
460
+ <p><strong>Degree:</strong> {{ store.education.degree }} ({{ store.education.college }})</p>
461
+ <p><strong>Experience:</strong> {{ store.experience.company }} - {{ store.experience.role }}</p>
462
+
463
+ <button @click="store.submitApplication" style="padding: 10px 20px; background: #16a34a; color: white; border: none; cursor: pointer;">
464
+ Submit Application
465
+ </button>
466
+ <p v-if="store.submitted" style="color: green; font-weight: bold;">✅ Submitted Successfully!</p>
467
+ </div>
468
+
469
+ <!-- Navigation Buttons -->
470
+ <div style="margin-top: 20px; display: flex; justify-content: space-between;">
471
+ <button :disabled="store.currentStep === 1" @click="store.goBack">Back</button>
472
+ <button v-if="store.currentStep < 4" @click="store.goNext">Next</button>
473
+ <button v-if="store.submitted" @click="store.resetForm">Start Over</button>
474
+ </div>
475
+ </div>
476
+ </template>`
477
+ },
478
+ {
479
+ name: 'server.js (Backend)',
480
+ path: 'backend/server.js',
481
+ code: `const express = require('express');
482
+ const mongoose = require('mongoose');
483
+ const cors = require('cors');
484
+
485
+ const app = express();
486
+ app.use(cors());
487
+ app.use(express.json());
488
+
489
+ // MongoDB Application Schema
490
+ const Application = mongoose.model('Application', new mongoose.Schema({
491
+ personal: Object,
492
+ education: Object,
493
+ experience: Object,
494
+ createdAt: { type: Date, default: Date.now }
495
+ }));
496
+
497
+ // POST /api/applications
498
+ app.post('/api/applications', async (req, res) => {
499
+ try {
500
+ const doc = await Application.create(req.body);
501
+ res.status(201).json(doc);
502
+ } catch (err) {
503
+ res.status(400).json({ error: err.message });
504
+ }
505
+ });
506
+
507
+ // GET /api/applications
508
+ app.get('/api/applications', async (req, res) => {
509
+ const docs = await Application.find().sort({ createdAt: -1 });
510
+ res.json(docs);
511
+ });
512
+
513
+ mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost:27017/job_app_db')
514
+ .then(() => app.listen(5000, () => console.log('Server running on port 5000')))
515
+ .catch(err => console.error(err));`
516
+ }
517
+ ]
518
+ },
519
+
520
+ '5': {
521
+ id: '5',
522
+ fileName: 'practical-5.txt',
523
+ title: 'Practical 5: Routing in Nuxt and Shared Layouts',
524
+ setupSteps: `1. Run:
525
+ npx nuxi@latest init nuxt-lab
526
+
527
+ 2. Prompts:
528
+ - Template: minimal
529
+ - Package manager: npm
530
+ - Git repository: No
531
+ - Install dependencies: Yes
532
+
533
+ 3. cd nuxt-lab && npm run dev
534
+ (Runs on http://localhost:3000)
535
+
536
+ 4. Create the following files inside app/ (or root):
537
+ - app/app.vue
538
+ - app/layouts/default.vue
539
+ - app/layouts/minimal.vue
540
+ - app/pages/index.vue
541
+ - app/pages/about.vue
542
+ - app/pages/items/index.vue
543
+ - app/pages/items/[id].vue`,
544
+ components: [
545
+ {
546
+ name: 'app.vue',
547
+ path: 'app/app.vue',
548
+ code: `<!-- app.vue: Wraps the active NuxtLayout and NuxtPage -->
549
+ <template>
550
+ <NuxtLayout>
551
+ <NuxtPage />
552
+ </NuxtLayout>
553
+ </template>`
554
+ },
555
+ {
556
+ name: 'default.vue (Shared Layout)',
557
+ path: 'app/layouts/default.vue',
558
+ code: `<!-- app/layouts/default.vue: Shared Layout with persistent header & footer -->
559
+ <template>
560
+ <div style="font-family: sans-serif; padding: 20px;">
561
+ <header style="background: #f3f4f6; padding: 12px; margin-bottom: 20px;">
562
+ <nav>
563
+ <NuxtLink to="/" style="margin-right: 15px; font-weight: bold;">Home</NuxtLink>
564
+ <NuxtLink to="/items" style="margin-right: 15px; font-weight: bold;">Book Catalogue</NuxtLink>
565
+ <NuxtLink to="/about" style="font-weight: bold;">About</NuxtLink>
566
+ </nav>
567
+ </header>
568
+
569
+ <main>
570
+ <!-- Page content is automatically rendered inside this slot -->
571
+ <slot />
572
+ </main>
573
+
574
+ <footer style="margin-top: 40px; padding-top: 10px; border-top: 1px solid #ddd; color: #666;">
575
+ <p>&copy; 2026 My Nuxt App</p>
576
+ </footer>
577
+ </div>
578
+ </template>`
579
+ },
580
+ {
581
+ name: 'minimal.vue (Minimal Layout)',
582
+ path: 'app/layouts/minimal.vue',
583
+ code: `<!-- app/layouts/minimal.vue: Custom headerless layout -->
584
+ <template>
585
+ <div style="border: 2px dashed #ef4444; padding: 25px; text-align: center; font-family: sans-serif; max-width: 400px; margin: 30px auto;">
586
+ <h3>[Minimal Headerless Layout]</h3>
587
+ <slot />
588
+ <hr style="margin: 20px 0;" />
589
+ <NuxtLink to="/">← Return Home</NuxtLink>
590
+ </div>
591
+ </template>`
592
+ },
593
+ {
594
+ name: 'index.vue (Home Page)',
595
+ path: 'app/pages/index.vue',
596
+ code: `<!-- app/pages/index.vue: Root landing page (Route: /) -->
597
+ <template>
598
+ <div>
599
+ <h1>Welcome to Book Catalogue</h1>
600
+ <p>This is the home page using default layout.</p>
601
+ <NuxtLink to="/items" style="color: #2563eb; font-weight: bold;">View Books →</NuxtLink>
602
+ </div>
603
+ </template>`
604
+ },
605
+ {
606
+ name: 'about.vue (About Page)',
607
+ path: 'app/pages/about.vue',
608
+ code: `<!-- app/pages/about.vue: Route: /about (uses minimal layout) -->
609
+ <script setup>
610
+ // Switch this page to minimal layout instead of default
611
+ definePageMeta({
612
+ layout: 'minimal'
613
+ })
614
+ </script>
615
+
616
+ <template>
617
+ <div>
618
+ <h2>About Us</h2>
619
+ <p>This page uses <code>definePageMeta({ layout: 'minimal' })</code>.</p>
620
+ </div>
621
+ </template>`
622
+ },
623
+ {
624
+ name: 'items/index.vue (List Page)',
625
+ path: 'app/pages/items/index.vue',
626
+ code: `<!-- app/pages/items/index.vue: Route: /items -->
627
+ <script setup>
628
+ const books = [
629
+ { id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien' },
630
+ { id: 2, title: '1984', author: 'George Orwell' },
631
+ { id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }
632
+ ]
633
+ </script>
634
+
635
+ <template>
636
+ <div>
637
+ <h2>Book Catalogue</h2>
638
+ <ul>
639
+ <li v-for="book in books" :key="book.id" style="margin-bottom: 8px;">
640
+ <!-- Dynamic link to /items/[id] -->
641
+ <NuxtLink :to="\`/items/\${book.id}\`">
642
+ <strong>{{ book.title }}</strong> by {{ book.author }}
643
+ </NuxtLink>
644
+ </li>
645
+ </ul>
646
+ </div>
647
+ </template>`
648
+ },
649
+ {
650
+ name: 'items/[id].vue (Dynamic Detail Page)',
651
+ path: 'app/pages/items/[id].vue',
652
+ code: `<!-- app/pages/items/[id].vue: Dynamic Wildcard Route: /items/:id -->
653
+ <script setup>
654
+ const route = useRoute()
655
+
656
+ const books = [
657
+ { id: 1, title: 'The Hobbit', author: 'J.R.R. Tolkien', desc: 'A fantasy quest across Middle-earth.' },
658
+ { id: 2, title: '1984', author: 'George Orwell', desc: 'A dystopian vision of totalitarian rule.' },
659
+ { id: 3, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', desc: 'The American jazz age critique.' }
660
+ ]
661
+
662
+ // Extract the id parameter from route
663
+ const bookId = parseInt(route.params.id)
664
+ const book = books.find(b => b.id === bookId)
665
+ </script>
666
+
667
+ <template>
668
+ <div>
669
+ <div v-if="!book">
670
+ <h2>Book Not Found</h2>
671
+ <p>No book exists with ID: {{ route.params.id }}</p>
672
+ <NuxtLink to="/items">Back to Catalogue</NuxtLink>
673
+ </div>
674
+
675
+ <div v-else>
676
+ <h2>{{ book.title }}</h2>
677
+ <h4>Author: {{ book.author }}</h4>
678
+ <p>{{ book.desc }}</p>
679
+ <hr style="margin: 15px 0;" />
680
+ <NuxtLink to="/items">← Back to Catalogue</NuxtLink>
681
+ </div>
682
+ </div>
683
+ </template>`
684
+ }
685
+ ]
686
+ },
687
+
688
+ 'crud': {
689
+ id: 'crud',
690
+ fileName: 'practical-crud.txt',
691
+ title: 'Practical CRUD: Express + Mongoose REST API',
692
+ setupSteps: `1. mkdir crud-backend && cd crud-backend
693
+ 2. npm init -y
694
+ 3. npm install express mongoose
695
+ 4. Create server.js and paste the server.js code below.
696
+ 5. node server.js
697
+ (Runs on http://localhost:5000)`,
698
+ components: [
699
+ {
700
+ name: 'server.js',
701
+ path: 'server.js',
702
+ code: `const express = require('express');
703
+ const mongoose = require('mongoose');
704
+
705
+ const app = express();
706
+ app.use(express.json());
707
+
708
+ // 1. Database Connection
709
+ mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/crud_db')
710
+ .then(() => console.log('✓ MongoDB Connected'))
711
+ .catch(err => console.error('MongoDB Error:', err));
712
+
713
+ // 2. Mongoose Model
714
+ const Item = mongoose.model('Item', new mongoose.Schema({
715
+ name: { type: String, required: true },
716
+ description: String
717
+ }, { timestamps: true }));
718
+
719
+ // 3. CREATE: POST /api/items
720
+ app.post('/api/items', async (req, res) => {
721
+ try {
722
+ const item = await Item.create(req.body);
723
+ res.status(201).json(item);
724
+ } catch (err) {
725
+ res.status(400).json({ error: err.message });
726
+ }
727
+ });
728
+
729
+ // 4. READ ALL: GET /api/items
730
+ app.get('/api/items', async (req, res) => {
731
+ const items = await Item.find().sort({ createdAt: -1 });
732
+ res.json(items);
733
+ });
734
+
735
+ // 5. READ ONE: GET /api/items/:id
736
+ app.get('/api/items/:id', async (req, res) => {
737
+ const item = await Item.findById(req.params.id);
738
+ if (!item) return res.status(404).json({ message: 'Item not found' });
739
+ res.json(item);
740
+ });
741
+
742
+ // 6. UPDATE: PUT /api/items/:id
743
+ app.put('/api/items/:id', async (req, res) => {
744
+ const item = await Item.findByIdAndUpdate(req.params.id, req.body, { new: true });
745
+ if (!item) return res.status(404).json({ message: 'Item not found' });
746
+ res.json(item);
747
+ });
748
+
749
+ // 7. DELETE: DELETE /api/items/:id
750
+ app.delete('/api/items/:id', async (req, res) => {
751
+ const item = await Item.findByIdAndDelete(req.params.id);
752
+ if (!item) return res.status(404).json({ message: 'Item not found' });
753
+ res.json({ message: 'Item deleted successfully' });
754
+ });
755
+
756
+ app.listen(5000, () => console.log('✓ Server running on http://localhost:5000'));`
757
+ }
758
+ ]
759
+ }
760
+ };
761
+
762
+ module.exports = practicalsData;