ui: Almost all interface done with Svelte

This commit is contained in:
nemunaire 2021-08-30 12:46:18 +02:00
parent 9fa1ede69c
commit 7e13cf28bd
54 changed files with 2809 additions and 16 deletions

View File

@ -1,5 +1,5 @@
{ {
"name": "~TODO~", "name": "fic-frontend",
"version": "0.0.1", "version": "0.0.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
@ -83,6 +83,12 @@
"integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==",
"dev": true "dev": true
}, },
"@popperjs/core": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.3.tgz",
"integrity": "sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==",
"dev": true
},
"@rollup/pluginutils": { "@rollup/pluginutils": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.1.tgz",
@ -93,6 +99,11 @@
"picomatch": "^2.2.2" "picomatch": "^2.2.2"
} }
}, },
"@sveltejs/adapter-static": {
"version": "1.0.0-next.17",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-1.0.0-next.17.tgz",
"integrity": "sha512-RKYNkQxtsMgt0wD8PhfXR1hGT1Tmq1E5eZeTr1KxIerczITRnWVT8LElfu/9Kusv44yYlyQtNc1mLoYqgloOQw=="
},
"@sveltejs/kit": { "@sveltejs/kit": {
"version": "1.0.0-next.156", "version": "1.0.0-next.156",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.156.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.0.0-next.156.tgz",
@ -185,6 +196,16 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "dev": true
}, },
"bootstrap": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.0.tgz",
"integrity": "sha512-bs74WNI9BgBo3cEovmdMHikSKoXnDgA6VQjJ7TyTotU6L7d41ZyCEEelPwkYEzsG/Zjv3ie9IE3EMAje0W9Xew=="
},
"bootstrap-icons": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.5.0.tgz",
"integrity": "sha512-44feMc7DE1Ccpsas/1wioN8ewFJNquvi5FewA06wLnqct7CwMdGDVy41ieHaacogzDqLfG8nADIvMNp9e4bfbA=="
},
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -1103,6 +1124,15 @@
"integrity": "sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==", "integrity": "sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==",
"dev": true "dev": true
}, },
"sveltestrap": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.6.2.tgz",
"integrity": "sha512-g+WhMNsQjdGtSIMGmfVrMVP5Pz0d+o2L49GdTZnKnd9CEeZHcgWObn7+DDnAIxxjQUsaCgdZPgPVSD8/bU8/zA==",
"dev": true,
"requires": {
"@popperjs/core": "^2.9.2"
}
},
"table": { "table": {
"version": "6.7.1", "version": "6.7.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz",

View File

@ -1,5 +1,5 @@
{ {
"name": "~TODO~", "name": "fic-frontend",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "svelte-kit dev", "dev": "svelte-kit dev",
@ -9,13 +9,20 @@
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ." "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
}, },
"devDependencies": { "devDependencies": {
"@popperjs/core": "^2.9.3",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"eslint": "^7.22.0", "eslint": "^7.22.0",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.1.0",
"eslint-plugin-svelte3": "^3.2.0", "eslint-plugin-svelte3": "^3.2.0",
"prettier": "~2.2.1", "prettier": "~2.2.1",
"prettier-plugin-svelte": "^2.2.0", "prettier-plugin-svelte": "^2.2.0",
"svelte": "^3.34.0" "svelte": "^3.34.0",
"sveltestrap": "^5.6.2"
}, },
"type": "module" "type": "module",
"dependencies": {
"@sveltejs/adapter-static": "^1.0.0-next.17",
"bootstrap": "^5.1.0",
"bootstrap-icons": "^1.5.0"
}
} }

View File

@ -1,9 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="fr">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8">
<link rel="icon" href="/favicon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<meta name="author" content="EPITA Laboratoire SRS">
<meta name="robots" content="all">
%svelte.head% %svelte.head%
</head> </head>
<body> <body>

View File

@ -0,0 +1,57 @@
<script>
import {
Alert,
Card,
CardBody,
CardTitle,
Col,
Icon,
Row,
} from 'sveltestrap';
export { className as class };
export let theme = {};
export let exercice = null;
let className = '';
</script>
<div class="theme-card h-100">
<Card
class="text-light h-100 rounded-3 niceborder {className}"
color="dark"
on:click
>
{#if theme.image}
<div class="card-img-top" style="background-image: url({ theme.image.substr(0, theme.image.length-3) }thumb.jpg)"></div>
{/if}
<CardBody class="text-indent">
<CardTitle class="fw-bolder">
{#if exercice}{exercice.title}{:else}{theme.name}{/if}
</CardTitle>
<p class="card-text text-justify">{#if exercice}{@html exercice.headline}{:else}{@html theme.headline}{/if}</p>
</CardBody>
</Card>
</div>
<style>
.theme-card {
cursor: pointer;
transition: transform 250ms;
}
.theme-card:hover {
transform: scale(1.07);
}
.card-img-top {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.theme-card .card-img-top {
height: 10rem;
}
p {
color: #ccc;
}
</style>

View File

@ -0,0 +1,46 @@
<script>
import {
Card,
CardBody,
CardHeader,
CardText,
Icon,
ListGroup,
ListGroupItem,
} from 'sveltestrap';
export let files = [];
</script>
{#if files.length}
<Card class="mb-2">
<CardHeader>
<Icon name="download" />
Téléchargements
</CardHeader>
<CardBody class="text-indent">
<CardText class="text-danger text-justify">
<strong>Attention&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>
{#each files as file (file.id)}
<ListGroupItem tag="a" href="{file.path}" target="_self" class="d-flex">
<h1><Icon name="arrow-down-circle" /></h1>
<div>
<h4 class="fw-bold"><samp>{file.name}</samp></h4>
<nobr>
Taille&nbsp;:
<span title="{file.size} octets">{file.size}</span>
</nobr>
&ndash;
<nobr>
<span title="blake2.net">b2sum</span>&nbsp;:
<samp class="cksum" title="{file.checksum}">{file.checksum}</samp>
</nobr>
</div>
</ListGroupItem>
{/each}
</ListGroup>
</Card>
{/if}

View File

@ -0,0 +1,74 @@
<script>
import {
Button,
Card,
CardBody,
CardHeader,
CardText,
Icon,
ListGroup,
ListGroupItem,
} from 'sveltestrap';
export let exercice = {};
export let flags = [];
function submitFlags(event) {
console.log(event);
}
let sberr = "";
let message = "";
</script>
<Card class="border-danger mb-2">
<CardHeader class="bg-danger">
<Icon name="flag-fill" />
Faire son rapport
</CardHeader>
{#if exercice.tries || exercice.submitted || sberr}
<ListGroup>
{#if exercice.solved_time && exercice.tries}
<ListGroupItem class="text-warning">
{exercice.tries} {exercice.tries==1?"tentative effectuée":"tentatives effectuées"}.
Dernière solution envoyée à {exercice.solved_time}.
</ListGroupItem>
{/if}
{#if exercice.solve_dist}
<ListGroupItem>
{exercice.solve_dist} {exercice.solve_dist == 1?"réponse erronée":"réponses erronées"}.
</ListGroupItem>
{/if}
{#if exercice.submitted || sberr}
<ListGroupItem>
{#if !sberr}
<strong>Votre solution a bien été envoyée !</strong>
{:else}
<strong>{sberr}</strong> {message}
{/if}
</ListGroupItem>
{/if}
{#if exercice.timeouted}
<ListGroupItem class="text-danger">
<strong>Oops</strong>
La requête a dépassé le délai d'attente. Vous devriez réessayer dans quelques instant&hellip;
</ListGroupItem>
{/if}
</ListGroup>
{/if}
{#if !exercice.submitted || sberr}
<CardBody>
<form on:submit|preventDefault={submitFlags}>
{JSON.stringify(flags)}
<div class="form-group mt-2">
<Button
type="submit"
color="danger"
>
Soumettre
</Button>
</div>
</form>
</CardBody>
{/if}
</Card>

View File

@ -0,0 +1,70 @@
<script>
import {
Card,
CardBody,
CardHeader,
CardText,
Icon,
ListGroup,
ListGroupItem,
} from 'sveltestrap';
import { settings } from '../stores/settings.js';
export let hints = [];
let hinterror = "";
</script>
{#if hints.length}
<Card class="mb-2">
<CardHeader class="bg-info">
<Icon name="lightbulb-fill" />
Indices
</CardHeader>
{#if hinterror}
<CardBody>
<CardText class="text-danger">
{hinterror}
</CardText>
</CardBody>
{/if}
<ListGroup>
{#each hints as hint (hint.id)}
<ListGroupItem tag="a" href="{hint.file}" target="_self" class="d-flex">
{#if hint.file}
<h1><Icon name="arrow-down-circle" /></h1>
{/if}
<div>
{#if !(hint.content || hint.file)}
<button type="button" ng-click="hsubmit(hint)" class="float-end btn btn-info" class:disabled={hint.submitted}>
<Icon name="lock" aria-hidden="true" />
Débloquer
</button>
{/if}
{#if !hint.file && hint.hidden}
<button type="button" ng-click="hint.hidden = false;" class="float-end btn btn-info">
<Icon name="lock" aria-hidden="true" />
Afficher
</button>
{/if}
<h4 class="fw-bold">{hint.name}</h4>
{#if hint.file}
<p>
Cliquez ici pour télécharger l'indice.<br>
b2sum&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>
</ListGroupItem>
{/each}
</ListGroup>
</Card>
{/if}

View File

@ -0,0 +1,41 @@
<script>
import {
Card,
CardBody,
CardHeader,
CardText,
Icon,
ListGroup,
ListGroupItem,
} from 'sveltestrap';
export let theme = {};
export let exercice = {};
</script>
<Card class="border-success mb-2">
<CardHeader class="bg-success">
<Icon name="flag-fill" />
Défi réussi&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 à {exercice.solved_time}.
{:else}
Bravo, vous avez résolu ce défi à {exercice.solved_time}.
{/if}
Vous avez marqué {exercice.gain} {exercice.gain==1?"point":"points"}&nbsp;!
</CardText>
{#if exercice.finished}
<hr>
<CardText>{@html exercice.finished}</CardText>
{#if exercice.next}
<hr>
{/if}
{/if}
{#if theme.exercices[exercice.id].next}
<a href="/{theme.urlid}/{theme.exercices[theme.exercices[exercice.id].next].urlid}" class="btn btn-success">Passer au défi suivant</a>
{/if}
</CardBody>
</Card>

View File

@ -0,0 +1,26 @@
<script>
import {
Card,
CardBody,
CardHeader,
CardText,
Icon,
ListGroup,
ListGroupItem,
} from 'sveltestrap';
export let uri = "";
</script>
<Card class="border-success mb-2">
<CardHeader class="bg-success">
<Icon name="laptop-fill" />
Solution du défi
</CardHeader>
<CardBody class="text-indent">
<div class="embed-responsive embed-responsive-16by9">
<iframe type="text/html" src="{uri}" class="embed-responsive-item" title="Vidéo de résolution">
Regardez la vidéo de résolution de ce défi&nbsp;: <a href="{uri}">{uri}</a>.
</iframe>
</CardBody>
</Card>

View File

@ -0,0 +1,47 @@
<script>
import {
Button,
Icon,
} from 'sveltestrap';
import { issues, issues_idx } from '../stores/issues.js';
export let exercice = null;
export let issue = { };
</script>
<form on:submit|preventDefault>
{#if exercice || issue.id_exercice}
<div class="row mb-3">
<label for="idExercice" class="col-sm-2 col-form-label">Défi</label>
<div class="col-sm-10">
{#if exercice.id}
<input type="text" readonly class="form-control-plaintext" id="idExercice" value={exercice.title}>
{:else}
<input type="text" readonly class="form-control-plaintext" id="idExercice" value="{issue.id_exercice}">
{/if}
</div>
</div>
{/if}
<div class="row mb-3">
<label for="subject" class="col col-form-label">Objet</label>
<div class="col-sm-10">
{#if issue.id && $issues_idx[issue.id]}
<input type="text" readonly class="form-control-plaintext" id="subject" value="Re: {$issues_idx[issue.id].subject}">
{:else}
<input type="text" class="form-control" id="subject" bind:value={issue.subject} placeholder="Intitulé succinct">
{/if}
</div>
</div>
<div class="row mb-3">
<label for="description" class="col col-form-label">Description</label>
<div class="col-sm-10">
<textarea class="form-control" id="description" bind:value={issue.description} placeholder="Décrivez en détail votre problème ici. Si nécessaire, incluez un lien vers une capture d'écran montrant votre problème."></textarea>
</div>
</div>
<Button type="submit" color="warning" class="float-end">
Envoyer le rapport
</Button>
</form>

View File

@ -0,0 +1,112 @@
<script>
import {
Badge,
Button,
ButtonGroup,
Col,
Collapse,
Container,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
Icon,
Navbar,
NavbarToggler,
NavbarBrand,
Nav,
NavItem,
NavLink,
Progress,
Row,
} from 'sveltestrap';
import { my } from '../stores/my.js';
import { teams } from '../stores/teams.js';
import { settings, time } from '../stores/settings.js';
import HeaderClock from './HeaderClock.svelte';
import HeaderIssues from './HeaderIssues.svelte';
import HeaderPartners from './HeaderPartners.svelte';
import NavThemes from './NavThemes.svelte';
import NavTags from './NavTags.svelte';
let isOpen = false;
function handleUpdate(event) {
isOpen = event.detail.isOpen;
}
</script>
<div class="container-fluid bg-dark" style="max-height: 15vh;">
<div style="height: 100%; max-height: inherit; width: 98%; position: absolute">
<Container class="d-flex justify-content-center align-items-center text-light" style="height: 100%; max-height: inherit">
<HeaderClock />
</Container>
</div>
<Container class="d-flex justify-content-between p-1" style="max-height: inherit">
<a href="/">
<img src="/img/fic.png" alt="Forum International de la Cybersécurité" class="h-100">
</a>
<HeaderPartners />
</Container>
</div>
<Navbar class="sticky-top" color="dark" dark expand="md">
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
<Collapse {isOpen} navbar expand="md" on:update={handleUpdate}>
<Nav navbar>
<NavItem>
<NavLink href="/">
<Icon name="box-seam" />
Accueil
</NavLink>
</NavItem>
<NavThemes />
<NavTags />
{#if $settings && $settings.end - $settings.start >= 0}
<NavItem>
<NavLink href="/rank">
<Icon name="sort-down" />
Classement
</NavLink>
</NavItem>
{/if}
<HeaderIssues />
<NavItem>
<NavLink href="/rules">
<Icon name="signpost-split" />
Aide
</NavLink>
</NavItem>
</Nav>
<Nav class="ms-auto text-light" navbar>
{#if $my && $my.team_id}
<NavItem>
{$my.score} {$my.score === 1 ? 'point' : 'points'}
{#if $teams[$my.team_id].rank}
&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}
<Badge href="/edit" style="background-color: {$teams[$my.team_id].color} !important; color: {$teams[$my.team_id].color};">
<span class="teamname">{$my.name}</span>
</Badge>
{/if}
</NavItem>
</Nav>
</Collapse>
</Navbar>
<Progress value={$time.progression * 100} color="info" style="height: 5px; border-radius: 0;" />
<style>
.teamname {
-webkit-filter: invert(100%);
filter: invert(100%);
}
</style>

View File

@ -0,0 +1,108 @@
<script>
import {
ButtonGroup,
Icon,
} from 'sveltestrap';
import { settings, time } from '../stores/settings.js';
</script>
{#if $settings}
{#if $settings.end - $settings.start > 0}
<div
class="clock display-2"
class:expired={$time.expired}
class:end={$time.end}
class:wait={$time.startIn}
>
{#if $time.seconds}
<span id="hours">
{$time.hours}
</span>
<span class="point">
:
</span>
<span id="minutes">
{$time.minutes}
</span>
<span class="point">
:
</span>
<span id="seconds">
{$time.seconds}
</span>
{:else}
Chargement
{/if}
</div>
{:else}
<div class="d-flex h-100 justify-content-center align-items-center">
<ButtonGroup size="lg">
<a
href="/"
class="btn btn-light"
>
<Icon name="ui-checks-grid" />
Accueil
</a>
<a
href="/rank"
class="btn btn-light"
>
<Icon name="sort-down" />
Classement
</a>
<a
href="{$settings.videoslink}"
class="btn btn-light"
class:disabled={$settings.videoslink === ''}
>
<Icon name="laptop-fill" />
Vidéos
</a>
</ButtonGroup>
</div>
{/if}
{:else}
<div class="d-flex h-100 justify-content-center align-items-center">
<h1 class="display-3 m-0">
Challenge forensic
</h1>
</div>
{/if}
<style>
.clock:not(.expired):not(.wait) .point, .clock.expired {
transition: color text-shadow 1s;
position: relative;
animation: clockanim 1s ease infinite;
-moz-animation: clockanim 1s ease infinite;
-webkit-animation: clockanim 1s ease infinite;
}
.clock.wait .point {
transition: color text-shadow 1s;
position: relative;
animation: clockwait 1s ease infinite;
-moz-animation: clockwait 1s ease infinite;
-webkit-animation: clockwait 1s ease infinite;
}
.end {
color: #e64143;
}
.point {
text-shadow: 0 0 20px #4eaee6;
}
.end .point {
text-shadow: 0 0 20px #e64143;
}
@keyframes clockanim {
0% { opacity: 1.0; }
50% { opacity: 0; }
100% { opacity: 1.0; }
}
@keyframes clockwait {
0% { text-shadow: 0 0 20px #A6D6F2; }
50% { text-shadow: 0 0 2px #A6D6F2; }
100% { text-shadow: 0 0 20px #A6D6F2; }
}
</style>

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 '../stores/issues.js';
import { settings } from '../stores/settings.js';
let badge_color = 'secondary';
$: {
if ($issues_known_responses != $issues_nb_responses) {
if ($issues_need_info) {
badge_color = 'danger';
} else {
badge_color = 'warning';
}
} else {
badge_color = 'light';
}
}
</script>
{#if $issues.length}
<NavItem>
<NavLink href="/issues">
<Icon name="bug" />
Problèmes
<Badge color={badge_color}>{$issues_nb_responses}</Badge>
</NavLink>
</NavItem>
{/if}

View File

@ -0,0 +1,40 @@
<script>
import {
Carousel,
CarouselItem,
} from 'sveltestrap';
let partners = [
{
img: '/img/epita.png',
alt: 'Epita',
href: 'https://www.epita.fr/',
},
{
img: '/img/srs.png',
alt: 'Laboratoire SRS Épita',
href: 'https://srs.epita.fr/',
},
{
img: '/img/comcyber.png',
alt: 'Réserves de cyberdéfense',
},
];
let activePartner = 0;
</script>
<Carousel items={partners} bind:activeIndex={activePartner} ride="carousel" pause="hover" interval={25000}>
<div class="carousel-inner h-100">
{#each partners as partner, index}
<CarouselItem bind:activeIndex={activePartner} itemIndex={index} class="h-100 text-end">
{#if partner.href}
<a href="{partner.href}" target="_blank" class="h-100">
<img src={partner.img} class="h-100" alt={partner.alt}>
</a>
{:else}
<img src={partner.img} class="h-100" alt={partner.alt}>
{/if}
</CarouselItem>
{/each}
</div>
</Carousel>

View File

@ -0,0 +1,49 @@
<script>
import {
Badge,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
Icon,
} from 'sveltestrap';
import { my } from '../stores/my.js';
import { tags } from '../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() as itag, index}
{#if 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,41 @@
<script>
import {
Badge,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
Icon,
} from 'sveltestrap';
import { my } from '../stores/my.js';
import { max_solved, themes } from '../stores/themes.js';
import { myThemes } from '../stores/mythemes.js';
</script>
<Dropdown nav inNavbar>
<DropdownToggle nav caret>
<Icon name="tv" />
Scénarii
</DropdownToggle>
<DropdownMenu class="niceborder" end>
{#each Object.keys($themes) as th, index}
<DropdownItem href="/{$themes[th].urlid}">
{$themes[th].name}
{#if $max_solved > 1 && $themes[th].solved == $max_solved}
<Badge color="danger">
<Icon name="heart-fill" />
</Badge>
{/if}
{#if $themes[th].exercice_coeff_max > 1}
<Badge color="success">
<Icon name="gift-fill" />
</Badge>
{/if}
<Badge>
{#if $my && $my.team_id}{$myThemes[th].exercice_solved}/{/if}{$themes[th].exercice_count}
</Badge>
</DropdownItem>
{/each}
</DropdownMenu>
</Dropdown>

View File

@ -0,0 +1,109 @@
<script>
import { createEventDispatcher } from 'svelte';
import {
Alert,
Badge,
Button,
Card,
Col,
Container,
Icon,
Row,
} from 'sveltestrap';
import { settings } from '../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 {
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">
<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 '../stores/settings.js';
import { teams } from '../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,36 @@
<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">
<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,109 @@
<script>
import {
Button,
Card,
CardHeader,
CardBody,
Icon,
} from 'sveltestrap';
import { my } from '../stores/my.js';
export let refresh_my;
let newTeamName = "";
function gotoHomeOnDiff(i) {
refresh_my((my) => {
if (my && my.name == newTeamName) {
newTeamName = "";
messageClass = "info";
sberr = "Votre nom d'équipe a été changé avec succès.";
message = "";
} else {
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",
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">
<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">Valider</Button>
</div>
</div>
</div>
</form>
</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.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,59 @@
<script>
import {
Breadcrumb,
BreadcrumbItem,
Card,
Icon,
} from 'sveltestrap';
import { my } from '../stores/my.js';
export let theme = {};
export let exercice = {};
</script>
<Card body class="mb-3" color="dark">
<Breadcrumb listClassName="mb-0">
{#each Object.keys(theme.exercices) as k, index}
<BreadcrumbItem active={k == exercice.id}>
{#if k == exercice.id}
<strong class="text-info">
{theme.exercices[k].title}
{#if theme.exercices[k].curcoeff > 1.0}
<Icon name="gift" aria-hidden="true" />
{/if}
{#if $my && $my.team_id && $my.exercices[k] && $my.exercices[k].solved}
<Icon name="check" class="text-success" aria-hidden="true" />
{/if}
</strong>
{:else if $my && $my.exercices[k]}
<a href="/{theme.urlid}/{theme.exercices[k].urlid}" class:text-success={$my.exercices[k].solved}>
{theme.exercices[k].title}
{#if theme.exercices[k].curcoeff > 1.0}
<Icon name="gift" aria-hidden="true" />
{/if}
{#if $my.team_id && $my.exercices[k].solved}
<Icon name="check" class="text-success" aria-hidden="true" />
{/if}
</a>
{:else}
<span class="text-muted">
{theme.exercices[k].title}
{#if theme.exercices[k].curcoeff > 1.0}
<Icon name="gift" aria-hidden="true" />
{/if}
</span>
{/if}
</BreadcrumbItem>
{/each}
</Breadcrumb>
</Card>
<style>
a {
text-decoration: none;
}
a[href]:hover {
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,161 @@
<script context="module">
import { get_store_value } from 'svelte/internal';
import { themes } from '../../stores/themes.js';
export async function load({ page, fetch, session, context }) {
let exercice = null;
for (let ex in context.theme.exercices) {
if (context.theme.exercices[ex].urlid === page.params.exercice) {
exercice = context.theme.exercices[ex];
exercice.id = ex;
break;
}
}
return {
props: {
theme: context.theme,
exercice: exercice,
}
};
}
</script>
<script>
import {
Alert,
Badge,
Card,
Col,
Icon,
Row,
} from 'sveltestrap';
import ExerciceDownloads from '../../components/ExerciceDownloads.svelte';
import ExerciceFlags from '../../components/ExerciceFlags.svelte';
import ExerciceHints from '../../components/ExerciceHints.svelte';
import ExerciceSolved from '../../components/ExerciceSolved.svelte';
import ExerciceVideo from '../../components/ExerciceVideo.svelte';
import ThemeNav from '../../components/ThemeNav.svelte';
import { my } from '../../stores/my.js';
import { settings } from '../../stores/settings.js';
export let theme;
export let exercice;
</script>
<svelte:head>
<title>{exercice.title} - {$settings.title}</title>
</svelte:head>
{#if exercice}
<ThemeNav {theme} {exercice} />
{/if}
{#if !$my || !$my.exercices[exercice.id]}
<Alert color="warning" class="mt-3" fade={false}>
<Icon name="dash-circle-fill" />
Vous n'avez pas encore accès à ce défi.
</Alert>
{/if}
{#if exercice}
<Card body class="niceborder text-indent my-3">
<h3 class="display-4">{exercice.title}</h3>
<div>
{#each 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[exercice.id]}
<p class="lead text-justify">{@html exercice.headline}</p>
{:else}
<p class="lead text-justify">{@html $my.exercices[exercice.id].statement}</p>
{#if $my.exercices[exercice.id].issue}
<Alert color="{$my.exercices[exercice.id].issuekind}">
{@html $my.exercices[exercice.id].issue}
</Alert>
{/if}
{/if}
<hr class="mt-0 mb-4">
<Row>
<Col>
<ul>
<li>
<strong>Gain&nbsp;:</strong>
{exercice.gain} {exercice.gain==1?"point":"points"}
{#if $settings.firstBlood && exercice.solved < 1}
<em>+{$settings.firstBlood * 100}% (prem's)</em>
{/if}
{#if exercice.curcoeff != 1.0 || $settings.exerciceCurrentCoefficient != 1.0}
<em>{#if exercice.curcoeff * $settings.exerciceCurrentCoefficient > 1}+{Math.round((exercice.curcoeff * $settings.exerciceCurrentCoefficient - 1) * 100)}{:else}-{Math.round((1-(exercice.curcoeff * $settings.exerciceCurrentCoefficient)) * 100)}{/if}% (bonus)</em>
{/if}
</li>
<li>
<strong>Tenté par&nbsp;:</strong>
{#if !exercice.tried}
aucune équipe
{:else}
{exercice.tried} {exercice.tried == 1?"équipe":"équipes"}
{#if $my && $my.exercices[exercice.id].total_tries}
(cumulant {$my.exercices[exercice.id].total_tries} {$my.exercices[exercice.id].total_tries == 1?"tentative":"tentatives"})
{/if}
{/if}
</li>
<li>
<strong>Résolu par&nbsp;:</strong>
{#if !exercice.solved}
aucune équipe
{:else}
{exercice.solved} {exercice.solved == 1?"équipe":"équipes"}
{/if}
</li>
</ul>
</Col>
{#if $my && $my.team_id}
<Col>
{#if $settings.acceptNewIssue}
<a href="/issues/?eid={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/{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[exercice.id]}
<Row class="mt-4">
{#if $my.exercices[exercice.id].files || $my.exercices[exercice.id].hints}
<Col lg class="mb-5">
{#if $my.exercices[exercice.id].files}
<ExerciceDownloads files={$my.exercices[exercice.id].files} />
{/if}
{#if $my.exercices[exercice.id].hints}
<ExerciceHints hints={$my.exercices[exercice.id].hints} />
{/if}
</Col>
{/if}
<Col lg class="mb-5">
{#if !$my.exercices[exercice.id].solved}
<ExerciceFlags {exercice} flags={$my.exercices[exercice.id].flags} />
{:else}
<ExerciceSolved theme={theme} exercice={$my.exercices[exercice.id]} />
{/if}
{#if $my.exercices[exercice.id].video_uri}
<ExerciceVideo uri={$my.exercices[exercice.id].video_uri} />
{/if}
</Col>
</Row>
{/if}
{/if}

View File

@ -0,0 +1,94 @@
<script context="module">
import { get_store_value } from 'svelte/internal';
import { themes } from '../../stores/themes.js';
export async function load({ page, fetch, session, context }) {
const thms = get_store_value(themes);
let theme = null;
for (let th in thms) {
if (thms[th].urlid === page.params.theme) {
theme = thms[th];
break;
}
}
return {
context: {
...context,
theme: theme,
}, props: {
theme: theme,
}
};
}
</script>
<script>
import {
Container,
} from 'sveltestrap';
import { settings } from '../../stores/settings.js';
export let theme = null;
</script>
<svelte:head>
<title>{theme.name} - {$settings.title}</title>
</svelte:head>
{#if theme}
<div style="background-image: url({theme.image})" class="page-header">
<Container class="text-primary">
<h1 class="display-2">
<a href="/{theme.urlid}">{theme.name}</a>
</h1>
<h2>{@html theme.authors}</h2>
</Container>
<div class="headerfade"></div>
</div>
{/if}
<Container>
<slot></slot>
</Container>
<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,103 @@
<script context="module">
export async function load({ page, fetch, session, context }) {
return {
props: {
theme: context.theme,
},
};
}
</script>
<script>
import {
Alert,
Badge,
Icon,
} from 'sveltestrap';
import { goto } from '$app/navigation';
import { my } from '../../stores/my.js';
export let theme = null;
</script>
{#if theme && theme.exercices}
<div class="card niceborder text-indent mt-2 mb-4">
<div class="card-body bg-dark text-light">
<p class="mt-4 mx-3 card-text lead text-justify">{@html theme.headline}</p>
<p class="mb-4 mx-3 card-text text-justify">{@html theme.intro}</p>
</div>
<ul class="list-group">
{#each Object.keys(theme.exercices) as k, index}
<li
class="list-group-item"
class:list-group-item-action={$my && $my.exercices[k]}
on:click={goto(`/${theme.urlid}/${theme.exercices[k].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[k] && $my.exercices[k].solved ? '62c462' : 'aaa'}">
</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[k] && (index < 1 || $my.exercices[Object.keys(theme.exercices)[index-1]].solved) ? '62c462' : 'aaa'}"
width="5"
height="30"
x="10"
y="0" />
<path
style="fill:#{$my && $my.exercices[k] ? ($my.exercices[k].solved ? '62c462' : (theme.exercices[k].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>
{#each theme.exercices[k].tags as tag, idx}
<Badge href="/tags/{tag}" pill color="secondary" class="mx-1 float-end">#{tag}</Badge>
{/each}
<h5 class="fw-bold">
{#if $my && $my.exercices[k]}
{theme.exercices[k].title}
{:else}
<span style="white-space: nowrap">
<Icon name="lock-fill" aria-hidden="true" title="Vous n'avez pas encore accès à ce défi" />
{theme.exercices[k].title}
</span>
{/if}
{#if theme.exercices[k].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>
<p>{@html theme.exercices[k].headline}</p>
</div>
<div class="d-none d-md-block col-1">
{#if $my && $my.exercices[k]}
<a class="float-right" href="{theme.urlid}/{theme.exercices[k].urlid}" style="font-size: 3rem">
<Icon name="chevron-right" aria-hidden="true" />
</a>
{:else}
<span class="float-right" style="font-size: 3rem">
<Icon name="chevron-right" aria-hidden="true" />
</span>
{/if}
</div>
</div>
</li>
{/each}
</ul>
</div>
{:else}
<Alert color="danger" fade={false}>
<Icon name="dash-circle-fill" />
Ce scénario n'existe pas.
</Alert>
{/if}
<style>
.list-group-item-action {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,138 @@
<script context="module">
import { issuesStore } from '../stores/issues.js';
import { my } from '../stores/my.js';
import { teamsStore } from '../stores/teams.js';
import { themesStore } from '../stores/themes.js';
import { settings, time } from '../stores/settings.js';
let refresh_interval_settings = null;
async function refresh_settings(cb=null, interval=null) {
if (refresh_interval_settings)
clearInterval(refresh_interval_settings);
if (interval === null) {
interval = Math.floor(Math.random() * 24000) + 32000;
}
refresh_interval_settings = setInterval(refresh_settings, 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(refresh_settings, Math.floor(Math.random() * 10000) + 2400)
} else if (startIn > 1500) {
setTimeout(refresh_settings, startIn - 1000 - Math.floor(Math.random() * 500))
} else {
// On scheduled start time, refresh my.json file
setTimeout(refresh_my, startIn + Math.floor(Math.random() * 200))
}
}
};
}
settings.update(await fetch('/settings.json'), cb);
}
let refresh_interval_teams = null;
async function refresh_teams(cb=null, interval=null) {
if (refresh_interval_teams)
clearInterval(refresh_interval_teams);
if (interval === null) {
interval = Math.floor(Math.random() * 24000) + 32000;
}
refresh_interval_teams = setInterval(refresh_teams, interval);
teamsStore.update(await fetch('/teams.json'), cb);
}
let refresh_interval_themes = null;
async function refresh_themes(cb=null, interval=null) {
if (refresh_interval_themes)
clearInterval(refresh_interval_themes);
if (interval === null) {
interval = Math.floor(Math.random() * 24000) + 32000;
}
refresh_interval_themes = setInterval(refresh_themes, interval);
themesStore.update(await fetch('/themes.json'), cb);
}
let refresh_interval_my = null;
async function refresh_my(cb=null, interval=null) {
if (refresh_interval_my)
clearInterval(refresh_interval_my);
if (interval === null) {
interval = Math.floor(Math.random() * 24000) + 24000;
}
refresh_interval_my = setInterval(refresh_my, interval);
my.update(await fetch('/my.json'), cb);
}
let refresh_interval_issues = null;
async function refresh_issues(cb=null, interval=null) {
if (refresh_interval_issues)
clearInterval(refresh_interval_issues);
if (interval === null) {
interval = Math.floor(Math.random() * 24000) + 32000;
}
refresh_interval_issues = setInterval(refresh_issues, interval);
issuesStore.update(await fetch('/issues.json'), cb);
}
export async function load({ page, fetch, session, context }) {
await refresh_settings();
await refresh_themes();
refresh_teams();
refresh_my();
refresh_issues();
return {
context: {
...context,
refresh_settings,
refresh_teams,
refresh_themes,
refresh_my,
refresh_issues,
}
};
}
</script>
<script>
import 'bootstrap/dist/css/bootstrap.min.css';
import "bootstrap-icons/font/bootstrap-icons.css";
import {
Container,
//Styles,
} from 'sveltestrap';
import Header from '../components/Header.svelte';
</script>
<svelte:head>
<title>{$settings.title}</title>
</svelte:head>
<!--Styles /-->
<Header />
<slot></slot>
<style>
:global(a.badge) {
text-decoration: none;
}
:global(.text-justify) {
text-align: justify;
}
:global(.niceborder) {
border-bottom: 5px #4eaee6 solid !important;
}
</style>

View File

@ -0,0 +1,57 @@
<script context="module">
export async function load({ page, fetch, session, context }) {
return {
props: {
refresh_my: context.refresh_my,
}
}
}
</script>
<script>
import {
Alert,
Badge,
Card,
Col,
Container,
Icon,
Row,
} from 'sveltestrap';
import TeamChangeName from '../components/TeamChangeName.svelte';
import TeamMembers from '../components/TeamMembers.svelte';
import { my } from '../stores/my.js';
import { settings } from '../stores/settings.js';
export let refresh_my;
</script>
<Container class="my-3">
<h1>
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 {refresh_my} />
{/if}
</Col>
<Col md>
<!--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

@ -1,2 +1,62 @@
<h1>Welcome to SvelteKit</h1> <script>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p> import {
Alert,
Container,
Card,
CardBody,
CardTitle,
Col,
Icon,
Row,
} from 'sveltestrap';
import { goto } from '$app/navigation';
import CardTheme from '../components/CardTheme.svelte';
import { my } from '../stores/my.js';
import { teams } from '../stores/teams.js';
import { themes } from '../stores/themes.js';
import { myThemes } from '../stores/mythemes.js';
import { settings } from '../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}
<Alert color="info" class="text-justify" fade={false}>
<strong>Félicitations {#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;!</strong> vous êtes maintenant connecté à l'espace de votre équipe <em>{$teams[$my.team_id].name}</em>. Vous pouvez changer ce nom dès maintenant en vous rendant sur la page de <a href="edit">votre équipe</a>.
</Alert>
{#if ($my.team_id && !$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="3">
{#each Object.keys($themes) as th, index}
<Col class="mb-3">
<CardTheme
class="{$my && $my.team_id && $myThemes[th].exercice_solved > 0?'border-success ':''}{$themes[th].exercice_coeff_max > 1?'border-warning ':''}"
theme={$themes[th]}
on:click={goto(`/${$themes[th].urlid}`)}
/>
</Col>
{/each}
</Row>
</Container>

View File

@ -0,0 +1,186 @@
<script context="module">
import { get_store_value } from 'svelte/internal';
import { exercices_idx } from '../stores/themes.js';
export async function load({ page, fetch, session, context }) {
const eidx = get_store_value(exercices_idx);
const exercice = eidx[page.query.get("eid")]?eidx[page.query.get("eid")]:null;
return {
props: {
exercice: exercice,
fillIssue: exercice !== null || page.query.get("fill-issue") !== null,
}
};
}
</script>
<script>
import {
Alert,
Button,
Card,
CardBody,
CardHeader,
Container,
Icon,
Table,
} from 'sveltestrap';
import { issues, issues_nb_responses, issues_known_responses } from '../stores/issues.js';
import { settings } from '../stores/settings.js';
import FormIssue from '../components/FormIssue.svelte';
export let exercice = null;
export let fillIssue = false;
let issue = {};
issues_known_responses.set($issues_nb_responses);
function newIssue() {
fillIssue = true;
}
let sberr = "";
let message = "";
let messageClass = "success";
function respondTo(_issue) {
exercice = null;
issue = {id: _issue.id, description: ''};
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",
body: JSON.stringify(issue),
});
if (response.status < 300) {
const data = await response.json();
messageClass = 'success';
message = data.errmsg;
issue = { };
exercice = null;
} 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 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}
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 !fillIssue}
<Button 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}
à {text.date}&nbsp;:
<span style="white-space: pre-line">{text.cnt}</span>
</p>
{/each}
</td>
<td>
<Button
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="?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 '../stores/my.js';
import { rank } from '../stores/teams.js';
import { settings } from '../stores/settings.js';
import CardTheme from '../components/CardTheme.svelte';
let search = "";
</script>
<Container fluid class="my-3">
<h1>
{$settings.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>{team.score}</td>
</tr>
{/if}
{/each}
</tbody>
</table>
</div>
</Container>

View File

@ -0,0 +1,141 @@
<script context="module">
export async function load({ page, fetch, session, context }) {
return {
props: {
refresh_my: context.refresh_my,
}
}
}
</script>
<script>
import {
Alert,
Badge,
Card,
Col,
Container,
Icon,
Row,
} from 'sveltestrap';
import { goto } from '$app/navigation';
import { my } from '../stores/my.js';
import { settings } from '../stores/settings.js';
import RegistrationFormCreateTeam from '../components/RegistrationFormCreateTeam.svelte';
import RegistrationFormJoinTeam from '../components/RegistrationFormJoinTeam.svelte';
export let refresh_my;
let form = { };
let partR = false;
let partJ = false;
let messageClass;
let message;
function gotoHomeOnDiff(i) {
refresh_my((my) => {
if (my && my.team_id) {
goto('/');
} else {
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",
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,146 @@
<script>
import {
Card,
CardBody,
Container,
Icon,
} from 'sveltestrap';
import { settings } from '../stores/settings.js';
</script>
<Container class="my-3">
<h1>
{$settings.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>
</div>
</div>
<div class="card niceborder">
<div class="card-body text-indent">
<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>
<p>
La dernière tentative (lorsque tous les flags sont bons) est comptabilisée
parmi ce nombre de tentatives.
</p>
<hr>
<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,69 @@
<script context="module">
export async function load({ page, fetch, session, context }) {
return {
props: {
tag: page.params.tag,
}
};
}
</script>
<script>
import {
Alert,
Card,
CardBody,
CardTitle,
Col,
Container,
Icon,
Row,
} from 'sveltestrap';
import { goto } from '$app/navigation';
import { themes } from '../../stores/themes.js';
import CardTheme from '../../components/CardTheme.svelte';
export let tag = "";
let exercices = [];
$: {
let tmp_exercices = [];
for (let th in $themes) {
for (let ex in $themes[th].exercices) {
if ($themes[th].exercices[ex].tags.indexOf(tag) >= 0) {
tmp_exercices.push({theme: $themes[th], exercice: $themes[th].exercices[ex], index: th + "," + ex});
}
}
}
exercices = tmp_exercices;
}
</script>
<Container class="mt-3">
<h1>
Challenges <em>{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,55 @@
import { derived, writable } from 'svelte/store';
function createIssuesStore() {
const { subscribe, set, update } = writable({issues: [], issues_idx: {}, issues_nb_responses: 0, issues_need_info: 0});
return {
subscribe,
update: (res_issues, cb=null) => {
if (res_issues.status === 200) {
res_issues.json().then((issues) => {
const issues_idx = {};
let issues_nb_responses = 0;
let issues_need_info = 0;
issues.forEach(function(issue, k) {
issues_idx[issue.id] = issue;
issues_nb_responses += issue.texts.length;
if (issue.state == 'need-info') issues_need_info++;
issues[k].texts.reverse();
})
update((i) => (Object.assign(i, {issues, issues_idx, issues_nb_responses, issues_need_info})));
if (cb) {
cb(issues, issues_idx, issues_nb_responses, issues_need_info);
}
});
} else if (res_issues.status === 404) {
update((i) => ({issues: [], issues_idx: {}, issues_nb_responses: 0, issues_need_info: 0}));
}
},
};
}
export const issuesStore = createIssuesStore();
export const issues = derived(
issuesStore,
($issuesStore) => ($issuesStore.issues),
);
export const issues_idx = derived(
issuesStore,
($issuesStore) => ($issuesStore.issues_idx),
);
export const issues_nb_responses = derived(
issuesStore,
($issuesStore) => ($issuesStore.issues_nb_responses),
);
export const issues_need_info = derived(
issuesStore,
($issuesStore) => ($issuesStore.issues_need_info),
);
export const issues_known_responses = writable(0);

View File

@ -0,0 +1,28 @@
import { writable } from 'svelte/store';
function createMyStore() {
const { subscribe, set, update } = writable(null);
return {
subscribe,
update: (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;
}
update((m) => (Object.assign(m?m:{}, my)));
if (cb) {
cb(my);
}
});
} else if (res_my.status === 404) {
update((m) => (null));
}
},
};
}
export const my = createMyStore();

View File

@ -0,0 +1,42 @@
import { derived } from 'svelte/store';
import { my } from './my.js';
import { themesStore } from './themes.js';
export const myThemes = derived([my, themesStore], ([$my, $themesStore]) => {
const themes = {};
for (let key in $themesStore.themes) {
themes[key] = {exercice_solved: 0};
if ($my && $my.exercices) {
for (let k in $themesStore.themes[key].exercices) {
if ($my.exercices[k] && $my.exercices[k].solved) {
themes[key].exercice_solved++;
}
}
}
}
return themes;
});
export const tags = derived([my, themesStore], ([$my, $themesStore]) => {
const tags = {};
for (let key in $themesStore.themes) {
for (let k in $themesStore.themes[key].exercices) {
$themesStore.themes[key].exercices[k].tags.forEach((tag) => {
if (!tags[tag])
tags[tag] = {count: 1, solved: 0};
else
tags[tag].count += 1;
if ($my && $my.exercices && $my.exercices[k] && $my.exercices[k].solved)
tags[tag].solved += 1;
});
}
}
return tags;
});

View File

@ -0,0 +1,108 @@
import { readable, writable } from 'svelte/store';
function createSettingsStore() {
const { subscribe, set, update } = writable({});
return {
subscribe,
update: (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);
settings.recvTime = recvTime;
const x_fic_time = res_settings.headers["x-fic-time"];
if (x_fic_time) {
settings.currentTime = Math.floor(x_fic_time * 1000);
} else {
settings.currentTime = settings.recvTime;
}
update((s) => (Object.assign(s, settings)));
if (cb) {
cb(settings);
}
});
}
},
}
}
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,45 @@
import { derived, writable } from 'svelte/store';
function createTeamsStore() {
const { subscribe, set, update } = writable({teams:{}, teams_count: 0, rank: []});
return {
subscribe,
update: (res_teams, cb=null) => {
if (res_teams.status === 200) {
res_teams.json().then((teams) => {
const teams_count = Object.keys(teams).length
const rank = [];
for (const tid in teams) {
teams[tid].id = Number(tid);
rank.push(teams[tid]);
}
update((t) => (Object.assign(t, {teams, teams_count, rank})));
if (cb) {
cb(teams, teams_count, rank);
}
});
}
},
};
}
export const teamsStore = createTeamsStore();
export const teams = derived(
teamsStore,
($teamsStore) => ($teamsStore.teams)
);
export const teams_count = derived(
teamsStore,
($teamsStore) => ($teamsStore.teams_count)
);
export const rank = derived(
teamsStore,
($teamsStore) => ($teamsStore.rank)
);

View File

@ -0,0 +1,68 @@
import { derived, writable } from 'svelte/store';
function createThemesStore() {
const { subscribe, set, update } = writable({themes: {}, exercices_idx: {}, max_solved: 0});
return {
subscribe,
update: (res_themes, cb=null) => {
if (res_themes.status === 200) {
res_themes.json().then((themes) => {
let max_solved = 0;
const exercices_idx = {};
for (let key in themes) {
const theme = themes[key];
if (theme.solved > max_solved) {
max_solved = theme.solved;
}
themes[key].exercice_count = Object.keys(theme.exercices).length;
themes[key].exercice_coeff_max = 0;
themes[key].max_gain = 0;
let last_exercice = null;
for (let 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 (last_exercice != null)
themes[key].exercices[last_exercice].next = k;
last_exercice = k;
exercice.id = k;
exercices_idx[k] = exercice;
}
}
update((t) => (Object.assign(t, {themes, exercices_idx, max_solved})));
if (cb) {
cb(themes, exercices_idx, max_solved);
}
});
}
},
};
}
export const themesStore = createThemesStore();
export const themes = derived(
themesStore,
($themesStore) => ($themesStore.themes),
);
export const exercices_idx = derived(
themesStore,
($themesStore) => ($themesStore.exercices_idx),
);
export const max_solved = derived(
themesStore,
($themesStore) => ($themesStore.max_solved),
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

@ -1,6 +1,12 @@
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
import adapt from '@sveltejs/adapter-static';
const config = { const config = {
kit: { kit: {
adapter: adapt({
fallback: 'index.html'
}),
ssr: false,
// hydrate the <div id="svelte"> element in src/app.html // hydrate the <div id="svelte"> element in src/app.html
target: '#svelte' target: '#svelte'
} }