feat(ui): add Lobby and Main Menu Body #38

Merged
Janis merged 7 commits from feat/31-mainmenu-body into main 2025-11-06 09:03:09 +01:00
16 changed files with 460 additions and 145 deletions
Showing only changes of commit 802b76ac1a - Show all commits

View File

@@ -1,6 +1,13 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--background-image: url('/assets/images/background.png'); --background-image: url('/assets/images/background.png');
--color: white; --color: #f8f9fa; /* Light text on dark bg */
/* Bootstrap variable overrides for dark mode */
--bs-body-color: var(--color);
--bs-link-color: #66b2ff;
--bs-link-hover-color: #99ccff;
--bs-border-color: rgba(255, 255, 255, 0.2);
--bs-heading-color: var(--color);
} }
} }

View File

@@ -2,6 +2,19 @@
@import "dark-mode.less"; @import "dark-mode.less";
@import "login.less"; @import "login.less";
/* Provide default (light) variables so the site works even if light-mode.less fails */
:root {
--background-image: url('/assets/images/img.png');
--color: #212529; /* Bootstrap body text default */
/* Bootstrap variable overrides for light mode */
--bs-body-color: var(--color) !important;
--bs-link-color: #0d6efd !important;
--bs-link-hover-color: #0a58ca !important;
--bs-border-color: rgba(0, 0, 0, 0.125) !important;
--bs-heading-color: var(--color) !important;
}
@background-image: var(--background-image); @background-image: var(--background-image);
@color: var(--color); @color: var(--color);
@keyframes slideIn { @keyframes slideIn {
@@ -10,8 +23,33 @@
} }
.game-field-background { .game-field-background {
background-image: @background-image; background-image: @background-image;
background-size: 100vw 100vh; background-size: cover;
background-position: center center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-attachment: fixed;
}
.navbar-header{
text-align:center;
}
.navbar-toggle {
float: none;
margin-right:0;
}
/* Ensure body text color follows theme variable and works with Bootstrap */
body {
color: @color;
}
.footer {
width: 100%;
text-align: center;
font-size: 12px;
color: @color;
padding: 0.5rem 0;
flex-grow: 1; /* fill remaining vertical space as visual footer background */
} }
.game-field { .game-field {
@@ -19,6 +57,7 @@
inset: 0; inset: 0;
overflow: auto; overflow: auto;
} }
#sessions { #sessions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -61,9 +61,7 @@ class MainMenuController @Inject()(
} }
} }
def rules(): Action[AnyContent] = { def rules(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
Action { implicit request => Ok(views.html.mainmenu.rules(Some(request.user)))
Ok(views.html.mainmenu.rules())
}
} }
} }

View File

@@ -22,6 +22,12 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"), id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"),
name = "Leon", name = "Leon",
passwordHash = UserHash.hashPW("password123") passwordHash = UserHash.hashPW("password123")
),
"Jakob" -> User(
internalId = 2L,
id = java.util.UUID.fromString("323e4567-e89b-12d3-a456-426614174000"),
name = "Jakob",
passwordHash = UserHash.hashPW("password123")
) )
) )

View File

@@ -1,20 +1,21 @@
@() @()
@main("Login") { @main("Login") {
<div class="login-box"> <div class="login-box">
<div class="card login-card p-4"> <div class="card login-card p-4">
<div class="card-body"> <div class="card-body">
<h3 class="text-center mb-4">Login</h3> <h3 class="text-center mb-4 text-body">Login</h3>
<form action="@routes.UserController.login_Post()" method="post"> <form action="@routes.UserController.login_Post()" method="post">
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label">Username</label> <label for="username" class="form-label text-body">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Enter Username" required> <input type="text" class="form-control text-body" id="username" name="username" placeholder="Enter Username" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Password</label> <label for="password" class="form-label text-body">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter password" required> <input type="password" class="form-control text-body" id="password" name="password" placeholder="Enter password" required>
</div> </div>
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
@@ -34,6 +35,11 @@
</div> </div>
</div> </div>
<script src="@routes.Assets.versioned("/javascripts/particles.js")"></script> <script src="@routes.Assets.versioned("/javascripts/particles.js")"></script>
<script>
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
console.log('callback - particles.js config loaded');
});
</script>
<div id="particles-js" style="background-color: rgb(182, 25, 36); <div id="particles-js" style="background-color: rgb(182, 25, 36);
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;

View File

@@ -3,24 +3,30 @@
* handles the rendering of the page header and body tags. It takes * handles the rendering of the page header and body tags. It takes
* two arguments, a `String` for the title of the page and an `Html` * two arguments, a `String` for the title of the page and an `Html`
* object to insert into the body of the page. * object to insert into the body of the page.
*@ *@
@(title: String)(content: Html) @(title: String)(content: Html)
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@* Here's where we render the page title `String`. *@ @* Here's where we render the page title `String`. *@
<title>@title</title> <title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")"> <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")"> <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head> </head>
<body> <body class="d-flex flex-column min-vh-100 game-field-background">
<main>
@* And here's where we render the `Html` object containing @* And here's where we render the `Html` object containing
* the page content. *@ * the page content. *@
@content @content
</main>
<footer class="footer">
</footer>
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script> <script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

View File

@@ -2,7 +2,7 @@
@main("Create Game") { @main("Create Game") {
@navbar(user) @navbar(user)
<form action="@routes.MainMenuController.createGame()" method="post"> <form action="@routes.MainMenuController.createGame()" method="post" class="game-field-background">
<div class="w-50 mx-auto"> <div class="w-50 mx-auto">
<div class="mt-3"> <div class="mt-3">
<label for="lobbyname" class="form-label">Lobby-Name</label> <label for="lobbyname" class="form-label">Lobby-Name</label>
@@ -14,7 +14,7 @@
</div> </div>
<div class="mt-3"> <div class="mt-3">
<label for="playeramount" class="form-label">Playeramount:</label> <label for="playeramount" class="form-label">Playeramount:</label>
<input type="range" class="form-range" min="2" max="7" value="2" id="playeramount" name="playeramount"> <input type="range" class="form-range text-body" min="2" max="7" value="2" id="playeramount" name="playeramount">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<span>2</span> <span>2</span>
<span>3</span> <span>3</span>

View File

@@ -1,16 +1,16 @@
@(user: Option[model.users.User]) @(user: Option[model.users.User])
<nav class="navbar navbar-expand-lg bg-body-tertiary"> <nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid"> <div class="container d-flex justify-content-center">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navBar"> <div class="collapse navbar-collapse justify-content-center" id="navBar">
<a class="navbar-brand" href="@routes.MainMenuController.mainMenu()">KnockOutWhist</a> <a class="navbar-brand ms-auto" href="@routes.MainMenuController.mainMenu()">KnockOutWhist</a>
<div class="navbar-nav me-auto mb-2 mb-lg-0"> <div class="navbar-nav me-auto mb-2 mb-lg-0">
<ul class="navbar-nav mb-2 mb-lg-0"> <ul class="navbar-nav mb-2 mb-lg-0">
@if(user.isDefined) { @if(user.isDefined) {
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Create Game</a> <a class="nav-link active" aria-current="page" href="@routes.MainMenuController.mainMenu()">Create Game</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Lobbies</a> <a class="nav-link disabled" aria-disabled="true">Lobbies</a>

View File

@@ -1,13 +1,20 @@
@() @(user: Option[model.users.User])
@main("Rules") { @main("Rules") {
<div id="rules" class="game-field game-field-background"> @navbar(user)
<table> <div id="rules">
<caption>Rules Overview and Equipment</caption> <div class="container my-4">
<thead> <div class="card shadow-sm rounded-3">
<div class="card-header text-white text-center">
<h4 class="mb-0 text-body">Game Rules Overview</h4>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped table-hover mb-0 align-middle">
<thead class="table-dark">
<tr> <tr>
<th>Section</th> <th scope="col">Section</th>
<th>Details</th> <th scope="col">Details</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -17,7 +24,7 @@
</tr> </tr>
<tr> <tr>
<td>Aim</td> <td>Aim</td>
<td>To be the last player left in at the end of the game, with the object in each hand being to win a majority of tricks.</td> <td>To be the last player left at the end of the game, with the object in each hand being to win a majority of tricks.</td>
</tr> </tr>
<tr> <tr>
<td>Equipment</td> <td>Equipment</td>
@@ -33,7 +40,7 @@
</tr> </tr>
<tr> <tr>
<td>Deal (Subsequent Hands)</td> <td>Deal (Subsequent Hands)</td>
<td>The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie for the highest number of tricks, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.</td> <td>The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.</td>
</tr> </tr>
<tr> <tr>
<td>Play</td> <td>Play</td>
@@ -55,9 +62,15 @@
<td>Winning the Game</td> <td>Winning the Game</td>
<td>The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.</td> <td>The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.</td>
</tr> </tr>
</tbody> <tr>
<td>Dog Life</td> <td>Dog Life</td>
<td>The first player who takes no tricks is awarded a "dog's life". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the "dog" may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads to the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.</td> <td>The first player who takes no tricks is awarded a "dog's life". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the "dog" may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.</td>
</tr>
</tbody>
</table> </table>
</div>
</div>
</div>
</div>
</div> </div>
} }

View File

@@ -1,3 +1,80 @@
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() { /*!
console.log('callback - particles.js config loaded'); * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
}); * Copyright 2011-2025 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(() => {
'use strict'
const getStoredTheme = () => localStorage.getItem('theme')
const setStoredTheme = theme => localStorage.setItem('theme', theme)
const getPreferredTheme = () => {
const storedTheme = getStoredTheme()
if (storedTheme) {
return storedTheme
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const setTheme = theme => {
if (theme === 'auto') {
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
} else {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
setTheme(getPreferredTheme())
const showActiveTheme = (theme, focus = false) => {
const themeSwitcher = document.querySelector('#bd-theme')
if (!themeSwitcher) {
return
}
const themeSwitcherText = document.querySelector('#bd-theme-text')
const activeThemeIcon = document.querySelector('.theme-icon-active use')
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active')
element.setAttribute('aria-pressed', 'false')
})
btnToActive.classList.add('active')
btnToActive.setAttribute('aria-pressed', 'true')
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
if (focus) {
themeSwitcher.focus()
}
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme()
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme())
}
})
window.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme())
document.querySelectorAll('[data-bs-theme-value]')
.forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value')
setStoredTheme(theme)
setTheme(theme)
showActiveTheme(theme, true)
})
})
})
})()