ui: Almost all interface done with Svelte
This commit is contained in:
parent
9fa1ede69c
commit
7e13cf28bd
54 changed files with 2809 additions and 16 deletions
32
frontend/ui/package-lock.json
generated
32
frontend/ui/package-lock.json
generated
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "~TODO~",
|
||||
"name": "fic-frontend",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
|
|
@ -83,6 +83,12 @@
|
|||
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==",
|
||||
"dev": true
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.3.tgz",
|
||||
"integrity": "sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@rollup/pluginutils": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.1.tgz",
|
||||
|
|
@ -93,6 +99,11 @@
|
|||
"picomatch": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"@sveltejs/adapter-static": {
|
||||
"version": "1.0.0-next.17",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-1.0.0-next.17.tgz",
|
||||
"integrity": "sha512-RKYNkQxtsMgt0wD8PhfXR1hGT1Tmq1E5eZeTr1KxIerczITRnWVT8LElfu/9Kusv44yYlyQtNc1mLoYqgloOQw=="
|
||||
},
|
||||
"@sveltejs/kit": {
|
||||
"version": "1.0.0-next.156",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.156.tgz",
|
||||
|
|
@ -185,6 +196,16 @@
|
|||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"bootstrap": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.0.tgz",
|
||||
"integrity": "sha512-bs74WNI9BgBo3cEovmdMHikSKoXnDgA6VQjJ7TyTotU6L7d41ZyCEEelPwkYEzsG/Zjv3ie9IE3EMAje0W9Xew=="
|
||||
},
|
||||
"bootstrap-icons": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.5.0.tgz",
|
||||
"integrity": "sha512-44feMc7DE1Ccpsas/1wioN8ewFJNquvi5FewA06wLnqct7CwMdGDVy41ieHaacogzDqLfG8nADIvMNp9e4bfbA=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
|
|
@ -1103,6 +1124,15 @@
|
|||
"integrity": "sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==",
|
||||
"dev": true
|
||||
},
|
||||
"sveltestrap": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.6.2.tgz",
|
||||
"integrity": "sha512-g+WhMNsQjdGtSIMGmfVrMVP5Pz0d+o2L49GdTZnKnd9CEeZHcgWObn7+DDnAIxxjQUsaCgdZPgPVSD8/bU8/zA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@popperjs/core": "^2.9.2"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "~TODO~",
|
||||
"name": "fic-frontend",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "svelte-kit dev",
|
||||
|
|
@ -9,13 +9,20 @@
|
|||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@popperjs/core": "^2.9.3",
|
||||
"@sveltejs/kit": "next",
|
||||
"eslint": "^7.22.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-svelte3": "^3.2.0",
|
||||
"prettier": "~2.2.1",
|
||||
"prettier-plugin-svelte": "^2.2.0",
|
||||
"svelte": "^3.34.0"
|
||||
"svelte": "^3.34.0",
|
||||
"sveltestrap": "^5.6.2"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.17",
|
||||
"bootstrap": "^5.1.0",
|
||||
"bootstrap-icons": "^1.5.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%svelte.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id="svelte">%svelte.body%</div>
|
||||
</body>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
|
||||
<meta name="author" content="EPITA Laboratoire SRS">
|
||||
<meta name="robots" content="all">
|
||||
%svelte.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id="svelte">%svelte.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
57
frontend/ui/src/components/CardTheme.svelte
Normal file
57
frontend/ui/src/components/CardTheme.svelte
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
Col,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
export { className as class };
|
||||
export let theme = {};
|
||||
export let exercice = null;
|
||||
let className = '';
|
||||
</script>
|
||||
|
||||
<div class="theme-card h-100">
|
||||
<Card
|
||||
class="text-light h-100 rounded-3 niceborder {className}"
|
||||
color="dark"
|
||||
on:click
|
||||
>
|
||||
{#if theme.image}
|
||||
<div class="card-img-top" style="background-image: url({ theme.image.substr(0, theme.image.length-3) }thumb.jpg)"></div>
|
||||
{/if}
|
||||
<CardBody class="text-indent">
|
||||
<CardTitle class="fw-bolder">
|
||||
{#if exercice}{exercice.title}{:else}{theme.name}{/if}
|
||||
</CardTitle>
|
||||
<p class="card-text text-justify">{#if exercice}{@html exercice.headline}{:else}{@html theme.headline}{/if}</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.theme-card {
|
||||
cursor: pointer;
|
||||
transition: transform 250ms;
|
||||
}
|
||||
.theme-card:hover {
|
||||
transform: scale(1.07);
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.theme-card .card-img-top {
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #ccc;
|
||||
}
|
||||
</style>
|
||||
46
frontend/ui/src/components/ExerciceDownloads.svelte
Normal file
46
frontend/ui/src/components/ExerciceDownloads.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<script>
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardText,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from 'sveltestrap';
|
||||
|
||||
export let files = [];
|
||||
</script>
|
||||
|
||||
{#if files.length}
|
||||
<Card class="mb-2">
|
||||
<CardHeader>
|
||||
<Icon name="download" />
|
||||
Téléchargements
|
||||
</CardHeader>
|
||||
<CardBody class="text-indent">
|
||||
<CardText class="text-danger text-justify">
|
||||
<strong>Attention :</strong> puisqu'il s'agit de captures effectuées dans le but de découvrir si des actes malveillants ont été commis, les contenus qui sont téléchargeables <em>peuvent</em> contenir du contenu malveillant !
|
||||
</CardText>
|
||||
</CardBody>
|
||||
<ListGroup>
|
||||
{#each files as file (file.id)}
|
||||
<ListGroupItem tag="a" href="{file.path}" target="_self" class="d-flex">
|
||||
<h1><Icon name="arrow-down-circle" /></h1>
|
||||
<div>
|
||||
<h4 class="fw-bold"><samp>{file.name}</samp></h4>
|
||||
<nobr>
|
||||
Taille :
|
||||
<span title="{file.size} octets">{file.size}</span>
|
||||
</nobr>
|
||||
–
|
||||
<nobr>
|
||||
<span title="blake2.net">b2sum</span> :
|
||||
<samp class="cksum" title="{file.checksum}">{file.checksum}</samp>
|
||||
</nobr>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
{/if}
|
||||
74
frontend/ui/src/components/ExerciceFlags.svelte
Normal file
74
frontend/ui/src/components/ExerciceFlags.svelte
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardText,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from 'sveltestrap';
|
||||
|
||||
export let exercice = {};
|
||||
export let flags = [];
|
||||
|
||||
function submitFlags(event) {
|
||||
console.log(event);
|
||||
}
|
||||
|
||||
let sberr = "";
|
||||
let message = "";
|
||||
</script>
|
||||
|
||||
<Card class="border-danger mb-2">
|
||||
<CardHeader class="bg-danger">
|
||||
<Icon name="flag-fill" />
|
||||
Faire son rapport
|
||||
</CardHeader>
|
||||
{#if exercice.tries || exercice.submitted || sberr}
|
||||
<ListGroup>
|
||||
{#if exercice.solved_time && exercice.tries}
|
||||
<ListGroupItem class="text-warning">
|
||||
{exercice.tries} {exercice.tries==1?"tentative effectuée":"tentatives effectuées"}.
|
||||
Dernière solution envoyée à {exercice.solved_time}.
|
||||
</ListGroupItem>
|
||||
{/if}
|
||||
{#if exercice.solve_dist}
|
||||
<ListGroupItem>
|
||||
{exercice.solve_dist} {exercice.solve_dist == 1?"réponse erronée":"réponses erronées"}.
|
||||
</ListGroupItem>
|
||||
{/if}
|
||||
{#if exercice.submitted || sberr}
|
||||
<ListGroupItem>
|
||||
{#if !sberr}
|
||||
<strong>Votre solution a bien été envoyée !</strong>
|
||||
{:else}
|
||||
<strong>{sberr}</strong> {message}
|
||||
{/if}
|
||||
</ListGroupItem>
|
||||
{/if}
|
||||
{#if exercice.timeouted}
|
||||
<ListGroupItem class="text-danger">
|
||||
<strong>Oops</strong>
|
||||
La requête a dépassé le délai d'attente. Vous devriez réessayer dans quelques instant…
|
||||
</ListGroupItem>
|
||||
{/if}
|
||||
</ListGroup>
|
||||
{/if}
|
||||
{#if !exercice.submitted || sberr}
|
||||
<CardBody>
|
||||
<form on:submit|preventDefault={submitFlags}>
|
||||
{JSON.stringify(flags)}
|
||||
<div class="form-group mt-2">
|
||||
<Button
|
||||
type="submit"
|
||||
color="danger"
|
||||
>
|
||||
Soumettre
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardBody>
|
||||
{/if}
|
||||
</Card>
|
||||
70
frontend/ui/src/components/ExerciceHints.svelte
Normal file
70
frontend/ui/src/components/ExerciceHints.svelte
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardText,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { settings } from '../stores/settings.js';
|
||||
|
||||
export let hints = [];
|
||||
|
||||
let hinterror = "";
|
||||
</script>
|
||||
|
||||
{#if hints.length}
|
||||
<Card class="mb-2">
|
||||
<CardHeader class="bg-info">
|
||||
<Icon name="lightbulb-fill" />
|
||||
Indices
|
||||
</CardHeader>
|
||||
{#if hinterror}
|
||||
<CardBody>
|
||||
<CardText class="text-danger">
|
||||
{hinterror}
|
||||
</CardText>
|
||||
</CardBody>
|
||||
{/if}
|
||||
<ListGroup>
|
||||
{#each hints as hint (hint.id)}
|
||||
<ListGroupItem tag="a" href="{hint.file}" target="_self" class="d-flex">
|
||||
{#if hint.file}
|
||||
<h1><Icon name="arrow-down-circle" /></h1>
|
||||
{/if}
|
||||
<div>
|
||||
{#if !(hint.content || hint.file)}
|
||||
<button type="button" ng-click="hsubmit(hint)" class="float-end btn btn-info" class:disabled={hint.submitted}>
|
||||
<Icon name="lock" aria-hidden="true" />
|
||||
Débloquer
|
||||
</button>
|
||||
{/if}
|
||||
{#if !hint.file && hint.hidden}
|
||||
<button type="button" ng-click="hint.hidden = false;" class="float-end btn btn-info">
|
||||
<Icon name="lock" aria-hidden="true" />
|
||||
Afficher
|
||||
</button>
|
||||
{/if}
|
||||
<h4 class="fw-bold">{hint.name}</h4>
|
||||
{#if hint.file}
|
||||
<p>
|
||||
Cliquez ici pour télécharger l'indice.<br>
|
||||
b2sum :
|
||||
<samp class="cksum" title="Somme de contrôle BLAKE2b : {hint.content}">{hint.content}</samp>
|
||||
</p>
|
||||
{:else if hint.content && !hint.hidden}
|
||||
<p>{@html hint.content}</p>
|
||||
{:else}
|
||||
<p>
|
||||
Débloquer cet indice vous fera perdre {hint.cost * settings.hintCurrentCoefficient} {hint.cost * settings.hintCurrentCoefficient==1?"point":"points"}.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
{/if}
|
||||
41
frontend/ui/src/components/ExerciceSolved.svelte
Normal file
41
frontend/ui/src/components/ExerciceSolved.svelte
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<script>
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardText,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from 'sveltestrap';
|
||||
|
||||
export let theme = {};
|
||||
export let exercice = {};
|
||||
</script>
|
||||
|
||||
<Card class="border-success mb-2">
|
||||
<CardHeader class="bg-success">
|
||||
<Icon name="flag-fill" />
|
||||
Défi réussi !
|
||||
</CardHeader>
|
||||
<CardBody class="text-indent">
|
||||
<CardText>
|
||||
{#if exercice.solved_rank}
|
||||
Vous êtes la {exercice.solved_rank}<sup>{exercice.solved_rank==1?"re":"e"}</sup> équipe à avoir résolu ce défi à {exercice.solved_time}.
|
||||
{:else}
|
||||
Bravo, vous avez résolu ce défi à {exercice.solved_time}.
|
||||
{/if}
|
||||
Vous avez marqué {exercice.gain} {exercice.gain==1?"point":"points"} !
|
||||
</CardText>
|
||||
{#if exercice.finished}
|
||||
<hr>
|
||||
<CardText>{@html exercice.finished}</CardText>
|
||||
{#if exercice.next}
|
||||
<hr>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if theme.exercices[exercice.id].next}
|
||||
<a href="/{theme.urlid}/{theme.exercices[theme.exercices[exercice.id].next].urlid}" class="btn btn-success">Passer au défi suivant</a>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
26
frontend/ui/src/components/ExerciceVideo.svelte
Normal file
26
frontend/ui/src/components/ExerciceVideo.svelte
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<script>
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardText,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from 'sveltestrap';
|
||||
|
||||
export let uri = "";
|
||||
</script>
|
||||
|
||||
<Card class="border-success mb-2">
|
||||
<CardHeader class="bg-success">
|
||||
<Icon name="laptop-fill" />
|
||||
Solution du défi
|
||||
</CardHeader>
|
||||
<CardBody class="text-indent">
|
||||
<div class="embed-responsive embed-responsive-16by9">
|
||||
<iframe type="text/html" src="{uri}" class="embed-responsive-item" title="Vidéo de résolution">
|
||||
Regardez la vidéo de résolution de ce défi : <a href="{uri}">{uri}</a>.
|
||||
</iframe>
|
||||
</CardBody>
|
||||
</Card>
|
||||
47
frontend/ui/src/components/FormIssue.svelte
Normal file
47
frontend/ui/src/components/FormIssue.svelte
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { issues, issues_idx } from '../stores/issues.js';
|
||||
|
||||
export let exercice = null;
|
||||
export let issue = { };
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault>
|
||||
{#if exercice || issue.id_exercice}
|
||||
<div class="row mb-3">
|
||||
<label for="idExercice" class="col-sm-2 col-form-label">Défi</label>
|
||||
<div class="col-sm-10">
|
||||
{#if exercice.id}
|
||||
<input type="text" readonly class="form-control-plaintext" id="idExercice" value={exercice.title}>
|
||||
{:else}
|
||||
<input type="text" readonly class="form-control-plaintext" id="idExercice" value="{issue.id_exercice}">
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="row mb-3">
|
||||
<label for="subject" class="col col-form-label">Objet</label>
|
||||
<div class="col-sm-10">
|
||||
{#if issue.id && $issues_idx[issue.id]}
|
||||
<input type="text" readonly class="form-control-plaintext" id="subject" value="Re: {$issues_idx[issue.id].subject}">
|
||||
{:else}
|
||||
<input type="text" class="form-control" id="subject" bind:value={issue.subject} placeholder="Intitulé succinct">
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="description" class="col col-form-label">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="description" bind:value={issue.description} placeholder="Décrivez en détail votre problème ici. Si nécessaire, incluez un lien vers une capture d'écran montrant votre problème."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" color="warning" class="float-end">
|
||||
Envoyer le rapport
|
||||
</Button>
|
||||
</form>
|
||||
112
frontend/ui/src/components/Header.svelte
Normal file
112
frontend/ui/src/components/Header.svelte
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<script>
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Col,
|
||||
Collapse,
|
||||
Container,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
Icon,
|
||||
Navbar,
|
||||
NavbarToggler,
|
||||
NavbarBrand,
|
||||
Nav,
|
||||
NavItem,
|
||||
NavLink,
|
||||
Progress,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { my } from '../stores/my.js';
|
||||
import { teams } from '../stores/teams.js';
|
||||
import { settings, time } from '../stores/settings.js';
|
||||
|
||||
import HeaderClock from './HeaderClock.svelte';
|
||||
import HeaderIssues from './HeaderIssues.svelte';
|
||||
import HeaderPartners from './HeaderPartners.svelte';
|
||||
import NavThemes from './NavThemes.svelte';
|
||||
import NavTags from './NavTags.svelte';
|
||||
|
||||
let isOpen = false;
|
||||
|
||||
function handleUpdate(event) {
|
||||
isOpen = event.detail.isOpen;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container-fluid bg-dark" style="max-height: 15vh;">
|
||||
<div style="height: 100%; max-height: inherit; width: 98%; position: absolute">
|
||||
<Container class="d-flex justify-content-center align-items-center text-light" style="height: 100%; max-height: inherit">
|
||||
<HeaderClock />
|
||||
</Container>
|
||||
</div>
|
||||
<Container class="d-flex justify-content-between p-1" style="max-height: inherit">
|
||||
<a href="/">
|
||||
<img src="/img/fic.png" alt="Forum International de la Cybersécurité" class="h-100">
|
||||
</a>
|
||||
<HeaderPartners />
|
||||
</Container>
|
||||
</div>
|
||||
<Navbar class="sticky-top" color="dark" dark expand="md">
|
||||
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
|
||||
<Collapse {isOpen} navbar expand="md" on:update={handleUpdate}>
|
||||
<Nav navbar>
|
||||
<NavItem>
|
||||
<NavLink href="/">
|
||||
<Icon name="box-seam" />
|
||||
Accueil
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavThemes />
|
||||
<NavTags />
|
||||
{#if $settings && $settings.end - $settings.start >= 0}
|
||||
<NavItem>
|
||||
<NavLink href="/rank">
|
||||
<Icon name="sort-down" />
|
||||
Classement
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
{/if}
|
||||
<HeaderIssues />
|
||||
<NavItem>
|
||||
<NavLink href="/rules">
|
||||
<Icon name="signpost-split" />
|
||||
Aide
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
<Nav class="ms-auto text-light" navbar>
|
||||
{#if $my && $my.team_id}
|
||||
<NavItem>
|
||||
{$my.score} {$my.score === 1 ? 'point' : 'points'}
|
||||
{#if $teams[$my.team_id].rank}
|
||||
– {$teams[$my.team_id].rank}<sup>e</sup> sur {Object.keys($teams).length}
|
||||
{/if}
|
||||
</NavItem>
|
||||
{/if}
|
||||
<NavItem class="ms-2">
|
||||
{#if !$my}
|
||||
<Badge href="/register" color="warning">
|
||||
Inscription
|
||||
</Badge>
|
||||
{:else if $my.team_id}
|
||||
<Badge href="/edit" style="background-color: {$teams[$my.team_id].color} !important; color: {$teams[$my.team_id].color};">
|
||||
<span class="teamname">{$my.name}</span>
|
||||
</Badge>
|
||||
{/if}
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
<Progress value={$time.progression * 100} color="info" style="height: 5px; border-radius: 0;" />
|
||||
|
||||
<style>
|
||||
.teamname {
|
||||
-webkit-filter: invert(100%);
|
||||
filter: invert(100%);
|
||||
}
|
||||
</style>
|
||||
108
frontend/ui/src/components/HeaderClock.svelte
Normal file
108
frontend/ui/src/components/HeaderClock.svelte
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<script>
|
||||
import {
|
||||
ButtonGroup,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { settings, time } from '../stores/settings.js';
|
||||
</script>
|
||||
|
||||
{#if $settings}
|
||||
{#if $settings.end - $settings.start > 0}
|
||||
<div
|
||||
class="clock display-2"
|
||||
class:expired={$time.expired}
|
||||
class:end={$time.end}
|
||||
class:wait={$time.startIn}
|
||||
>
|
||||
{#if $time.seconds}
|
||||
<span id="hours">
|
||||
{$time.hours}
|
||||
</span>
|
||||
<span class="point">
|
||||
:
|
||||
</span>
|
||||
<span id="minutes">
|
||||
{$time.minutes}
|
||||
</span>
|
||||
<span class="point">
|
||||
:
|
||||
</span>
|
||||
<span id="seconds">
|
||||
{$time.seconds}
|
||||
</span>
|
||||
{:else}
|
||||
Chargement
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="d-flex h-100 justify-content-center align-items-center">
|
||||
<ButtonGroup size="lg">
|
||||
<a
|
||||
href="/"
|
||||
class="btn btn-light"
|
||||
>
|
||||
<Icon name="ui-checks-grid" />
|
||||
Accueil
|
||||
</a>
|
||||
<a
|
||||
href="/rank"
|
||||
class="btn btn-light"
|
||||
>
|
||||
<Icon name="sort-down" />
|
||||
Classement
|
||||
</a>
|
||||
<a
|
||||
href="{$settings.videoslink}"
|
||||
class="btn btn-light"
|
||||
class:disabled={$settings.videoslink === ''}
|
||||
>
|
||||
<Icon name="laptop-fill" />
|
||||
Vidéos
|
||||
</a>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="d-flex h-100 justify-content-center align-items-center">
|
||||
<h1 class="display-3 m-0">
|
||||
Challenge forensic
|
||||
</h1>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.clock:not(.expired):not(.wait) .point, .clock.expired {
|
||||
transition: color text-shadow 1s;
|
||||
position: relative;
|
||||
animation: clockanim 1s ease infinite;
|
||||
-moz-animation: clockanim 1s ease infinite;
|
||||
-webkit-animation: clockanim 1s ease infinite;
|
||||
}
|
||||
.clock.wait .point {
|
||||
transition: color text-shadow 1s;
|
||||
position: relative;
|
||||
animation: clockwait 1s ease infinite;
|
||||
-moz-animation: clockwait 1s ease infinite;
|
||||
-webkit-animation: clockwait 1s ease infinite;
|
||||
}
|
||||
.end {
|
||||
color: #e64143;
|
||||
}
|
||||
.point {
|
||||
text-shadow: 0 0 20px #4eaee6;
|
||||
}
|
||||
.end .point {
|
||||
text-shadow: 0 0 20px #e64143;
|
||||
}
|
||||
@keyframes clockanim {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0; }
|
||||
100% { opacity: 1.0; }
|
||||
}
|
||||
@keyframes clockwait {
|
||||
0% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
50% { text-shadow: 0 0 2px #A6D6F2; }
|
||||
100% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
}
|
||||
</style>
|
||||
34
frontend/ui/src/components/HeaderIssues.svelte
Normal file
34
frontend/ui/src/components/HeaderIssues.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<script>
|
||||
import {
|
||||
Badge,
|
||||
Icon,
|
||||
NavItem,
|
||||
NavLink,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { issues, issues_need_info, issues_nb_responses, issues_known_responses } from '../stores/issues.js';
|
||||
import { settings } from '../stores/settings.js';
|
||||
|
||||
let badge_color = 'secondary';
|
||||
$: {
|
||||
if ($issues_known_responses != $issues_nb_responses) {
|
||||
if ($issues_need_info) {
|
||||
badge_color = 'danger';
|
||||
} else {
|
||||
badge_color = 'warning';
|
||||
}
|
||||
} else {
|
||||
badge_color = 'light';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $issues.length}
|
||||
<NavItem>
|
||||
<NavLink href="/issues">
|
||||
<Icon name="bug" />
|
||||
Problèmes
|
||||
<Badge color={badge_color}>{$issues_nb_responses}</Badge>
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
{/if}
|
||||
40
frontend/ui/src/components/HeaderPartners.svelte
Normal file
40
frontend/ui/src/components/HeaderPartners.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<script>
|
||||
import {
|
||||
Carousel,
|
||||
CarouselItem,
|
||||
} from 'sveltestrap';
|
||||
|
||||
let partners = [
|
||||
{
|
||||
img: '/img/epita.png',
|
||||
alt: 'Epita',
|
||||
href: 'https://www.epita.fr/',
|
||||
},
|
||||
{
|
||||
img: '/img/srs.png',
|
||||
alt: 'Laboratoire SRS Épita',
|
||||
href: 'https://srs.epita.fr/',
|
||||
},
|
||||
{
|
||||
img: '/img/comcyber.png',
|
||||
alt: 'Réserves de cyberdéfense',
|
||||
},
|
||||
];
|
||||
let activePartner = 0;
|
||||
</script>
|
||||
|
||||
<Carousel items={partners} bind:activeIndex={activePartner} ride="carousel" pause="hover" interval={25000}>
|
||||
<div class="carousel-inner h-100">
|
||||
{#each partners as partner, index}
|
||||
<CarouselItem bind:activeIndex={activePartner} itemIndex={index} class="h-100 text-end">
|
||||
{#if partner.href}
|
||||
<a href="{partner.href}" target="_blank" class="h-100">
|
||||
<img src={partner.img} class="h-100" alt={partner.alt}>
|
||||
</a>
|
||||
{:else}
|
||||
<img src={partner.img} class="h-100" alt={partner.alt}>
|
||||
{/if}
|
||||
</CarouselItem>
|
||||
{/each}
|
||||
</div>
|
||||
</Carousel>
|
||||
49
frontend/ui/src/components/NavTags.svelte
Normal file
49
frontend/ui/src/components/NavTags.svelte
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
import {
|
||||
Badge,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
| ||||