frontend init
This commit is contained in:
11
front/src/App.vue
Normal file
11
front/src/App.vue
Normal 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>
|
||||
6
front/src/assets/scss/_app.scss
Normal file
6
front/src/assets/scss/_app.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
// App-level global styles
|
||||
|
||||
.auth-card {
|
||||
max-width: 420px;
|
||||
margin: 4rem auto;
|
||||
}
|
||||
26
front/src/assets/scss/_variables.scss
Normal file
26
front/src/assets/scss/_variables.scss
Normal 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);
|
||||
3
front/src/assets/scss/main.scss
Normal file
3
front/src/assets/scss/main.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@import 'variables';
|
||||
@import 'bootstrap/scss/bootstrap';
|
||||
@import 'app';
|
||||
58
front/src/components/AppTopBar.vue
Normal file
58
front/src/components/AppTopBar.vue
Normal 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
20
front/src/main.ts
Normal 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
47
front/src/router/index.ts
Normal 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
37
front/src/services/api.ts
Normal 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),
|
||||
}
|
||||
26
front/src/services/authService.ts
Normal file
26
front/src/services/authService.ts
Normal 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
53
front/src/stores/auth.ts
Normal 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
20
front/src/types/index.ts
Normal 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
|
||||
}
|
||||
3
front/src/views/AboutView.vue
Normal file
3
front/src/views/AboutView.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<h1>About</h1>
|
||||
</template>
|
||||
3
front/src/views/HomeView.vue
Normal file
3
front/src/views/HomeView.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<h1>Home</h1>
|
||||
</template>
|
||||
71
front/src/views/LoginView.vue
Normal file
71
front/src/views/LoginView.vue
Normal 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>
|
||||
84
front/src/views/SignupView.vue
Normal file
84
front/src/views/SignupView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user