ui: Almost all interface done with Svelte

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

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>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<script>
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>