Skip to main content

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>

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 /login if 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

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

ThingConventionExample
View filesPascalCase + .vueExamAttempt.vue
Component filesPascalCase + .vueTerminalComponent.vue
API service filescamelCase + .jsapi.js
Script refscamelCaseconst examData = ref(null)
Template propskebab-case:container-id="id"
Event handlershandle prefixhandleSubmit, handleDelete