This commit is contained in:
danilbodry-mac
2026-04-10 19:58:29 +03:00
parent 5821006bf2
commit c5287dc81d
18 changed files with 2495 additions and 161 deletions

View File

@@ -1,43 +1,159 @@
<template>
<div>
<h2>Dashboard</h2>
<button @click="logout">Logout</button>
<h3>Users</h3>
<table>
<thead>
<tr><th>ID</th><th>Login</th><th>Created</th><th>IP</th></tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.login }}</td>
<td>{{ user.created }}</td>
<td>{{ user.ip }}</td>
</tr>
</tbody>
</table>
</div>
<AppLayout>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="card animate-fade-in">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Total Users</p>
<p class="text-3xl font-bold text-gray-900 mt-2">{{ stats.totalUsers }}</p>
</div>
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
</div>
<div class="mt-4 flex items-center text-sm">
<span class="text-green-600 font-medium"> 12%</span>
<span class="text-gray-500 ml-2">from last month</span>
</div>
</div>
<div class="card animate-fade-in" style="animation-delay: 0.1s">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Active Sessions</p>
<p class="text-3xl font-bold text-gray-900 mt-2">{{ stats.activeSessions }}</p>
</div>
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
</div>
<div class="mt-4 flex items-center text-sm">
<span class="text-green-600 font-medium"> 5%</span>
<span class="text-gray-500 ml-2">from last hour</span>
</div>
</div>
<div class="card animate-fade-in" style="animation-delay: 0.2s">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">System Health</p>
<p class="text-3xl font-bold text-gray-900 mt-2">{{ stats.systemHealth }}%</p>
</div>
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
<div class="mt-4">
<div class="h-2 bg-gray-200 rounded-full">
<div class="h-2 bg-blue-600 rounded-full" :style="{ width: `${stats.systemHealth}%` }"></div>
</div>
</div>
</div>
<div class="card animate-fade-in" style="animation-delay: 0.3s">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Uptime</p>
<p class="text-3xl font-bold text-gray-900 mt-2">{{ stats.uptime }}</p>
</div>
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<div class="mt-4 flex items-center text-sm">
<div class="flex items-center text-green-600">
<div class="w-2 h-2 bg-green-600 rounded-full mr-2"></div>
Operational
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="card">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Recent Users</h3>
<div class="space-y-3">
<div v-for="user in recentUsers" :key="user.id" class="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg transition-colors">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-gradient-to-br from-primary-500 to-primary-700 rounded-full flex items-center justify-center text-white font-semibold">
{{ user.login[0].toUpperCase() }}
</div>
<div>
<p class="font-medium text-gray-900">{{ user.login }}</p>
<p class="text-sm text-gray-500">{{ formatDate(user.created) }}</p>
</div>
</div>
<span class="px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">New</span>
</div>
</div>
</div>
<div class="card">
<h3 class="text-lg font-semibold text-gray-900 mb-4">System Status</h3>
<div class="space-y-4">
<div v-for="service in systemServices" :key="service.name" class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div :class="['w-2 h-2 rounded-full', service.status === 'healthy' ? 'bg-green-500' : 'bg-red-500']"></div>
<span class="text-gray-700">{{ service.name }}</span>
</div>
<span class="text-sm text-gray-500">{{ service.latency }}ms</span>
</div>
</div>
</div>
</div>
</AppLayout>
</template>
<script setup>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
import AppLayout from '../components/Layout/AppLayout.vue'
const router = useRouter()
const users = ref([])
onMounted(async () => {
try {
const res = await axios.get('/api/admin/users')
users.value = res.data
} catch {
router.push('/login')
}
const stats = ref({
totalUsers: 0,
activeSessions: 0,
systemHealth: 98,
uptime: '99.9%'
})
async function logout() {
await axios.post('/api/logout')
router.push('/login')
const recentUsers = ref([])
const systemServices = ref([
{ name: 'Database', status: 'healthy', latency: 12 },
{ name: 'Redis Cache', status: 'healthy', latency: 3 },
{ name: 'API Gateway', status: 'healthy', latency: 45 },
{ name: 'File Storage', status: 'healthy', latency: 28 }
])
onMounted(async () => {
await loadData()
})
async function loadData() {
try {
const res = await fetch('/api/admin/users')
const users = await res.json()
stats.value.totalUsers = users.length
recentUsers.value = users.slice(-5).reverse()
} catch (e) {
console.error('Failed to load data', e)
}
}
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
</script>

View File

@@ -1,19 +1,31 @@
<template>
<div>
<div class="login-container">
<h2>Login</h2>
<form @submit.prevent="login">
<input v-model="loginForm.login" placeholder="Login" />
<input v-model="loginForm.password" type="password" placeholder="Password" />
<div>
<input
v-model="loginForm.login"
placeholder="Login"
required
/>
</div>
<div>
<input
v-model="loginForm.password"
type="password"
placeholder="Password"
required
/>
</div>
<button type="submit">Login</button>
</form>
<p v-if="error">{{ error }}</p>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
const router = useRouter()
const loginForm = ref({ login: '', password: '' })
@@ -21,10 +33,39 @@ const error = ref('')
async function login() {
try {
await axios.post('/api/login', loginForm.value)
router.push('/')
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(loginForm.value)
})
if (res.ok) {
router.push('/')
} else {
error.value = 'Invalid credentials'
}
} catch (e) {
error.value = 'Invalid credentials'
error.value = 'Network error'
}
}
</script>
<style scoped>
.login-container {
max-width: 300px;
margin: 100px auto;
}
input {
width: 100%;
padding: 8px;
margin: 5px 0;
}
button {
width: 100%;
padding: 10px;
margin-top: 10px;
}
.error {
color: red;
}
</style>

View File

@@ -1,19 +1,33 @@
<template>
<div>
<div class="setup-container">
<h2>Setup Admin Account</h2>
<form @submit.prevent="setup">
<input v-model="form.login" placeholder="Admin login" />
<input v-model="form.password" type="password" placeholder="Password (min 6 chars)" />
<div>
<input
v-model="form.login"
placeholder="Admin login (min 3 chars)"
required
minlength="3"
/>
</div>
<div>
<input
v-model="form.password"
type="password"
placeholder="Password (min 6 chars)"
required
minlength="6"
/>
</div>
<button type="submit">Create Admin</button>
</form>
<p v-if="error">{{ error }}</p>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
const router = useRouter()
const form = ref({ login: '', password: '' })
@@ -21,10 +35,41 @@ const error = ref('')
async function setup() {
try {
await axios.post('/api/setup', form.value)
router.push('/login')
const res = await fetch('/api/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form.value)
})
const data = await res.json()
if (res.ok) {
router.push('/login')
} else {
error.value = data.error || 'Setup failed'
}
} catch (e) {
error.value = e.response?.data || 'Setup failed'
error.value = 'Network error'
}
}
</script>
<style scoped>
.setup-container {
max-width: 300px;
margin: 100px auto;
}
input {
width: 100%;
padding: 8px;
margin: 5px 0;
}
button {
width: 100%;
padding: 10px;
margin-top: 10px;
}
.error {
color: red;
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 via-white to-primary-50">
<div class="w-full max-w-md">
<!-- Logo -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl mb-4">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
<h1 class="text-3xl font-bold text-gray-900">Welcome Back</h1>
<p class="text-gray-600 mt-2">Sign in to your account</p>
</div>
<!-- Login Form -->
<div class="bg-white rounded-2xl shadow-xl p-8">
<form @submit.prevent="handleLogin" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<input
v-model="form.login"
type="text"
required
class="input-field pl-10"
placeholder="Enter your username"
/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<input
v-model="form.password"
:type="showPassword ? 'text' : 'password'"
required
class="input-field pl-10 pr-10"
placeholder="Enter your password"
/>
<button
type="button"
@click="showPassword = !showPassword"
class="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path v-if="showPassword" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path v-if="showPassword" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
</button>
</div>
</div>
<div class="flex items-center justify-between">
<label class="flex items-center">
<input type="checkbox" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" />
<span class="ml-2 text-sm text-gray-600">Remember me</span>
</label>
<a href="#" class="text-sm text-primary-600 hover:text-primary-700">Forgot password?</a>
</div>
<button
type="submit"
:disabled="loading"
class="w-full btn-primary py-3 relative"
>
<span v-if="!loading">Sign In</span>
<svg v-else class="animate-spin h-5 w-5 mx-auto text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</form>
<!-- Error Message -->
<transition name="fade">
<div v-if="error" class="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<p class="text-sm text-red-600">{{ error }}</p>
</div>
</transition>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const form = ref({ login: '', password: '' })
const loading = ref(false)
const error = ref('')
const showPassword = ref(false)
async function handleLogin() {
loading.value = true
error.value = ''
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form.value)
})
if (res.ok) {
router.push('/dashboard')
} else {
error.value = 'Invalid username or password'
}
} catch (e) {
error.value = 'Network error. Please try again.'
} finally {
loading.value = false
}
}
</script>

View File

@@ -0,0 +1,206 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 via-white to-primary-50">
<div class="w-full max-w-md">
<!-- Logo -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl mb-4 animate-pulse-slow">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
<h1 class="text-3xl font-bold text-gray-900">Setup Admin Account</h1>
<p class="text-gray-600 mt-2">Create your administrator account</p>
</div>
<!-- Setup Form -->
<div class="bg-white rounded-2xl shadow-xl p-8">
<!-- Progress Steps -->
<div class="mb-8">
<div class="flex items-center justify-center">
<div class="flex items-center">
<div class="w-8 h-8 bg-primary-600 rounded-full flex items-center justify-center text-white font-semibold">1</div>
<div class="ml-2 text-sm font-medium text-gray-900">Account Details</div>
</div>
<div class="mx-4 w-12 h-0.5 bg-gray-300"></div>
<div class="flex items-center">
<div class="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center text-gray-600 font-semibold">2</div>
<div class="ml-2 text-sm font-medium text-gray-500">Complete</div>
</div>
</div>
</div>
<form @submit.prevent="handleSetup" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<input
v-model="form.login"
type="text"
required
minlength="3"
class="input-field pl-10"
:class="{ 'border-red-300 focus:ring-red-500': validation.login }"
placeholder="Choose a username"
/>
</div>
<p v-if="validation.login" class="mt-1 text-xs text-red-600">{{ validation.login }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<input
v-model="form.password"
:type="showPassword ? 'text' : 'password'"
required
minlength="6"
class="input-field pl-10"
:class="{ 'border-red-300 focus:ring-red-500': validation.password }"
placeholder="Choose a strong password"
/>
<button
type="button"
@click="showPassword = !showPassword"
class="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path v-if="showPassword" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path v-if="showPassword" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
</button>
</div>
<p v-if="validation.password" class="mt-1 text-xs text-red-600">{{ validation.password }}</p>
<!-- Password Strength -->
<div v-if="form.password" class="mt-2">
<div class="flex items-center justify-between mb-1">
<span class="text-xs text-gray-600">Password strength</span>
<span class="text-xs font-medium" :class="strengthColor">{{ strengthText }}</span>
</div>
<div class="h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
class="h-full transition-all duration-300"
:class="strengthBarColor"
:style="{ width: `${strengthPercent}%` }"
></div>
</div>
</div>
</div>
<button
type="submit"
:disabled="loading"
class="w-full btn-primary py-3 relative overflow-hidden group"
>
<span v-if="!loading" class="relative z-10">Create Account</span>
<svg v-else class="animate-spin h-5 w-5 mx-auto text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<div class="absolute inset-0 bg-gradient-to-r from-primary-600 to-primary-700 transform scale-x-0 group-hover:scale-x-100 transition-transform origin-left"></div>
</button>
</form>
<!-- Error Message -->
<transition name="fade">
<div v-if="error" class="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<div class="flex items-start">
<svg class="h-5 w-5 text-red-400 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="ml-3 text-sm text-red-600">{{ error }}</p>
</div>
</div>
</transition>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const form = ref({ login: '', password: '' })
const loading = ref(false)
const error = ref('')
const showPassword = ref(false)
const validation = computed(() => {
const errors: any = {}
if (form.value.login && form.value.login.length < 3) {
errors.login = 'Username must be at least 3 characters'
}
if (form.value.password && form.value.password.length < 6) {
errors.password = 'Password must be at least 6 characters'
}
return errors
})
const passwordStrength = computed(() => {
const pwd = form.value.password
let strength = 0
if (pwd.length >= 6) strength++
if (pwd.length >= 8) strength++
if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) strength++
if (/\d/.test(pwd)) strength++
if (/[^a-zA-Z0-9]/.test(pwd)) strength++
return Math.min(strength, 5)
})
const strengthPercent = computed(() => (passwordStrength.value / 5) * 100)
const strengthText = computed(() => {
const texts = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong']
return texts[passwordStrength.value - 1] || ''
})
const strengthColor = computed(() => {
const colors = ['text-red-600', 'text-orange-600', 'text-yellow-600', 'text-green-600', 'text-green-700']
return colors[passwordStrength.value - 1] || ''
})
const strengthBarColor = computed(() => {
const colors = ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-green-500', 'bg-green-600']
return colors[passwordStrength.value - 1] || ''
})
async function handleSetup() {
if (Object.keys(validation.value).length > 0) return
loading.value = true
error.value = ''
try {
const res = await fetch('/api/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form.value)
})
const data = await res.json()
if (res.ok) {
router.push('/login')
} else {
error.value = data.error || 'Failed to create account'
}
} catch (e) {
error.value = 'Network error. Please try again.'
} finally {
loading.value = false
}
}
</script>