Compare commits

...
7 Commits
Author SHA1 Message Date
Esa Kataja 8cf2557896 refactor: improve login form and button styling
- Replace hardcoded credentials with appropriate placeholders
- Add descriptive Finnish placeholders for login inputs
- Style 'Start game' button with primary button class and center alignment
- Improve security by removing exposed default credentials
2024-12-02 16:34:04 +02:00
Esa Kataja ba8db120b4 style: enhance checkbox appearance in Card component
- Increase checkbox size to 24x24px
- Add rounded corners
- Implement visible border with theme color
- Add smooth transition animation
- Improve checked state visibility
2024-12-02 16:30:28 +02:00
Esa Kataja a8809ace9b Add admin panel 2024-12-02 16:18:31 +02:00
Esa Kataja aed5c675d3 Add admin panel 2024-12-02 15:26:24 +02:00
Esa Kataja fe14660601 Add Play logic 2024-11-25 18:51:32 +02:00
Esa Kataja b652447121 Change Push footer to bottom of page 2024-11-25 18:51:01 +02:00
Esa Kataja 23c738d218 Add Final score display 2024-11-25 18:46:53 +02:00
11 changed files with 804 additions and 16 deletions
+11
View File
@@ -13,6 +13,17 @@ export const handle: Handle = async ({ event, resolve }) => {
} }
if (event.url.pathname.startsWith('/admin')) {
const current_user = await authenticateUser(event);
if (!current_user) {
throw redirect(302, '/login');
}
if (!current_user.is_admin) {
throw redirect(302, '/');
}
event.locals.user = current_user;
}
const response = await resolve(event); const response = await resolve(event);
return response; return response;
}; };
+13 -4
View File
@@ -1,13 +1,22 @@
<script lang="ts"> <script lang="ts">
import type { Cliche } from '$lib/types'; import type { Cliche } from '$lib/types';
let { cardData, onCardChecked }: { cardData: Cliche; onCardChecked: any } = $props(); let {
cardData,
onCardChecked,
isBonus = false
}: { cardData: Cliche; onCardChecked: any; isBonus?: boolean } = $props();
let isCardChecked: boolean = $state(false);
let isSelectedAtStart: boolean = $state(false);
function toggleCardChecked() { function toggleCardChecked() {
onCardChecked(cardData); isCardChecked = !isCardChecked;
onCardChecked(cardData, isCardChecked, isBonus);
} }
console.log('isBonus', isBonus);
</script> </script>
<div class="card"> <div class="card" id="card">
<div class="relative"> <div class="relative">
<div> <div>
<input <input
@@ -15,7 +24,7 @@
name="" name=""
id="card-{cardData.id}" id="card-{cardData.id}"
onchange={toggleCardChecked} onchange={toggleCardChecked}
class="appearance-none" class="appearance-none h-6 w-6 rounded-lg border-2 border-primary-FestiveRed-dark checked:bg-primary-FestiveRed-dark checked:border-primary-FestiveRed-dark cursor-pointer transition-all duration-200 ease-in-out"
/> />
</div> </div>
<div> <div>
+2 -2
View File
@@ -15,7 +15,7 @@
class="input-text" class="input-text"
required required
minlength="3" minlength="3"
value="admin" placeholder="käyttäjätunnus"
/> />
<label for="password">Salasana</label> <label for="password">Salasana</label>
<input <input
@@ -24,8 +24,8 @@
id="password" id="password"
class="input-text" class="input-text"
required required
value="password"
minlength="5" minlength="5"
placeholder="salasana"
/> />
<hr class="my-4 border-0" /> <hr class="my-4 border-0" />
<button type="submit" class="button button-primary">Sisään</button> <button type="submit" class="button button-primary">Sisään</button>
+1 -1
View File
@@ -12,7 +12,7 @@
<div class="flex min-h-[100dvh] flex-col"> <div class="flex min-h-[100dvh] flex-col">
<Header user={data.user} /> <Header user={data.user} />
<main class="container mx-auto max-w-4xl"> <main class="container mx-auto max-w-4xl flex-grow">
{@render children()} {@render children()}
</main> </main>
<Footer /> <Footer />
+192
View File
@@ -0,0 +1,192 @@
import { error } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
const api_url = import.meta.env.VITE_API_URL;
interface Movie {
name: string;
imdb_id: string;
showtime: string;
plot: string;
actors: string[];
release_date: string;
poster_url: string;
id: string;
created_at: string;
modified_at: string;
is_watched: boolean;
}
export const load: PageServerLoad = async ({ fetch }) => {
try {
const [usersRes, moviesRes, cardsRes] = await Promise.all([
fetch(`${api_url}/user`),
fetch(`${api_url}/movie`),
fetch(`${api_url}/card`)
]);
if (!usersRes.ok) throw error(usersRes.status, 'Failed to load users');
if (!moviesRes.ok) throw error(moviesRes.status, 'Failed to load movies');
if (!cardsRes.ok) throw error(cardsRes.status, 'Failed to load cards');
const users = await usersRes.json();
const movies: Movie[] = await moviesRes.json();
const cards = await cardsRes.json();
return {
users,
movies,
cards
};
} catch (error) {
console.error('Error loading data:', error);
return {
users: [],
movies: [],
cards: []
};
}
};
export const actions: Actions = {
addUser: async ({ request }) => {
const data = await request.formData();
const username = data.get('username');
const password = data.get('password');
try {
const response = await fetch(`${api_url}/admin/user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return { success: true };
} catch (error) {
console.error('Error adding user:', error);
return { success: false, message: 'Failed to add user' };
}
},
deleteUser: async ({ request }) => {
const data = await request.formData();
const userId = data.get('userId');
try {
const response = await fetch(`${api_url}/admin/user?user_id=${userId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return { success: true };
} catch (error) {
console.error('Error deleting user:', error);
return { success: false, message: 'Failed to delete user' };
}
},
addMovie: async ({ request }) => {
const data = await request.formData();
const movieData = {
imdb_id: data.get('imdb_id'),
showtime: data.get('showtime'),
plot: data.get('plot')
};
try {
const response = await fetch(`${api_url}/admin/movie`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(movieData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return { success: true };
} catch (error) {
console.error('Error adding movie:', error);
return { success: false, message: 'Failed to add movie' };
}
},
setMovieWatched: async ({ request, fetch }) => {
const formData = await request.formData();
const movieId = formData.get('movieId');
const response = await fetch(`${api_url}/admin/movie/${movieId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ is_watched: true })
});
if (!response.ok) {
throw error(response.status, 'Failed to update movie status');
}
return { success: true };
},
addCard: async ({ request, fetch }) => {
const data = await request.formData();
const cardData = {
title: data.get('title'),
description: data.get('description'),
point_value: Number(data.get('point_value'))
};
try {
const response = await fetch(`${api_url}/admin/card`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(cardData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return { success: true };
} catch (error) {
console.error('Error adding card:', error);
return { success: false, message: 'Failed to add card' };
}
},
deleteCard: async ({ request, fetch }) => {
const formData = await request.formData();
const cardId = formData.get('cardId');
const response = await fetch(`${api_url}/admin/card/${cardId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw error(response.status, 'Failed to delete card');
}
return { success: true };
}
};
+322
View File
@@ -0,0 +1,322 @@
<script lang="ts">
import { enhance } from '$app/forms';
let activeTab = 'users';
export let data;
const { users, movies, cards } = data;
</script>
<div class="admin-panel">
<nav class="tabs">
<button
class:active={activeTab === 'users'}
on:click={() => activeTab = 'users'}>Users</button>
<button
class:active={activeTab === 'movies'}
on:click={() => activeTab = 'movies'}>Movies</button>
<button
class:active={activeTab === 'cards'}
on:click={() => activeTab = 'cards'}>Cards</button>
</nav>
<div class="tab-content">
{#if activeTab === 'users'}
<div class="section">
<h2>Existing Users</h2>
<div class="table-container">
<table>
<thead>
<tr>
<th>Username</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each users as user}
<tr>
<td>{user.username}</td>
<td>
<form method="POST" action="?/deleteUser" use:enhance>
<input type="hidden" name="userId" value={user.id}>
<button type="submit" class="danger small">Delete</button>
</form>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<h2>Add User</h2>
<form method="POST" action="?/addUser" use:enhance>
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Add User</button>
</form>
</div>
{/if}
{#if activeTab === 'movies'}
<div class="section">
<h2>Existing Movies</h2>
<div class="table-container">
<table>
<thead>
<tr>
<th>Name</th>
<th>Plot</th>
<th>IMDB</th>
<th>Showtime</th>
<th>Watched</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each movies as movie}
<tr>
<td>{movie.name}</td>
<td>{movie.plot}</td>
<td>
<a href="https://www.imdb.com/title/{movie.imdb_id}" target="_blank" rel="noopener noreferrer">
View on IMDB
</a>
</td>
<td>{new Date(movie.showtime).toLocaleDateString()}</td>
<td>{movie.is_watched ? 'Yes' : 'No'}</td>
<td>
{#if !movie.is_watched}
<form method="POST" action="?/setMovieWatched" use:enhance>
<input type="hidden" name="movieId" value={movie.id}>
<button type="submit" class="small">Mark Watched</button>
</form>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<h2>Add Movie</h2>
<form method="POST" action="?/addMovie" use:enhance>
<div class="form-group">
<label for="imdb_id">IMDB ID:</label>
<input type="text" id="imdb_id" name="imdb_id" placeholder="tt1234567" required>
</div>
<div class="form-group">
<label for="showtime">Showtime:</label>
<input type="date" id="showtime" name="showtime" required>
</div>
<div class="form-group">
<label for="plot">Plot:</label>
<textarea id="plot" name="plot" rows="3" required></textarea>
</div>
<button type="submit">Add Movie</button>
</form>
</div>
{/if}
{#if activeTab === 'cards'}
<div class="section">
<h2>Existing Cards</h2>
<div class="table-container">
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Points</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each cards as card}
<tr>
<td>{card.title}</td>
<td>{card.description}</td>
<td>{card.point_value}</td>
<td>
<form method="POST" action="?/deleteCard" use:enhance>
<input type="hidden" name="cardId" value={card.id}>
<button type="submit" class="danger small">Delete</button>
</form>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<h2>Add Card</h2>
<form method="POST" action="?/addCard" use:enhance>
<div class="form-group">
<label for="title">Title:</label>
<input type="text" id="title" name="title" required>
</div>
<div class="form-group">
<label for="description">Description:</label>
<textarea id="description" name="description" rows="3" required></textarea>
</div>
<div class="form-group">
<label for="point_value">Points:</label>
<input type="number" id="point_value" name="point_value" min="1" required>
</div>
<button type="submit">Add Card</button>
</form>
</div>
{/if}
</div>
</div>
<style>
.admin-panel {
max-width: 1200px;
margin: 2rem auto;
padding: 1rem;
background-color: #1a472a; /* Dark Christmas green */
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.tabs {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
border-bottom: 1px solid #2c6842;
padding-bottom: 1rem;
}
.tabs button {
padding: 0.5rem 1rem;
border: none;
background: none;
cursor: pointer;
font-size: 1rem;
color: #fff;
border-radius: 4px;
transition: background-color 0.2s;
}
.tabs button:hover {
background: #2c6842;
}
.tabs button.active {
background: #c41e3a; /* Christmas red for active tab */
color: white;
}
.section {
background: #234332; /* Slightly lighter green for sections */
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
margin-bottom: 1rem;
}
h2 {
margin-top: 2rem;
margin-bottom: 1rem;
color: #fff;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #fff;
}
input, textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #2c6842;
border-radius: 4px;
font-size: 1rem;
background: #1a472a;
color: #fff;
}
input:focus, textarea:focus {
outline: none;
border-color: #c41e3a;
}
textarea {
height: 100px;
resize: vertical;
}
button {
padding: 0.5rem 1rem;
background: #c41e3a; /* Christmas red for buttons */
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
button:hover {
background: #a01830;
}
button.danger {
background: #dc3545;
}
button.danger:hover {
background: #c82333;
}
button.small {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.table-container {
overflow-x: auto;
margin-bottom: 2rem;
background: #1a472a;
border-radius: 8px;
padding: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #2c6842;
color: #fff;
}
th {
background-color: #2c6842;
font-weight: 600;
color: #fff;
}
tr:hover {
background-color: #2c6842;
}
/* Add a placeholder style for better visibility */
input::placeholder, textarea::placeholder {
color: rgba(255, 255, 255, 0.6);
}
</style>
+96 -2
View File
@@ -1,13 +1,107 @@
import type { PageServerLoad } from "./$types"; import type { PageServerLoad, Actions } from "./$types";
import { getNextMovie, getCards } from "$lib"; import { getNextMovie, getCards } from "$lib";
import { redirect } from "@sveltejs/kit";
import type { Cliche } from "$lib/types";
const api_url = import.meta.env.VITE_API_URL
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const cards = await getCards(); const cards = await getCards();
const movies = await getNextMovie(); const movies = await getNextMovie();
const selected_cliches = {
baseCards: JSON.parse(event.cookies.get('selected_cliches') ?? '[]'),
bonusCards: JSON.parse(event.cookies.get('selected_bonus_cliches') ?? '[]')
};
return { return {
movie: movies, movie: movies,
cards: cards, cards: cards,
user: event.locals.user user: event.locals.user,
selected_cliches: selected_cliches
}; };
}; };
export const actions: Actions = {
/**
* Starts a new game. Sets the selected cliche and bonus cliche cookies for the user.
* @param {RequestEvent} event - The request event.
* @param {Cookie | undefined} cookies - The cookies object.
* @returns {Promise<void>} - The response object.
*/
startGame: async ({ request, cookies }) => {
const formData = await request.formData();
const selected_cliches = formData.get('selected_cliches')?.toString() ?? '[]';
const selected_bonus_cliches = formData.get('selected_bonus_cliches')?.toString() ?? '[]';
cookies.set('selected_cliches', selected_cliches, {
path: '/',
sameSite: 'strict',
secure: false,
maxAge: 60 * 60 * 24
});
cookies.set('selected_bonus_cliches', selected_bonus_cliches, {
path: '/',
sameSite: 'strict',
secure: false,
maxAge: 60 * 60 * 24
});
},
/**
* End game action. Calculates the total score for the user and sends it to the
* server to be saved. Redirects to the game over page.
* @param {RequestEvent} event - The request event.
* @param {Cookie | undefined} cookies - The cookies object.
* @returns {Promise<Response>} - The response object.
*/
endGame: async ({ request, cookies }) => {
const formData = await request.formData();
console.log(formData);
const movieId = formData.get('movieId');
const userId = formData.get('userId');
const resultCards = JSON.parse(formData.get('result_cards')?.toString() ?? '[]');
const resultBonusCards = JSON.parse(formData.get('result_bonus_cards')?.toString() ?? '[]');
const selectedBonusCards = JSON.parse(cookies.get('selected_bonus_cliches') ?? '[]');
let totalScore: number = 0;
for (let i = 0; i < resultCards.length; i++) {
console.log('resultCards[i].point_value', resultCards[i].point_value);
totalScore += resultCards[i].point_value;
}
for (let i = 0; i < selectedBonusCards.length; i++) {
if (resultBonusCards.some((card: Cliche) => card.id === selectedBonusCards[i].id)) {
totalScore += selectedBonusCards[i].point_value*2;
} else {
totalScore -= selectedBonusCards[i].point_value*2;
}
}
const response = await fetch(`${api_url}/score`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: userId,
movie_id: movieId,
score: totalScore
})
});
console.log('response', await response);
// console.log(baseCards, bonusCards);
// cookies.delete('selected_cliches', {
// path: '/',
// sameSite: 'strict',
// secure: false,
// });
// cookies.delete('selected_bonus_cliches', {
// path: '/',
// sameSite: 'strict',
// secure: false,
// });
throw redirect(302, `/gameroom/${totalScore}`);
}
}
+138 -6
View File
@@ -1,11 +1,54 @@
<script lang="ts"> <script lang="ts">
import MovieDetails from '$lib/components/MovieDetails.svelte'; import MovieDetails from '$lib/components/MovieDetails.svelte';
import Card from '$lib/components/Card.svelte';
import type { Cliche } from '$lib/types';
import type { PageData } from './$types'; import type { PageData } from './$types';
let { data }: { data: PageData } = $props(); import { enhance } from '$app/forms';
import CardList from '$lib/components/CardList.svelte';
let isBonusCards: boolean = true; let { data }: { data: PageData } = $props();
let roundNumber: number = $state(1);
const cards: Cliche[] = data.cards;
let selected_cliches: Cliche[] = $state([]);
let selected_bonus_cliches: Cliche[] = $state([]);
let result_cards: Cliche[] = $state([]);
let result_bonus_cards: Cliche[] = $state([]);
function onCardChecked(card: Cliche) {
for (let i = 0; i < selected_cliches.length; i++) {
if (selected_cliches[i].id == card.id) {
selected_cliches.splice(i, 1);
return;
}
}
selected_cliches = [...selected_cliches, card];
}
function onBonusCardChecked(card: Cliche) {
for (let i = 0; i < selected_bonus_cliches.length; i++) {
if (selected_bonus_cliches[i].id == card.id) {
selected_bonus_cliches.splice(i, 1);
return;
}
}
selected_bonus_cliches = [...selected_bonus_cliches, card];
}
function setResult(cliche: Cliche, isCardChecked: boolean, isBonus: boolean) {
if (isCardChecked) {
if (isBonus) {
result_bonus_cards = [...result_bonus_cards, cliche];
} else {
result_cards = [...result_cards, cliche];
}
} else {
if (isBonus) {
result_bonus_cards = result_bonus_cards.filter((card) => card.id != cliche.id);
} else {
result_cards = result_cards.filter((card) => card.id != cliche.id);
}
}
}
$inspect(result_bonus_cards);
</script> </script>
<div class="space-y-8"> <div class="space-y-8">
@@ -19,6 +62,95 @@
</div> </div>
<div class="mt-8 justify-center space-y-4"> <div class="mt-8 justify-center space-y-4">
<p class="text-center text-xl font-semibold">Valitse kortit</p> {#if roundNumber == 1 && data.selected_cliches.baseCards.length == 0}
<CardList {...data.cards} /> <h2 class="text-center text-xl font-semibold">Valitse kortit</h2>
<div class="flex flex-col space-y-4 lg:grid lg:grid-cols-2">
{#each cards as card}
<Card cardData={card} {onCardChecked} />
{/each}
</div>
<p>Kortteja valittu: {selected_cliches.length} / 5</p>
{#if selected_cliches.length == 5}
<button onclick={() => roundNumber++} class="button button-primary mx-auto"
>Valitse kortit</button
>
{:else}
<button class="button button-disabled mx-auto" disabled>Korjaa valintaa</button>
{/if}
{/if}
{#if roundNumber == 2 && data.selected_cliches.baseCards.length == 0}
<h2 class="text-center text-xl font-semibold">Bonus kortit</h2>
<div class="flex flex-col space-y-4 lg:grid lg:grid-cols-2">
{#each cards as card}
{#if !selected_cliches.some((c) => c.id === card.id)}
<!-- <p>Kortti {card.title} valittu</p> -->
<Card cardData={card} onCardChecked={onBonusCardChecked} />
<!-- {:else} -->
{/if}
{/each}
</div>
<p>Kortteja valittu: {selected_bonus_cliches.length} / 3</p>
{#if selected_bonus_cliches.length < 4}
<!-- <button onclick={() => roundNumber++} class="button button-primary mx-auto"
>Aloita peli</button
> -->
<form action="?/startGame" method="post" use:enhance>
<input type="hidden" name="selected_cliches" value={JSON.stringify(selected_cliches)} />
<input
type="hidden"
name="selected_bonus_cliches"
value={JSON.stringify(selected_bonus_cliches)}
/>
<button type="submit" class="button button-primary mx-auto">Aloita peli</button>
</form>
{:else}
<button class="button button-disabled mx-auto" disabled>Korjaa valintaa</button>
{/if}
{/if}
{#if data.selected_cliches.baseCards.length > 0}
<p class="text-center text-xl font-semibold">Peli alkaa</p>
<form action="?/endGame" method="post" use:enhance>
<div>
<p class="text-center">Valitut kortit</p>
{#each data.selected_cliches.baseCards as cliche}
<Card cardData={cliche} onCardChecked={setResult} isBonus={false} />
{/each}
</div>
<div>
<p class="text-center">Valitut bonus kortit</p>
{#each data.selected_cliches.bonusCards as cliche}
<Card cardData={cliche} onCardChecked={setResult} isBonus={true} />
{/each}
</div>
<input type="hidden" name="userId" id="userId" value={data.user.id} />
<input type="hidden" name="movieId" id="movieId" value={data.movie.id} />
<input
type="hidden"
name="result_cards"
id="result_cards"
value={JSON.stringify(result_cards)}
/>
<input
type="hidden"
name="result_bonus_cards"
id="result_bonus_cards"
value={JSON.stringify(result_bonus_cards)}
/>
<input
type="hidden"
name="bonusCards"
id="bonusCards"
value={JSON.stringify(data.selected_cliches.bonusCards)}
/>
<button type="submit" class="button button-primary mx-auto">Lopeta</button>
</form>
{/if}
</div> </div>
<style>
</style>
+13
View File
@@ -0,0 +1,13 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<div class="flex flex-col items-center space-y-4">
<h1 class="text-2xl font-semibold">Game Over!</h1>
<p class="text-md">Peli on päättynyt!</p>
<p class="text-md">Sait</p>
<p class="rounded-xl border-2 p-4 text-xl font-semibold">{data.score}</p>
<p class="text-md">pistettä!</p>
</div>
+5
View File
@@ -0,0 +1,5 @@
export function load({ params }) {
return {
score: params.score
};
}
+10
View File
@@ -6,5 +6,15 @@ export const load: PageServerLoad = async (event) => {
sameSite: 'strict', sameSite: 'strict',
secure: false, secure: false,
}); });
event.cookies.delete('selected_cliches', {
path: '/',
sameSite: 'strict',
secure: false,
});
event.cookies.delete('selected_bonus_cliches', {
path: '/',
sameSite: 'strict',
secure: false,
});
throw redirect(302, '/login'); throw redirect(302, '/login');
}; };