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