bug fixes, half-finished admin ui, and a more
This commit is contained in:
10
ui/pages/admin/config/settings.vue
Normal file
10
ui/pages/admin/config/settings.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ["auth", "admin"],
|
||||
layout: "admin"
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Hey
|
||||
</template>
|
||||
156
ui/pages/admin/index.vue
Normal file
156
ui/pages/admin/index.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<script setup lang="ts">
|
||||
import { useUser } from '~/composables/useUser'
|
||||
const { getUser } = useUser()
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth", "admin"],
|
||||
layout: "admin"
|
||||
});
|
||||
|
||||
let systemStatusData = await $fetch("/api/admin/system-status")
|
||||
|
||||
const calculateTimeSince = (time) => {
|
||||
const now = new Date();
|
||||
const date = new Date(time);
|
||||
const diffInSeconds = Math.floor((now - date) / 1000);
|
||||
|
||||
const days = Math.floor(diffInSeconds / (3600 * 24));
|
||||
const hours = Math.floor((diffInSeconds % (3600 * 24)) / 3600);
|
||||
const minutes = Math.floor((diffInSeconds % 3600) / 60);
|
||||
const seconds = diffInSeconds % 60;
|
||||
|
||||
// Constructing the output based on non-zero values
|
||||
const timeParts = [];
|
||||
if (days > 0) timeParts.push(`${days} days`);
|
||||
if (hours > 0 || days > 0) timeParts.push(`${hours} hours`);
|
||||
if (minutes > 0 || hours > 0 || days > 0) timeParts.push(`${minutes} minutes`);
|
||||
timeParts.push(`${seconds} seconds`);
|
||||
|
||||
return timeParts.join(', ');
|
||||
}
|
||||
|
||||
let uptime = ref('');
|
||||
let lastGcTime = ref('');
|
||||
|
||||
let systemStatusInterval;
|
||||
let timeInterval;
|
||||
|
||||
const updateTime = () => {
|
||||
uptime.value = calculateTimeSince(systemStatusData.uptime);
|
||||
lastGcTime.value = calculateTimeSince(systemStatusData.last_gc_time)
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updateTime();
|
||||
|
||||
systemStatusInterval = setInterval(async () => {
|
||||
console.log("refresh")
|
||||
systemStatusData = await $fetch("/api/admin/system-status")
|
||||
}, 5000);
|
||||
|
||||
timeInterval = setInterval(updateTime, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(systemStatusInterval);
|
||||
clearInterval(timeInterval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full overflow-hidden rounded-md border h-fit text-[15px]">
|
||||
<h4 class="bg-surface px-3.5 py-3 border-b">System Status</h4>
|
||||
<div class="p-3.5 text-sm">
|
||||
<dl class="flex-wrap">
|
||||
<dt>Server Uptime</dt>
|
||||
<dd>{{ uptime }}</dd>
|
||||
<dt>Current Goroutine</dt>
|
||||
<dd>{{ systemStatusData.num_goroutine }}</dd>
|
||||
<hr />
|
||||
<dt>Current Memory Usage</dt>
|
||||
<dd>{{ systemStatusData.cur_mem_usage }}</dd>
|
||||
<dt>Total Memory Allocated</dt>
|
||||
<dd>{{ systemStatusData.total_mem_usage }}</dd>
|
||||
<dt>Memory Obtained</dt>
|
||||
<dd>{{ systemStatusData.mem_obtained }}</dd>
|
||||
<dt>Pointer Lookup Times</dt>
|
||||
<dd>{{ systemStatusData.ptr_lookup_times }}</dd>
|
||||
<dt>Memory Allocations</dt>
|
||||
<dd>{{ systemStatusData.mem_allocations }}</dd>
|
||||
<dt>Memory Frees</dt>
|
||||
<dd>{{ systemStatusData.mem_frees }}</dd>
|
||||
<hr />
|
||||
<dt>Current Heap Usage</dt>
|
||||
<dd>{{ systemStatusData.cur_heap_usage }}</dd>
|
||||
<dt>Heap Memory Obtained</dt>
|
||||
<dd>{{ systemStatusData.heap_mem_obtained }}</dd>
|
||||
<dt>Heap Memory Idle</dt>
|
||||
<dd>{{ systemStatusData.heap_mem_idle }}</dd>
|
||||
<dt>Heap Memory In Use</dt>
|
||||
<dd>{{ systemStatusData.heap_mem_inuse }}</dd>
|
||||
<dt>Heap Memory Released</dt>
|
||||
<dd>{{ systemStatusData.heap_mem_release }}</dd>
|
||||
<dt>Heap Objects</dt>
|
||||
<dd>{{ systemStatusData.heap_objects }}</dd>
|
||||
<hr />
|
||||
<dt>Bootstrap Stack Usage</dt>
|
||||
<dd>{{ systemStatusData.bootstrap_stack_usage }}</dd>
|
||||
<dt>Stack Memory Obtained</dt>
|
||||
<dd>{{ systemStatusData.stack_mem_obtained }}</dd>
|
||||
<dt>MSpan Structures Usage</dt>
|
||||
<dd>{{ systemStatusData.mspan_structures_usage }}</dd>
|
||||
<dt>MSpan Structures Obtained</dt>
|
||||
<dd>{{ systemStatusData.mspan_structures_obtained }}</dd>
|
||||
<dt>MCache Structures Usage</dt>
|
||||
<dd>{{ systemStatusData.mcache_structures_usage }}</dd>
|
||||
<dt>MCache Structures Obtained</dt>
|
||||
<dd>{{ systemStatusData.mcache_structures_obtained }}</dd>
|
||||
<dt>Profiling Bucket Hash Table Obtained</dt>
|
||||
<dd>{{ systemStatusData.buck_hash_sys }}</dd>
|
||||
<dt>GC Metadata Obtained</dt>
|
||||
<dd>{{ systemStatusData.gc_sys }}</dd>
|
||||
<dt>Other System Allocation Obtained</dt>
|
||||
<dd>{{ systemStatusData.other_sys }}</dd>
|
||||
<hr />
|
||||
<dt>Next GC Recycle</dt>
|
||||
<dd>{{ systemStatusData.next_gc }}</dd>
|
||||
<dt>Since Last GC Time</dt>
|
||||
<dd>{{ lastGcTime }}</dd>
|
||||
<dt>Total GC Pause</dt>
|
||||
<dd>{{ systemStatusData.pause_total_ns }}</dd>
|
||||
<dt>Last GC Pause</dt>
|
||||
<dd>{{ systemStatusData.pause_ns }}</dd>
|
||||
<dt>GC Times</dt>
|
||||
<dd>{{ systemStatusData.num_gc }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
dl {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 600;
|
||||
width: 300px;
|
||||
max-width: calc(100% - 100px - 1em);
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
dd {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
width: calc(100% - 300px);
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
92
ui/pages/admin/users/index.vue
Normal file
92
ui/pages/admin/users/index.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { useUser } from "~/composables/useUser"
|
||||
import type { User } from "~/types/user";
|
||||
const { getUser } = useUser()
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth", "admin"],
|
||||
layout: "admin"
|
||||
});
|
||||
|
||||
let page = ref(0)
|
||||
|
||||
const { data: users } = await useFetch<User[]>('/api/admin/get-users/' + page.value);
|
||||
const { data: usersCount } = await useFetch<{ total_users: number }>('/api/admin/get-total-users');
|
||||
|
||||
const fetchNextPage = async () => {
|
||||
page.value += 1;
|
||||
let moreUsers = await $fetch('/api/admin/get-users/' + page.value);
|
||||
console.log(moreUsers)
|
||||
users.value = users.value?.concat(moreUsers)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-fit mb-4">
|
||||
<div class="overflow-hidden rounded-md border text-[15px]">
|
||||
<h4 class="bg-surface px-3.5 py-3 border-b">User Account Management (Total: {{ usersCount.total_users }})
|
||||
</h4>
|
||||
<div class="overflow-x-scroll max-w-full">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr class="text-left">
|
||||
<th class="py-2 px-4">ID</th>
|
||||
<th class="py-2 px-4">Username</th>
|
||||
<th class="py-2 px-4">Email Address</th>
|
||||
<th class="py-2 px-4">Restricted</th>
|
||||
<th class="py-2 px-4">Created</th>
|
||||
<th class="py-2 px-4 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" class="border-t">
|
||||
<td class="py-2 px-4 max-w-44" :title="user.id">{{ user.id }}</td>
|
||||
<td class="py-2 px-4">
|
||||
{{ user.username }}
|
||||
<span v-if="user.is_admin"
|
||||
class="ml-2 text-xs bg-accent/10 text-accent py-1 px-2 rounded">Admin</span>
|
||||
</td>
|
||||
<td class="py-2 px-4">{{ user.email }} </td>
|
||||
<td class="py-2 px-4">
|
||||
<svg v-if="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="m5 12l5 5L20 7" />
|
||||
</svg>
|
||||
</td>
|
||||
<td class="py-2 px-4">{{ new Date(user.created_at).toLocaleDateString('en-US', {
|
||||
year:
|
||||
'numeric', month: 'short', day: 'numeric'
|
||||
}) }}</td>
|
||||
<td class="py-2 px-4 h-full">
|
||||
<div class="flex items-center justify-end">
|
||||
<NuxtLink :to="`/admin/users/${user.id}/edit`"></NuxtLink>
|
||||
<button
|
||||
class="my-auto hover:bg-muted/10 p-1 transition-bg active:bg-muted/20 rounded-md">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
viewBox="0 0 24 24">
|
||||
<g class="stroke-blue-400/90" fill="none" stroke="currentColor"
|
||||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M7 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-1" />
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0-2.97-2.97L9 12v3h3zM16 5l3 3" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-full flex justify-center mt-4" v-if="users?.length != usersCount.total_users">
|
||||
<button class="bg-accent/10 text-accent px-2 py-1 rounded-md hover:" v-on:click="fetchNextPage()">Load
|
||||
More</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,7 +8,7 @@ definePageMeta({
|
||||
middleware: "auth"
|
||||
});
|
||||
|
||||
const user = await getUser()
|
||||
const user = await getUser();
|
||||
const route = useRoute();
|
||||
|
||||
let { data: files } = await useFetch<File[]>('/api/files/get/' + route.path.replace(/^\/home/, ''))
|
||||
@@ -33,7 +33,7 @@ const sortedFiles = computed(() => {
|
||||
let selectAll: Ref<"unchecked" | "some" | "checked"> = ref('unchecked');
|
||||
let selectedFiles = computed(() => sortedFiles.value?.filter(file => file.toggled === 'checked'))
|
||||
|
||||
watch(sortedFiles, (newVal, oldVal) => {
|
||||
watch(sortedFiles, (newVal) => {
|
||||
let checkedFilesLength = newVal?.filter(file => file.toggled === 'checked').length;
|
||||
if (newVal !== undefined && checkedFilesLength !== undefined && checkedFilesLength > 0) {
|
||||
if (checkedFilesLength < newVal.length) {
|
||||
@@ -46,7 +46,7 @@ watch(sortedFiles, (newVal, oldVal) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectAll, (newVal, oldVal) => {
|
||||
watch(selectAll, (newVal) => {
|
||||
if (newVal === 'some') {
|
||||
return
|
||||
}
|
||||
@@ -231,8 +231,6 @@ const createFolder = async () => {
|
||||
})
|
||||
)
|
||||
|
||||
console.log(error.value)
|
||||
|
||||
if (data.value != null) {
|
||||
user.usage = data.value.usage
|
||||
files.value?.push(data.value.file)
|
||||
@@ -318,27 +316,28 @@ const downloadFiles = async () => {
|
||||
<div class="flex flex-col p-2">
|
||||
<div class="mb-3 flex flex-col">
|
||||
<label for="folderNameInput" class="text-sm">name</label>
|
||||
<!-- TODO figure out why I cant focus this when the popup opens -->
|
||||
<Input id="folderNameInput" v-model="folderName" placeholder="Folder name" />
|
||||
<p class="text-love">{{ folderError }}</p>
|
||||
</div>
|
||||
<div class="ml-auto flex gap-x-1.5">
|
||||
<button v-on:click="popupVisable = !popupVisable"
|
||||
class=" px-2 py-1 rounded-md text-sm border bg-muted/10 hover:bg-muted/15 active:bg-muted/25 transition-bg">Close</button>
|
||||
class=" px-2 py-1 rounded-md text-sm border bg-muted/10 hover:bg-muted/15 active:bg-muted/25 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">Close</button>
|
||||
<button v-on:click="createFolder" :disabled="folderName === ''"
|
||||
class=" px-2 py-1 rounded-md text-sm
|
||||
disabled:bg-highlight-med/50 bg-highlight-med not:hover:brightness-105 not:active:brightness-110 transition-[background-color,filter] text-surface disabled:cursor-not-allowed">Confirm</button>
|
||||
disabled:bg-highlight-med/50 bg-highlight-med not:hover:brightness-105 not:active:brightness-110 transition-[background-color,filter] text-surface disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
<div class="w-full">
|
||||
<Nav v-on:update:filenav="(e) => fileNavClosed = e" :filenav="fileNavClosed" />
|
||||
<Nav v-on:update:filenav="(e) => fileNavClosed = e" :filenav="fileNavClosed" :user="user" />
|
||||
<div class="pt-6 pl-12 overflow-y-auto max-h-[calc(100vh-var(--nav-height))]" id="main">
|
||||
<div class="flex gap-x-4 flex-col">
|
||||
<div class="py-5 flex flex-row gap-x-4">
|
||||
<input type="file" ref="fileInput" @change="handleFileChange" multiple class="hidden" />
|
||||
<button v-on:click="openFilePicker"
|
||||
class="focus:outline-none focus:ring focus:ring-inset rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg">
|
||||
class="rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
@@ -349,7 +348,7 @@ const downloadFiles = async () => {
|
||||
Upload
|
||||
</button>
|
||||
<button v-on:click="popupVisable = !popupVisable"
|
||||
class="focus:outline-none focus:ring focus:ring-inset rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg">
|
||||
class="rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
@@ -371,7 +370,7 @@ const downloadFiles = async () => {
|
||||
<div class="flex flex-row gap-x-2"
|
||||
v-if="selectedFiles !== undefined && selectedFiles.length > 0">
|
||||
<button v-on:click="downloadFiles"
|
||||
class="flex flex-row px-2 py-1 rounded-md transition-bg text-xs border hover:bg-muted/10 active:bg-muted/20 items-center focus:outline-none focus:ring focus:ring-inset">
|
||||
class="flex flex-row px-2 py-1 rounded-md transition-bg text-xs border hover:bg-muted/10 active:bg-muted/20 items-center focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
@@ -380,7 +379,7 @@ const downloadFiles = async () => {
|
||||
Download
|
||||
</button>
|
||||
<button v-on:click="deleteFiles"
|
||||
class="flex flex-row px-2 py-1 rounded-md transition-bg text-xs border hover:bg-love/10 active:bg-love/20 hover:text-love active:text-love items-center focus:outline-none focus:ring focus:ring-inset">
|
||||
class="flex flex-row px-2 py-1 rounded-md transition-bg text-xs border hover:bg-love/10 active:bg-love/20 hover:text-love active:text-love items-center focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
|
||||
<svg class="mr-1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
@@ -393,15 +392,18 @@ const downloadFiles = async () => {
|
||||
</div>
|
||||
<table class="w-full text-sm mt-2 table-fixed">
|
||||
<thead class="border-b">
|
||||
<tr class="flex flex-row h-10 group pl-[30px] -ml-7 relative items-center">
|
||||
<th class="left-0 absolute">
|
||||
<div>
|
||||
<tr class="flex flex-row h-10 group relative items-center focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"
|
||||
v-on:keypress.enter="selectAll === 'unchecked' ? selectAll = 'checked' : selectAll = 'unchecked'"
|
||||
v-on:keypress.space.prevent="selectAll === 'unchecked' ? selectAll = 'checked' : selectAll = 'unchecked'"
|
||||
tabindex="0">
|
||||
<th class="-ml-7 flex-shrink-0">
|
||||
<div class="w-5 h-5">
|
||||
<Checkbox :class="{ 'hidden': selectAll === 'unchecked' }"
|
||||
v-model="selectAll" class="group-hover:flex" type="checkbox" />
|
||||
</div>
|
||||
</th>
|
||||
<th v-on:click="selectAll === 'unchecked' ? selectAll = 'checked' : selectAll = 'unchecked'"
|
||||
class="flex-grow min-w-40 text-start flex items-center h-full">
|
||||
class="pl-4 flex-grow min-w-40 text-start flex items-center h-full">
|
||||
Name
|
||||
</th>
|
||||
<th class="min-w-32 text-start">
|
||||
@@ -413,9 +415,12 @@ const downloadFiles = async () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block">
|
||||
<tr class="flex border-l-2 flex-row h-10 group items-center border-b active:bg-surface/45 transition-bg relative"
|
||||
v-for="file in sortedFiles"
|
||||
:class="file.toggled === 'checked' ? 'bg-accent/20 border-l-accent' : 'border-l-transparent hover:bg-surface'">
|
||||
<tr v-for="file in sortedFiles"
|
||||
class="flex border-l-2 flex-row h-10 group items-center border-b active:bg-surface/45 transition-bg relative focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"
|
||||
:class="file.toggled === 'checked' ? 'bg-accent/20 border-l-accent' : 'border-l-transparent hover:bg-surface'"
|
||||
v-on:keypress.enter="file.toggled === 'unchecked' ? file.toggled = 'checked' : file.toggled = 'unchecked'"
|
||||
v-on:keypress.space.prevent="file.toggled === 'unchecked' ? file.toggled = 'checked' : file.toggled = 'unchecked'"
|
||||
tabindex="0">
|
||||
<td class="-ml-7 flex-shrink-0">
|
||||
<div class="w-5 h-5">
|
||||
<Checkbox class="group-hover:flex"
|
||||
@@ -424,10 +429,7 @@ const downloadFiles = async () => {
|
||||
</div>
|
||||
</td>
|
||||
<td v-on:click="file.toggled === 'unchecked' ? file.toggled = 'checked' : file.toggled = 'unchecked'"
|
||||
v-on:keypress.enter="file.toggled === 'unchecked' ? file.toggled = 'checked' : file.toggled = 'unchecked'"
|
||||
v-on:keypress.space="file.toggled === 'unchecked' ? file.toggled = 'checked' : file.toggled = 'unchecked'"
|
||||
class="flex-grow text-start flex items-center h-full min-w-40 focus:outline-none focus:ring focus:ring-inset pl-4"
|
||||
tabindex="0">
|
||||
class="flex-grow text-start flex items-center h-full min-w-40 pl-4">
|
||||
<div class="flex items-center min-w-40">
|
||||
<svg v-if="!file.is_dir" class="mr-2 flex-shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
@@ -463,7 +465,7 @@ const downloadFiles = async () => {
|
||||
<td :class="file.toggled === 'checked' ? 'context-active' : 'context'"
|
||||
class="absolute pl-6 top-0 bottom-0 right-0 hidden group-hover:flex group-focus-within:flex items-center pr-8">
|
||||
<button v-on:click="downloadFile(file)"
|
||||
class="p-2 rounded hover:bg-muted/10 active:bg-muted/20 focus:outline-none focus:ring focus:ring-inset">
|
||||
class="p-2 rounded hover:bg-muted/10 active:bg-muted/20 focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
||||
viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
|
||||
@@ -12,6 +12,7 @@ let password = ref('')
|
||||
|
||||
let error = ref('')
|
||||
|
||||
let timeout;
|
||||
const submitForm = async () => {
|
||||
let { data, error: fetchError } = await useAsyncData<User, NuxtError<{ message: string }>>(
|
||||
() => $fetch('/api/login', {
|
||||
@@ -25,12 +26,16 @@ const submitForm = async () => {
|
||||
|
||||
if (fetchError.value !== null && fetchError.value.data !== undefined) {
|
||||
error.value = fetchError.value.data.message
|
||||
setTimeout(() => error.value = "", 15000)
|
||||
timeout = setTimeout(() => error.value = "", 15000)
|
||||
} else if (data.value !== null) {
|
||||
setUser(data.value)
|
||||
await navigateTo('/home')
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -13,6 +13,7 @@ let password = ref('')
|
||||
|
||||
let error = ref('')
|
||||
|
||||
let timeout;
|
||||
const submitForm = async () => {
|
||||
let { data, error: fetchError } = await useAsyncData<User, NuxtError<{ message: string }>>(
|
||||
() => $fetch('/api/signup', {
|
||||
@@ -27,12 +28,16 @@ const submitForm = async () => {
|
||||
|
||||
if (fetchError.value != null && fetchError.value.data !== undefined) {
|
||||
error.value = fetchError.value.data.message
|
||||
setTimeout(() => error.value = "", 15000)
|
||||
timeout = setTimeout(() => error.value = "", 15000)
|
||||
} else if (data.value !== null) {
|
||||
setUser(data.value)
|
||||
await navigateTo('/home')
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
Reference in New Issue
Block a user