Projektdateien hinzufügen.

This commit is contained in:
Kevin Kandlbinder 2024-06-21 13:11:34 +02:00
parent 72ecb03642
commit 76fbb232a7
48 changed files with 3779 additions and 0 deletions

21
webem-ui/.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
webem-ui/.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

4
webem-ui/.prettierignore Normal file
View file

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

8
webem-ui/.prettierrc Normal file
View file

@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

38
webem-ui/README.md Normal file
View file

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

1894
webem-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
webem-ui/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "webem-ui",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^5.0.0-next.1",
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
"type": "module",
"dependencies": {
"@fontsource-variable/montserrat": "^5.0.19",
"@sveltejs/adapter-static": "^3.0.2",
"lucide-svelte": "^0.395.0"
}
}

13
webem-ui/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
webem-ui/src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,86 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { EventHandler } from 'svelte/elements';
import { LoaderPinwheel } from 'lucide-svelte';
let {
onclick,
children,
loading = $bindable(false)
}: {
onclick: EventHandler<MouseEvent>;
children: Snippet<[]>;
loading: boolean;
} = $props();
</script>
<div class="loaderButton" class:loading>
<button {onclick} disabled={loading}>
{@render children()}
</button>
<div class="loader">
<LoaderPinwheel />
</div>
</div>
<style>
.loaderButton {
position: relative;
border: thin solid currentColor;
border-radius: var(--radius);
button {
font: inherit;
color: inherit;
border: none;
background-color: transparent;
padding: calc(var(--padding) / 2) var(--padding);
cursor: pointer;
transition: filter 0.25s;
}
.loader {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
opacity: 0;
transition: opacity 0.25s;
& > :global(svg) {
animation-name: loaderButtonAnimation;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
width: 45px;
height: 45px;
stroke-width: 1px;
}
}
&.loading {
button {
filter: blur(2px);
}
.loader {
opacity: 1;
pointer-events: auto;
}
}
}
@keyframes loaderButtonAnimation {
0% {
rotate: 0deg;
}
100% {
rotate: 360deg;
}
}
</style>

View file

@ -0,0 +1,34 @@
<script lang="ts">
import type { Match } from '$lib/tournamentApi';
import TeamComponent from './TeamComponent.svelte';
let {
match = $bindable(),
matchId = $bindable()
}: {
match: Match;
matchId: string;
} = $props();
</script>
<div class="match">
<span class="matchId">{matchId}</span>
<TeamComponent team={match.teamA} score={match.goalsA} />
<TeamComponent team={match.teamB} score={match.goalsB} />
</div>
<style>
.match {
display: flex;
flex-direction: column;
border: thin solid currentColor;
padding: calc(var(--padding) / 2);
gap: var(--gap);
border-radius: var(--radius);
.matchId {
text-align: center;
font-weight: 900;
}
}
</style>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { createTournament } from '$lib/tournamentApi';
import LoaderButton from './LoaderButton.svelte';
let loading = $state(false);
const startSimulation = async () => {
loading = true;
const tournament = await createTournament();
if (tournament == null) {
alert('something went wrong - fuck');
loading = false;
return;
}
loading = false;
goto(`/tournament/${tournament.id}`);
};
</script>
<div class="centerCta">
<LoaderButton onclick={startSimulation} bind:loading>Start a new tournament!</LoaderButton>
</div>
<style>
.centerCta {
padding: var(--padding);
display: flex;
justify-content: center;
}
</style>

View file

@ -0,0 +1,76 @@
<script lang="ts">
import { numberToLetter } from '$lib';
import type { Table, Team } from '$lib/tournamentApi';
let {
table = $bindable()
}: {
table: Table;
} = $props();
</script>
<div class="table">
<table>
<thead>
<tr>
<th>Rank</th>
<th>Group</th>
<th>Team</th>
<th>Points</th>
<th>Wins</th>
<th>Losses</th>
<th>Draws</th>
<th>For</th>
<th>Against</th>
<th>Goal Diff</th>
<th>World Rank</th>
</tr>
</thead>
<tbody>
{#each Object.keys(table.rankings).map((rank) => {
return { rank, team: table.rankings[rank as unknown as number] };
}) as entry}
<tr>
<td>{entry.rank}</td>
<td>{numberToLetter(entry.team.group)}</td>
<td>{entry.team.name}</td>
<td>{entry.team.points}</td>
<td>{entry.team.wins}</td>
<td>{entry.team.losses}</td>
<td>{entry.team.draws}</td>
<td>{entry.team.for}</td>
<td>{entry.team.against}</td>
<td>{entry.team.goalDelta}</td>
<td>{entry.team.worldRank}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<style>
table {
width: 100%;
th,
td {
text-align: center;
padding: 5px;
}
border-collapse: collapse;
th {
border-bottom: thin solid currentColor;
}
tr:nth-child(2n) {
background-color: color-mix(in display-p3, 20% var(--color-fg), var(--color-bg));
}
}
.table {
padding: var(--padding);
overflow: auto;
}
</style>

View file

@ -0,0 +1,43 @@
<script lang="ts">
import type { Team } from '$lib/tournamentApi';
let {
team = $bindable(),
score = $bindable()
}: {
team: Team;
score?: number;
} = $props();
</script>
<div class="team">
<img src={team.flagUrl} alt={team.name} />
<span class="name">{team.name}</span>
{#if score != null}
<span class="score">{score}</span>
{/if}
</div>
<style>
.team {
display: flex;
align-items: center;
gap: var(--gap);
img {
width: 35px;
height: 35px;
object-fit: cover;
object-position: center;
border-radius: 100%;
}
.name {
font-weight: 500;
}
.score {
margin-left: auto;
}
}
</style>

View file

@ -0,0 +1,3 @@
export const numberToLetter = (number: number) => {
return (number + 10).toString(36).toUpperCase();
};

View file

@ -0,0 +1,23 @@
type Round = {
name: string;
matches: string[];
};
export const rounds: Round[] = [
{
name: 'Round of 16',
matches: ['W39', 'W37', 'W41', 'W42', 'W43', 'W44', 'W40', 'W38']
},
{
name: 'Quarter-Finals',
matches: ['W45', 'W46', 'W47', 'W48']
},
{
name: 'Semi-Finals',
matches: ['W49', 'W50']
},
{
name: 'Final',
matches: ['WIN']
}
];

View file

@ -0,0 +1,86 @@
type Tournament = {
id: string;
hasPlayed: boolean;
matches: Matches;
groups: Group[];
overallTable: Table;
};
type Matches = {
[matchId: string]: Match;
};
export const MATCH_RESULT_TEAM_A_WIN = 0,
MATCH_RESULT_TEAM_B_WIN = 1,
MATCH_RESULT_DRAW = 2;
export type Match = {
result:
| typeof MATCH_RESULT_TEAM_A_WIN
| typeof MATCH_RESULT_TEAM_B_WIN
| typeof MATCH_RESULT_DRAW;
winner: Team;
teamA: Team;
teamB: Team;
goalsA: number;
goalsB: number;
};
type Group = {
table: Table;
letter: string;
};
export type Table = {
rankings: {
[rank: number]: Team;
};
};
export type Team = {
name: string;
flagUrl: string;
group: number;
for: number;
points: number;
goalDelta: number;
wins: number;
losses: number;
against: number;
draws: number;
worldRank: number;
};
export const getTournament = async (id: string) => {
const response = await fetch(`/api/Tournament/${id}`);
if (!response.ok) return null;
const tournament = (await response.json()) as Tournament;
return tournament;
};
export const createTournament = async () => {
const response = await fetch(`/api/Tournament`, {
method: 'POST'
});
if (!response.ok) return null;
const tournament = (await response.json()) as Tournament;
return tournament;
};
export const deleteTournament = async (id: string) => {
const response = await fetch(`/api/Tournament/${id}`, {
method: 'DELETE'
});
if (!response.ok) return null;
const success = (await response.json()) as boolean;
return success;
};

View file

@ -0,0 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
</script>
<h1>{$page.status}: {$page.error?.message || 'Something went wrong'}</h1>

View file

@ -0,0 +1,73 @@
<script lang="ts">
import type { LayoutData } from './$types';
import './global.css';
export let data: LayoutData;
</script>
<div class="layout">
<div class="navigation">
<nav>
<a href="/">WebEM Sim ❤</a>
</nav>
</div>
<div class="main">
<slot />
</div>
<footer>
<span> Made with ❤ by Lynn and Kevin </span>
</footer>
</div>
<style>
.navigation {
nav {
justify-content: center;
display: flex;
a {
padding: var(--padding);
font-weight: 900;
font-size: 2em;
text-decoration: none;
}
}
}
.layout {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main {
flex-grow: 1;
}
.main,
.navigation,
footer {
display: grid;
max-width: 100vw;
overflow: hidden;
grid-template-columns:
[full-start] minmax(max(4vmin, var(--gap)), auto)
[wide-start] minmax(auto, 240px)
[main-start] min(var(--width), calc(100% - max(8vmin, calc(var(--gap) * 2))))
[main-end] minmax(auto, 240px)
[wide-end] minmax(max(4vmin, var(--gap)), auto)
[full-end];
& > :global(*) {
grid-column: main-start/main-end;
min-width: 0;
}
}
footer {
padding: var(--padding);
text-align: center;
}
</style>

View file

@ -0,0 +1,7 @@
import type { LayoutLoad } from './$types';
export const load = (async () => {
return {};
}) satisfies LayoutLoad;
export const prerender = true;

View file

@ -0,0 +1,28 @@
<script>
import { goto } from '$app/navigation';
import LoaderButton from '$lib/components/LoaderButton.svelte';
import StartTournamentButton from '$lib/components/StartTournamentButton.svelte';
import { createTournament } from '$lib/tournamentApi';
</script>
<section>
<h1>WebEM Sim</h1>
<p>
Welcome to the best EM simulator you will ever see. I'm serious, this is the pinnacle of EM
simulation. Actually come to think of it... this is actually the best application ever written
overall.
</p>
<p>
Having just now come to the realization I've written the best piece of software ever to be
written, I'm feeling proud and accomplished. IT is over, it's finished, you can go home now.
</p>
<p>
Anyways, how about you start your simulation instead of reading the B.S. I've written here to
make the page feel less like my soul (empty).
</p>
<StartTournamentButton />
</section>

View file

@ -0,0 +1,49 @@
/* montserrat-latin-wght-normal */
@font-face {
font-family: 'Montserrat Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url(@fontsource-variable/montserrat/files/montserrat-latin-wght-normal.woff2)
format('woff2-variations');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
U+FFFD;
}
:root {
--color-a: color(display-p3 0.86237 0.99601 0.87596);
--color-b: color(display-p3 0.01112 0.55902 0.74888);
--color-fg: var(--color-a);
--color-bg: var(--color-b);
--padding: 30px;
--gap: 20px;
--radius: 20px;
--width: 700px;
}
* {
box-sizing: border-box;
scrollbar-width: thin;
scrollbar-color: var(--color-fg) color-mix(in display-p3, 20% var(--color-fg), var(--color-bg));
}
html,
body {
margin: 0;
padding: 0;
min-height: 100vh;
}
body {
background-color: var(--color-bg);
color: var(--color-fg);
font-family: 'Montserrat Variable', sans-serif;
}
a {
color: inherit;
text-decoration: underline dotted currentColor;
}

View file

@ -0,0 +1,143 @@
<script lang="ts">
import MatchComponent from '$lib/components/MatchComponent.svelte';
import StartTournamentButton from '$lib/components/StartTournamentButton.svelte';
import TableOutlet from '$lib/components/TableOutlet.svelte';
import TeamComponent from '$lib/components/TeamComponent.svelte';
import { rounds } from '$lib/parameters';
import type { PageData } from './$types';
let activeRound = $state(0);
let activeGroupTable = $state<number | 'overall'>(0);
const { data }: { data: PageData } = $props();
</script>
<section>
<div class="winner">
<h2>Your Winner:</h2>
<TeamComponent team={data.tournament.matches['WIN'].winner} />
</div>
</section>
<section>
<h2>Rounds</h2>
<div class="tabSwitcher">
{#each rounds as round, roundIdx}
<button
onclick={() => {
activeRound = roundIdx;
}}
class:active={activeRound == roundIdx}
>
{round.name}
</button>
{/each}
</div>
{#each rounds as round, roundIdx}
<div class="round" class:active={activeRound == roundIdx}>
{#each round.matches as match}
<MatchComponent match={data.tournament.matches[match]} matchId={match} />
{/each}
</div>
{/each}
</section>
<section class="tableSection">
<div class="title">
<h2>Tables</h2>
</div>
<div class="tabSwitcher">
<button
onclick={() => {
activeGroupTable = 'overall';
}}
class:active={activeGroupTable == 'overall'}
>
Overall
</button>
{#each data.tournament.groups as groupTable, groupTableIdx}
<button
onclick={() => {
activeGroupTable = groupTableIdx;
}}
class:active={activeGroupTable == groupTableIdx}
>
Group {groupTable.letter}
</button>
{/each}
</div>
<TableOutlet
table={activeGroupTable == 'overall'
? data.tournament.overallTable
: data.tournament.groups[activeGroupTable].table}
/>
</section>
<section>
<h2>One more time?</h2>
<p>
I guess you just can't get enough of this awesome website - that's fine, I know it's hard to say
goodbye to perfection. What do you think? Just one more match - for old time's sake?
</p>
<StartTournamentButton />
</section>
<style>
section {
margin: 20px 0;
}
.tableSection {
grid-column: wide-start/wide-end;
}
.winner {
display: flex;
gap: var(--gap);
}
.title {
margin: 0 auto;
max-width: var(--width);
}
.tabSwitcher {
display: flex;
justify-content: center;
white-space: nowrap;
overflow: auto;
width: 100%;
button {
padding: calc(var(--padding) / 2) var(--padding);
background-color: transparent;
border: none;
font: inherit;
color: inherit;
cursor: pointer;
border-bottom: 2px solid color-mix(in display-p3, var(--color-fg) 40%, var(--color-bg));
transition: border-bottom 0.25s;
&.active {
border-bottom: 2px solid var(--color-fg);
}
}
}
.round {
display: none;
flex-direction: column;
gap: var(--gap);
padding-top: var(--gap);
&.active {
display: flex;
}
}
</style>

View file

@ -0,0 +1,22 @@
import { getTournament } from '$lib/tournamentApi';
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const { id } = params;
const tournament = await getTournament(id);
if (!tournament) {
error(404, {
message: 'That tournament does not exist'
});
}
return {
tournament
};
}) satisfies PageLoad;
export const ssr = false;
export const prerender = false;

BIN
webem-ui/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

20
webem-ui/svelte.config.js Normal file
View file

@ -0,0 +1,20 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter({
fallback: '404.html'
})
}
};
export default config;

19
webem-ui/tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

11
webem-ui/vite.config.ts Normal file
View file

@ -0,0 +1,11 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
server: {
proxy: {
'/api': 'http://localhost:32768'
}
}
});