Rename frontend as receiver

This commit is contained in:
nemunaire 2023-07-09 20:40:53 +02:00
commit 1ca5452707
111 changed files with 79 additions and 81 deletions

View 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
View 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
View file

@ -0,0 +1,6 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}

View 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

File diff suppressed because it is too large Load diff

35
frontend/fic/package.json Normal file
View 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
View 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
View 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
View file

@ -0,0 +1 @@
/// <reference types="@sveltejs/kit" />

View 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>

View 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>

View 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)}

View 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&nbsp;:</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&nbsp;!
</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&nbsp;:
{#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>&nbsp;:
<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>

View 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&hellip;
</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>

View 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&nbsp;:
<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}

View 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&nbsp;!
</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"}&nbsp;!
</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>

View 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&nbsp;: <a href="{uri.replace('$RFILES$',base+'/resolution')}">{uri.replace('$RFILES$',base+'/resolution')}</a>.
</iframe>
{/if}
</CardBody>

View 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>

View 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}&nbsp;pts
</div>
{/if}
{#if !no_label}
<label for="sol_{flag.type}{flag.id}_0">{flag.label}&nbsp;:</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>

View 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}&nbsp;:
{#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}&nbsp;:{/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>

View 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>

View 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}
&ndash; {$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>

View 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}

View 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}

View 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}

View 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>

View 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>

View file

@ -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&nbsp;!
<Icon name="chevron-right" />
</Button>
</Col>
</Row>
{/if}
</form>

View file

@ -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&nbsp;!
<Icon name="chevron-right" />
</Button>
</Col>
</Row>
{/if}
</form>
{:else}
<p class="card-text">
Aucune équipe enregistrée pour l'instant.
</p>
{/if}

View 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>

View 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>

View 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> &times; <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}

View 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>

View 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&nbsp;:
</p>
<Button
href="issues?fill-issue"
color="warning"
>
Contactez-nous&nbsp;!
</Button>
</CardBody>
</Card>

View 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}&ndash; {member.company}{/if}
</ListGroupItem>
{/each}
</ListGroup>
{:else}
<CardBody>
Passez voir l'équipe d'organisation pour compléter ces informations.
</CardBody>
{/if}
</Card>

View 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>

View 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();

View file

@ -0,0 +1 @@
export let stop_refresh = { state: false };

View 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;
}
)

View 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);

View 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();

View 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;
});

View 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();
}
});

View 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;
}
);

View 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;
}
)

View file

@ -0,0 +1,5 @@
<script>
import { page } from '$app/stores';
</script>
<h1>{$page.status} : {$page.error.message}</h1>

View 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();
}

View 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>

View 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&nbsp;:</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&nbsp;!
</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}&nbsp;et {:else}, {/if}{/if}{member.firstname} {member.lastname}{/each}&nbsp;{/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>

View file

@ -0,0 +1,5 @@
import { set_current_theme } from '$lib/stores/themes';
export function load({ params }) {
set_current_theme.set(params.theme);
}

View 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&hellip;</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>

View 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>

View file

@ -0,0 +1,5 @@
import { set_current_exercice } from '$lib/stores/exercices';
export function load({ params }) {
set_current_exercice.set(params.exercice);
}

View 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&hellip;</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}

View 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&nbsp;!
</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}

View 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&nbsp;!</strong>
Rendez-vous sur <a href="register">la page d'inscription</a> pour plus d'information.
</Alert>
{/if}
</Container>

View 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,
};
}

View 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é&nbsp;!</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} />&nbsp;:
<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>&nbsp;?
</td>
</tr>
{/each}
</tbody>
</Table>
</Card>
</Container>

View 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>

View 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&nbsp;! vous êtes maintenant authentifié auprès de notre serveur&nbsp;!</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&nbsp;:
</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à&nbsp;!
{:else}
Vous n'êtes pas encore enregistré&middot;e sur notre serveur. Afin de
pouvoir participer au challenge, nous vous remercions de bien vouloir
rejoindre votre équipe&nbsp;:
{/if}
</p>
<RegistrationFormJoinTeam bind:partJ={partJ} bind:value={form} on:submit={submit} />
</Card>
{/if}
{/if}
{/if}
</Container>

View 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&nbsp;: 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&nbsp;tentatives pour trouver la/les solutions d'un
challenge. Au delà, chaque tentative vous fait perdre une petite quantité
de points comme suit&nbsp;:
</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&nbsp;point</td>
</tr>
<tr>
<td>11 à 20</td>
<td>{Math.round($settings.submissionCostBase * 10) / 10}&nbsp;{$settings.submissionCostBase < 2?"point":"points"}</td>
</tr>
<tr>
<td>21 à 30</td>
<td>{Math.round($settings.submissionCostBase * 20) / 10}&nbsp;{$settings.submissionCostBase * 2 < 2?"point":"points"}</td>
</tr>
<tr>
<td>31 à 40</td>
<td>{Math.round($settings.submissionCostBase * 30) / 10}&nbsp;{$settings.submissionCostBase * 3 < 2?"point":"points"}</td>
</tr>
<tr>
<td>41 à 50</td>
<td>{Math.round($settings.submissionCostBase * 40) / 10}&nbsp;{$settings.submissionCostBase * 4 < 2?"point":"points"}</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
</tr>
</tbody>
</table>
<p>
Par exemple&nbsp;:
</p>
<ul>
<li>À&nbsp;10 tentatives, vous aurez perdu {$settings.submissionCostBase * 0}&nbsp;{$settings.submissionCostBase * 0 < 2?"point":"points"}.</li>
<li>À&nbsp;15 tentatives, vous aurez perdu en tout {$settings.submissionCostBase * 5}&nbsp;{$settings.submissionCostBase * 5 < 2?"point":"points"}&nbsp;: <samp> {$settings.submissionCostBase} &times; 5</samp>.</li>
<li>25 tentatives vous coûteront en tout {$settings.submissionCostBase * 20}&nbsp;{$settings.submissionCostBase * 20 < 2?"point":"points"}&nbsp;: <samp>{$settings.submissionCostBase} &times; 10 + {$settings.submissionCostBase} &times; 2 &times; 5</samp>.</li>
<li>50 tentatives vous coûteront en tout {$settings.submissionCostBase * 100}&nbsp;{$settings.submissionCostBase * 100 < 2?"point":"points"}&nbsp;: <samp>{$settings.submissionCostBase} &times; 10 + {$settings.submissionCostBase} &times; 2 &times; 10 + {$settings.submissionCostBase} &times; 3 &times; 10 + {$settings.submissionCostBase} &times; 4 &times; 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}&nbsp;% la cote de l'exercice.
</p>
<p>
Ainsi, pour un exercice d'une valeur initiale de {10*$settings.globalScoreCoefficient}&nbsp;points&nbsp;:
</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}&nbsp;points</td>
</tr>
<tr>
<td>2</td>
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor)}&nbsp;points</td>
</tr>
<tr>
<td>5</td>
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*5)}&nbsp;points</td>
</tr>
<tr>
<td>10</td>
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*10)}&nbsp;points</td>
</tr>
<tr>
<td>20</td>
<td>{10*$settings.globalScoreCoefficient*(1-$settings.discountedFactor*20)}&nbsp;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}&nbsp;% 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&nbsp;: <Icon name="gift"
aria-hidden="true" title="Des bonus existent pour au moins un challenge de ce
thème" />
</p>
</div>
</div>
</div>
</Container>

View file

@ -0,0 +1,5 @@
export async function load({ params }) {
return {
tag: params.tag,
};
}

View 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>

View 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 &agrave; laquelle vous tentez d'acc&eacute;der n'existe pas ou l'adresse que vous avez tap&eacute;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>

View 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."}

View 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>

View 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."}

View 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&eacute; de r&eacute;pondre &agrave; votre requ&ecirc;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>

View file

@ -0,0 +1 @@
{"errmsg": "Notre serveur est actuellement dans l'incapacité de répondre à votre requête. \nVeuillez recommencer dans quelques instants."}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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;

View file

@ -0,0 +1,8 @@
import { sveltekit } from '@sveltejs/kit/vite';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()]
};
export default config;