CSS Grid + Admin Page Content

This commit is contained in:
Neshura 2023-08-30 04:17:16 +02:00
parent 6135d18366
commit 3992d964da
Signed by: Neshura
GPG key ID: B6983AAA6B9A7A6C
19 changed files with 430 additions and 306 deletions

View file

@ -0,0 +1 @@
export const apiBaseUrl = 'https://wip.chellaris.net/api';

View file

@ -0,0 +1,5 @@
import { writable, type Writable } from "svelte/store";
const AuthTokenStore: Writable<string> = writable("");
export default AuthTokenStore;

View file

@ -0,0 +1,5 @@
import { writable, type Writable } from "svelte/store";
const AdminSelectedEmpireStore: Writable<{ [key: number]: number }> = writable({});
export default AdminSelectedEmpireStore;

View file

@ -0,0 +1,7 @@
import { writable, type Writable } from "svelte/store";
import type { ChellarisInfo } from '../../types/chellaris';
import { createChellarisInfo } from '../../types/chellaris';
const AdminGameDataStore: Writable<ChellarisInfo> = writable(createChellarisInfo());
export default AdminGameDataStore;

View file

@ -0,0 +1,5 @@
import { writable, type Writable } from "svelte/store";
const AdminSelectedGameStore: Writable<number> = writable();
export default AdminSelectedGameStore;

View file

@ -29,6 +29,11 @@ export type ChellarisEmpire = {
ethics: { [key: number]: Ethic },
}
export type ChellarisGameInfo = {
id: number,
name: string
}
export const createChellarisInfo = (): ChellarisInfo => {
const newChellarisInfo = {
games: [],

View file

@ -11,19 +11,12 @@
<style>
.app {
display: flex;
flex-direction: column;
display: grid;
grid-template-areas:
'header'
'app';
grid-template-rows: 3rem 1fr;
min-height: 100vh;
}
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 0rem;
width: 100%;
max-height: 95vh;
margin: 0 auto;
box-sizing: border-box;
max-height: 100vh;
}
</style>

View file

@ -53,6 +53,7 @@
<style>
header {
grid-area: header;
display: flex;
justify-content: space-between;
}

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { browser } from "$app/environment";
import Modal from "$lib/components/Modal.svelte";
import AuthTokenStore from "$lib/stores/AuthTokenStore";
import { fade } from "svelte/transition";
export let showSettings: boolean;
@ -13,13 +14,14 @@
showAuthSaved = false;
}, 2000);
document.cookie = "authToken=" + authToken;
$AuthTokenStore = authToken;
}
let authToken: string;
$: console.log(authToken);
$: {
if (browser) {
authToken = document.cookie.split("=")[document.cookie.split("=").length - 1];
$AuthTokenStore = authToken;
}
}

View file

@ -1,69 +0,0 @@
import { fail } from '@sveltejs/kit';
import { Game } from './game';
import type { PageServerLoad, Actions } from './$types';
export const load = (({ cookies }) => {
const game = new Game(cookies.get('sverdle'));
return {
/**
* The player's guessed words so far
*/
guesses: game.guesses,
/**
* An array of strings like '__x_c' corresponding to the guesses, where 'x' means
* an exact match, and 'c' means a close match (right letter, wrong place)
*/
answers: game.answers,
/**
* The correct answer, revealed if the game is over
*/
answer: game.answers.length >= 6 ? game.answer : null
};
}) satisfies PageServerLoad;
export const actions = {
/**
* Modify game state in reaction to a keypress. If client-side JavaScript
* is available, this will happen in the browser instead of here
*/
update: async ({ request, cookies }) => {
const game = new Game(cookies.get('sverdle'));
const data = await request.formData();
const key = data.get('key');
const i = game.answers.length;
if (key === 'backspace') {
game.guesses[i] = game.guesses[i].slice(0, -1);
} else {
game.guesses[i] += key;
}
cookies.set('sverdle', game.toString());
},
/**
* Modify game state in reaction to a guessed word. This logic always runs on
* the server, so that people can't cheat by peeking at the JavaScript
*/
enter: async ({ request, cookies }) => {
const game = new Game(cookies.get('sverdle'));
const data = await request.formData();
const guess = data.getAll('guess') as string[];
if (!game.enter(guess)) {
return fail(400, { badGuess: true });
}
cookies.set('sverdle', game.toString());
},
restart: async ({ cookies }) => {
cookies.delete('sverdle');
}
} satisfies Actions;

View file

@ -1,121 +1,298 @@
<script lang="ts">
import { confetti } from '@neoconfetti/svelte';
import { enhance } from '$app/forms';
import type { PageData, ActionData } from './$types';
import { reduced_motion } from './reduced-motion';
import { browser } from '$app/environment';
import { apiBaseUrl } from '$lib/components/consts';
import AuthTokenStore from '$lib/stores/AuthTokenStore';
import AdminSelectedEmpireStore from '$lib/stores/admin-page/EmpireStore';
import AdminSelectedGameStore from '$lib/stores/admin-page/GameStore';
import List from './List.svelte';
export let data: PageData;
export let data;
export let form: ActionData;
/** Whether or not the user has won */
$: won = data.answers.at(-1) === 'xxxxx';
/** The index of the current guess */
$: i = won ? -1 : data.answers.length;
/** Whether the current guess can be submitted */
$: submittable = data.guesses[i]?.length === 5;
/**
* A map of classnames for all letters that have been guessed,
* used for styling the keyboard
*/
let classnames: Record<string, 'exact' | 'close' | 'missing'>;
/**
* A map of descriptions for all letters that have been guessed,
* used for adding text for assistive technology (e.g. screen readers)
*/
let description: Record<string, string>;
let newGameForm = false;
let addingNewGame = false;
let newGameName = '';
$: gameList = data.games;
$: {
classnames = {};
description = {};
data.answers.forEach((answer, i) => {
const guess = data.guesses[i];
for (let i = 0; i < 5; i += 1) {
const letter = guess[i];
if (answer[i] === 'x') {
classnames[letter] = 'exact';
description[letter] = 'correct';
} else if (!classnames[letter]) {
classnames[letter] = answer[i] === 'c' ? 'close' : 'missing';
description[letter] = answer[i] === 'c' ? 'present' : 'absent';
if (typeof localStorage !== 'undefined') {
localStorage.setItem('adminGameSelection', JSON.stringify($AdminSelectedGameStore));
}
};
$: {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('adminEmpireSelection', JSON.stringify($AdminSelectedEmpireStore));
}
};
const getGameList = () => {
fetch(apiBaseUrl + '/v3/list_games').then((response) => response.json().then((result) => (gameList = result)));
};
const setActiveGame = (game: number) => {
$AdminSelectedGameStore = game;
};
const addGame = () => {
let newGame = {
auth: { token: $AuthTokenStore },
game_name: newGameName
};
fetch(apiBaseUrl + '/v3/game', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newGame)
}).then((response) => {
getGameList();
addingNewGame = false;
response.json().then((result) => $AdminSelectedGameStore = result.id);
});
newGameName = '';
newGameForm = false;
addingNewGame = true;
};
const deleteGame = (game_id: number) => {
fetch(apiBaseUrl + "/v3/game?game_id=" + game_id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({token: $AuthTokenStore}),
}).then(() => {
getGameList();
if ($AdminSelectedGameStore == game_id) {
$AdminSelectedGameStore = Object.values(gameList)[0].id;
}
});
}
};
/**
* Modify the game state without making a trip to the server,
* if client-side JavaScript is enabled
*/
function update(event: MouseEvent) {
const guess = data.guesses[i];
const key = (event.target as HTMLButtonElement).getAttribute(
'data-key'
);
const startNewGameForm = () => {
newGameForm = true;
setActiveGame(Object.values(gameList).length);
};
if (key === 'backspace') {
data.guesses[i] = guess.slice(0, -1);
if (form?.badGuess) form.badGuess = false;
} else if (guess.length < 5) {
data.guesses[i] += key;
}
}
const setActiveEmpire = (empireId: number) => {
$AdminSelectedEmpireStore[$AdminSelectedGameStore] = empireId;
};
/**
* Trigger form logic in response to a keydown event, so that
* desktop users can use the keyboard to play the game
*/
function keydown(event: KeyboardEvent) {
if (event.metaKey) return;
const addEmpire = () => {
$AdminSelectedEmpireStore[$AdminSelectedGameStore] = list2.length;
list2.push(list2.length);
};
document
.querySelector(`[data-key="${event.key}" i]`)
?.dispatchEvent(new MouseEvent('click', { cancelable: true }));
let list2 = new Array();
for (let i = 0; i < 100; i++) {
list2.push(i);
}
</script>
<svelte:window on:keydown={keydown} />
<svelte:head>
<title>Admin Menu</title>
<meta name="description" content="Admin Menu for managing Chellaris Sign-Ups" />
</svelte:head>
<div class="text-column">
<h1>Admin Menu: Not yet implemented</h1>
<p>Next on the list after proper graphs</p>
<div class="frame">
<List area="games" listTitle="Games">
{#each Object.values(gameList) as game}
<button class="list-card" class:active={game.id == $AdminSelectedGameStore ? 'active' : ''} on:click={() => setActiveGame(game.id)}>
<div class="card-content">{game.name}</div>
<button class="delete-box" on:click={() => deleteGame(game.id)}>
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" d="M0 0 24 24 M24 0 0 24" />
</svg>
</button>
</button>
{/each}
{#if newGameForm}
<div class="list-card active">
<form on:submit={addGame}>
<input bind:value={newGameName} />
<input type="submit" value="Add Game" />
</form>
</div>
{:else if addingNewGame}
<div class="list-card active">
<div class="card-content">Adding Game...</div>
</div>
{:else}
<button class="list-card" on:click={startNewGameForm}>
<div class="card-content button">+</div>
</button>
{/if}
</List>
<List area="empires" listTitle="Empires">
<div class="empires-table">
<div class="table-headers">
<div class="table-header">Empire Name</div>
<div class="table-header">Discord User</div>
</div>
<div class="table-content">
{#each list2 as elem}
<button class="list-card" class:active={elem == $AdminSelectedEmpireStore[$AdminSelectedGameStore] ? 'active' : ''} on:click={() => setActiveEmpire(elem)}>
<div class="card-content" class:active={elem == $AdminSelectedEmpireStore[$AdminSelectedGameStore] ? 'active' : ''}>{elem}</div>
<div class="card-content" class:active={elem == $AdminSelectedEmpireStore[$AdminSelectedGameStore] ? 'active' : ''}>{elem}</div>
</button>
{/each}
<button class="list-card" on:click={addEmpire}>
<div class="card-content button">+</div>
</button>
</div>
</div>
</List>
<List area="details" listTitle="Empire Details">
{$AdminSelectedEmpireStore[$AdminSelectedGameStore]}
</List>
</div>
<style>
@keyframes wiggle {
0% {
transform: translateX(0);
.frame {
display: grid;
grid-template-areas: 'games empires details';
grid-template-columns: 20% 40% 40%;
grid-template-rows: 100%;
max-height: calc(100%);
overflow: hidden;
margin: 3rem;
box-sizing: border-box;
border: 1px solid white;
}
10% {
transform: translateX(-2px);
/* General Classes */
.active {
border-color: darkorange !important;
}
30% {
transform: translateX(4px);
.list-card {
display: flex;
margin: 0.5rem;
border: 1px solid darkcyan;
cursor: pointer;
background-color: var(--color-bg);
color: var(--color-text);
width: calc(100% - 1rem);
text-align: left;
}
50% {
transform: translateX(-6px);
.list-card:hover {
border: 1px solid darkorange;
}
70% {
transform: translateX(+4px);
.list-card form {
padding: 4px 9px;
}
90% {
transform: translateX(-2px);
.list-card input {
background-color: var(--color-bg);
color: var(--color-text);
border: 1px solid darkcyan;
font-size: 1rem;
padding: 3px 6px;
}
100% {
transform: translateX(0);
.list-card input:focus {
border: 1px solid darkorange;
outline: none !important;
}
.list-card input[type='submit']:hover {
border: 1px solid darkorange;
cursor: pointer;
}
.card-content {
flex-grow: 1;
padding: 0.5rem 1rem;
box-sizing: border-box;
border-left: 1px solid darkcyan;
}
.button {
text-align: center;
}
.delete-box {
background-color: var(--color-bg);
width: 1.5rem;
height: 1.5rem;
justify-self: center;
align-self: center;
margin-right: 0.2rem;
border: 1px solid darkcyan;
color: darkcyan;
cursor: pointer;
text-align: center;
}
.checkmark {
display: block;
stroke-width: 2;
stroke: darkcyan;
stroke-miterlimit: 10;
}
.delete-box:hover .checkmark {
stroke: darkorange;
}
.delete-box:hover {
color: darkorange;
border-color: darkorange;
}
/* Empire Classes */
.empires-table {
margin: 0.5rem;
height: calc(100% - 1rem);
overflow: hidden;
border: 2px solid blueviolet;
box-sizing: border-box;
}
.table-headers {
grid-area: empires-header;
display: flex;
height: 2.3rem;
width: calc(100%);
padding-right: 12px;
box-sizing: border-box;
border-bottom: 1px solid orange;
justify-items: center;
align-items: center;
}
.table-header {
flex-grow: 1;
text-align: center;
width: 50%;
background-color: var(--color-bg);
padding: 0.5rem 1.5rem;
border-left: 1px solid orange;
}
.table-header:first-child {
border-left: none;
}
.table-content {
grid-area: empires-list;
height: calc(100% - 2.3rem);
overflow-y: scroll;
}
.list-card:hover .card-content {
border-left: 1px solid darkorange;
}
.list-card:hover .card-content:first-child {
border-left: none;
}
.card-content:first-child {
border-left: none;
}
</style>

30
src/routes/admin/+page.ts Normal file
View file

@ -0,0 +1,30 @@
import { apiBaseUrl } from "$lib/components/consts";
import AdminSelectedEmpireStore from "$lib/stores/admin-page/EmpireStore";
import AdminSelectedGameStore from "$lib/stores/admin-page/GameStore";
import type { ChellarisGameInfo } from "$lib/types/chellaris";
export async function load ({ fetch }) {
const gameList: { [key: number]: ChellarisGameInfo } = await (await fetch(apiBaseUrl + "/v3/list_games")).json();
let store: string | null;
if (typeof localStorage !== 'undefined') {
// Game Selection
store = localStorage.getItem('adminGameSelection');
if (typeof store === 'string') {
AdminSelectedGameStore.set(JSON.parse(store));
}
// Empire Selection
store = localStorage.getItem('adminEmpireSelection');
if (typeof store === 'string' && store != "\"\"") {
AdminSelectedEmpireStore.set(JSON.parse(store));
}
else if (typeof store === 'string') {
AdminSelectedEmpireStore.set({});
}
}
return { games: gameList };
}

View file

@ -0,0 +1,43 @@
<script lang="ts">
export let area = '';
export let listTitle = 'List';
</script>
<div class="container" style={'--area:' + area}>
<div class="header">
{listTitle}
</div>
<div class="list">
<slot />
</div>
</div>
<style>
.container {
grid-area: var(--area);
display: grid;
grid-template-areas:
'list-header'
'list-content';
grid-template-rows: 2rem calc(100% - 2rem);
grid-template-columns: 100%;
min-height: none;
max-height: 100%;
}
.header {
grid-area: list-header;
line-height: 2;
font-weight: bold;
text-align: center;
border: 1px solid var(--color-text);
}
.list {
grid-area: list-content;
min-height: none;
max-height: 100%;
overflow: hidden;
border: 1px solid var(--color-text);
}
</style>

View file

@ -44,19 +44,16 @@
<style>
.app {
display: flex;
grid-area: app;
display: grid;
grid-template-areas:
'sub-header'
'content';
grid-template-rows: 3rem 1fr;
flex-direction: column;
min-height: 100%;
}
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 2rem;
width: 100%;
max-height: 100%;
margin: 0 auto;
box-sizing: border-box;
grid-area: content;
}
</style>

View file

@ -23,83 +23,21 @@
<LoadingSpinner size="60" />
{:else}
<div class="fullscreen-margin">
<div class="half-vertical">
<div class="two-thirds-horizontal">
<EthicsWeb {data} />
</div>
<div class="one-third-horizontal">
<EthicsBar {data} />
</div>
</div>
<div class="half-vertical">
<div class="half-horizontal">
<EmpireStats {data} />
</div>
<div class="half-horizontal">
<PopsPie {data} />
</div>
</div>
</div>
{/if}
<style>
.fullscreen-margin {
display: flex;
flex-direction: row;
height: 90vh;
flex-grow: 1;
padding: 2rem;
}
.half-vertical {
display: flex;
flex-direction: column;
width: 50%;
max-width: 50%;
min-height: 100%;
max-height: 100%;
justify-content: center;
}
.half-horizontal {
display: flex;
flex-direction: row;
width: 100%;
max-width: 100%;
min-height: 50%;
max-height: 50%;
justify-content: center;
}
.one-third-horizontal {
display: flex;
flex-direction: row;
width: 100%;
max-width: 100%;
min-height: 34%;
max-height: 34%;
justify-content: center;
}
.two-thirds-horizontal {
display: flex;
flex-direction: row;
width: 100%;
max-width: 100%;
min-height: 66%;
max-height: 66%;
justify-content: center;
display: grid;
grid-template-areas:
'uple upri'
'lole lori';
grid-template-columns: 50% 50%;
grid-template-rows: 50% 50%;
grid-area: app;
}
</style>

View file

@ -14,16 +14,8 @@
<style>
.chart {
display: flex;
flex-direction: column;
min-width: 100%;
max-width: 100%;
min-height: 100%;
max-height: 100%;
justify-content: center;
align-items: center;
grid-area: upri;
padding: 2rem;
place-self: center;
}
</style>

View file

@ -61,6 +61,8 @@
};
const options = {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0
},
@ -100,16 +102,7 @@
<style>
.chart {
display: flex;
flex-direction: column;
min-width: 100%;
max-width: 100%;
min-height: 100%;
max-height: 100%;
justify-content: center;
align-items: center;
grid-area: lole;
padding: 2rem;
}
</style>

View file

@ -96,6 +96,8 @@
};
const options = {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0
},
@ -157,29 +159,33 @@
}
</script>
<div class="chart">
<div class="container">
<div class="chart">
<Radar data={chart_data} {options} />
</div>
<label>
<input type="checkbox" bind:checked={weighted} on:change={updateChartData} />
Use weighted LegacyEthics
</label>
</div>
<style>
.chart {
.container {
grid-area: uple;
padding: 2rem;
display: flex;
flex-direction: column;
box-sizing: border-box;
padding: 1rem;
min-width: 100%;
max-width: 100%;
min-height: 100%;
max-height: 100%;
gap: 1rem;
justify-content: center;
align-items: center;
}
.chart {
width: 100%;
height: 100%;
}
</style>

View file

@ -42,6 +42,8 @@
};
const options = {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0
},
@ -86,16 +88,7 @@
<style>
.chart {
display: flex;
flex-direction: column;
min-width: 100%;
max-width: 100%;
min-height: 100%;
max-height: 100%;
justify-content: center;
align-items: center;
grid-area: lori;
padding: 2rem;
}
</style>