Rename frontend as receiver
15
frontend/fic/.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: ['eslint:recommended', 'prettier'],
|
||||
plugins: ['svelte3'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2019
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
||||
12
frontend/fic/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
|
||||
static/issues.json
|
||||
static/my.json
|
||||
static/settings.json
|
||||
static/teams.json
|
||||
static/themes.json
|
||||
static/wait.json
|
||||
6
frontend/fic/.prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100
|
||||
}
|
||||
11
frontend/fic/jsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"$lib": ["src/lib"],
|
||||
"$lib/*": ["src/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
||||
5150
frontend/fic/package-lock.json
generated
Normal file
35
frontend/fic/package.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "fic-frontend",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.0.0",
|
||||
"eslint": "^8.4.2",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier-plugin-svelte": "^2.7.0",
|
||||
"sass": "^1.51.0",
|
||||
"sass-loader": "^13.0.0",
|
||||
"svelte": "^3.48.0",
|
||||
"sveltestrap": "^5.9.0",
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"bootstrap": "^5.1.3",
|
||||
"bootstrap-icons": "^1.8.1",
|
||||
"bootswatch": "^5.1.3",
|
||||
"hash-wasm": "^4.9.0",
|
||||
"seedrandom": "^3.0.5",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
||||
14
frontend/fic/src/app.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<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">
|
||||
<meta name="robots" content="all">
|
||||
<base href="/">
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
45
frontend/fic/src/fic.scss
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Your variable overrides can go here, e.g.:
|
||||
// $h1-font-size: 3rem;
|
||||
$white: white;
|
||||
$gray-500: #999;
|
||||
$gray-900: #272b30;
|
||||
|
||||
$body-bg: $white;
|
||||
|
||||
$old-body-bg: $gray-900;
|
||||
$dropdown-link-hover-bg: $old-body-bg;
|
||||
$card-bg: lighten($old-body-bg, 5%);
|
||||
$popover-bg: lighten($old-body-bg, 5%);
|
||||
$toast-background-color: lighten($old-body-bg, 5%);
|
||||
$modal-content-bg: lighten($old-body-bg, 5%);
|
||||
$list-group-bg: lighten($old-body-bg, 5%);
|
||||
$list-group-hover-bg: lighten($old-body-bg, 10%);
|
||||
|
||||
$list-group-border-color: rgba($gray-500, .6);
|
||||
|
||||
$enable-print-styles: false;
|
||||
|
||||
@import "bootswatch/dist/slate/_variables";
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
@import "bootswatch/dist/slate/_bootswatch";
|
||||
|
||||
p img {
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100vh;
|
||||
}
|
||||
p:has(img) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.level .level-item {
|
||||
text-align: center;
|
||||
}
|
||||
.level .level-item .heading {
|
||||
font-variant: small-caps;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.level .level-item .value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bolder;
|
||||
}
|
||||
1
frontend/fic/src/global.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
||||
96
frontend/fic/src/lib/components/CardTheme.svelte
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
Col,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { max_solved } from '$lib/stores/themes.js';
|
||||
import { myThemes } from '$lib/stores/mythemes.js';
|
||||
|
||||
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)"
|
||||
style:filter={theme.locked ? "grayscale(60%)":null}
|
||||
></div>
|
||||
{/if}
|
||||
<CardBody class="text-indent">
|
||||
<CardTitle class="fw-bolder">
|
||||
{#if $my && $my.team_id}
|
||||
<div class="float-end">
|
||||
{#if theme.locked}
|
||||
<Badge color="light">
|
||||
<Icon name="lock-fill" />
|
||||
</Badge>
|
||||
{:else}
|
||||
{#if $max_solved > 1 && theme.solved == $max_solved}
|
||||
<Badge color="danger">
|
||||
<Icon name="heart-fill" />
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if theme.exercice_coeff_max > 1 || (exercice && exercice.curcoeff > 1)}
|
||||
<Badge color="success">
|
||||
<Icon name="gift-fill" />
|
||||
</Badge>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if exercice && $my && $my.exercices[exercice.id] && $my.exercices[exercice.id].solved_rank}
|
||||
<Badge color="light" class="text-success fw-bold">
|
||||
<Icon name="check" />
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if !exercice && $myThemes[theme.id].exercice_solved > 0}
|
||||
<Badge color="light">
|
||||
{$myThemes[theme.id].exercice_solved}/{theme.exercice_count}
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#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>
|
||||
74
frontend/fic/src/lib/components/Clock.svelte
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<script>
|
||||
import {
|
||||
ButtonGroup,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { time } from '$lib/stores/settings.js';
|
||||
|
||||
export { className as class };
|
||||
let className = '';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="clock {className}"
|
||||
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>
|
||||
|
||||
<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>
|
||||
17
frontend/fic/src/lib/components/DateFormat.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script>
|
||||
export let date;
|
||||
export let dateStyle = "long";
|
||||
export let timeStyle = "long";
|
||||
|
||||
function formatDate(input, dateStyle, timeStyle) {
|
||||
if (typeof input === 'string') {
|
||||
input = new Date(input);
|
||||
}
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle,
|
||||
timeStyle,
|
||||
}).format(input);
|
||||
}
|
||||
</script>
|
||||
|
||||
{formatDate(date, dateStyle, timeStyle)}
|
||||
70
frontend/fic/src/lib/components/ExerciceDownloads.svelte
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardText,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import FileSize from './FileSize.svelte';
|
||||
|
||||
export let files = [];
|
||||
</script>
|
||||
|
||||
{#if files.length}
|
||||
<Card class="mb-4">
|
||||
<CardHeader class="text-light">
|
||||
<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 class="border-dark">
|
||||
{#each files as file, index}
|
||||
<ListGroupItem tag="a" href={file.path} target={(file.name.endsWith(".txt") || file.name.endsWith(".jpg") || file.name.endsWith(".png") || file.name.endsWith(".pdf"))?"_blank":"_self"} class="d-flex">
|
||||
<h1 class="me-3">
|
||||
<Icon name="arrow-down-circle" />
|
||||
</h1>
|
||||
<div style="min-width: 0">
|
||||
<h4 class="fw-bold"><samp>{file.name}</samp></h4>
|
||||
{#if file.disclamer}
|
||||
<div class="file-disclamer text-warning">
|
||||
{file.disclamer}
|
||||
</div>
|
||||
{/if}
|
||||
<nobr>
|
||||
Taille :
|
||||
{#if file.compressed}
|
||||
<acronym title="Nous ne sommes pas en mesure de calculer la taille exacte de ce fichier, votre navigateur est susceptible d'afficher une progression non représentative. Tant que le téléchargement se poursuit (même au delà de 100%), n'arrêtez pas, vous n'auriez pas le fichier en entier. Ce phénomène est du au fait que le fichier est stocké sous une forme compressé sur notre serveur, alors que vous le récupérez décompressé." class="fst-italic">
|
||||
environ
|
||||
<FileSize size={file.size} />
|
||||
</acronym>
|
||||
{:else}
|
||||
<FileSize size={file.size} />
|
||||
{/if}
|
||||
</nobr>
|
||||
<nobr class="d-block text-truncate">
|
||||
<span title="blake2.net">b2sum</span> :
|
||||
<samp class="cksum" title="{file.checksum}">{file.checksum}</samp>
|
||||
</nobr>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.file-disclamer {
|
||||
display: none;
|
||||
}
|
||||
:global(.list-group-item:hover .file-disclamer) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
244
frontend/fic/src/lib/components/ExerciceFlags.svelte
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardText,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Progress,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { blake2b } from 'hash-wasm';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { teams } from '$lib/stores/teams.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
|
||||
import DateFormat from './DateFormat.svelte';
|
||||
import FlagKey from './FlagKey.svelte';
|
||||
import FlagMCQ from './FlagMCQ.svelte';
|
||||
|
||||
export let exercice = { };
|
||||
export let flags = [];
|
||||
export let readonly = false;
|
||||
|
||||
function waitDiff(i) {
|
||||
my.refresh((my) => {
|
||||
if (my && (my.exercices[exercice.id].tries != exercice.tries || my.exercices[exercice.id].solved_rank != exercice.solved_rank || my.exercices[exercice.id].solved_time != exercice.solved_time)) {
|
||||
submitInProgress = false;
|
||||
teams.refresh();
|
||||
} else if (i > 0) {
|
||||
setTimeout(waitDiff, (12-i)*50+440, i-1);
|
||||
} else {
|
||||
timeouted = true;
|
||||
submitInProgress = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export let forcesolved = false;
|
||||
let responses = { };
|
||||
async function submitFlags() {
|
||||
submitInProgress = true;
|
||||
sberr = "";
|
||||
message = "";
|
||||
|
||||
if ($my && $my.team_id === 0) {
|
||||
let allGoodResponse = true;
|
||||
for (const f in flags) {
|
||||
const flag = flags[f];
|
||||
|
||||
let soluce = "";
|
||||
if (flag.type == "mcq") {
|
||||
for (const c in flag.choices) {
|
||||
soluce += responses.mcqs[c] ? "t" : "f";
|
||||
}
|
||||
} else {
|
||||
soluce = responses.flags[flag.id];
|
||||
}
|
||||
|
||||
if (flag.ignore_case) {
|
||||
soluce = soluce.toLowerCase();
|
||||
}
|
||||
|
||||
if (flag.validator_regexp) {
|
||||
let re = new RegExp(flag.validator_regexp, flag.ignore_case?'i':'');
|
||||
soluce = soluce.match(re).slice(1).join("+");
|
||||
}
|
||||
|
||||
if (await blake2b(soluce) == flag.soluce) {
|
||||
flags[f].found = new Date();
|
||||
} else if (!flag.found) {
|
||||
allGoodResponse = false;
|
||||
}
|
||||
flags = flags;
|
||||
}
|
||||
|
||||
if (allGoodResponse) {
|
||||
forcesolved = true;
|
||||
}
|
||||
|
||||
if (exercice.tries) {
|
||||
exercice.tries += 1;
|
||||
} else {
|
||||
exercice.tries = 1;
|
||||
}
|
||||
exercice.solved_time = new Date();
|
||||
exercice = exercice;
|
||||
|
||||
submitInProgress = false;
|
||||
} else {
|
||||
const response = await fetch(
|
||||
"submit/" + exercice.id,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {'Accept': 'application/json'},
|
||||
body: JSON.stringify(responses),
|
||||
}
|
||||
)
|
||||
|
||||
if (response.status < 300) {
|
||||
const data = await response.json();
|
||||
messageClass = 'text-success';
|
||||
message = data.errmsg;
|
||||
waitDiff(13);
|
||||
} else {
|
||||
submitInProgress = false;
|
||||
|
||||
messageClass = 'text-danger';
|
||||
|
||||
let data = "";
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch(e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
if (data && data.errmsg)
|
||||
message = data.errmsg;
|
||||
if (response.statys != 402)
|
||||
sberr = "Oups !";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetResponses() {
|
||||
responses = {
|
||||
flags: { },
|
||||
mcqs: { },
|
||||
justifications: { },
|
||||
};
|
||||
}
|
||||
|
||||
let last_exercice = null;
|
||||
$: {
|
||||
if (!last_exercice || last_exercice != exercice.id) {
|
||||
last_exercice = exercice.id;
|
||||
resetResponses()
|
||||
}
|
||||
}
|
||||
|
||||
let sberr = "";
|
||||
let message = "";
|
||||
let messageClass = "text-danger";
|
||||
let timeouted = false;
|
||||
let submitInProgress = false;
|
||||
</script>
|
||||
|
||||
<Card class="border-danger mb-2">
|
||||
<CardHeader class="bg-danger text-light">
|
||||
<Icon name="flag-fill" />
|
||||
Faire son rapport
|
||||
</CardHeader>
|
||||
{#if exercice.flags.length != exercice.nb_flags}
|
||||
<Progress
|
||||
value={exercice.flags.length}
|
||||
max={exercice.nb_flags}
|
||||
class="rounded-0"
|
||||
barClassName="text-light"
|
||||
>
|
||||
{exercice.flags.filter((f) => f.type != "label").length}/{exercice.nb_flags}
|
||||
</Progress>
|
||||
{/if}
|
||||
{#if exercice.tries || exercice.solved_time || exercice.submitted || sberr || timeouted}
|
||||
<ListGroup class="border-dark">
|
||||
{#if exercice.solved_time || exercice.tries}
|
||||
<ListGroupItem class="text-warning rounded-0">
|
||||
{#if exercice.tries > 0}{exercice.tries} {exercice.tries==1?"tentative effectuée":"tentatives effectuées"}.{/if}
|
||||
Dernière solution envoyée à <DateFormat date={exercice.solved_time} />.
|
||||
</ListGroupItem>
|
||||
{/if}
|
||||
{#if exercice.solve_dist}
|
||||
<ListGroupItem class="rounded-0">
|
||||
{exercice.solve_dist} {exercice.solve_dist == 1?"réponse erronée":"réponses erronées"}.
|
||||
</ListGroupItem>
|
||||
{/if}
|
||||
{#if exercice.submitted || sberr}
|
||||
<ListGroupItem class="{messageClass} rounded-0">
|
||||
{#if !sberr}
|
||||
<strong>Votre solution a bien été envoyée !</strong>
|
||||
{:else}
|
||||
<strong>{sberr}</strong> {message}
|
||||
{/if}
|
||||
</ListGroupItem>
|
||||
{/if}
|
||||
{#if timeouted}
|
||||
<ListGroupItem class="text-danger rounded-0">
|
||||
<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}>
|
||||
{#each flags as flag ((flag.type?flag.type:"i") + flag.id)}
|
||||
{#if !flag.type && !flag.id}
|
||||
<div class="form-group mb-3">
|
||||
<span class="{flag.variant?('text-'+flag.variant):''}">{@html flag.label}</span>
|
||||
</div>
|
||||
{:else if flag.type == "mcq"}
|
||||
<FlagMCQ
|
||||
exercice_id={exercice.id}
|
||||
{flag}
|
||||
bind:values={responses.mcqs}
|
||||
bind:justifications={responses.justifications}
|
||||
/>
|
||||
{:else}
|
||||
<FlagKey
|
||||
class="mb-3"
|
||||
exercice_id={exercice.id}
|
||||
{flag}
|
||||
bind:value={responses.flags[flag.id]}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<div class="form-group mt-2">
|
||||
<Button
|
||||
type="submit"
|
||||
color="danger"
|
||||
id="submission-{exercice.id}"
|
||||
disabled={submitInProgress || $settings.disablesubmitbutton || readonly}
|
||||
>
|
||||
{#if submitInProgress}
|
||||
<Spinner size="sm" class="me-2" />
|
||||
{/if}
|
||||
Soumettre
|
||||
</Button>
|
||||
{#if $settings.disablesubmitbutton}
|
||||
<span class="text-muted">{$settings.disablesubmitbutton}</span>
|
||||
{:else if readonly}
|
||||
<span class="text-muted">Ce défi est désactivé</span>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</CardBody>
|
||||
{/if}
|
||||
</Card>
|
||||
146
frontend/fic/src/lib/components/ExerciceHints.svelte
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<script>
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardText,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
|
||||
export let hints = [];
|
||||
export let exercice = {};
|
||||
export let locked_theme = false;
|
||||
|
||||
let hints_submitted = {};
|
||||
|
||||
function waitDiff(i, hint) {
|
||||
my.refresh((my) => {
|
||||
let openedHint = false;
|
||||
|
||||
if (my && my.exercices[exercice.id].hints) {
|
||||
my.exercices[exercice.id].hints.forEach((h) => {
|
||||
if (h.id == hint.id && (h.content || h.file)) {
|
||||
openedHint = true;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (openedHint) {
|
||||
hints_submitted[hint.id] = false;
|
||||
hinterror = "";
|
||||
} else if (i > 0) {
|
||||
setTimeout(waitDiff, 650, i-1, hint);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showHint(hint) {
|
||||
hint.hidden = false;
|
||||
hints = hints; // Force Svelte update
|
||||
}
|
||||
|
||||
async function openHint(hint) {
|
||||
if (!confirm("Êtes-vous sûr de vouloir utiliser " + (hint.cost * $settings.hintCurrentCoefficient) + " points pour dévoiler cet indice ?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
hints_submitted[hint.id] = true;
|
||||
hinterror = "";
|
||||
|
||||
const response = await fetch(
|
||||
"openhint/" + exercice.id,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {'Accept': 'application/json'},
|
||||
body: JSON.stringify({ id: hint.id }),
|
||||
}
|
||||
)
|
||||
|
||||
if (response.status < 300) {
|
||||
waitDiff(15, hint);
|
||||
} else {
|
||||
hints_submitted[hint.id] = false;
|
||||
|
||||
let data = "";
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch(e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
if (data && data.errmsg)
|
||||
hinterror = data.errmsg;
|
||||
}
|
||||
}
|
||||
|
||||
let hinterror = "";
|
||||
</script>
|
||||
|
||||
{#if hints.length}
|
||||
<Card class="mb-2">
|
||||
<CardHeader class="bg-info text-light">
|
||||
<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 align-items-center">
|
||||
{#if hint.file}
|
||||
<h1 class="me-3">
|
||||
<Icon name="arrow-down-circle" />
|
||||
</h1>
|
||||
{/if}
|
||||
<div class="flex-fill" style="min-width:0">
|
||||
<h4 class="fw-bold">{hint.title}</h4>
|
||||
{#if hint.file}
|
||||
<p style="overflow-x: auto">
|
||||
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>
|
||||
{#if !(hint.content || hint.file) || (!hint.file && hint.hidden)}
|
||||
<div>
|
||||
{#if !(hint.content || hint.file)}
|
||||
<button type="button" on:click={openHint(hint)} class="btn btn-info" class:disabled={hints_submitted[hint.id]} disabled={$settings.disablesubmitbutton || locked_theme}>
|
||||
{#if hints_submitted[hint.id]}
|
||||
<Spinner size="sm" class="me-2" />
|
||||
{:else}
|
||||
<Icon name="lock" aria-hidden="true" />
|
||||
{/if}
|
||||
Débloquer
|
||||
</button>
|
||||
{/if}
|
||||
{#if !hint.file && hint.hidden}
|
||||
<button type="button" on:click={() => showHint(hint)} class="btn btn-info">
|
||||
<Icon name="lock" aria-hidden="true" />
|
||||
Afficher
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
{/if}
|
||||
52
frontend/fic/src/lib/components/ExerciceSolved.svelte
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardText,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import DateFormat from './DateFormat.svelte';
|
||||
|
||||
export let theme = {};
|
||||
export let exercice = {};
|
||||
|
||||
let next = null;
|
||||
$: {
|
||||
for (const ex of theme.exercices) {
|
||||
if (ex.id == exercice.id && ex.next) {
|
||||
next = ex.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="border-success mb-2">
|
||||
<CardHeader class="bg-success text-light">
|
||||
<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 à <DateFormat date={exercice.solved_time} />.
|
||||
{:else}
|
||||
Bravo, vous avez résolu ce défi à <DateFormat date={exercice.solved_time} />{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 next}
|
||||
<a href="{theme.urlid}/{theme.exercices[next].urlid}" class="btn btn-success">Passer au défi suivant</a>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
21
frontend/fic/src/lib/components/ExerciceVideo.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import { base } from '$app/paths';
|
||||
|
||||
import {
|
||||
CardBody,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
export let uri = "";
|
||||
</script>
|
||||
|
||||
<CardBody class="text-indent ratio ratio-16x9">
|
||||
{#if uri.length > 0 && uri[0] === '/'}
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video controls src={uri} />
|
||||
{:else}
|
||||
<iframe type="text/html" src="{uri.replace('$RFILES$', base+'/resolution')}" class="embed-responsive-item" title="Vidéo de résolution">
|
||||
Regardez la vidéo de résolution de ce défi : <a href="{uri.replace('$RFILES$',base+'/resolution')}">{uri.replace('$RFILES$',base+'/resolution')}</a>.
|
||||
</iframe>
|
||||
{/if}
|
||||
</CardBody>
|
||||
28
frontend/fic/src/lib/components/FileSize.svelte
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<script>
|
||||
export let size;
|
||||
|
||||
const units = [
|
||||
"o",
|
||||
"kio",
|
||||
"Mio",
|
||||
"Gio",
|
||||
"Tio",
|
||||
"Pio",
|
||||
"Eio",
|
||||
"Zio",
|
||||
"Yio",
|
||||
]
|
||||
function formatSize(input) {
|
||||
var res = input;
|
||||
var unit = 0;
|
||||
while (res > 1024) {
|
||||
unit += 1;
|
||||
res = res / 1024;
|
||||
}
|
||||
return (Math.round(res * 100) / 100) + " " + units[unit];
|
||||
}
|
||||
</script>
|
||||
|
||||
<span title="{size} octets">
|
||||
{formatSize(size)}
|
||||
</span>
|
||||
225
frontend/fic/src/lib/components/FlagKey.svelte
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
|
||||
export { className as class };
|
||||
let className = '';
|
||||
|
||||
export let exercice_id = 0;
|
||||
export let flag = { };
|
||||
export let no_label = false;
|
||||
export let value = "";
|
||||
let values = [""];
|
||||
|
||||
function waitChoices(i) {
|
||||
my.refresh((my) => {
|
||||
let haveChoices = false;
|
||||
|
||||
if (my && my.exercices[exercice_id].flags) {
|
||||
my.exercices[exercice_id].flags.forEach((f) => {
|
||||
if (f.id == flag.id && f.choices) {
|
||||
haveChoices = true;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (haveChoices) {
|
||||
wcsubmitted = false;
|
||||
} else if (i > 0) {
|
||||
setTimeout(waitChoices, 450, i-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let wcsubmitted = false;
|
||||
async function wantchoices() {
|
||||
if (!confirm("Êtes-vous sûr de vouloir utiliser " + (flag.choices_cost * $settings.wchoiceCurrentCoefficient) + " points pour avoir une liste de propositions à la place de ce champ de texte à compléter ?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
wcsubmitted = true;
|
||||
|
||||
const response = await fetch(
|
||||
"wantchoices/" + exercice_id,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {'Accept': 'application/json'},
|
||||
body: JSON.stringify({ id: Number(flag.id) }),
|
||||
}
|
||||
)
|
||||
|
||||
if (response.status < 300) {
|
||||
waitChoices(15);
|
||||
} else {
|
||||
wcsubmitted = false;
|
||||
}
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
values.push("");
|
||||
values = values;
|
||||
}
|
||||
|
||||
$: {
|
||||
let v = values.slice();
|
||||
|
||||
// Remove empty cells
|
||||
if (!flag.nb_lines) {
|
||||
for (let i = v.length - 1; i > 0; i--) {
|
||||
if (!v[i].length) {
|
||||
v.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort cells
|
||||
if (flag.ignore_order) {
|
||||
v = v.sort();
|
||||
}
|
||||
|
||||
value = v.join(flag.separator ? flag.separator : ',');
|
||||
if (flag.separator) {
|
||||
value += flag.separator;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (flag.nb_lines) {
|
||||
while (values.length != flag.nb_lines) {
|
||||
if (values.length > flag.nb_lines) {
|
||||
values.pop();
|
||||
} else {
|
||||
values.push("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={className + " form-group"}>
|
||||
{#if flag.bonus_gain}
|
||||
<div class={'float-end badge bg-' + (flag.found?'success':'danger')} title={'Ce flag est optionnel, si vous le complétez il vous rapportera ' + flag.bonus_gain + ' points supplémentaires'}>
|
||||
optionnel | {#if flag.bonus_gain > 0}+{/if}{flag.bonus_gain} pts
|
||||
</div>
|
||||
{/if}
|
||||
{#if !no_label}
|
||||
<label for="sol_{flag.type}{flag.id}_0">{flag.label} :</label>
|
||||
{/if}
|
||||
{#if flag.found && flag.value}
|
||||
<span>{flag.value}</span>
|
||||
{/if}
|
||||
{#if !flag.found}
|
||||
{#each values as v, index}
|
||||
{#if !flag.choices}
|
||||
<div class="input-group" class:mt-1={index != 0}>
|
||||
{#if flag.type == 'number'}
|
||||
<input
|
||||
type="number"
|
||||
class="form-control flag"
|
||||
id="sol_{flag.type}{flag.id}_{index}"
|
||||
autocomplete="off"
|
||||
bind:value={values[index]}
|
||||
placeholder={flag.placeholder}
|
||||
title={flag.placeholder}
|
||||
min={flag.min}
|
||||
max={flag.max}
|
||||
step={flag.step}
|
||||
>
|
||||
{:else if !flag.multiline}
|
||||
<input
|
||||
type="text"
|
||||
class="form-control flag"
|
||||
id="sol_{flag.type}{flag.id}_{index}"
|
||||
autocomplete="off"
|
||||
bind:value={values[index]}
|
||||
placeholder={flag.placeholder}
|
||||
title={flag.placeholder}
|
||||
on:keydown={(e) => {if (flag.separator && e.keyCode === 13) { e.preventDefault(); addItem(); tick().then(() => { document.getElementById('sol_' + flag.type + '' + flag.id + '_' + (values.length - 1)).focus(); }); return false;}}}
|
||||
>
|
||||
{:else}
|
||||
<textarea
|
||||
class="form-control flag"
|
||||
id="sol_{flag.type}{flag.id}_{index}"
|
||||
autocomplete="off"
|
||||
bind:value={values[index]}
|
||||
placeholder="{flag.placeholder}"
|
||||
title="{flag.placeholder}"
|
||||
></textarea>
|
||||
{/if}
|
||||
{#if flag.unit}
|
||||
<span class="input-group-text">{flag.unit}</span>
|
||||
{/if}
|
||||
{#if flag.choices_cost > 0}
|
||||
<Button
|
||||
color="success"
|
||||
type="button"
|
||||
on:click={wantchoices}
|
||||
disabled={wcsubmitted || $settings.disablesubmitbutton}
|
||||
title="Cliquez pour échanger ce champ de texte par une liste de choix. L'opération vous coûtera {flag.choices_cost * $settings.wchoiceCurrentCoefficient} points."
|
||||
>
|
||||
{#if wcsubmitted}
|
||||
<Spinner size="sm" class="me-2" />
|
||||
{/if}
|
||||
<Icon name="list-task" />
|
||||
Liste de propositions ({flag.choices_cost * $settings.wchoiceCurrentCoefficient} {flag.choices_cost * $settings.wchoiceCurrentCoefficient===1?"point":"points"})
|
||||
</Button>
|
||||
{:else if flag.separator && !flag.nb_lines && index == values.length - 1}
|
||||
<Button
|
||||
color="success"
|
||||
type="button"
|
||||
title="Ajouter un élément."
|
||||
on:click={addItem}
|
||||
>
|
||||
<Icon name="plus" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if flag.type == 'radio'}
|
||||
{#each Object.keys(flag.choices) as l, i}
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="sol_{flag.type}{flag.id}_{index}_{i}"
|
||||
type="radio"
|
||||
value={l}
|
||||
bind:group={values[index]}
|
||||
class="form-check-input"
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="sol_{flag.type}{flag.id}_{index}_{i}"
|
||||
>
|
||||
{flag.choices[l]}
|
||||
</label>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<select
|
||||
class="form-select"
|
||||
id="sol_{flag.type}{flag.id}_{index}"
|
||||
bind:value={values[index]}
|
||||
>
|
||||
{#each Object.keys(flag.choices) as l}
|
||||
<option value={l}>{flag.choices[l]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if flag.help}
|
||||
<small class="form-text text-muted">{@html flag.help}</small>
|
||||
{/if}
|
||||
{:else}
|
||||
<Icon
|
||||
name="check"
|
||||
class="form-control-feedback text-success"
|
||||
aria-hidden="true"
|
||||
title="Flag trouvé à {flag.found}"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
55
frontend/fic/src/lib/components/FlagMCQ.svelte
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import FlagKey from './FlagKey.svelte';
|
||||
|
||||
export let exercice_id = 0;
|
||||
export let flag = { };
|
||||
export let values = { };
|
||||
export let justifications = { };
|
||||
</script>
|
||||
|
||||
{#if flag.label}
|
||||
<p class="mb-1">
|
||||
{flag.label} :
|
||||
{#if flag.found}
|
||||
<Icon name="check" class="form-control-feedback text-success" aria-hidden="true" title="QCM réussi à {flag.solved}" />
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
{#if !flag.found || flag.justify}
|
||||
{#each Object.keys(flag.choices) as cid, index}
|
||||
<div class="form-check ms-3">
|
||||
{#if typeof flag.choices[cid] != "object"}
|
||||
<input class="form-check-input" type="checkbox" id="mcq_{flag.id}_{cid}" bind:checked={values[Number(cid)]} disabled={flag.found || flag.part_solved}>
|
||||
<label class="form-check-label" for="mcq_{flag.id}_{cid}">
|
||||
{flag.choices[cid]}{#if values[Number(cid)] && flag.justify} :{/if}
|
||||
</label>
|
||||
{#if values[Number(cid)] && flag.justify}
|
||||
<FlagKey
|
||||
class="mb-3"
|
||||
{exercice_id}
|
||||
flag={{id: cid, placeholder: "Flag correspondant"}}
|
||||
no_label={true}
|
||||
bind:value={justifications[cid]}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<input class="form-check-input" type="checkbox" id="mcq_{flag.id}_{cid}" checked disabled>
|
||||
<FlagKey
|
||||
class={flag.choices[cid].justification.found?"":"mb-3"}
|
||||
{exercice_id}
|
||||
flag={flag.choices[cid].justification}
|
||||
bind:value={justifications[cid]}
|
||||
/>
|
||||
{/if}
|
||||
{#if flag.choices[cid].justification && flag.choices[cid].justification.solved}
|
||||
<Icon name="check" class="form-control-feedback text-success" aria-hidden="true" title="Flag trouvé !" />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<hr>
|
||||
54
frontend/fic/src/lib/components/FormIssue.svelte
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { issues, issues_idx } from '$lib/stores/issues.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
|
||||
export let exercice = null;
|
||||
export let issue = { };
|
||||
|
||||
$: if (exercice != null) {
|
||||
issue.id_exercice = exercice.id;
|
||||
} else {
|
||||
issue.id_exercice = undefined;
|
||||
}
|
||||
</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" disabled={$settings.disablesubmitbutton}>
|
||||
Envoyer le rapport
|
||||
</Button>
|
||||
</form>
|
||||
131
frontend/fic/src/lib/components/Header.svelte
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<script>
|
||||
import { base } from '$app/paths';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Col,
|
||||
Collapse,
|
||||
Container,
|
||||
Icon,
|
||||
Navbar,
|
||||
NavbarToggler,
|
||||
NavbarBrand,
|
||||
Nav,
|
||||
NavItem,
|
||||
NavLink,
|
||||
Progress,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { challengeInfo } from '$lib/stores/challengeinfo.js';
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { teams } from '$lib/stores/teams.js';
|
||||
import { settings, time } from '$lib/stores/settings.js';
|
||||
|
||||
import Clock from './Clock.svelte';
|
||||
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>
|
||||
|
||||
{#if !$settings.hide_header}
|
||||
<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="." style="max-width: 50%">
|
||||
{#if $challengeInfo && $challengeInfo.main_logo}
|
||||
{#each $challengeInfo.main_logo as logo, i}
|
||||
<img src={logo.replace('$FILES$', base + '/files/')} alt={'Logo principal #' + i} class={'h-100' + (i > 0?' d-none d-md-inline ms-2':'')}>
|
||||
{/each}
|
||||
{/if}
|
||||
</a>
|
||||
<HeaderPartners />
|
||||
</Container>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="sticky-top">
|
||||
<Navbar 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.start >= $settings.recvTime && $teams && Object.keys($teams).length}
|
||||
<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>
|
||||
{#if $settings.hide_header && $settings.end - $settings.start > 0}
|
||||
<Nav class="ms-auto text-light display-6" navbar>
|
||||
<Clock />
|
||||
</Nav>
|
||||
{/if}
|
||||
<Nav class="ms-auto text-light" navbar>
|
||||
{#if $my && $my.team_id}
|
||||
<NavItem>
|
||||
{Math.round($my.score*100)/100} {$my.score === 1 ? 'point' : 'points'}
|
||||
{#if $teams && $teams[$my.team_id] && $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}
|
||||
{#if $teams && $teams[$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>
|
||||
{:else}
|
||||
<Badge href="edit" color="info">
|
||||
<span class="teamname">{$my.name}</span>
|
||||
</Badge>
|
||||
{/if}
|
||||
{/if}
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
<Progress value={$time.progression * 100} color="info" style="height: 5px; border-radius: 0;" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.teamname {
|
||||
-webkit-filter: invert(100%);
|
||||
filter: invert(100%);
|
||||
}
|
||||
</style>
|
||||
53
frontend/fic/src/lib/components/HeaderClock.svelte
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<script>
|
||||
import {
|
||||
ButtonGroup,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import Clock from './Clock.svelte';
|
||||
import { challengeInfo } from '$lib/stores/challengeinfo.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
</script>
|
||||
|
||||
{#if $settings && $settings.end}
|
||||
{#if $settings.end - $settings.start > 0}
|
||||
<Clock
|
||||
class="display-2 text-end text-md-center"
|
||||
/>
|
||||
{:else}
|
||||
<div class="d-flex h-100 justify-content-center align-items-center">
|
||||
<ButtonGroup size="lg">
|
||||
|
||||
<a
|
||||
href="{$challengeInfo.main_link}"
|
||||
class="btn btn-light"
|
||||
class:disabled={$challengeInfo.main_link === ''}
|
||||
>
|
||||
<Icon name="ui-checks-grid" />
|
||||
Accueil
|
||||
</a>
|
||||
<a
|
||||
href="rank"
|
||||
class="btn btn-light"
|
||||
>
|
||||
<Icon name="sort-down" />
|
||||
Classement
|
||||
</a>
|
||||
<a
|
||||
href="{$challengeInfo.videoslink}"
|
||||
class="btn btn-light"
|
||||
class:disabled={$challengeInfo.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">
|
||||
{$challengeInfo.title}
|
||||
</h1>
|
||||
</div>
|
||||
{/if}
|
||||
34
frontend/fic/src/lib/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 '$lib/stores/issues.js';
|
||||
import { settings } from '$lib/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}
|
||||
30
frontend/fic/src/lib/components/HeaderPartners.svelte
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
import { base } from '$app/paths';
|
||||
|
||||
import {
|
||||
Carousel,
|
||||
CarouselItem,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { challengeInfo } from '$lib/stores/challengeinfo.js';
|
||||
|
||||
let activePartner = 0;
|
||||
</script>
|
||||
|
||||
{#if $challengeInfo && $challengeInfo.partners}
|
||||
<Carousel items={$challengeInfo.partners} bind:activeIndex={activePartner} ride="carousel" pause="hover" interval={25000}>
|
||||
<div class="carousel-inner h-100">
|
||||
{#each $challengeInfo.partners as partner, index}
|
||||
<CarouselItem bind:activeIndex={activePartner} itemIndex={index} class="h-100 text-end">
|
||||
{#if partner.href}
|
||||
<a href="{partner.href}" target="_blank" rel="noreferrer" class="h-100">
|
||||
<img src={partner.img.replace('$FILES$', base + '/files')} class="h-100" alt={partner.alt}>
|
||||
</a>
|
||||
{:else}
|
||||
<img src={partner.img.replace('$FILES$', base + '/files')} class="h-100" alt={partner.alt}>
|
||||
{/if}
|
||||
</CarouselItem>
|
||||
{/each}
|
||||
</div>
|
||||
</Carousel>
|
||||
{/if}
|
||||
49
frontend/fic/src/lib/components/NavTags.svelte
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
import {
|
||||
Badge,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { tags } from '$lib/stores/mythemes.js';
|
||||
|
||||
let filter = "";
|
||||
</script>
|
||||
|
||||
<Dropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>
|
||||
<Icon name="tags" />
|
||||
Tags
|
||||
</DropdownToggle>
|
||||
<DropdownMenu class="niceborder" end>
|
||||
<input
|
||||
type="text"
|
||||
class="dropdown-item"
|
||||
placeholder="Filtrer"
|
||||
bind:value={filter}
|
||||
>
|
||||
<div>
|
||||
{#each Object.keys($tags).sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); }) as itag, index}
|
||||
{#if (filter === "" && $tags[itag].count > 1) || (filter !== "" && itag.toLowerCase().indexOf(filter.toLowerCase()) >= 0)}
|
||||
<DropdownItem href="tags/{itag}">
|
||||
#{itag}
|
||||
<Badge>
|
||||
{#if $my && $my.team_id}{$tags[itag].solved}/{/if}{$tags[itag].count}
|
||||
</Badge>
|
||||
</DropdownItem>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
|
||||
<style>
|
||||
div {
|
||||
overflow-y: auto;
|
||||
max-height: calc(66vh - 100px);
|
||||
}
|
||||
</style>
|
||||
55
frontend/fic/src/lib/components/NavThemes.svelte
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<script>
|
||||
import {
|
||||
Badge,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { max_solved } from '$lib/stores/themes.js';
|
||||
import { myThemes, themes } from '$lib/stores/mythemes.js';
|
||||
</script>
|
||||
|
||||
<Dropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>
|
||||
<Icon name="tv" />
|
||||
Scenarii
|
||||
</DropdownToggle>
|
||||
<DropdownMenu class="niceborder">
|
||||
<div>
|
||||
{#each $themes as th, index}
|
||||
<DropdownItem href="{th.urlid}">
|
||||
{th.name}
|
||||
{#if $max_solved > 1 && th.solved == $max_solved}
|
||||
<Badge color="danger">
|
||||
<Icon name="heart-fill" />
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if th.exercice_coeff_max > 1}
|
||||
<Badge color="success">
|
||||
<Icon name="gift-fill" />
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if th.locked}
|
||||
<Badge color="light">
|
||||
<Icon name="lock-fill" />
|
||||
</Badge>
|
||||
{/if}
|
||||
<Badge>
|
||||
{#if $my && $my.team_id}{$myThemes[th.id].exercice_solved}/{/if}{th.exercice_count}
|
||||
</Badge>
|
||||
</DropdownItem>
|
||||
{/each}
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
|
||||
<style>
|
||||
div {
|
||||
overflow-y: auto;
|
||||
max-height: calc(80vh - 100px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
|
||||
import RegistrationRowMember from './RegistrationRowMember.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let value = {members:[{}]};
|
||||
|
||||
function validateTeamName() {
|
||||
if (typeof value.members !== 'Array') {
|
||||
value.members = [{ }];
|
||||
} else if (value.members.length <= 0) {
|
||||
value.members.push({ });
|
||||
}
|
||||
partR = true;
|
||||
}
|
||||
|
||||
function AddMember() {
|
||||
value.members.push({ });
|
||||
value = value;
|
||||
}
|
||||
|
||||
function RemoveMember(mid) {
|
||||
console.log(mid);
|
||||
}
|
||||
|
||||
function submit(event) {
|
||||
if (!partR) {
|
||||
validateTeamName();
|
||||
} else {
|
||||
// Ensure jTeam, defined by RegistrationFormJoinTeam is not defined
|
||||
delete value.jTeam;
|
||||
|
||||
dispatch('submit', event);
|
||||
}
|
||||
}
|
||||
|
||||
const max_team_members = 3;
|
||||
let jTeam = false;
|
||||
export let partR = false;
|
||||
let message = "";
|
||||
let messageClass = "danger";
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<Row>
|
||||
<label for="teamName" class="col col-form-label">Nom d'équipe</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input type="text" class="form-control" id="teamName" bind:value={value.teamName} placeholder="" autofocus required>
|
||||
<Button color="info" type="button" on:click={validateTeamName} disabled={jTeam}>Valider</Button>
|
||||
<div class="invalid-feedback">
|
||||
Veuillez indiquer un nom d'équipe valide.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
{#if partR}
|
||||
<h4 class="mt-4">
|
||||
{#if !$settings.canJoinTeam}
|
||||
Membres d'équipe
|
||||
<Button
|
||||
color="success"
|
||||
disabled={value.members.length >= max_team_members}
|
||||
size="sm"
|
||||
type="button"
|
||||
on:click={AddMember}
|
||||
>
|
||||
<Icon name="person-plus-fill" />
|
||||
Ajouter un membre
|
||||
</Button>
|
||||
{:else}
|
||||
Chef d'équipe
|
||||
{/if}
|
||||
</h4>
|
||||
{#if message}
|
||||
<p class={messageClass}>{message}</p>
|
||||
{/if}
|
||||
|
||||
{#each value.members as member, mid}
|
||||
<RegistrationRowMember
|
||||
canDelete={!$settings.canJoinTeam && value.members.length > 1}
|
||||
bind:member={member}
|
||||
on:delete={RemoveMember}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<Row>
|
||||
<Col sm={{ size: 9, offset: 3 }} md={{ size: 8, offset: 4 }}>
|
||||
<Button color="info" class="mt-4" type="submit" disabled={jTeam}>
|
||||
C'est parti !
|
||||
<Icon name="chevron-right" />
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
</form>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
import { teams } from '$lib/stores/teams.js';
|
||||
|
||||
import RegistrationRowMember from './RegistrationRowMember.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let value = { };
|
||||
|
||||
function JvalidateTeam() {
|
||||
if (!value.members || value.members.length == 0) {
|
||||
value.members = [{ }];
|
||||
}
|
||||
value = value;
|
||||
|
||||
partJ = true;
|
||||
}
|
||||
|
||||
function submit(event) {
|
||||
if (!partJ) {
|
||||
JvalidateTeam();
|
||||
} else {
|
||||
dispatch('submit', event);
|
||||
}
|
||||
}
|
||||
|
||||
export let partJ = false;
|
||||
let message = "";
|
||||
let messageClass = "danger";
|
||||
</script>
|
||||
|
||||
{#if Object.keys($teams).length}
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<Row>
|
||||
<label for="jTeam" class="col col-form-label">Nom d'équipe</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<select
|
||||
class="form-select"
|
||||
id="jTeam"
|
||||
bind:value={value.jTeam}
|
||||
required
|
||||
disabled={partJ}
|
||||
>
|
||||
{#each Object.keys($teams) as tid, index}
|
||||
<option value={$teams[tid].id}>
|
||||
{$teams[tid].name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Button color="info" type="button" on:click={JvalidateTeam} disabled={partJ}>Valider</Button>
|
||||
<div class="invalid-feedback">
|
||||
Veuillez indiquer une équipe valide.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
{#if partJ}
|
||||
<h4 class="mt-4">
|
||||
Vos informations
|
||||
</h4>
|
||||
{#if message}
|
||||
<p class={messageClass}>{message}</p>
|
||||
{/if}
|
||||
|
||||
<RegistrationRowMember
|
||||
bind:member={value.members[0]}
|
||||
/>
|
||||
|
||||
<Row>
|
||||
<Col sm={{ size: 9, offset: 3 }} md={{ size: 8, offset: 4 }}>
|
||||
<Button color="info" class="mt-4" type="submit" disabled={!value.jTeam}>
|
||||
C'est parti !
|
||||
<Icon name="chevron-right" />
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
</form>
|
||||
{:else}
|
||||
<p class="card-text">
|
||||
Aucune équipe enregistrée pour l'instant.
|
||||
</p>
|
||||
{/if}
|
||||
37
frontend/fic/src/lib/components/RegistrationRowMember.svelte
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let member = {};
|
||||
export let canDelete = false;
|
||||
</script>
|
||||
|
||||
<Row class="form-group my-3">
|
||||
<div class="col-sm">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input type="text" class="form-control" bind:value={member.lastname} placeholder="Nom" autofocus>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<input type="text" class="form-control" bind:value={member.firstname} placeholder="Prénom">
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<input type="text" class="form-control" bind:value={member.nickname} placeholder="Pseudo">
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<input type="text" class="form-control" bind:value={member.company} placeholder="Entreprise">
|
||||
</div>
|
||||
{#if canDelete}
|
||||
<div class="col-sm-auto">
|
||||
<Button color="danger" type="button" on:click={dispatch('delete', member)}>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</Row>
|
||||
26
frontend/fic/src/lib/components/ResolutionModal.svelte
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<script>
|
||||
import {
|
||||
Icon,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader
|
||||
} from 'sveltestrap';
|
||||
const toggle = () => (open = !open);
|
||||
|
||||
export let exercice = null;
|
||||
export let open = false;
|
||||
export let resolution;
|
||||
</script>
|
||||
|
||||
<Modal isOpen={open} {toggle} size="xl">
|
||||
<ModalHeader {toggle} class="bg-success text-light">
|
||||
<Icon name="laptop-fill" />
|
||||
Solution du défi
|
||||
{#if exercice}
|
||||
: {exercice.title}
|
||||
{/if}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{@html resolution}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
84
frontend/fic/src/lib/components/ScoreGrid.svelte
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
import {
|
||||
Badge,
|
||||
CardBody,
|
||||
Column,
|
||||
Icon,
|
||||
Table,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import DateFormat from '$lib/components/DateFormat.svelte';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { themes, exercices_idx } from '$lib/stores/themes.js';
|
||||
|
||||
let req = null;
|
||||
function refresh_scores() {
|
||||
req = fetch('scores.json', {headers: {'Accept': 'application/json'}}).then((data) => data.json());
|
||||
}
|
||||
refresh_scores();
|
||||
|
||||
export { className as class };
|
||||
let className = '';
|
||||
</script>
|
||||
|
||||
{#await req}
|
||||
<CardBody>
|
||||
Veuillez patienter &hellips;
|
||||
</CardBody>
|
||||
{:then scores}
|
||||
{#if scores}
|
||||
<Table class="{className} mb-0" hover striped rows={scores} let:row>
|
||||
<Column header="Heure">
|
||||
<DateFormat date={new Date(row.time)} />
|
||||
</Column>
|
||||
<Column header="Raison">
|
||||
{#if row.reason == "Validation"}
|
||||
<Badge color="success"><Icon name="check" /></Badge>
|
||||
Étape validée
|
||||
{:else if row.reason == "First blood"}
|
||||
<Badge color="light"><Icon name="trophy" /></Badge>
|
||||
Bonus premier sang
|
||||
{:else if row.reason == "Bonus flag"}
|
||||
<Badge color="danger"><Icon name="flag-fill" /></Badge>
|
||||
Flag bonus complété
|
||||
{:else if row.reason == "Tries"}
|
||||
<Badge color="warning"><Icon name="backspace" /></Badge>
|
||||
Malus nombre de tentatives
|
||||
{:else if row.reason == "Hint"}
|
||||
<Badge color="info"><Icon name="lightbulb" /></Badge>
|
||||
Indice dévoilé
|
||||
{:else if row.reason == "Display choices"}
|
||||
<Badge color="secondary"><Icon name="info-square" /></Badge>
|
||||
Échange champ de texte contre liste de choix
|
||||
{:else}
|
||||
<Badge color="primary"><Icon name="question" /></Badge>
|
||||
{row.reason}
|
||||
{/if}
|
||||
{#if row.id_exercice && $exercices_idx[row.id_exercice]}
|
||||
sur <a href="/{$themes[$exercices_idx[row.id_exercice].id_theme].urlid}/{$exercices_idx[row.id_exercice].urlid}">
|
||||
{$exercices_idx[row.id_exercice].title}
|
||||
</a>
|
||||
{/if}
|
||||
</Column>
|
||||
<Column header="Détail">
|
||||
<span title="Valeur initiale (cette valeur est fixe)">{Math.trunc(10*row.points)/10}</span> × <span title="Coefficient multiplicateur (il varie selon les événements en cours sur la plateforme)">{row.coeff}</span>
|
||||
</Column>
|
||||
<Column header="Points">
|
||||
{Math.trunc(10*row.points * row.coeff)/10}
|
||||
</Column>
|
||||
</Table>
|
||||
{:else}
|
||||
Vous n'avez fait aucune action vous faisant gagner ou perdre des points.
|
||||
{/if}
|
||||
<button class="btn btn-primary" on:click={refresh_scores}>
|
||||
<Icon name="arrow-clockwise" />
|
||||
</button>
|
||||
{:catch error}
|
||||
<CardBody>
|
||||
Une erreur s'est produite: {JSON.stringify(error)}
|
||||
</CardBody>
|
||||
<button class="btn btn-primary" on:click={refresh_scores}>
|
||||
<Icon name="arrow-clockwise" />
|
||||
</button>
|
||||
{/await}
|
||||
109
frontend/fic/src/lib/components/TeamChangeName.svelte
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
|
||||
let newTeamName = "";
|
||||
|
||||
function gotoHomeOnDiff(i) {
|
||||
my.refresh((my) => {
|
||||
if (my && my.name == newTeamName) {
|
||||
newTeamName = "";
|
||||
messageClass = "info";
|
||||
sberr = "Votre nom d'équipe a été changé avec succès.";
|
||||
message = "";
|
||||
} else if (i > 0) {
|
||||
setTimeout(gotoHomeOnDiff, 850, i-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function submitChangeName(event) {
|
||||
message = "";
|
||||
sberr = "";
|
||||
|
||||
if (newTeamName.length < 1) {
|
||||
messageClass = "danger";
|
||||
sberr = "Nom d'équipe invalide: pas d'entrée.";
|
||||
return false;
|
||||
}
|
||||
else if (newTeamName.length > 32) {
|
||||
messageClass = "danger";
|
||||
sberr = "Nom d'équipe invalide: pas plus de 32 caractères.";
|
||||
return false;
|
||||
}
|
||||
else if (!newTeamName.match(/^[A-Za-z0-9 àéèêëîïôùûü_-]+$/)) {
|
||||
messageClass = "danger";
|
||||
sberr = "Nom d'équipe invalide: seuls les caractères alpha-numériques sont autorisés.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await fetch('chname', {
|
||||
method: "POST",
|
||||
headers: {'Accept': 'application/json'},
|
||||
body: JSON.stringify({newName: newTeamName}),
|
||||
});
|
||||
|
||||
if (response.status < 300) {
|
||||
const data = await response.json();
|
||||
messageClass = 'success';
|
||||
message = data.errmsg;
|
||||
gotoHomeOnDiff(15);
|
||||
} else {
|
||||
messageClass = 'danger';
|
||||
|
||||
let data = "";
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch(e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
if (data && data.errmsg)
|
||||
message = data.errmsg;
|
||||
if (response.statys != 402)
|
||||
sberr = "Une erreur est survenue lors de la demande de changement de nom. Veuillez réessayer dans quelques instants.";
|
||||
}
|
||||
}
|
||||
|
||||
let sberr = "";
|
||||
let message = "";
|
||||
let messageClass = "danger";
|
||||
</script>
|
||||
|
||||
<Card class="mb-3 border-info">
|
||||
<CardHeader class="bg-info text-light">
|
||||
<Icon name="input-cursor-text" />
|
||||
Changer de nom d'équipe
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{#if sberr || message}
|
||||
<p class="card-text text-{messageClass}">
|
||||
{#if !sberr}
|
||||
<strong>Votre demande a bien été envoyée !</strong>
|
||||
{:else}
|
||||
<strong>{sberr}</strong>
|
||||
{/if}
|
||||
{message}
|
||||
</p>
|
||||
{/if}
|
||||
<form on:submit|preventDefault={submitChangeName}>
|
||||
<div class="form-group row">
|
||||
<label for="newName" class="col col-form-label">Nouveau nom</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="newName" bind:value={newTeamName} placeholder="{$my.name}">
|
||||
<Button type="submit" class="btn btn-info" disabled={$settings.disablesubmitbutton}>Valider</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
27
frontend/fic/src/lib/components/TeamChangePassword.svelte
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
</script>
|
||||
|
||||
<Card class="mb-3 border-warning">
|
||||
<CardHeader class="bg-warning text-light">
|
||||
<Icon name="incognito" />
|
||||
Changer de mot de passe
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<p>
|
||||
Si vous souhaitez changer de mot de passe :
|
||||
</p>
|
||||
<Button
|
||||
href="issues?fill-issue"
|
||||
color="warning"
|
||||
>
|
||||
Contactez-nous !
|
||||
</Button>
|
||||
</CardBody>
|
||||
</Card>
|
||||
37
frontend/fic/src/lib/components/TeamMembers.svelte
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Icon,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from 'sveltestrap';
|
||||
|
||||
export let members = [];
|
||||
</script>
|
||||
|
||||
<Card class="mb-3">
|
||||
<CardHeader>
|
||||
<Icon name="people-fill" />
|
||||
Membres de l'équipe
|
||||
</CardHeader>
|
||||
{#if members && members.length}
|
||||
<ListGroup>
|
||||
{#each members as member (member.id)}
|
||||
<ListGroupItem class="list-group-item-action">
|
||||
{member.firstname}
|
||||
{#if member.nickname}
|
||||
<span style="font-style: italic">{member.nickname}</span>
|
||||
{/if}
|
||||
<span style="font-variant: small-caps;">{member.lastname}</span>
|
||||
{#if member.company}– {member.company}{/if}
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
{:else}
|
||||
<CardBody>
|
||||
Passez voir l'équipe d'organisation pour compléter ces informations.
|
||||
</CardBody>
|
||||
{/if}
|
||||
</Card>
|
||||
57
frontend/fic/src/lib/components/ThemeNav.svelte
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<script>
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
Card,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
|
||||
export let theme = {};
|
||||
export let exercice = {};
|
||||
</script>
|
||||
|
||||
<Breadcrumb listClassName="mb-0 px-3 py-2">
|
||||
{#each theme.exercices as ex, index}
|
||||
<BreadcrumbItem active={ex.id == exercice.id}>
|
||||
{#if ex.id == exercice.id}
|
||||
<strong class="text-info">
|
||||
{ex.title}
|
||||
{#if ex.curcoeff > 1.0}
|
||||
<Icon name="gift" aria-hidden="true" />
|
||||
{/if}
|
||||
{#if $my && $my.team_id && $my.exercices[ex.id] && $my.exercices[ex.id].solved}
|
||||
<Icon name="check" class="text-success" aria-hidden="true" />
|
||||
{/if}
|
||||
</strong>
|
||||
{:else if $my && $my.exercices[ex.id]}
|
||||
<a href="{theme.urlid}/{ex.urlid}" class:text-success={$my.exercices[ex.id].solved}>
|
||||
{ex.title}
|
||||
{#if ex.curcoeff > 1.0}
|
||||
<Icon name="gift" aria-hidden="true" />
|
||||
{/if}
|
||||
{#if $my.team_id && $my.exercices[ex.id].solved}
|
||||
<Icon name="check" class="text-success" aria-hidden="true" />
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<span class="text-muted">
|
||||
{ex.title}
|
||||
{#if ex.curcoeff > 1.0}
|
||||
<Icon name="gift" aria-hidden="true" />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</BreadcrumbItem>
|
||||
{/each}
|
||||
</Breadcrumb>
|
||||
|
||||
<style>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
a[href]:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
27
frontend/fic/src/lib/stores/challengeinfo.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { readable, writable } from 'svelte/store';
|
||||
|
||||
function createChallengeStore() {
|
||||
const { subscribe, set, update } = writable({});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
refresh: async (cb) => {
|
||||
challengeInfo.update(await fetch('challenge.json', {headers: {'Accept': 'application/json'}}), cb);
|
||||
},
|
||||
|
||||
update: (res_challenge, cb) => {
|
||||
if (res_challenge.status === 200) {
|
||||
res_challenge.json().then((challenge) => {
|
||||
update((s) => (Object.assign({}, challenge)));
|
||||
|
||||
if (cb) {
|
||||
cb(challenge);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const challengeInfo = createChallengeStore();
|
||||
1
frontend/fic/src/lib/stores/common.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export let stop_refresh = { state: false };
|
||||
19
frontend/fic/src/lib/stores/exercices.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
import { exercices_idx_urlid } from './themes';
|
||||
|
||||
export const set_current_exercice = writable(null)
|
||||
|
||||
export const current_exercice = derived(
|
||||
[set_current_exercice, exercices_idx_urlid],
|
||||
([$set_current_exercice, $exercices_idx_urlid]) => {
|
||||
if ($exercices_idx_urlid === null || Object.keys($exercices_idx_urlid).length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($exercices_idx_urlid[$set_current_exercice])
|
||||
return $exercices_idx_urlid[$set_current_exercice];
|
||||
|
||||
return undefined;
|
||||
}
|
||||
)
|
||||
98
frontend/fic/src/lib/stores/issues.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
import { stop_refresh } from './common';
|
||||
|
||||
let refresh_interval_issues = null;
|
||||
|
||||
function createIssuesStore() {
|
||||
const { subscribe, set, update } = writable([]);
|
||||
|
||||
function updateFunc (res_issues, cb=null) {
|
||||
if (res_issues.status === 200) {
|
||||
res_issues.json().then((issues) => {
|
||||
update((i) => issues);
|
||||
|
||||
if (cb) {
|
||||
cb(issues);
|
||||
}
|
||||
});
|
||||
} else if (res_issues.status === 404) {
|
||||
update((i) => ([]));
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshFunc(cb=null, interval=null) {
|
||||
if (refresh_interval_issues)
|
||||
clearInterval(refresh_interval_issues);
|
||||
if (interval === null) {
|
||||
interval = Math.floor(Math.random() * 24000) + 32000;
|
||||
}
|
||||
if (stop_refresh.state) {
|
||||
return;
|
||||
}
|
||||
refresh_interval_issues = setInterval(refreshFunc, interval);
|
||||
|
||||
updateFunc(await fetch('issues.json', {headers: {'Accept': 'application/json'}}), cb);
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
refresh: refreshFunc,
|
||||
|
||||
update: updateFunc,
|
||||
};
|
||||
}
|
||||
|
||||
export const issuesStore = createIssuesStore();
|
||||
|
||||
export const issues = derived(
|
||||
issuesStore,
|
||||
($issuesStore) => {
|
||||
$issuesStore.forEach(function(issue, k) {
|
||||
$issuesStore[k].texts.reverse();
|
||||
})
|
||||
return $issuesStore;
|
||||
},
|
||||
);
|
||||
|
||||
export const issues_idx = derived(
|
||||
issues,
|
||||
($issues) => {
|
||||
const issues_idx = {};
|
||||
|
||||
$issues.forEach(function(issue, k) {
|
||||
issues_idx[issue.id] = issue;
|
||||
})
|
||||
|
||||
return issues_idx;
|
||||
},
|
||||
);
|
||||
|
||||
export const issues_nb_responses = derived(
|
||||
issuesStore,
|
||||
($issuesStore) => {
|
||||
let issues_nb_responses = 0;
|
||||
|
||||
$issuesStore.forEach(function(issue, k) {
|
||||
issues_nb_responses += issue.texts.length;
|
||||
})
|
||||
|
||||
return issues_nb_responses;
|
||||
},
|
||||
);
|
||||
|
||||
export const issues_need_info = derived(
|
||||
issuesStore,
|
||||
($issuesStore) => {
|
||||
let issues_need_info = 0;
|
||||
|
||||
$issuesStore.forEach(function(issue, k) {
|
||||
if (issue.state == 'need-info') issues_need_info++;
|
||||
})
|
||||
|
||||
return issues_need_info;
|
||||
},
|
||||
);
|
||||
|
||||
export const issues_known_responses = writable(0);
|
||||
66
frontend/fic/src/lib/stores/my.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
import { stop_refresh } from './common';
|
||||
|
||||
let refresh_interval_my = null;
|
||||
|
||||
function createMyStore() {
|
||||
const { subscribe, set, update } = writable(null);
|
||||
|
||||
function updateFunc(res_my, cb=null) {
|
||||
if (res_my.status === 200) {
|
||||
res_my.json().then((my) => {
|
||||
for (let k in my.exercices) {
|
||||
my.exercices[k].id = k;
|
||||
|
||||
if (my.exercices[k].flags) {
|
||||
let nb = 0;
|
||||
for (let j in my.exercices[k].flags) {
|
||||
if (!my.exercices[k].flags[j].found)
|
||||
nb += 1;
|
||||
}
|
||||
my.exercices[k].non_found_flags = nb;
|
||||
}
|
||||
|
||||
if (my.team_id === 0 && my.exercices[k].hints) {
|
||||
for (let j in my.exercices[k].hints) {
|
||||
my.exercices[k].hints[j].hidden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update((m) => (Object.assign(m?m:{}, my)));
|
||||
|
||||
if (cb) {
|
||||
cb(my);
|
||||
}
|
||||
});
|
||||
} else if (res_my.status === 404) {
|
||||
update((m) => (null));
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshFunc(cb=null, interval=null) {
|
||||
if (refresh_interval_my)
|
||||
clearInterval(refresh_interval_my);
|
||||
if (interval === null) {
|
||||
interval = Math.floor(Math.random() * 24000) + 24000;
|
||||
}
|
||||
if (stop_refresh.state) {
|
||||
return;
|
||||
}
|
||||
refresh_interval_my = setInterval(refreshFunc, interval);
|
||||
|
||||
updateFunc(await fetch('my.json', {headers: {'Accept': 'application/json'}}), cb);
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
refresh: refreshFunc,
|
||||
|
||||
update: updateFunc,
|
||||
};
|
||||
}
|
||||
|
||||
export const my = createMyStore();
|
||||
0
frontend/fic/src/lib/stores/myresponses.js
Normal file
74
frontend/fic/src/lib/stores/mythemes.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { derived } from 'svelte/store';
|
||||
|
||||
import seedrandom from 'seedrandom';
|
||||
|
||||
import { my } from './my.js';
|
||||
import { themes as themesStore } from './themes.js';
|
||||
|
||||
export const myThemes = derived([my, themesStore], ([$my, $themesStore]) => {
|
||||
const mythemes = {};
|
||||
|
||||
for (let key in $themesStore) {
|
||||
mythemes[key] = {exercice_solved: 0};
|
||||
|
||||
if ($my && $my.exercices) {
|
||||
for (const exercice of $themesStore[key].exercices) {
|
||||
if ($my.exercices[exercice.id] && $my.exercices[exercice.id].solved_rank) {
|
||||
mythemes[key].exercice_solved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mythemes;
|
||||
});
|
||||
|
||||
export const themes = derived(
|
||||
[my, themesStore],
|
||||
([$my, $themesStore]) => {
|
||||
const arr = [];
|
||||
for (let th in $themesStore) {
|
||||
$themesStore[th].id = th
|
||||
arr.push($themesStore[th]);
|
||||
}
|
||||
const size = arr.length;
|
||||
const rng = new seedrandom($my && $my.team_id ? $my.team_id : 0);
|
||||
const respD = [];
|
||||
const respE = [];
|
||||
const keys = [];
|
||||
|
||||
for(let i=0;i<size;i++) keys.push(i);
|
||||
for(let i=0;i<size;i++) {
|
||||
const r = Math.floor(rng() * keys.length);
|
||||
const g = keys[r];
|
||||
keys.splice(r,1);
|
||||
if (arr[g].locked || arr[g].exercices.length == 0 || ($my && (!$my.exercices[arr[g].exercices[0].id] || $my.exercices[arr[g].exercices[0].id].disabled))) {
|
||||
respD.push(arr[g]);
|
||||
} else {
|
||||
respE.push(arr[g]);
|
||||
}
|
||||
}
|
||||
|
||||
return respE.concat(respD);
|
||||
},
|
||||
);
|
||||
|
||||
export const tags = derived([my, themesStore], ([$my, $themesStore]) => {
|
||||
const tags = {};
|
||||
|
||||
for (const key in $themesStore) {
|
||||
for (const exercice of $themesStore[key].exercices) {
|
||||
exercice.tags.forEach((tag) => {
|
||||
if (!tags[tag])
|
||||
tags[tag] = {count: 1, solved: 0};
|
||||
else
|
||||
tags[tag].count += 1;
|
||||
|
||||
if ($my && $my.exercices && $my.exercices[exercice.id] && $my.exercices[exercice.id].solved_rank)
|
||||
tags[tag].solved += 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
});
|
||||
153
frontend/fic/src/lib/stores/settings.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { readable, writable } from 'svelte/store';
|
||||
|
||||
import { stop_refresh } from './common';
|
||||
import { my } from './my';
|
||||
|
||||
let refresh_interval_settings = null;
|
||||
|
||||
function createSettingsStore() {
|
||||
const { subscribe, set, update } = writable({});
|
||||
|
||||
function updateFunc(res_settings, cb) {
|
||||
const recvTime = (new Date()).getTime();
|
||||
|
||||
if (res_settings.status === 200) {
|
||||
res_settings.json().then((settings) => {
|
||||
if (settings.start)
|
||||
settings.start = new Date(settings.start);
|
||||
if (settings.end)
|
||||
settings.end = new Date(settings.end);
|
||||
if (settings.generation)
|
||||
settings.generation = new Date(settings.generation);
|
||||
if (settings.activateTime)
|
||||
settings.activateTime = new Date(settings.activateTime);
|
||||
if (!settings.disablesubmitbutton)
|
||||
settings.disablesubmitbutton = null;
|
||||
|
||||
settings.recvTime = recvTime;
|
||||
const x_fic_time = res_settings.headers.get("x-fic-time");
|
||||
if (x_fic_time) {
|
||||
settings.currentTime = Math.floor(x_fic_time * 1000);
|
||||
} else {
|
||||
settings.currentTime = settings.recvTime;
|
||||
}
|
||||
|
||||
update((s) => (Object.assign({}, settings)));
|
||||
|
||||
if (cb) {
|
||||
cb(settings);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshFunc(cb=null, interval=null) {
|
||||
if (refresh_interval_settings)
|
||||
clearInterval(refresh_interval_settings);
|
||||
if (interval === null) {
|
||||
interval = Math.floor(Math.random() * 24000) + 32000;
|
||||
}
|
||||
if (stop_refresh.state) {
|
||||
return;
|
||||
}
|
||||
refresh_interval_settings = setInterval(refreshFunc, interval);
|
||||
|
||||
if (!cb) {
|
||||
// Before we start, update settings more frequently.
|
||||
cb = function(stgs) {
|
||||
const srv_cur = new Date(Date.now() + (stgs.currentTime - stgs.recvTime));
|
||||
|
||||
if (settings.start > srv_cur) {
|
||||
const startIn = settings.start - srv_cur;
|
||||
if (startIn > 15000) {
|
||||
setTimeout(refreshFunc, Math.floor(Math.random() * 10000) + 2400)
|
||||
} else if (startIn > 1500) {
|
||||
setTimeout(refreshFunc, startIn - 1000 - Math.floor(Math.random() * 500))
|
||||
} else {
|
||||
// On scheduled start time, refresh my.json file
|
||||
setTimeout(my.refresh, startIn + Math.floor(Math.random() * 200))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
updateFunc(await fetch('settings.json', {headers: {'Accept': 'application/json'}}), cb);
|
||||
};
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
refresh: refreshFunc,
|
||||
|
||||
update: updateFunc,
|
||||
}
|
||||
}
|
||||
|
||||
export const settings = createSettingsStore();
|
||||
|
||||
function updateTime(settings) {
|
||||
const time = {};
|
||||
|
||||
const srv_cur = new Date(Date.now() + (settings.currentTime - settings.recvTime));
|
||||
|
||||
let remain = 0;
|
||||
if (settings.start === undefined || settings.start == 0) {
|
||||
return time;
|
||||
} else if (settings.start > srv_cur) {
|
||||
time.startIn = Math.floor((settings.start - srv_cur) / 1000);
|
||||
remain = settings.end - settings.start;
|
||||
} else if (settings.end > srv_cur) {
|
||||
time.startIn = 0;
|
||||
remain = settings.end - srv_cur;
|
||||
}
|
||||
|
||||
time.progression = 1 - remain / (settings.end - settings.start);
|
||||
|
||||
remain = remain / 1000;
|
||||
|
||||
if (remain < 0) {
|
||||
remain = 0;
|
||||
time.end = true;
|
||||
time.expired = true;
|
||||
} else if (remain < 60) {
|
||||
time.end = false;
|
||||
time.expired = true;
|
||||
} else {
|
||||
time.end = false;
|
||||
time.expired = false;
|
||||
}
|
||||
|
||||
time.remaining = remain;
|
||||
time.hours = Math.floor(remain / 3600);
|
||||
time.minutes = Math.floor((remain % 3600) / 60);
|
||||
time.seconds = Math.floor(remain % 60);
|
||||
|
||||
if (time.hours <= 9) {
|
||||
time.hours = "0" + time.hours;
|
||||
}
|
||||
if (time.minutes <= 9) {
|
||||
time.minutes = "0" + time.minutes;
|
||||
}
|
||||
if (time.seconds <= 9) {
|
||||
time.seconds = "0" + time.seconds;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
export const time = readable({}, function start(set) {
|
||||
let _settings = {};
|
||||
|
||||
const unsubscribe = settings.subscribe((settings) => {
|
||||
_settings = settings;
|
||||
});
|
||||
|
||||
const interval = setInterval(() => {
|
||||
set(updateTime(_settings));
|
||||
}, 1000);
|
||||
|
||||
return function stop() {
|
||||
clearInterval(interval);
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
78
frontend/fic/src/lib/stores/teams.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
import { stop_refresh } from './common';
|
||||
|
||||
let refresh_interval_teams = null;
|
||||
|
||||
function createTeamsStore() {
|
||||
const { subscribe, set, update } = writable({});
|
||||
|
||||
function updateFunc(res_teams, cb=null) {
|
||||
if (res_teams.status === 200) {
|
||||
res_teams.json().then((teams) => {
|
||||
update((t) => teams);
|
||||
|
||||
if (cb) {
|
||||
cb(teams);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshFunc(cb=null, interval=null) {
|
||||
if (refresh_interval_teams)
|
||||
clearInterval(refresh_interval_teams);
|
||||
if (interval === null) {
|
||||
interval = Math.floor(Math.random() * 24000) + 32000;
|
||||
}
|
||||
if (stop_refresh.state) {
|
||||
return;
|
||||
}
|
||||
refresh_interval_teams = setInterval(refreshFunc, interval);
|
||||
|
||||
updateFunc(await fetch('teams.json', {headers: {'Accept': 'application/json'}}), cb);
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
refresh: refreshFunc,
|
||||
|
||||
update: updateFunc,
|
||||
};
|
||||
}
|
||||
|
||||
export const teamsStore = createTeamsStore();
|
||||
|
||||
export const teams = derived(
|
||||
teamsStore,
|
||||
($teamsStore) => {
|
||||
const teams = {};
|
||||
|
||||
for (const tid in $teamsStore) {
|
||||
teams[tid] = $teamsStore[tid];
|
||||
teams[tid].id = Number(tid);
|
||||
}
|
||||
|
||||
return teams;
|
||||
}
|
||||
);
|
||||
|
||||
export const teams_count = derived(
|
||||
teamsStore,
|
||||
($teamsStore) => Object.keys(teams).length
|
||||
);
|
||||
|
||||
export const rank = derived(
|
||||
teams,
|
||||
($teams) => {
|
||||
const rank = [];
|
||||
|
||||
for (const tid in $teams) {
|
||||
rank.push($teams[tid]);
|
||||
}
|
||||
rank.sort((a, b) => (a.rank > b.rank ? 1 : (a.rank == b.rank ? 0 : -1)));
|
||||
|
||||
return rank;
|
||||
}
|
||||
);
|
||||
157
frontend/fic/src/lib/stores/themes.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
import { stop_refresh } from './common';
|
||||
|
||||
let refresh_interval_themes = null;
|
||||
|
||||
function createThemesStore() {
|
||||
const { subscribe, set, update } = writable({});
|
||||
|
||||
async function updateFunc (res_themes, cb=null) {
|
||||
if (res_themes.status === 200) {
|
||||
const themes = await res_themes.json();
|
||||
|
||||
update((t) => themes);
|
||||
|
||||
if (cb) {
|
||||
cb(themes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshFunc(cb=null, interval=null) {
|
||||
if (refresh_interval_themes)
|
||||
clearInterval(refresh_interval_themes);
|
||||
if (interval === null) {
|
||||
interval = Math.floor(Math.random() * 24000) + 32000;
|
||||
}
|
||||
if (stop_refresh.state) {
|
||||
return;
|
||||
}
|
||||
refresh_interval_themes = setInterval(refreshFunc, interval);
|
||||
|
||||
await updateFunc(await fetch('themes.json', {headers: {'Accept': 'application/json'}}), cb);
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
refresh: refreshFunc,
|
||||
|
||||
update: updateFunc,
|
||||
};
|
||||
}
|
||||
|
||||
export const themesStore = createThemesStore();
|
||||
|
||||
export const themes = derived(
|
||||
themesStore,
|
||||
($themesStore) => {
|
||||
const themes = {};
|
||||
|
||||
for (const key in $themesStore) {
|
||||
const theme = $themesStore[key];
|
||||
|
||||
themes[key] = theme
|
||||
themes[key].exercice_count = theme.exercices.length;
|
||||
themes[key].exercice_coeff_max = 0;
|
||||
themes[key].max_gain = 0;
|
||||
|
||||
for (const k in theme.exercices) {
|
||||
const exercice = theme.exercices[k];
|
||||
|
||||
themes[key].max_gain += exercice.gain;
|
||||
if (themes[key].exercice_coeff_max < exercice.curcoeff) {
|
||||
themes[key].exercice_coeff_max = exercice.curcoeff;
|
||||
}
|
||||
|
||||
if (k > 0)
|
||||
themes[key].exercices[k-1].next = k;
|
||||
}
|
||||
}
|
||||
|
||||
return themes;
|
||||
},
|
||||
);
|
||||
|
||||
export const themes_idx = derived(
|
||||
themes,
|
||||
($themes) => {
|
||||
const ret = {};
|
||||
|
||||
for (const key in $themes) {
|
||||
const theme = $themes[key];
|
||||
|
||||
ret[theme.urlid] = theme;
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
export const exercices_idx = derived(
|
||||
themesStore,
|
||||
($themesStore) => {
|
||||
const ret = {};
|
||||
|
||||
for (const key in $themesStore) {
|
||||
const theme = $themesStore[key];
|
||||
for (let exercice of theme.exercices) {
|
||||
ret[exercice.id] = exercice;
|
||||
ret[exercice.id].id_theme = key;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
);
|
||||
|
||||
export const exercices_idx_urlid = derived(
|
||||
themesStore,
|
||||
($themesStore) => {
|
||||
const ret = {};
|
||||
|
||||
for (const key in $themesStore) {
|
||||
const theme = $themesStore[key];
|
||||
for (let exercice of theme.exercices) {
|
||||
ret[exercice.urlid] = exercice;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
export const max_solved = derived(
|
||||
themesStore,
|
||||
($themesStore) => {
|
||||
let ret = 0;
|
||||
|
||||
for (const key in $themesStore) {
|
||||
const theme = $themesStore[key];
|
||||
if (theme.solved > ret) {
|
||||
ret = theme.solved;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
);
|
||||
|
||||
export const set_current_theme = writable(null)
|
||||
|
||||
export const current_theme = derived(
|
||||
[set_current_theme, themes_idx],
|
||||
([$set_current_theme, $themes_idx]) => {
|
||||
if ($themes_idx === null || Object.keys($themes_idx).length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($themes_idx[$set_current_theme])
|
||||
return $themes_idx[$set_current_theme];
|
||||
|
||||
return undefined;
|
||||
}
|
||||
)
|
||||
5
frontend/fic/src/routes/+error.svelte
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<h1>{$page.status} : {$page.error.message}</h1>
|
||||
22
frontend/fic/src/routes/+layout.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { challengeInfo } from '$lib/stores/challengeinfo.js';
|
||||
import { stop_refresh } from '$lib/stores/common';
|
||||
import { issuesStore } from '$lib/stores/issues.js';
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { teamsStore } from '$lib/stores/teams.js';
|
||||
import { themesStore } from '$lib/stores/themes.js';
|
||||
import { settings, time } from '$lib/stores/settings.js';
|
||||
|
||||
export const ssr = false;
|
||||
|
||||
export async function load() {
|
||||
await challengeInfo.refresh();
|
||||
await settings.refresh();
|
||||
await themesStore.refresh();
|
||||
teamsStore.refresh();
|
||||
my.refresh((my) => {
|
||||
if (my && my.team_id === 0) {
|
||||
stop_refresh.state = true;
|
||||
}
|
||||
});
|
||||
issuesStore.refresh();
|
||||
}
|
||||
52
frontend/fic/src/routes/+layout.svelte
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import '../fic.scss'
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
import { base } from '$app/paths';
|
||||
import {
|
||||
Container,
|
||||
//Styles,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
|
||||
import { challengeInfo } from '$lib/stores/challengeinfo';
|
||||
import { settings } from '$lib/stores/settings';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if $challengeInfo}
|
||||
<title>{$challengeInfo.title}</title>
|
||||
<meta name="author" content="{$challengeInfo.authors}">
|
||||
{#if $challengeInfo.main_logo && $challengeInfo.main_logo.length}
|
||||
<link rel="icon" href="{$challengeInfo.main_logo[0].replace('$FILES$', '/files/')}">
|
||||
{/if}
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<!--Styles /-->
|
||||
|
||||
{#if $settings.globaltopmessage}
|
||||
<div class={'position-fixed w-100 text-center fw-bolder p-0 alert alert-' + ($settings.globaltopmessagevariant?$settings.globaltopmessagevariant:'primary')} style="z-index:1024; border-radius:0">
|
||||
{$settings.globaltopmessage}
|
||||
</div>
|
||||
{/if}
|
||||
<Header />
|
||||
<slot></slot>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
:global(a.badge) {
|
||||
text-decoration: none;
|
||||
}
|
||||
:global(.text-justify) {
|
||||
text-align: justify;
|
||||
}
|
||||
:global(.niceborder) {
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 5px !important;
|
||||
border-bottom-color: #4eaee6;
|
||||
}
|
||||
</style>
|
||||
62
frontend/fic/src/routes/+page.svelte
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Container,
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
Col,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import CardTheme from '$lib/components/CardTheme.svelte';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { teams } from '$lib/stores/teams.js';
|
||||
import { myThemes, themes } from '$lib/stores/mythemes.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
</script>
|
||||
|
||||
<Container class="mt-3">
|
||||
{#if !$my}
|
||||
{#if $settings.allowRegistration}
|
||||
<Alert color="warning" class="text-justify" fade={false}>
|
||||
<strong>Votre équipe n'est pas encore enregistrée.</strong> Rendez-vous sur <a href="register">cette page</a> pour procéder à votre inscription.
|
||||
</Alert>
|
||||
{:else}
|
||||
<Alert color="danger" class="text-justify" fade={false}>
|
||||
<strong>Il semblerait qu'il y ait eu un problème lors de l'attribution de votre certificat.</strong> Veuillez vous signaler auprès de notre équipe afin de corriger ce problème.
|
||||
</Alert>
|
||||
{/if}
|
||||
{:else if !($my.team_id)}
|
||||
<Alert color="danger" fade={false}>
|
||||
<strong>Attention :</strong> puisqu'il s'agit de captures effectuées dans le but de découvrir si des actes malveillants ont été commis sur différents systèmes d'information, les contenus qui sont téléchargeables <em>peuvent</em> contenir du contenu malveillant !
|
||||
</Alert>
|
||||
{:else if $teams[$my.team_id]}
|
||||
<Alert color="info" class="text-justify" fade={false}>
|
||||
<strong>Félicitations {#if $my.members}{#each $my.members as member, index (member.id)}{#if member.id !== $my.members[0].id}{#if member.id === $my.members[$my.members.length - 1].id} et {:else}, {/if}{/if}{member.firstname} {member.lastname}{/each} {/if}!</strong> vous êtes maintenant connecté à l'espace de votre équipe <em>{$teams[$my.team_id].name}</em>.
|
||||
{#if !$settings.denyNameChange}Vous pouvez changer ce nom dès maintenant en vous rendant sur la page de <a href="edit">votre équipe</a>.{/if}
|
||||
</Alert>
|
||||
|
||||
{#if !$settings.ignoreTeamMembers && $my.team_id && (!$my.members || !$my.members.length)}
|
||||
<Alert color="warning" class="text-justify" fade={false}>
|
||||
<strong>Les membres de votre équipe ne sont pas encore enregistrés.</strong> Passez voir l'équipe serveur pour corriger cela.
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Row cols={{ lg: 3, md: 2, sm: 1 }}>
|
||||
{#each $themes as th, index}
|
||||
<Col class="mb-3">
|
||||
<CardTheme
|
||||
class="{$my && $my.team_id && $myThemes[th.id].exercice_solved > 0?'border-light ':''}{th.exercice_coeff_max > 1?'border-success ':''}{th.locked?' border-secondary ':''}"
|
||||
theme={th}
|
||||
on:click={goto(`${th.urlid}`)}
|
||||
/>
|
||||
</Col>
|
||||
{/each}
|
||||
</Row>
|
||||
</Container>
|
||||
5
frontend/fic/src/routes/[theme]/+layout.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { set_current_theme } from '$lib/stores/themes';
|
||||
|
||||
export function load({ params }) {
|
||||
set_current_theme.set(params.theme);
|
||||
}
|
||||
100
frontend/fic/src/routes/[theme]/+layout.svelte
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Container,
|
||||
Icon,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { challengeInfo } from '$lib/stores/challengeinfo';
|
||||
import { current_exercice } from '$lib/stores/exercices';
|
||||
import { current_theme } from '$lib/stores/themes';
|
||||
|
||||
let heading_image = "";
|
||||
let current_authors = "";
|
||||
$: if ($current_theme) {
|
||||
if ($current_exercice && $current_exercice.image) {
|
||||
heading_image = $current_exercice.image;
|
||||
} else {
|
||||
heading_image = $current_theme.image;
|
||||
}
|
||||
|
||||
if ($current_exercice && $current_exercice.authors) {
|
||||
current_authors = $current_exercice.authors;
|
||||
} else {
|
||||
current_authors = $current_theme.authors;
|
||||
}
|
||||
} else {
|
||||
heading_image = "";
|
||||
current_authors = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$current_theme?($current_theme.name + " - "):""}{$challengeInfo.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if $current_theme === null}
|
||||
<Container class="d-flex justify-content-center mt-5 text-dark align-items-center">
|
||||
<Spinner size="lg" type="border" color="dark" />
|
||||
<span class="ms-3 display-6">Chargement en cours…</span>
|
||||
</Container>
|
||||
{:else if !$current_theme}
|
||||
<Container>
|
||||
<Alert color="danger" class="mt-3" fade={false}>
|
||||
<Icon name="dash-circle-fill" />
|
||||
Ce scénario n'existe pas.
|
||||
</Alert>
|
||||
</Container>
|
||||
{:else}
|
||||
<div style="background-image: url({heading_image})" class="page-header">
|
||||
<Container class="text-primary">
|
||||
<h1 class="display-2">
|
||||
<a href="{$current_theme.urlid}">{$current_theme.name}</a>
|
||||
</h1>
|
||||
<h2>{@html current_authors}</h2>
|
||||
</Container>
|
||||
<div class="headerfade"></div>
|
||||
</div>
|
||||
<Container>
|
||||
<slot></slot>
|
||||
</Container>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.page-header {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
margin-bottom: -15rem;
|
||||
}
|
||||
.page-header h1 {
|
||||
text-shadow: 0 0 15px rgba(255,255,255,0.95), 0 0 5px rgb(255,255,255)
|
||||
}
|
||||
.page-header h1, .page-header h1 a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
.page-header h2 {
|
||||
font-size: 100%;
|
||||
text-shadow: 1px 1px 1px rgba(0,0,0,0.9)
|
||||
}
|
||||
.page-header h2, .page-header h2 a {
|
||||
color: #4eaee6;
|
||||
}
|
||||
.page-header h2 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.page-header h1 {
|
||||
padding-top: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
.page-header h2 {
|
||||
padding-bottom: 14rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-header .headerfade {
|
||||
background: linear-gradient(transparent 0%, rgb(233,236,239) 100%);
|
||||
height: 3rem;
|
||||
}
|
||||
</style>
|
||||
147
frontend/fic/src/routes/[theme]/+page.svelte
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
<script>
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
Col,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { current_theme } from '$lib/stores/themes';
|
||||
import { my } from '$lib/stores/my.js';
|
||||
</script>
|
||||
|
||||
<Card class="bg-dark niceborder text-indent mt-2 mb-4">
|
||||
|
||||
<Row>
|
||||
<Col lg={6} xl={7}>
|
||||
<CardBody class="text-light">
|
||||
<div style="position: relative; display: inline-block;">
|
||||
{#if $current_theme.locked}
|
||||
<div style="position: absolute; z-index: 0; top: 0; bottom: 0; left: 0; right: 0;" class="d-flex justify-content-center align-items-center">
|
||||
<div style="transform: rotate(-25deg)">
|
||||
<div class="display-3 font-weight-bolder border border-danger text-danger px-3 py-1" style="opacity: 0.5; border-radius: 20px; border-width: 5px !important; font-family: 'Courier New'">
|
||||
CONFIDENTIEL
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Row>
|
||||
<Col>
|
||||
<p class="mt-4 mx-3 card-text lead text-justify">{@html $current_theme.headline}</p>
|
||||
<p class="mb-4 mx-3 card-text text-justify">{@html $current_theme.intro}</p>
|
||||
</Col>
|
||||
{#if $current_theme.partner_txt || $current_theme.partner_img || $current_theme.partner_href}
|
||||
<Col md="2" lg="3" class="d-none d-md-block">
|
||||
<Card class="pt-3 px-3">
|
||||
{#if $current_theme.partner_img}
|
||||
<img src="{$current_theme.partner_img}" alt="En-tête du scénario" class="card-img-top">
|
||||
{/if}
|
||||
{#if $current_theme.partner_txt || $current_theme.partner_href}
|
||||
<CardBody class="p-0 mt-3">
|
||||
{#if $current_theme.partner_txt}
|
||||
{@html $current_theme.partner_txt}
|
||||
{/if}
|
||||
{#if $current_theme.partner_href}
|
||||
<Button tag="a" color="primary" href="{$current_theme.partner_href}">
|
||||
Visiter le site
|
||||
</Button>
|
||||
{/if}
|
||||
</CardBody>
|
||||
{/if}
|
||||
</Card>
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Col>
|
||||
<Col lg={6} xl={5}>
|
||||
{#if $current_theme.exercices && $current_theme.exercices.length}
|
||||
<ul class="list-group">
|
||||
{#each $current_theme.exercices as exercice, index}
|
||||
<li
|
||||
class="list-group-item"
|
||||
class:list-group-item-action={$my && $my.exercices[exercice.id]}
|
||||
on:click={goto(`${$current_theme.urlid}/${exercice.urlid}`)}
|
||||
on:keypress={goto(`${$current_theme.urlid}/${exercice.urlid}`)}
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-1" style="margin-top: -0.5rem; margin-bottom: -0.5rem; text-align: right; border-right: 5px solid #{$my && $my.exercices[exercice.id] && $my.exercices[exercice.id].solved_rank ? '62c462' : 'bbb'}">
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div style="position: absolute; margin-left: calc(var(--bs-gutter-x) * -.5 - 15px); margin-top: -0.5rem;">
|
||||
<svg style="height: 50px; width: 23px;">
|
||||
<rect
|
||||
style="fill:#{$my && $my.exercices[exercice.id] && (index < 1 || ($my.exercices[$current_theme.exercices[index-1].id] && $my.exercices[$current_theme.exercices[index-1].id].solved_rank)) ? '62c462' : 'bbb'}"
|
||||
width="5"
|
||||
height="30"
|
||||
x="10"
|
||||
y="0" />
|
||||
<path
|
||||
style="fill:#{$my && $my.exercices[exercice.id] ? ($my.exercices[exercice.id].solved_rank ? '62c462' : (exercice.curcoeff > 1.0 ? 'f89406' : '5bc0de')) : '555'}"
|
||||
d="m 22,20 a 9.5700617,9.5700617 0 0 1 -9.5690181,9.57006 9.5700617,9.5700617 0 0 1 -9.57110534,-9.56797 9.5700617,9.5700617 0 0 1 9.56692984,-9.57215 9.5700617,9.5700617 0 0 1 9.5731926,9.56588" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between flex-wrap">
|
||||
<h5 class="fw-bold">
|
||||
{#if $my && $my.exercices[exercice.id]}
|
||||
<span style="white-space: nowrap">
|
||||
{#if $my.exercices[exercice.id].wip}
|
||||
<Icon name="cone-striped" aria-hidden="true" title="Cette étape est encore en construction." />
|
||||
{/if}
|
||||
{exercice.title}
|
||||
</span>
|
||||
{:else}
|
||||
<span style="white-space: nowrap">
|
||||
<Icon name="lock-fill" aria-hidden="true" title="Vous n'avez pas encore accès à ce défi" />
|
||||
{exercice.title}
|
||||
</span>
|
||||
{/if}
|
||||
{#if exercice.curcoeff > 1.0}
|
||||
<Icon name="gift" aria-hidden="true" title="Un bonus est actuellement appliqué lors de la résolution de ce défi" />
|
||||
{/if}
|
||||
</h5>
|
||||
<div>
|
||||
{#each exercice.tags as tag, idx}
|
||||
<Badge href="tags/{tag}" pill color="secondary" class="mx-1 float-end">#{tag}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<p>{@html exercice.headline}</p>
|
||||
</div>
|
||||
<div class="d-none d-md-block col-1 pe-0">
|
||||
{#if $my && $my.exercices[exercice.id]}
|
||||
<a class="float-end" href="{$current_theme.urlid}/{exercice.urlid}" style="font-size: 3rem">
|
||||
<Icon name="chevron-right" aria-hidden="true" />
|
||||
</a>
|
||||
{:else}
|
||||
<span class="float-end" style="font-size: 3rem">
|
||||
<Icon name="chevron-right" aria-hidden="true" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="text-center my-5">
|
||||
Aucun contenu disponible actuellement.
|
||||
</p>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Card>
|
||||
|
||||
<style>
|
||||
.list-group-item-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
5
frontend/fic/src/routes/[theme]/[exercice]/+layout.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { set_current_exercice } from '$lib/stores/exercices';
|
||||
|
||||
export function load({ params }) {
|
||||
set_current_exercice.set(params.exercice);
|
||||
}
|
||||
32
frontend/fic/src/routes/[theme]/[exercice]/+layout.svelte
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Icon,
|
||||
Spinner,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import ThemeNav from '$lib/components/ThemeNav.svelte';
|
||||
|
||||
import { challengeInfo } from '$lib/stores/challengeinfo';
|
||||
import { current_exercice } from '$lib/stores/exercices';
|
||||
import { current_theme } from '$lib/stores/themes';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$current_exercice?$current_exercice.title+" - ":""}{$challengeInfo.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if $current_exercice === null}
|
||||
<div class="d-flex justify-content-center mt-5 text-dark align-items-center">
|
||||
<Spinner size="lg" type="border" color="dark" />
|
||||
<span class="ms-3 display-6">Chargement en cours…</span>
|
||||
</div>
|
||||
{:else if !$current_exercice}
|
||||
<Alert color="warning" class="mt-3" fade={false}>
|
||||
<Icon name="dash-circle-fill" />
|
||||
Vous n'avez pas encore accès à ce défi.
|
||||
</Alert>
|
||||
{:else}
|
||||
<ThemeNav theme={$current_theme} exercice={$current_exercice} />
|
||||
<slot></slot>
|
||||
{/if}
|
||||
258
frontend/fic/src/routes/[theme]/[exercice]/+page.svelte
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardText,
|
||||
Col,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import ExerciceDownloads from '$lib/components/ExerciceDownloads.svelte';
|
||||
import ExerciceFlags from '$lib/components/ExerciceFlags.svelte';
|
||||
import ExerciceHints from '$lib/components/ExerciceHints.svelte';
|
||||
import ExerciceSolved from '$lib/components/ExerciceSolved.svelte';
|
||||
import ExerciceVideo from '$lib/components/ExerciceVideo.svelte';
|
||||
import ResolutionModal from '$lib/components/ResolutionModal.svelte';
|
||||
|
||||
import { current_exercice } from '$lib/stores/exercices';
|
||||
import { my } from '$lib/stores/my';
|
||||
import { current_theme } from '$lib/stores/themes';
|
||||
import { settings } from '$lib/stores/settings';
|
||||
|
||||
let solved = {};
|
||||
let openResolution = false;
|
||||
</script>
|
||||
|
||||
{#if $current_exercice}
|
||||
<Card body class="niceborder text-indent my-3">
|
||||
{#if $current_theme.locked}
|
||||
<div style="position: absolute; z-index: 0; top: 0; bottom: 0; left: 0; right: 0;" class="d-flex justify-content-center align-items-center">
|
||||
<div style="transform: rotate(-25deg)">
|
||||
<div class="display-3 font-weight-bolder border border-danger text-danger px-3 py-1" style="opacity: 0.5; border-radius: 20px; border-width: 5px !important; font-family: 'Courier New'">
|
||||
CONFIDENTIEL
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<h3 class="display-4">{$current_exercice.title}</h3>
|
||||
<div>
|
||||
{#each $current_exercice.tags as tag, index}
|
||||
<Badge href="tags/{tag}" pill color="secondary" class="mx-1 mb-2" >#{tag}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !$my || !$my.exercices[$current_exercice.id]}
|
||||
<p class="lead text-justify">{@html $current_exercice.headline}</p>
|
||||
{:else}
|
||||
{#if $my.exercices[$current_exercice.id].wip}
|
||||
<Alert color="warning">
|
||||
<Icon name="cone-striped" />
|
||||
<strong>
|
||||
Cette étape est marquée comme étant en cours d'élaboration.
|
||||
</strong>
|
||||
Elle n'est pas prête à être tentée. Vous devriez directement passer à l'étape suivante.
|
||||
</Alert>
|
||||
{/if}
|
||||
<p class="lead text-justify">{@html $my.exercices[$current_exercice.id].statement}</p>
|
||||
{#if $my.exercices[$current_exercice.id].issue}
|
||||
<Alert color="{$my.exercices[$current_exercice.id].issuekind}">
|
||||
{@html $my.exercices[$current_exercice.id].issue}
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
<hr class="mt-0 mb-4">
|
||||
<Row>
|
||||
<Col>
|
||||
<Row class="level" cols={{xs:2, md:3, xl:4}}>
|
||||
<Col>
|
||||
<div class="level-item">
|
||||
{#if $settings.discountedFactor > 0 && $my && $my.exercices[$current_exercice.id]}
|
||||
<div class="heading">
|
||||
Cote
|
||||
</div>
|
||||
{:else}
|
||||
<div class="heading">
|
||||
Gain
|
||||
</div>
|
||||
{/if}
|
||||
<div class="value">
|
||||
{#if $settings.discountedFactor && $current_exercice.solved}
|
||||
<!-- This display the number of points gained by the team if it validates the exercice (current teams have more points than that) -->
|
||||
{Math.trunc($current_exercice.gain * (1-$settings.discountedFactor*$current_exercice.solved)*10)/10} {$current_exercice.gain==1?"point":"points"}
|
||||
{:else}
|
||||
{$current_exercice.gain} {$current_exercice.gain==1?"point":"points"}
|
||||
{/if}
|
||||
</div>
|
||||
{#if $settings.firstBlood && $current_exercice.solved < 1}
|
||||
<div class="text-muted">
|
||||
<em>+{$settings.firstBlood * 100}% (prem's)</em>
|
||||
</div>
|
||||
{:else if $settings.discountedFactor > 0 && $my && $my.exercices[$current_exercice.id]}
|
||||
<div class="text-muted">
|
||||
<em>initialement {$current_exercice.gain} {$current_exercice.gain==1?"point":"points"}</em>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $current_exercice.curcoeff != 1.0 || $settings.exerciceCurrentCoefficient != 1.0}
|
||||
<div class="text-muted">
|
||||
<em>{#if $current_exercice.curcoeff * $settings.exerciceCurrentCoefficient > 1}+{Math.round(($current_exercice.curcoeff * $settings.exerciceCurrentCoefficient - 1) * 100)}{:else}-{Math.round((1-($current_exercice.curcoeff * $settings.exerciceCurrentCoefficient)) * 100)}{/if}% (bonus)</em>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Col>
|
||||
<Col class="d-none d-md-block">
|
||||
<div class="level-item">
|
||||
<div class="heading">
|
||||
Tenté par
|
||||
</div>
|
||||
<div class="value">
|
||||
{#if !$current_exercice.tried}
|
||||
aucune équipe
|
||||
{:else}
|
||||
{$current_exercice.tried} {$current_exercice.tried == 1?"équipe":"équipes"}
|
||||
{#if $my && $my.exercices[$current_exercice.id] && $my.exercices[$current_exercice.id].total_tries}
|
||||
(cumulant {$my.exercices[$current_exercice.id].total_tries} {$my.exercices[$current_exercice.id].total_tries == 1?"tentative":"tentatives"})
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col class="d-none d-md-block">
|
||||
<div class="level-item">
|
||||
<div class="heading">
|
||||
Résolu par
|
||||
</div>
|
||||
<div class="value">
|
||||
{#if !$current_exercice.solved}
|
||||
aucune équipe
|
||||
{:else}
|
||||
{$current_exercice.solved} {$current_exercice.solved == 1?"équipe":"équipes"}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col class="d-block d-md-none">
|
||||
<div class="level-item">
|
||||
<div class="heading">
|
||||
{#if !$current_exercice.solved}
|
||||
Tenté par
|
||||
{:else}
|
||||
Résolu par
|
||||
{/if}
|
||||
</div>
|
||||
<div class="value">
|
||||
{#if !$current_exercice.solved}
|
||||
{#if !$current_exercice.tried}
|
||||
aucune équipe
|
||||
{:else}
|
||||
{$current_exercice.tried} {$current_exercice.tried == 1?"équipe":"équipes"}
|
||||
{#if $my && $my.exercices[$current_exercice.id] && $my.exercices[$current_exercice.id].total_tries}
|
||||
(cumulant {$my.exercices[$current_exercice.id].total_tries} {$my.exercices[$current_exercice.id].total_tries == 1?"tentative":"tentatives"})
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
{$current_exercice.solved} {$current_exercice.solved == 1?"équipe":"équipes"}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
{#if $my && $my.team_id}
|
||||
<Col xs="auto">
|
||||
{#if $settings.acceptNewIssue}
|
||||
<a href="issues/?eid={$current_exercice.id}" class="float-end btn btn-sm btn-warning">
|
||||
<Icon name="bug" />
|
||||
Rapporter une anomalie sur ce défi
|
||||
</a>
|
||||
{/if}
|
||||
{#if $settings.QAenabled}
|
||||
<a href="qa/exercices/{$current_exercice.id}" class="float-end btn btn-sm btn-info" target="_self">
|
||||
<Icon name="bug" />
|
||||
Voir les éléments QA sur ce défi
|
||||
</a>
|
||||
{/if}
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{#if $my && $my.exercices[$current_exercice.id]}
|
||||
<Row class="mt-4">
|
||||
<Col lg="6" class="mb-5">
|
||||
{#if $my.exercices[$current_exercice.id].files}
|
||||
<ExerciceDownloads
|
||||
files={$my.exercices[$current_exercice.id].files}
|
||||
/>
|
||||
{/if}
|
||||
{#if $my.exercices[$current_exercice.id].hints}
|
||||
<ExerciceHints
|
||||
locked_theme={$current_theme.locked}
|
||||
exercice={$my.exercices[$current_exercice.id]}
|
||||
hints={$my.exercices[$current_exercice.id].hints}
|
||||
/>
|
||||
{/if}
|
||||
</Col>
|
||||
<Col lg="6" class="mb-5">
|
||||
{#if $my.exercices[$current_exercice.id].flags && $my.exercices[$current_exercice.id].non_found_flags > 0 && !solved[$current_exercice.id]}
|
||||
{#if $current_theme.locked}
|
||||
<Card class="border-danger mb-2">
|
||||
<CardHeader class="bg-black text-light">
|
||||
<Icon name="lock-fill" />
|
||||
Faire son rapport
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<p>
|
||||
Ce scénario n'est pas accessible !
|
||||
</p>
|
||||
<p>
|
||||
Vous ne pouvez pas compléter son rapport.
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{:else}
|
||||
<ExerciceFlags
|
||||
exercice={$my.exercices[$current_exercice.id]}
|
||||
bind:forcesolved={solved[$current_exercice.id]}
|
||||
flags={$my.exercices[$current_exercice.id].flags}
|
||||
readonly={$current_exercice.disabled}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if $my.exercices[$current_exercice.id].solved_rank || solved[$current_exercice.id]}
|
||||
<ExerciceSolved
|
||||
theme={$current_theme}
|
||||
exercice={$my.exercices[$current_exercice.id]}
|
||||
/>
|
||||
{/if}
|
||||
{#if $my.exercices[$current_exercice.id].resolution || $my.exercices[$current_exercice.id].video_uri}
|
||||
<Card class="border-success mb-2">
|
||||
<CardHeader class="bg-success text-light d-flex justify-content-between">
|
||||
<div>
|
||||
<Icon name="laptop-fill" />
|
||||
Solution du défi
|
||||
</div>
|
||||
{#if $my.exercices[$current_exercice.id].resolution}
|
||||
<Button color="success" size="sm" on:click={() => openResolution = true}>
|
||||
<Icon name="arrows-angle-expand" />
|
||||
</Button>
|
||||
<ResolutionModal exercice={$current_exercice} bind:open={openResolution} resolution={$my.exercices[$current_exercice.id].resolution} />
|
||||
{/if}
|
||||
</CardHeader>
|
||||
{#if $my.exercices[$current_exercice.id].resolution}
|
||||
<CardBody>
|
||||
{@html $my.exercices[$current_exercice.id].resolution}
|
||||
</CardBody>
|
||||
{/if}
|
||||
{#if $my.exercices[$current_exercice.id].video_uri}
|
||||
<ExerciceVideo uri={$my.exercices[$current_exercice.id].video_uri} />
|
||||
{/if}
|
||||
</Card>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
{/if}
|
||||
58
frontend/fic/src/routes/edit/+page.svelte
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Card,
|
||||
CardHeader,
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import ScoreGrid from '$lib/components/ScoreGrid.svelte';
|
||||
import TeamChangeName from '$lib/components/TeamChangeName.svelte';
|
||||
import TeamChangePassword from '$lib/components/TeamChangePassword.svelte';
|
||||
import TeamMembers from '$lib/components/TeamMembers.svelte';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
</script>
|
||||
|
||||
<Container class="my-3">
|
||||
<h1 class="text-dark">
|
||||
Votre équipe
|
||||
{#if $my}
|
||||
<small class="text-muted">{$my.name}</small>
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
{#if $my}
|
||||
<Row>
|
||||
<Col md>
|
||||
<TeamMembers members={$my.members} />
|
||||
{#if !$settings.denyNameChange}
|
||||
<TeamChangeName />
|
||||
{/if}
|
||||
{#if $settings.acceptNewIssue}
|
||||
<TeamChangePassword />
|
||||
{/if}
|
||||
</Col>
|
||||
<Col md>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon name="table" />
|
||||
Détail du score
|
||||
</CardHeader>
|
||||
<ScoreGrid />
|
||||
</Card>
|
||||
<!--BrowserNotify /-->
|
||||
</Col>
|
||||
</Row>
|
||||
{:else}
|
||||
<Alert color="danger">
|
||||
<strong>Vous n'avez pas encore d'équipe !</strong>
|
||||
Rendez-vous sur <a href="register">la page d'inscription</a> pour plus d'information.
|
||||
</Alert>
|
||||
{/if}
|
||||
</Container>
|
||||
14
frontend/fic/src/routes/issues/+page.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { get_store_value } from 'svelte/internal';
|
||||
|
||||
import { exercices_idx } from '$lib/stores/themes.js';
|
||||
|
||||
export async function load({ url }) {
|
||||
const eidx = get_store_value(exercices_idx);
|
||||
|
||||
const exercice = eidx[url.searchParams.get("eid")]?eidx[url.searchParams.get("eid")]:null;
|
||||
|
||||
return {
|
||||
exercice: exercice,
|
||||
fillIssue: exercice !== null || url.searchParams.get("fill-issue") !== null,
|
||||
};
|
||||
}
|
||||
181
frontend/fic/src/routes/issues/+page.svelte
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Container,
|
||||
Icon,
|
||||
Table,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import DateFormat from '$lib/components/DateFormat.svelte';
|
||||
|
||||
import { issues, issues_nb_responses, issues_known_responses } from '$lib/stores/issues.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
|
||||
import FormIssue from '$lib/components/FormIssue.svelte';
|
||||
|
||||
export let data;
|
||||
let issue = {};
|
||||
|
||||
issues_known_responses.set($issues_nb_responses);
|
||||
|
||||
function newIssue() {
|
||||
data.fillIssue = true;
|
||||
}
|
||||
|
||||
let sberr = "";
|
||||
let message = "";
|
||||
let messageClass = "success";
|
||||
|
||||
function waitDiff(curissues, i) {
|
||||
issues.refresh((issues) => {
|
||||
if (i > 0 && (!issues || issues.length <= curissues)) {
|
||||
setTimeout(waitDiff, 850, curissues, i-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function respondTo(_issue) {
|
||||
data.exercice = null;
|
||||
issue = {id: _issue.id, description: ''};
|
||||
data.fillIssue = true;
|
||||
}
|
||||
|
||||
async function submit_issue(event) {
|
||||
sberr = "";
|
||||
if (!issue.id && issue.subject.length < 3) {
|
||||
messageClass = "warning";
|
||||
sberr = "L'objet de votre rapport d'anomalie est trop court !";
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await fetch('issue', {
|
||||
method: "POST",
|
||||
headers: {'Accept': 'application/json'},
|
||||
body: JSON.stringify(issue),
|
||||
});
|
||||
|
||||
if (response.status < 300) {
|
||||
const data = await response.json();
|
||||
messageClass = 'success';
|
||||
message = data.errmsg;
|
||||
issue = { };
|
||||
data.exercice = null;
|
||||
data.fillIssue = false;
|
||||
|
||||
const currentissues = get_store_value(issues);
|
||||
waitDiff(currentissues.length, 7);
|
||||
} else {
|
||||
messageClass = 'danger';
|
||||
|
||||
let data = "";
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch(e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
if (data && data.errmsg)
|
||||
message = data.errmsg;
|
||||
if (response.statys != 402)
|
||||
sberr = "Une erreur est survenue lors de l'envoi. Veuillez réessayer dans quelques instants.";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container fluid class="my-3">
|
||||
{#if message || sberr}
|
||||
<Alert color={messageClass} fade={false}>
|
||||
{#if !sberr}
|
||||
<strong>Votre rapport a bien été envoyé !</strong>
|
||||
{:else}
|
||||
<strong>{sberr}</strong>
|
||||
{/if}
|
||||
{message}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if data.fillIssue}
|
||||
<Card class="border-warning mt-3 mb-5">
|
||||
<CardHeader class="bg-warning text-light">
|
||||
<Icon name="file-earmark-plus" />
|
||||
{#if issue.id}
|
||||
Répondre à un message
|
||||
{:else}
|
||||
Rapporter une anomalie sur un défi
|
||||
{/if}
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{#if !$settings.acceptNewIssue}
|
||||
<p class="card-text">Rapprochez-vous d'un membre de l'équipe afin d'obtenir de l'aide.</p>
|
||||
{:else}
|
||||
<FormIssue
|
||||
exercice={data.exercice}
|
||||
bind:issue={issue}
|
||||
on:submit={submit_issue}
|
||||
/>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<Card>
|
||||
<Table hover striped>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Objet</th>
|
||||
<th>État / Priorité</th>
|
||||
<th>Géré par</th>
|
||||
<th>Messages</th>
|
||||
<th>
|
||||
{#if !data.fillIssue}
|
||||
<Button size="sm" color="warning" on:click={newIssue}>
|
||||
<Icon name="file-earmark-plus" />
|
||||
</Button>
|
||||
{/if}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $issues as issue (issue.id)}
|
||||
<tr>
|
||||
<td>
|
||||
{issue.subject}
|
||||
{#if issue.exercice} (défi <a href="{issue.url}">{issue.exercice}</a>){/if}
|
||||
</td>
|
||||
<td>{issue.state} / {issue.priority}</td>
|
||||
<td>{#if issue.assignee}{issue.assignee}{:else}En attente d'attribution{/if}</td>
|
||||
<td>
|
||||
{#each issue.texts as text, index}
|
||||
<p style="margin-left: 15px; text-indent: -15px">
|
||||
{#if !text.assignee || text.assignee == '$team'}Vous{:else}{text.assignee}{/if}
|
||||
le <DateFormat date={text.date} /> :
|
||||
<span style="white-space: pre-line">{text.cnt}</span>
|
||||
</p>
|
||||
{/each}
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
size="sm"
|
||||
color={issue.state == 'need-info'?'danger':'light'}
|
||||
on:click={respondTo(issue)}
|
||||
>
|
||||
<Icon name={issue.state == 'need-info'?'envelope-fill':'envelope-open-fill'} />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-2">
|
||||
Aucune anomalie remontée pour l'instant.<br>
|
||||
Vous souhaitez nous faire <a href="issues?fill-issue">remonter un problème</a> ?
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Container>
|
||||
52
frontend/fic/src/routes/rank/+page.svelte
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { rank } from '$lib/stores/teams.js';
|
||||
import { challengeInfo } from '$lib/stores/challengeinfo.js';
|
||||
|
||||
import CardTheme from '$lib/components/CardTheme.svelte';
|
||||
|
||||
let search = "";
|
||||
</script>
|
||||
|
||||
<Container fluid class="my-3">
|
||||
<h1 class="text-dark">
|
||||
{$challengeInfo.title}
|
||||
<small class="text-muted">Classement</small>
|
||||
</h1>
|
||||
<div class="card niceborder text-light">
|
||||
<div class="card-body">
|
||||
<input type="text" class="form-control" placeholder="Rechercher" bind:value={search}>
|
||||
</div>
|
||||
<table class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rang</th>
|
||||
<th>Équipe</th>
|
||||
<th>Points</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $rank as team (team.id)}
|
||||
{#if team.rank != 0 || ($my && $my.team_id == team.id)}
|
||||
<tr class:bg-info={$my && $my.team_id == team.id} class:bg-warning={search.length && team.name.toLowerCase().indexOf(search.toLowerCase()) >= 0}>
|
||||
<td>{team.rank}</td>
|
||||
<td>{team.name}</td>
|
||||
<td>{Math.round(team.score*100)/100}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Container>
|
||||
130
frontend/fic/src/routes/register/+page.svelte
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Card,
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { my } from '$lib/stores/my.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
|
||||
import RegistrationFormCreateTeam from '$lib/components/RegistrationFormCreateTeam.svelte';
|
||||
import RegistrationFormJoinTeam from '$lib/components/RegistrationFormJoinTeam.svelte';
|
||||
|
||||
let form = { };
|
||||
let partR = false;
|
||||
let partJ = false;
|
||||
let messageClass;
|
||||
let message;
|
||||
|
||||
function gotoHomeOnDiff(i) {
|
||||
my.refresh((my) => {
|
||||
if (my && my.team_id) {
|
||||
goto('.');
|
||||
} else if (i > 0) {
|
||||
setTimeout(gotoHomeOnDiff, 650, i-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function submit(event) {
|
||||
message = "";
|
||||
|
||||
// Remove empty members
|
||||
form.members = form.members.filter(function(m) {
|
||||
return ((m.lastname != undefined && m.lastname != "") || (m.firstname != undefined && m.firstname != "") || (m.nickname != undefined && m.nickname != ""));
|
||||
});
|
||||
|
||||
if (form.members.length == 0) {
|
||||
messageClass = 'danger';
|
||||
if (partJ) {
|
||||
message = "Veuillez compléter vos informations avant de rejoindre l'équipe.";
|
||||
} else {
|
||||
message = "Veuillez ajouter au moins un membre dans votre équipe !";
|
||||
}
|
||||
|
||||
form.members.push({ });
|
||||
form = form
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('registration', {
|
||||
method: "POST",
|
||||
headers: {'Accept': 'application/json'},
|
||||
body: JSON.stringify(form),
|
||||
})
|
||||
|
||||
if (response.status < 300) {
|
||||
const data = await response.json();
|
||||
messageClass = 'success';
|
||||
message = data.errmsg;
|
||||
|
||||
gotoHomeOnDiff(20);
|
||||
} else {
|
||||
messageClass = 'danger';
|
||||
|
||||
let data = "";
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch(e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
if (data && data.errmsg)
|
||||
message = data.errmsg;
|
||||
else
|
||||
message = "Une erreur est survenue lors de l'inscription de l'équipe. Veuillez réessayer dans quelques instants.";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container class="my-3">
|
||||
<Alert color="success" class="my-3">
|
||||
<Icon name="shield-check" />
|
||||
<strong>Félicitations ! vous êtes maintenant authentifié auprès de notre serveur !</strong>
|
||||
</Alert>
|
||||
{#if !$my}
|
||||
{#if message}
|
||||
<Alert color="{messageClass}" class="my-3">
|
||||
<strong>{message}</strong>
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if !$settings.allowRegistration}
|
||||
<Alert color="danger" class="my-3">
|
||||
<strong>Oups, il semblerait qu'il y ait eu un problème lors de l'attribution de votre certificat.</strong>
|
||||
Veuillez vous signaler auprès de notre équipe afin de corriger ce problème.
|
||||
</Alert>
|
||||
{:else}
|
||||
{#if !$settings.denyTeamCreation && !partJ}
|
||||
<Card body class="niceborder my-3">
|
||||
<p>
|
||||
Votre équipe n'est pas encore enregistrée sur notre serveur. Afin de
|
||||
pouvoir participer au challenge, nous vous remercions de bien vouloir
|
||||
remplir le formulaire d'inscription suivant :
|
||||
</p>
|
||||
<RegistrationFormCreateTeam bind:partR={partR} bind:value={form} on:submit={submit} />
|
||||
</Card>
|
||||
{/if}
|
||||
{#if $settings.canJoinTeam && !partR}
|
||||
<Card body class="niceborder my-3">
|
||||
<p>
|
||||
{#if !$settings.denyTeamCreation}
|
||||
Si votre équipe est déjà créée, rejoignez-là !
|
||||
{:else}
|
||||
Vous n'êtes pas encore enregistré·e sur notre serveur. Afin de
|
||||
pouvoir participer au challenge, nous vous remercions de bien vouloir
|
||||
rejoindre votre équipe :
|
||||
{/if}
|
||||
</p>
|
||||
<RegistrationFormJoinTeam bind:partJ={partJ} bind:value={form} on:submit={submit} />
|
||||
</Card>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</Container>
|
||||
202
frontend/fic/src/routes/rules/+page.svelte
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<script>
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
Container,
|
||||
Icon,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { challengeInfo } from '$lib/stores/challengeinfo.js';
|
||||
import { settings } from '$lib/stores/settings.js';
|
||||
</script>
|
||||
|
||||
<Container class="my-3">
|
||||
<h1 class="text-dark">
|
||||
{$challengeInfo.title}
|
||||
<small class="text-muted">Règles générales</small>
|
||||
</h1>
|
||||
|
||||
<div class="card-group text-justify mb-5">
|
||||
<div class="card niceborder">
|
||||
<div class="card-body text-indent">
|
||||
<h2>Débloquage des challenges</h2>
|
||||
<p>
|
||||
Au début, seul le premier défi de chaque scénario est
|
||||
accessible. Les défis de niveau supérieur sont débloqués en
|
||||
validant celui du niveau qui le précéde.
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<h2>Le classement</h2>
|
||||
<p>
|
||||
Pour figurer dans le classement, il faut avoir réalisé au moins une
|
||||
action : qu'elle ajoute ou retire des points.
|
||||
</p>
|
||||
<p>
|
||||
En cas d'égalité au score, les équipes sont départagées selon leur
|
||||
ordre d'arrivée à ce score.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
<h2>Calcul des points</h2>
|
||||
<p>
|
||||
Pour gagner des points, vous devez résoudre les défis qui vous sont
|
||||
proposés. Plus le challenge est compliqué, plus il rapporte de points.
|
||||
</p>
|
||||
|
||||
<h3>Coût des tentatives</h3>
|
||||
<p>
|
||||
Vous disposez de 10 tentatives pour trouver la/les solutions d'un
|
||||
challenge. Au delà, chaque tentative vous fait perdre une petite quantité
|
||||
de points comme suit :
|
||||
</p>
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre de tentatives</th>
|
||||
<th>Coût par tentative</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0 à 10</td>
|
||||
<td>0 point</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>11 à 20</td>
|
||||
<td>{Math.round($settings.submissionCostBase * 10) / 10} {$settings.submissionCostBase < 2?"point":"points"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>21 à 30</td>
|
||||
<td>{Math.round($settings.submissionCostBase * 20) / 10} {$settings.submissionCostBase * 2 < 2?"point":"points"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>31 à 40</td>
|
||||
<td>{Math.round($settings.submissionCostBase * 30) / 10} {$settings.submissionCostBase * 3 < 2?"point":"points"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>41 à 50</td>
|
||||
<td>{Math.round($settings.submissionCostBase * 40) / 10} {$settings.submissionCostBase * 4 < 2?"point":"points"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>...</td>
|
||||
<td>...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Par exemple :
|
||||
</p>
|
||||
<ul>
|
||||
<li>À 10 tentatives, vous aurez perdu {$settings.submissionCostBase * 0} {$settings.submissionCostBase * 0 < 2?"point":"points"}.</li>
|
||||
<li>À 15 tentatives, vous aurez perdu en tout {$settings.submissionCostBase * 5} {$settings.submissionCostBase * 5 < 2?"point":"points"} : <samp> {$settings.submissionCostBase} × 5</samp>.</li>
|
||||
<li>25 tentatives vous coûteront en tout {$settings.submissionCostBase * 20} {$settings.submissionCostBase * 20 < 2?"point":"points"} : <samp>{$settings.submissionCostBase} × 10 + {$settings.submissionCostBase} × 2 × 5</samp>.</li>
|
||||
<li>50 tentatives vous coûteront en tout {$settings.submissionCostBase * 100} {$settings.submissionCostBase * 100 < 2?"point":"points"} : <samp>{$settings.submissionCostBase} × 10 + {$settings.submissionCostBase} × 2 × 10 + {$settings.submissionCostBase} × 3 × 10 + {$settings.submissionCostBase} × 4 × 10</samp>.</li>
|
||||
</ul>
|
||||
{#if $settings.countOnlyNotGoodTries}
|
||||
<p>
|
||||
Seules les tentatives sans aucune bonne réponse sont prises en compte dans ce calcul. Lorsque vous complétez un formulaire avec un champ valide et un/des champs invalides, ceci n'est pas pris en compte dans le nombre de tentatives.
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
La dernière tentative (lorsque tous les flags sont bons) est comptabilisée
|
||||
parmi ce nombre de tentatives.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card niceborder">
|
||||
<div class="card-body text-indent">
|
||||
{#if $settings.discountedFactor > 0}
|
||||
<h3>Décote des gains</h3>
|
||||
<p>
|
||||
Une validation d'étape ne vous garanti pas un solde de points fixe.
|
||||
</p>
|
||||
<p>
|
||||
Selon le nombre d'équipe qui valident un challenge donné, sa cote diminue et vous rapporte alors moins de points. Le gain est donc indépendemment du fait que vous ayez validé l'étape avant une autre équipe : le gain affiché est un gain maximum, entendu si aucune autre équipe ne le valide.
|
||||
</p>
|
||||
<p>
|
||||
Chaque validation réduit de {$settings.discountedFactor*100} % la cote de l'exercice.
|
||||
</p>
|
||||
<p>
|
||||
Ainsi, pour un exercice d'une valeur initiale de {10*$settings.globalScoreCoefficient} points :
|
||||
</p>
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre d'équipes validant l'étape<br>à la fin de la compétition</th>
|
||||
<th>Gain réel</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>{10*$settings.globalScoreCoefficient} points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor)} points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*5)} points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10</td>
|
||||
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*10)} points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>20</td>
|
||||
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*20)} points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>...</td>
|
||||
<td>...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
{/if}
|
||||
|
||||
<h3>Coût des indices</h3>
|
||||
<p>
|
||||
Pour vous aider, certains défis vous proposent un ou
|
||||
plusieurs <strong>indices</strong>. Ces indices vous font perdre des
|
||||
points, la valeur de points perdus est indiquée pour chaque indice.
|
||||
</p>
|
||||
<p>
|
||||
Ces points sont perdus, que vous réussissiez ou non le défi.
|
||||
</p>
|
||||
<p>
|
||||
Vous pouvez débloquer des indices même si vous ne disposez pas de
|
||||
suffisamment de points (ou même si vous n'en avez pas encore) ; dans ce
|
||||
cas, votre score sera négatif.
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<h3>Bonus</h3>
|
||||
<p>
|
||||
Plusieurs bonus peuvent s'appliquer en même temps, dans ce cas, le calcul
|
||||
du bonus est toujours effectué à partir du nombre de points initiaux du
|
||||
défi.
|
||||
</p>
|
||||
|
||||
<h4>Prem's</h4>
|
||||
<p>
|
||||
Un bonus de +{$settings.firstBlood * 100} % est attribué à la première équipe qui résout un défi.
|
||||
</p>
|
||||
|
||||
<h4>Bonus temporaires <small><Icon name="gift" aria-hidden="true" title="Des
|
||||
bonus existent pour au moins un challenge de ce thème" /></small></h4>
|
||||
<p>
|
||||
Au cours du challenge, afin de booster les équipes ou certains challenges,
|
||||
un bonus peut-être attribué si une tentative valide est envoyée durant la
|
||||
période d'activité du bonus. Restez à l'écoute et observez les challenges
|
||||
portant cette icône : <Icon name="gift"
|
||||
aria-hidden="true" title="Des bonus existent pour au moins un challenge de ce
|
||||
thème" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
5
frontend/fic/src/routes/tags/[tag]/+page.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export async function load({ params }) {
|
||||
return {
|
||||
tag: params.tag,
|
||||
};
|
||||
}
|
||||
60
frontend/fic/src/routes/tags/[tag]/+page.svelte
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import {
|
||||
Alert,
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
Col,
|
||||
Container,
|
||||
Icon,
|
||||
Row,
|
||||
} from 'sveltestrap';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { themes } from '$lib/stores/themes.js';
|
||||
|
||||
import CardTheme from '$lib/components/CardTheme.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
let exercices = [];
|
||||
$: {
|
||||
let tmp_exercices = [];
|
||||
|
||||
for (let k in $themes) {
|
||||
const th = $themes[k];
|
||||
for (const ex of th.exercices) {
|
||||
if (ex.tags.indexOf(data.tag) >= 0) {
|
||||
tmp_exercices.push({theme: th, exercice: ex, index: k + "," + ex});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exercices = tmp_exercices;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container class="mt-3">
|
||||
<h1 class="text-dark">
|
||||
Challenges <em>{data.tag}</em>
|
||||
</h1>
|
||||
|
||||
{#if exercices.length}
|
||||
<Row cols="3">
|
||||
{#each exercices as {theme, exercice, index} (index)}
|
||||
<Col class="mb-3">
|
||||
<CardTheme
|
||||
theme={theme}
|
||||
exercice={exercice}
|
||||
on:click={goto(`${theme.urlid}/${exercice.urlid}`)}
|
||||
/>
|
||||
</Col>
|
||||
{/each}
|
||||
</Row>
|
||||
{:else}
|
||||
<p class="lead">
|
||||
Il n'y a aucun défi sur ce thème.
|
||||
</p>
|
||||
{/if}
|
||||
</Container>
|
||||
44
frontend/fic/static/e404.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Challenge Forensic</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<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">
|
||||
<link href="../../static/main.css" rel="stylesheet">
|
||||
<link href="../../theme/styles.css" rel="stylesheet">
|
||||
<style>
|
||||
.niceborder {
|
||||
border-bottom-color: #ee5f5b !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="theme-body">
|
||||
|
||||
<div class="theme-navbar niceborder">
|
||||
<div class="theme-navbar__logo-wrap">
|
||||
<img class="theme-navbar__logo" src="/img/fic.png" alt="Forum International de la Cybersécurité">
|
||||
</div>
|
||||
<div class="theme-navbar__logo-wrap">
|
||||
<img class="theme-navbar__logo" src="/img/epita.png" alt="Épita">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container dex-container" style="margin-top:20px;">
|
||||
<div class="jumbotron theme-panel niceborder">
|
||||
<h1>Page introuvable <small>Erreur 404</small></h1>
|
||||
<hr>
|
||||
<p class="lead">
|
||||
La page à laquelle vous tentez d'accéder n'existe pas ou l'adresse que vous avez tapée est incorrecte.
|
||||
</p>
|
||||
<p>
|
||||
Si le problème persiste, <a href="mailto:root@srs.epita.fr">contactez un administrateur</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1
frontend/fic/static/e404.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"errmsg": "La page à laquelle vous tentez d'accéder n'existe pas ou l'adresse que vous avez tapée est incorrecte."}
|
||||
41
frontend/fic/static/e413.html
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Challenge Forensic</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<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">
|
||||
<link href="../../static/main.css" rel="stylesheet">
|
||||
<link href="../../theme/styles.css" rel="stylesheet">
|
||||
<style>
|
||||
.niceborder {
|
||||
border-bottom-color: #ee5f5b !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="theme-body">
|
||||
|
||||
<div class="theme-navbar niceborder">
|
||||
<div class="theme-navbar__logo-wrap">
|
||||
<img class="theme-navbar__logo" src="/img/fic.png" alt="Forum International de la Cybersécurité">
|
||||
</div>
|
||||
<div class="theme-navbar__logo-wrap">
|
||||
<img class="theme-navbar__logo" src="/img/epita.png" alt="Épita">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container dex-container" style="margin-top:20px;">
|
||||
<div class="jumbotron theme-panel niceborder">
|
||||
<h1>Requête trop grosse <small>Erreur 413</small></h1>
|
||||
<hr>
|
||||
<p class="lead">
|
||||
La quantité de données que vous souhaitez envoyer au serveur est trop importante pour qu'il accepte de la traiter.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1
frontend/fic/static/e413.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"errmsg": "La quantité de données que vous souhaitez envoyer au serveur est trop importante pour qu'il accepte de la traiter."}
|
||||
44
frontend/fic/static/e500.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Challenge Forensic</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<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">
|
||||
<link href="../../static/main.css" rel="stylesheet">
|
||||
<link href="../../theme/styles.css" rel="stylesheet">
|
||||
<style>
|
||||
.niceborder {
|
||||
border-bottom-color: #ee5f5b !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="theme-body">
|
||||
|
||||
<div class="theme-navbar niceborder">
|
||||
<div class="theme-navbar__logo-wrap">
|
||||
<img class="theme-navbar__logo" src="/img/fic.png" alt="Forum International de la Cybersécurité">
|
||||
</div>
|
||||
<div class="theme-navbar__logo-wrap">
|
||||
<img class="theme-navbar__logo" src="/img/epita.png" alt="Épita">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container dex-container" style="margin-top:20px;">
|
||||
<div class="jumbotron theme-panel niceborder">
|
||||
<h1>Erreur interne <small>Erreur 500</small></h1>
|
||||
<hr>
|
||||
<p class="lead">
|
||||
Notre serveur est actuellement dans l'incapacité de répondre à votre requête.<br>Veuillez recommencer dans quelques instants.
|
||||
</p>
|
||||
<p>
|
||||
Si le problème persiste, <a href="mailto:root@srs.epita.fr">contactez un administrateur</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1
frontend/fic/static/e500.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"errmsg": "Notre serveur est actuellement dans l'incapacité de répondre à votre requête. \nVeuillez recommencer dans quelques instants."}
|
||||
BIN
frontend/fic/static/img/icon-danger.ico
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/fic/static/img/icon-dark.ico
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/fic/static/img/icon-info.ico
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/fic/static/img/icon-light.ico
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/fic/static/img/icon-primary.ico
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/fic/static/img/icon-secondary.ico
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/fic/static/img/icon-success.ico
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/fic/static/img/icon-warning.ico
Normal file
|
After Width: | Height: | Size: 10 KiB |
16
frontend/fic/svelte.config.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/** @type {import('@sveltejs/kit').Config} */
|
||||
import adapt from '@sveltejs/adapter-static';
|
||||
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapt({
|
||||
fallback: 'index.html'
|
||||
}),
|
||||
paths: {
|
||||
// base: '/2022',
|
||||
relative: false,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
8
frontend/fic/vite.config.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
plugins: [sveltekit()]
|
||||
};
|
||||
|
||||
export default config;
|
||||