🐐
This commit is contained in:
9
frontend/src/app.css
Normal file
9
frontend/src/app.css
Normal file
@@ -0,0 +1,9 @@
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #0d0a04;
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
group: { bg: "#e8e4d0", header: "#748c12" },
|
||||
science_thing: { bg: "#c7c5c1", header: "#060c17" },
|
||||
vehicle: { bg: "#c7c1c4", header: "#801953" },
|
||||
business: { bg: "#b7c1c4", header: "#3c5251" },
|
||||
other: { bg: "#dddad4", header: "#6a6860" },
|
||||
};
|
||||
|
||||
@@ -210,7 +211,7 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
background: #222;
|
||||
background: #e8d8b8;
|
||||
overflow: hidden;
|
||||
border-bottom: 2px solid #000;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
<script>
|
||||
import Card from "$lib/Card.svelte";
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import Card from '$lib/Card.svelte';
|
||||
|
||||
let cards = $state([]);
|
||||
let loading = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
if (!localStorage.getItem('token')) goto('/auth');
|
||||
});
|
||||
|
||||
async function openPack() {
|
||||
loading = true;
|
||||
try {
|
||||
const res = await fetch("http://localhost:8000/pack/14");
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
cards = await res.json();
|
||||
} catch (e) {
|
||||
console.error("Failed to open pack:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
loading = true;
|
||||
try {
|
||||
const res = await fetch('http://localhost:8000/pack/10', {
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
|
||||
});
|
||||
if (res.status === 401) { goto('/auth'); return; }
|
||||
cards = await res.json();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
@@ -33,7 +41,7 @@
|
||||
<style>
|
||||
main {
|
||||
min-height: 100vh;
|
||||
background: #0d0a04;
|
||||
background: #887859;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
|
||||
201
frontend/src/routes/auth/+page.svelte
Normal file
201
frontend/src/routes/auth/+page.svelte
Normal file
@@ -0,0 +1,201 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let mode = $state('login');
|
||||
let username = $state('');
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let error = $state('');
|
||||
let loading = $state(false);
|
||||
|
||||
async function submit() {
|
||||
error = '';
|
||||
if (!username.trim()) {
|
||||
error = 'Username is required';
|
||||
return;
|
||||
}
|
||||
loading = true;
|
||||
try {
|
||||
if (mode === 'register') {
|
||||
const res = await fetch('http://localhost:8000/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail);
|
||||
mode = 'login';
|
||||
error = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.append('username', username);
|
||||
form.append('password', password);
|
||||
const res = await fetch('http://localhost:8000/login', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail);
|
||||
localStorage.setItem('token', data.access_token);
|
||||
goto('/');
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>{mode === 'login' ? 'Sign In' : 'Register'}</h1>
|
||||
|
||||
<div class="fields">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
bind:value={username}
|
||||
/>
|
||||
{#if mode === 'register'}
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
bind:value={email}
|
||||
/>
|
||||
{/if}
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
bind:value={password}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="error">{error}</p>
|
||||
{/if}
|
||||
|
||||
<button onclick={submit} disabled={loading}>
|
||||
{loading ? 'Please wait...' : mode === 'login' ? 'Sign In' : 'Create Account'}
|
||||
</button>
|
||||
|
||||
<p class="toggle">
|
||||
{mode === 'login' ? "Don't have an account?" : 'Already have an account?'}
|
||||
<button class="link" onclick={() => { mode = mode === 'login' ? 'register' : 'login'; error = ''; }}>
|
||||
{mode === 'login' ? 'Register' : 'Sign in'}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap');
|
||||
|
||||
main {
|
||||
min-height: 100vh;
|
||||
background: #0d0a04;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 340px;
|
||||
background: #2e1c05;
|
||||
border: 2px solid #6b4c1e;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 20px;
|
||||
color: #f0d080;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 9px 12px;
|
||||
background: #1a1008;
|
||||
border: 1.5px solid #8b6420;
|
||||
border-radius: 6px;
|
||||
color: #f0d080;
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 15px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: rgba(240, 180, 80, 0.4);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #f0d080;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: #6b4c1e;
|
||||
color: #f0d080;
|
||||
border: 1.5px solid #8b6420;
|
||||
border-radius: 6px;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #8b6420;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #c84040;
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 13px;
|
||||
color: rgba(240, 180, 80, 0.7);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
all: unset;
|
||||
color: #f0d080;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
font-family: 'Crimson Text', serif;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: #f0d080;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user