From b52429c25095766369a497998fa9b2e3b3ddcf09 Mon Sep 17 00:00:00 2001 From: Andrija Stevanovic Date: Sun, 15 Feb 2026 17:56:28 +0100 Subject: [PATCH] frontend init --- .gitignore | 27 + front/.editorconfig | 8 + front/.gitattributes | 1 + front/.gitignore | 39 ++ front/.oxlintrc.json | 10 + front/.vscode/extensions.json | 8 + front/CLAUDE.md | 80 +++ front/README.md | 48 ++ front/bun.lock | 851 ++++++++++++++++++++++++++ front/env.d.ts | 1 + front/eslint.config.ts | 23 + front/index.html | 13 + front/package.json | 44 ++ front/public/favicon.ico | Bin 0 -> 4286 bytes front/src/App.vue | 11 + front/src/assets/scss/_app.scss | 6 + front/src/assets/scss/_variables.scss | 26 + front/src/assets/scss/main.scss | 3 + front/src/components/AppTopBar.vue | 58 ++ front/src/main.ts | 20 + front/src/router/index.ts | 47 ++ front/src/services/api.ts | 37 ++ front/src/services/authService.ts | 26 + front/src/stores/auth.ts | 53 ++ front/src/types/index.ts | 20 + front/src/views/AboutView.vue | 3 + front/src/views/HomeView.vue | 3 + front/src/views/LoginView.vue | 71 +++ front/src/views/SignupView.vue | 84 +++ front/tsconfig.app.json | 12 + front/tsconfig.json | 11 + front/tsconfig.node.json | 19 + front/vite.config.ts | 25 + 33 files changed, 1688 insertions(+) create mode 100644 front/.editorconfig create mode 100644 front/.gitattributes create mode 100644 front/.gitignore create mode 100644 front/.oxlintrc.json create mode 100644 front/.vscode/extensions.json create mode 100644 front/CLAUDE.md create mode 100644 front/README.md create mode 100644 front/bun.lock create mode 100644 front/env.d.ts create mode 100644 front/eslint.config.ts create mode 100644 front/index.html create mode 100644 front/package.json create mode 100644 front/public/favicon.ico create mode 100644 front/src/App.vue create mode 100644 front/src/assets/scss/_app.scss create mode 100644 front/src/assets/scss/_variables.scss create mode 100644 front/src/assets/scss/main.scss create mode 100644 front/src/components/AppTopBar.vue create mode 100644 front/src/main.ts create mode 100644 front/src/router/index.ts create mode 100644 front/src/services/api.ts create mode 100644 front/src/services/authService.ts create mode 100644 front/src/stores/auth.ts create mode 100644 front/src/types/index.ts create mode 100644 front/src/views/AboutView.vue create mode 100644 front/src/views/HomeView.vue create mode 100644 front/src/views/LoginView.vue create mode 100644 front/src/views/SignupView.vue create mode 100644 front/tsconfig.app.json create mode 100644 front/tsconfig.json create mode 100644 front/tsconfig.node.json create mode 100644 front/vite.config.ts diff --git a/.gitignore b/.gitignore index 827c760..754da20 100644 --- a/.gitignore +++ b/.gitignore @@ -484,3 +484,30 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp + +# --- Frontend (Vue / Vite / Bun) --- + +# Bun +bun.lockb + +# Vite build output +dist/ + +# Vite cache +.vite/ + +# TypeScript cache +*.tsbuildinfo + +# Local env variants +.env.local +.env.*.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +bun-debug.log* + +# Coverage +coverage/ \ No newline at end of file diff --git a/front/.editorconfig b/front/.editorconfig new file mode 100644 index 0000000..3b510aa --- /dev/null +++ b/front/.editorconfig @@ -0,0 +1,8 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +max_line_length = 100 diff --git a/front/.gitattributes b/front/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/front/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/front/.gitignore b/front/.gitignore new file mode 100644 index 0000000..cd68f14 --- /dev/null +++ b/front/.gitignore @@ -0,0 +1,39 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + +.eslintcache + +# Cypress +/cypress/videos/ +/cypress/screenshots/ + +# Vitest +__screenshots__/ + +# Vite +*.timestamp-*-*.mjs diff --git a/front/.oxlintrc.json b/front/.oxlintrc.json new file mode 100644 index 0000000..d5648b9 --- /dev/null +++ b/front/.oxlintrc.json @@ -0,0 +1,10 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"], + "env": { + "browser": true + }, + "categories": { + "correctness": "error" + } +} diff --git a/front/.vscode/extensions.json b/front/.vscode/extensions.json new file mode 100644 index 0000000..55ad03c --- /dev/null +++ b/front/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "Vue.volar", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "oxc.oxc-vscode" + ] +} diff --git a/front/CLAUDE.md b/front/CLAUDE.md new file mode 100644 index 0000000..4c1ac73 --- /dev/null +++ b/front/CLAUDE.md @@ -0,0 +1,80 @@ +# AIPS Frontend + +Vue 3 SPA with Bootstrap 5 dark theme, authentication UI, and a service layer ready for backend integration. + +## Tech Stack +- **Vue 3** + **TypeScript** + **Vite** — Composition API (` + + diff --git a/front/package.json b/front/package.json new file mode 100644 index 0000000..59b25b8 --- /dev/null +++ b/front/package.json @@ -0,0 +1,44 @@ +{ + "name": "front", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build", + "lint": "run-s lint:*", + "lint:oxlint": "oxlint . --fix", + "lint:eslint": "eslint . --fix --cache" + }, + "dependencies": { + "@popperjs/core": "^2.11.8", + "bootstrap": "^5.3.8", + "pinia": "^3.0.4", + "vue": "^3.5.27", + "vue-router": "^5.0.1" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.10.9", + "@vitejs/plugin-vue": "^6.0.3", + "@vue/eslint-config-typescript": "^14.6.0", + "@vue/tsconfig": "^0.8.1", + "eslint": "^9.39.2", + "eslint-plugin-oxlint": "~1.42.0", + "eslint-plugin-vue": "~10.7.0", + "jiti": "^2.6.1", + "npm-run-all2": "^8.0.4", + "oxlint": "~1.42.0", + "sass-embedded": "^1.97.3", + "typescript": "~5.9.3", + "vite": "^7.3.1", + "vite-plugin-vue-devtools": "^8.0.5", + "vue-tsc": "^3.2.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/front/public/favicon.ico b/front/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/front/src/App.vue b/front/src/App.vue new file mode 100644 index 0000000..9d6fbef --- /dev/null +++ b/front/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/front/src/assets/scss/_app.scss b/front/src/assets/scss/_app.scss new file mode 100644 index 0000000..145df45 --- /dev/null +++ b/front/src/assets/scss/_app.scss @@ -0,0 +1,6 @@ +// App-level global styles + +.auth-card { + max-width: 420px; + margin: 4rem auto; +} diff --git a/front/src/assets/scss/_variables.scss b/front/src/assets/scss/_variables.scss new file mode 100644 index 0000000..00f0095 --- /dev/null +++ b/front/src/assets/scss/_variables.scss @@ -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); diff --git a/front/src/assets/scss/main.scss b/front/src/assets/scss/main.scss new file mode 100644 index 0000000..aa127f5 --- /dev/null +++ b/front/src/assets/scss/main.scss @@ -0,0 +1,3 @@ +@import 'variables'; +@import 'bootstrap/scss/bootstrap'; +@import 'app'; diff --git a/front/src/components/AppTopBar.vue b/front/src/components/AppTopBar.vue new file mode 100644 index 0000000..11b12e0 --- /dev/null +++ b/front/src/components/AppTopBar.vue @@ -0,0 +1,58 @@ + + + diff --git a/front/src/main.ts b/front/src/main.ts new file mode 100644 index 0000000..e3a679b --- /dev/null +++ b/front/src/main.ts @@ -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') diff --git a/front/src/router/index.ts b/front/src/router/index.ts new file mode 100644 index 0000000..f64d27c --- /dev/null +++ b/front/src/router/index.ts @@ -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 diff --git a/front/src/services/api.ts b/front/src/services/api.ts new file mode 100644 index 0000000..b3470e0 --- /dev/null +++ b/front/src/services/api.ts @@ -0,0 +1,37 @@ +import { useAuthStore } from '@/stores/auth' + +const BASE_URL = import.meta.env.VITE_API_URL ?? '/api' + +async function request(method: string, path: string, body?: unknown): Promise { + const auth = useAuthStore() + + const headers: Record = { + '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 +} + +export const api = { + get: (path: string) => request('GET', path), + post: (path: string, body?: unknown) => request('POST', path, body), + put: (path: string, body?: unknown) => request('PUT', path, body), + delete: (path: string) => request('DELETE', path), +} diff --git a/front/src/services/authService.ts b/front/src/services/authService.ts new file mode 100644 index 0000000..3d3e823 --- /dev/null +++ b/front/src/services/authService.ts @@ -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 { + // TODO: return api.post('/auth/login', credentials) + throw new Error('Not implemented') + }, + + async signup(_credentials: SignupCredentials): Promise { + // TODO: return api.post('/auth/signup', credentials) + throw new Error('Not implemented') + }, + + async logout(): Promise { + // TODO: return api.post('/auth/logout') + throw new Error('Not implemented') + }, + + async getCurrentUser(): Promise { + // TODO: return api.get('/auth/me') + throw new Error('Not implemented') + }, +} diff --git a/front/src/stores/auth.ts b/front/src/stores/auth.ts new file mode 100644 index 0000000..0b686e9 --- /dev/null +++ b/front/src/stores/auth.ts @@ -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(null) + const token = ref(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 } +}) diff --git a/front/src/types/index.ts b/front/src/types/index.ts new file mode 100644 index 0000000..a032564 --- /dev/null +++ b/front/src/types/index.ts @@ -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 +} diff --git a/front/src/views/AboutView.vue b/front/src/views/AboutView.vue new file mode 100644 index 0000000..484c798 --- /dev/null +++ b/front/src/views/AboutView.vue @@ -0,0 +1,3 @@ + diff --git a/front/src/views/HomeView.vue b/front/src/views/HomeView.vue new file mode 100644 index 0000000..bdd7d3e --- /dev/null +++ b/front/src/views/HomeView.vue @@ -0,0 +1,3 @@ + diff --git a/front/src/views/LoginView.vue b/front/src/views/LoginView.vue new file mode 100644 index 0000000..f332cb1 --- /dev/null +++ b/front/src/views/LoginView.vue @@ -0,0 +1,71 @@ + + + diff --git a/front/src/views/SignupView.vue b/front/src/views/SignupView.vue new file mode 100644 index 0000000..35d0f71 --- /dev/null +++ b/front/src/views/SignupView.vue @@ -0,0 +1,84 @@ + + + diff --git a/front/tsconfig.app.json b/front/tsconfig.app.json new file mode 100644 index 0000000..913b8f2 --- /dev/null +++ b/front/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/front/tsconfig.json b/front/tsconfig.json new file mode 100644 index 0000000..66b5e57 --- /dev/null +++ b/front/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/front/tsconfig.node.json b/front/tsconfig.node.json new file mode 100644 index 0000000..822562d --- /dev/null +++ b/front/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node24/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*", + "eslint.config.*" + ], + "compilerOptions": { + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/front/vite.config.ts b/front/vite.config.ts new file mode 100644 index 0000000..ae9cae0 --- /dev/null +++ b/front/vite.config.ts @@ -0,0 +1,25 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, + css: { + preprocessorOptions: { + scss: { + silenceDeprecations: ['import', 'global-builtin', 'color-functions', 'if-function'], + }, + }, + }, +})