feat(frontend): add login UI and connect to backend auth
This commit is contained in:
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
let fileRecords = $state<FileRecord[]>([]);
|
let fileRecords = $state<FileRecord[]>([]);
|
||||||
let confirmDeleteId = $state<number | null>(null);
|
let confirmDeleteId = $state<number | null>(null);
|
||||||
|
let selectedFileId = $state<number | null>(null);
|
||||||
|
let previewFile = $state<FileRecord | null>(null);
|
||||||
let search = $state('');
|
let search = $state('');
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
||||||
@@ -29,6 +31,12 @@
|
|||||||
const res = await fetch('http://localhost:3000/api/files', {
|
const res = await fetch('http://localhost:3000/api/files', {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
window.location.href = '/auth';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fileRecords = await res.json();
|
fileRecords = await res.json();
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
@@ -91,10 +99,30 @@
|
|||||||
if (bytes < 1000 * 1000 * 1000) return (bytes / 1000 / 1000).toFixed(1) + 'MB';
|
if (bytes < 1000 * 1000 * 1000) return (bytes / 1000 / 1000).toFixed(1) + 'MB';
|
||||||
return (bytes / 1000 / 1000 / 1000).toFixed(1) + 'GB';
|
return (bytes / 1000 / 1000 / 1000).toFixed(1) + 'GB';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await fetch('http://localhost:3000/api/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
window.location.href = '/auth';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full px-4 py-8">
|
<div class="w-full px-4 py-8">
|
||||||
<h1 class="text-4xl text-white mb-6" style="font-family: 'Caveat', cursive;">rafi</h1>
|
<div class="relative mb-6 flex items-center justify-center">
|
||||||
|
<h1 class="text-4xl text-white" style="font-family: 'Caveat', cursive;">loft</h1>
|
||||||
|
<button
|
||||||
|
onclick={handleLogout}
|
||||||
|
class="fixed top-4 right-4 flex items-center gap-1.5 px-2 py-1 border border-sky-200/20 text-white/60 hover:border-sky-200/40 text-sm transition-colors rounded-sm cursor-pointer">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||||
|
<polyline points="16 17 21 12 16 7"/>
|
||||||
|
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||||
|
</svg>
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center mb-4">
|
<div class="flex justify-center mb-4">
|
||||||
<input
|
<input
|
||||||
@@ -126,7 +154,9 @@
|
|||||||
<div
|
<div
|
||||||
transition:fade={{ duration: 200 }}
|
transition:fade={{ duration: 200 }}
|
||||||
animate:flip={{ duration: 200 }}
|
animate:flip={{ duration: 200 }}
|
||||||
class="flex items-stretch border-b border-sky-200/20 border-l-2 border-l-transparent last:border-b-0 hover:bg-white/2 hover:border-l-sky-400/50 transition-all"
|
onclick={() => selectedFileId = fileRecord.id}
|
||||||
|
ondblclick={() => previewFile = fileRecord}
|
||||||
|
class="flex items-stretch border-l hover:bg-white/2 transition-all {selectedFileId === fileRecord.id ? 'border-sky-400/50 bg-white/2 ring-1 ring-inset ring-sky-400/50' : 'border-sky-200/20 border-l-transparent hover:border-l-sky-400/50'}"
|
||||||
>
|
>
|
||||||
<span class="font-medium text-white text-sm flex-1 truncate py-2 pl-6">{formatName(fileRecord.name)}</span>
|
<span class="font-medium text-white text-sm flex-1 truncate py-2 pl-6">{formatName(fileRecord.name)}</span>
|
||||||
<span class="text-sm text-white/40 w-40 py-2">{formatSize(fileRecord.size)}</span>
|
<span class="text-sm text-white/40 w-40 py-2">{formatSize(fileRecord.size)}</span>
|
||||||
@@ -188,3 +218,29 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if previewFile}
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
|
||||||
|
onclick={() => previewFile = null}
|
||||||
|
transition:fade={{ duration: 150 }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[#0f1117] border border-sky-200/20 rounded-sm w-2/3 h-2/3 flex flex-col p-6"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<span class="text-white text-sm font-medium">{previewFile.name}</span>
|
||||||
|
<button class="text-white/40 hover:text-white transition-colors cursor-pointer" onclick={() => previewFile = null}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex items-center justify-center text-white/20 text-sm">
|
||||||
|
preview coming soon
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
74
frontend/src/routes/auth/+page.svelte
Normal file
74
frontend/src/routes/auth/+page.svelte
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let activeTab = $state<'login' | 'register'>('login');
|
||||||
|
let username = $state('');
|
||||||
|
let password = $state('');
|
||||||
|
let error = $state('');
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const endpoint = activeTab === 'login' ? '/api/auth/login' : '/api/auth/register';
|
||||||
|
const res = await fetch(`http://localhost:3000${endpoint}`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
error = 'Invalid credentials';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex-1 flex items-center justify-center -mt-16">
|
||||||
|
<div class="w-full max-w-xs">
|
||||||
|
<h1 class="text-4xl text-white mb-8" style="font-family: 'Caveat', cursive;">loft</h1>
|
||||||
|
|
||||||
|
<div class="flex mb-6 border-b border-sky-200/20">
|
||||||
|
<button
|
||||||
|
onclick={() => activeTab = 'login'}
|
||||||
|
class="flex-1 py-2 text-sm transition-colors cursor-pointer {activeTab === 'login' ? 'text-white border-b border-white -mb-px' : 'text-white/40 hover:text-white/60'}"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => activeTab = 'register'}
|
||||||
|
class="flex-1 py-2 text-sm transition-colors cursor-pointer {activeTab === 'register' ? 'text-white border-b border-white -mb-px' : 'text-white/40 hover:text-white/60'}"
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="username" class="text-white/30 text-xs">Username</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
bind:value={username}
|
||||||
|
type="text"
|
||||||
|
autocomplete="username"
|
||||||
|
class="px-4 py-2 rounded-sm !bg-[#0f1117] border border-sky-200/15 transition-colors hover:bg-black/20 hover:border-sky-200/30 text-white/80 text-sm focus:outline-none focus:border-sky-200/40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label for="password" class="text-white/30 text-xs">Password</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
bind:value={password}
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
class="px-4 py-2 rounded-sm !bg-[#0f1117] border border-sky-200/15 transition-colors hover:bg-black/20 hover:border-sky-200/30 text-white/80 text-sm focus:outline-none focus:border-sky-200/40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if error}<p class="text-red-400/60 text-xs">{error}</p>{/if}
|
||||||
|
<button
|
||||||
|
onclick={handleSubmit}
|
||||||
|
class="mt-1 px-4 py-2 border border-sky-200/15 text-white/40 hover:border-sky-200/30 hover:text-white/60 text-sm transition-colors rounded-sm cursor-pointer">
|
||||||
|
{activeTab === 'login' ? 'Login' : 'Register'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -75,6 +75,21 @@ button {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus,
|
||||||
|
input:-webkit-autofill:active {
|
||||||
|
-webkit-box-shadow: 0 0 0 1000px #0f1117 inset !important;
|
||||||
|
-webkit-text-fill-color: #ffffffcc !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:-moz-autofill {
|
||||||
|
background-color: #0f1117 !important;
|
||||||
|
color: #ffffffcc !important;
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
button:focus:not(:focus-visible) {
|
button:focus:not(:focus-visible) {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user