Frontend Standards
Stack
Vue 3 (Composition API) + Vite + Axios. No TypeScript — plain JavaScript.
Markdown rendering: marked + DOMPurify. Terminal: xterm.js. Charts: Chart.js.
Directory Layout
frontend/src/
├── views/ # Route-level components (one per route)
│ ├── Dashboard.vue
│ ├── LessonViewer.vue
│ ├── ExamAttempt.vue
│ ├── LaboratoryAttempt.vue
│ ├── Sandbox.vue
│ ├── AdminDashboard.vue
│ └── ...
├── components/ # Reusable components
│ ├── MainLayout.vue
│ ├── Sidebar.vue
│ ├── TerminalComponent.vue
│ ├── PerformanceChart.vue
│ ├── ActivityTimeline.vue
│ ├── ProgressCard.vue
│ └── ProfileMenu.vue
└── services/
└── api.js # All API methods — single source of truth
Composition API Only
Use <script setup> exclusively. Never use the Options API.
<!-- GOOD -->
<script setup>
import { ref, computed, onMounted } from 'vue'
const lessons = ref([])
onMounted(() => fetchLessons())
</script>
<!-- BAD -->
<script>
export default {
data() { return { lessons: [] } },
mounted() { this.fetchLessons() }
}
</script>
API Calls
Import all API methods from services/api.js. Never use axios directly in components.
// GOOD
import { lessonsAPI } from '../services/api.js'
const modules = await lessonsAPI.getModules()
// BAD
import axios from 'axios'
const modules = await axios.get('/api/lessons/modules')
Every component must handle loading and error states:
<script setup>
const data = ref(null)
const loading = ref(false)
const error = ref(null)
onMounted(async () => {
loading.value = true
try {
data.value = await someAPI.get()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else>{{ data }}</div>
</template>
Navigation
Use router.push() for navigation — never window.location.href.
// GOOD
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/exams')
// BAD
window.location.href = '/exams'
Markdown Rendering
Lessons use marked for parsing + DOMPurify for sanitization. Always sanitize before using v-html.
<script setup>
import { marked } from 'marked'
import DOMPurify from 'dompurify'
const renderedMarkdown = computed(() => {
const html = marked.parse(markdownContent.value || '')
return DOMPurify.sanitize(html)
})
</script>
<template>
<div class="markdown-content" v-html="renderedMarkdown"></div>
</template>
Never use v-html with unsanitized content. All user-submitted or external markdown must go through DOMPurify.
Terminal Component
The TerminalComponent.vue uses XTerm.js + WebSocket. Usage pattern:
<TerminalComponent :container-id="containerId" />
The WebSocket URL is: ws(s)://{api_host}/ws/terminal/{container_id}?token={jwt_token}
Router Guards
// main.js
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next('/login')
} else {
next()
}
})
Route metadata:
requiresAuth: true— redirects to/loginif no token- No client-side admin check — admin UI is hidden from non-admins but server enforces access
State Management
No Vuex/Pinia — state is managed per-component with ref/computed. User data is fetched fresh on each component mount, not cached globally.
Token stored in localStorage:
localStorage.setItem('token', response.data.access_token)
localStorage.removeItem('token') // on logout
Sidebar Navigation
Navigation items in Sidebar.vue:
- Home / Dashboard
- Lessons
- Exams
- Laboratories
- Sandbox
- Admin (hidden for non-admin users via
v-if="user?.role === 'admin'") - Profile
API Interceptors
Axios interceptors in services/api.js:
// Add token to every request
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// Redirect to login on 401
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
Naming Conventions
| Thing | Convention | Example |
|---|---|---|
| View files | PascalCase + .vue | ExamAttempt.vue |
| Component files | PascalCase + .vue | TerminalComponent.vue |
| API service files | camelCase + .js | api.js |
| Script refs | camelCase | const examData = ref(null) |
| Template props | kebab-case | :container-id="id" |
| Event handlers | handle prefix | handleSubmit, handleDelete |