frontend init

This commit is contained in:
2026-02-15 17:56:28 +01:00
parent 7afe2da63e
commit b52429c250
33 changed files with 1688 additions and 0 deletions

11
front/src/App.vue Normal file
View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import AppTopBar from './components/AppTopBar.vue'
</script>
<template>
<AppTopBar />
<main class="container py-4">
<RouterView />
</main>
</template>

View File

@@ -0,0 +1,6 @@
// App-level global styles
.auth-card {
max-width: 420px;
margin: 4rem auto;
}

View File

@@ -0,0 +1,26 @@
// Bootstrap variable overrides — dark theme
$body-bg: #121212;
$body-color: #e5e7eb;
$primary: #4f9dff;
$success: #22c55e;
$danger: #ef4444;
$card-bg: #1e1e1e;
$card-border-color: #2a2a2a;
$navbar-dark-color: #e5e7eb;
$input-bg: #1e1e1e;
$input-color: #e5e7eb;
$input-border-color: #333;
$input-focus-border-color: $primary;
$input-focus-box-shadow: 0 0 0 0.2rem rgba($primary, 0.25);
$input-placeholder-color: #6b7280;
$border-color: #2a2a2a;
$link-color: $primary;
$btn-close-color: #e5e7eb;
$btn-close-filter: invert(1);

View File

@@ -0,0 +1,3 @@
@import 'variables';
@import 'bootstrap/scss/bootstrap';
@import 'app';

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
</script>
<template>
<nav class="navbar navbar-expand-md navbar-dark bg-dark border-bottom border-secondary">
<div class="container">
<RouterLink class="navbar-brand" to="/">AIPS</RouterLink>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div id="navbarNav" class="collapse navbar-collapse">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<RouterLink class="nav-link" active-class="active" to="/">Home</RouterLink>
</li>
<li class="nav-item">
<RouterLink class="nav-link" active-class="active" to="/about">About</RouterLink>
</li>
</ul>
<ul class="navbar-nav">
<template v-if="auth.isAuthenticated">
<li class="nav-item">
<span class="nav-link text-light">{{ auth.user?.username }}</span>
</li>
<li class="nav-item">
<button class="btn btn-outline-light btn-sm my-1" @click="auth.logout()">
Logout
</button>
</li>
</template>
<template v-else>
<li class="nav-item">
<RouterLink class="nav-link" active-class="active" to="/login">Login</RouterLink>
</li>
<li class="nav-item">
<RouterLink class="nav-link" active-class="active" to="/signup">Sign Up</RouterLink>
</li>
</template>
</ul>
</div>
</div>
</nav>
</template>

20
front/src/main.ts Normal file
View File

@@ -0,0 +1,20 @@
import './assets/scss/main.scss'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { useAuthStore } from './stores/auth'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
const auth = useAuthStore()
auth.initialize()
app.use(router)
app.mount('#app')

47
front/src/router/index.ts Normal file
View File

@@ -0,0 +1,47 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
meta: { requiresAuth: false },
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
meta: { requiresAuth: false },
},
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue'),
meta: { guestOnly: true },
},
{
path: '/signup',
name: 'signup',
component: () => import('../views/SignupView.vue'),
meta: { guestOnly: true },
},
],
})
router.beforeEach((to) => {
const auth = useAuthStore()
if (to.meta.guestOnly && auth.isAuthenticated) {
return '/'
}
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return '/login'
}
})
export default router

37
front/src/services/api.ts Normal file
View File

@@ -0,0 +1,37 @@
import { useAuthStore } from '@/stores/auth'
const BASE_URL = import.meta.env.VITE_API_URL ?? '/api'
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
const auth = useAuthStore()
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (auth.token) {
headers['Authorization'] = `Bearer ${auth.token}`
}
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
})
if (!res.ok) {
const text = await res.text()
throw new Error(text || `Request failed: ${res.status}`)
}
if (res.status === 204) return undefined as T
return res.json() as Promise<T>
}
export const api = {
get: <T>(path: string) => request<T>('GET', path),
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body),
delete: <T>(path: string) => request<T>('DELETE', path),
}

View File

@@ -0,0 +1,26 @@
import type { AuthResponse, LoginCredentials, SignupCredentials, User } from '@/types'
// TODO: Wire up to real API endpoints via `api` helper
// import { api } from './api'
export const authService = {
async login(_credentials: LoginCredentials): Promise<AuthResponse> {
// TODO: return api.post<AuthResponse>('/auth/login', credentials)
throw new Error('Not implemented')
},
async signup(_credentials: SignupCredentials): Promise<AuthResponse> {
// TODO: return api.post<AuthResponse>('/auth/signup', credentials)
throw new Error('Not implemented')
},
async logout(): Promise<void> {
// TODO: return api.post<void>('/auth/logout')
throw new Error('Not implemented')
},
async getCurrentUser(): Promise<User> {
// TODO: return api.get<User>('/auth/me')
throw new Error('Not implemented')
},
}

53
front/src/stores/auth.ts Normal file
View File

@@ -0,0 +1,53 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import type { User, LoginCredentials, SignupCredentials } from '@/types'
const TOKEN_KEY = 'auth_token'
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const token = ref<string | null>(null)
const isAuthenticated = computed(() => !!user.value)
function initialize() {
const saved = localStorage.getItem(TOKEN_KEY)
if (saved) {
token.value = saved
// TODO: call authService.getCurrentUser() to validate token & hydrate user
user.value = { username: 'User', email: 'user@example.com' }
}
}
async function login(credentials: LoginCredentials) {
// TODO: const res = await authService.login(credentials)
// Mock successful response for now
const res = {
user: { username: credentials.email.split('@')[0] ?? credentials.email, email: credentials.email },
token: 'mock-jwt-token',
}
user.value = res.user
token.value = res.token
localStorage.setItem(TOKEN_KEY, res.token)
}
async function signup(credentials: SignupCredentials) {
// TODO: const res = await authService.signup(credentials)
// Mock successful response for now
const res = {
user: { username: credentials.username, email: credentials.email },
token: 'mock-jwt-token',
}
user.value = res.user
token.value = res.token
localStorage.setItem(TOKEN_KEY, res.token)
}
function logout() {
user.value = null
token.value = null
localStorage.removeItem(TOKEN_KEY)
}
return { user, token, isAuthenticated, initialize, login, signup, logout }
})

20
front/src/types/index.ts Normal file
View File

@@ -0,0 +1,20 @@
export interface User {
username: string
email: string
}
export interface LoginCredentials {
email: string
password: string
}
export interface SignupCredentials {
username: string
email: string
password: string
}
export interface AuthResponse {
user: User
token: string
}

View File

@@ -0,0 +1,3 @@
<template>
<h1>About</h1>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<h1>Home</h1>
</template>

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter, RouterLink } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const auth = useAuthStore()
const email = ref('')
const password = ref('')
const error = ref('')
const loading = ref(false)
async function onSubmit() {
error.value = ''
loading.value = true
try {
await auth.login({ email: email.value, password: password.value })
router.push('/')
} catch (e: any) {
error.value = e.message || 'Login failed'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="auth-card card">
<div class="card-body p-4">
<h2 class="card-title text-center mb-4">Log In</h2>
<div v-if="error" class="alert alert-danger">{{ error }}</div>
<form @submit.prevent="onSubmit">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input
id="email"
v-model="email"
type="email"
class="form-control"
required
autocomplete="email"
/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input
id="password"
v-model="password"
type="password"
class="form-control"
required
autocomplete="current-password"
/>
</div>
<button type="submit" class="btn btn-primary w-100" :disabled="loading">
{{ loading ? 'Logging in...' : 'Log In' }}
</button>
</form>
<p class="text-center mt-3 mb-0">
Don't have an account?
<RouterLink to="/signup">Sign up</RouterLink>
</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter, RouterLink } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const auth = useAuthStore()
const username = ref('')
const email = ref('')
const password = ref('')
const error = ref('')
const loading = ref(false)
async function onSubmit() {
error.value = ''
loading.value = true
try {
await auth.signup({ username: username.value, email: email.value, password: password.value })
router.push('/')
} catch (e: any) {
error.value = e.message || 'Signup failed'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="auth-card card">
<div class="card-body p-4">
<h2 class="card-title text-center mb-4">Sign Up</h2>
<div v-if="error" class="alert alert-danger">{{ error }}</div>
<form @submit.prevent="onSubmit">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input
id="username"
v-model="username"
type="text"
class="form-control"
required
autocomplete="username"
/>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input
id="email"
v-model="email"
type="email"
class="form-control"
required
autocomplete="email"
/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input
id="password"
v-model="password"
type="password"
class="form-control"
required
autocomplete="new-password"
/>
</div>
<button type="submit" class="btn btn-primary w-100" :disabled="loading">
{{ loading ? 'Signing up...' : 'Sign Up' }}
</button>
</form>
<p class="text-center mt-3 mb-0">
Already have an account?
<RouterLink to="/login">Log in</RouterLink>
</p>
</div>
</div>
</template>