Compare commits

...
4 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
6 changed files with 529 additions and 4 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;
}; };
+1 -1
View File
@@ -24,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>
+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>
+1 -1
View File
@@ -104,7 +104,7 @@
name="selected_bonus_cliches" name="selected_bonus_cliches"
value={JSON.stringify(selected_bonus_cliches)} value={JSON.stringify(selected_bonus_cliches)}
/> />
<button type="submit">Aloita peli</button> <button type="submit" class="button button-primary mx-auto">Aloita peli</button>
</form> </form>
{:else} {:else}
<button class="button button-disabled mx-auto" disabled>Korjaa valintaa</button> <button class="button button-disabled mx-auto" disabled>Korjaa valintaa</button>