initial commit
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import {ref,computed,onMounted} from 'vue'
|
||||
import {load,get,save,remove,exportData,importData} from './storage'
|
||||
import {genererId,tirer} from './models'
|
||||
const classes=ref([]),view=ref('list'),active=ref(null),editId=ref(null),showHelp=ref(false),fileInput=ref(null)
|
||||
const form=ref({nom:'',nombreEleves:24,ponderationMax:2})
|
||||
const sorted=computed(()=>[...classes.value].sort((a,b)=>a.nom.localeCompare(b.nom,'fr')))
|
||||
async function refresh(){classes.value=await load()}
|
||||
function newClass(){editId.value=null;form.value={nom:'',nombreEleves:24,ponderationMax:2};view.value='form'}
|
||||
async function edit(c){editId.value=c.id;form.value={nom:c.nom,nombreEleves:c.nombreEleves,ponderationMax:c.ponderationMax};view.value='form'}
|
||||
async function submit(){if(!form.value.nom.trim())return alert('Nom obligatoire');if(!Number.isInteger(Number(form.value.nombreEleves))||Number(form.value.nombreEleves)<1)return alert('Nombre d’élèves invalide');if(Number(form.value.ponderationMax)<1)return alert('Pondération invalide');const old=editId.value?await get(editId.value):null;const c={id:editId.value||genererId(),nom:form.value.nom.trim(),nombreEleves:Number(form.value.nombreEleves),ponderationMax:Number(form.value.ponderationMax),dateCreation:old?.dateCreation||new Date().toISOString(),tirages:old?.tirages||[]};await save(c);await refresh();view.value='list'}
|
||||
async function del(c){if(confirm(`Supprimer « ${c.nom} » ? Cette action est irréversible.`)){await remove(c.id);await refresh()}}
|
||||
async function open(c){active.value=await get(c.id);view.value='detail'}
|
||||
async function draw(){tirer(active.value);await save(active.value);active.value=await get(active.value.id)}
|
||||
async function redraw(){if(!active.value.tirages.length)return;active.value.tirages.shift();await save(active.value);await draw()}
|
||||
async function doExport(){const text=await exportData();const blob=new Blob([text],{type:'application/json'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=`tirages-export-${new Date().toISOString().slice(0,10)}.json`;a.click();URL.revokeObjectURL(url)}
|
||||
function chooseImport(){fileInput.value?.click()}
|
||||
async function onImport(e){const f=e.target.files?.[0];if(!f)return;try{await importData(await f.text());await refresh();alert('Import terminé. Les données du navigateur ont été remplacées par celles du fichier.')}catch(err){alert(err.message||'Import impossible.')}e.target.value=''}
|
||||
onMounted(refresh)
|
||||
</script>
|
||||
<template><div class="app"><header><button v-if="view!=='list'" class="icon" @click="view='list';active=null">‹</button><h1>{{view==='list'?'Tirages':view==='form'?(editId?'Modifier la classe':'Nouvelle classe'):active?.nom}}</h1><div class="actions"><button v-if="view==='list'" class="icon" @click="showHelp=true">?</button><button v-if="view==='list'" class="icon" @click="newClass">+</button></div></header>
|
||||
<main>
|
||||
<section v-if="view==='list'"><div v-if="!sorted.length" class="empty"><b>Aucune classe</b><span>Créez une classe pour commencer.</span><button class="primary" @click="newClass">Créer une classe</button></div><div v-else class="cards"><article v-for="c in sorted" :key="c.id"><button class="main" @click="open(c)"><span><b>{{c.nom}}</b><small>{{c.nombreEleves}} élèves</small></span><strong>›</strong></button><footer><button @click="edit(c)">Modifier</button><button @click="del(c)">Supprimer</button></footer></article></div><div class="data-actions"><button @click="doExport">Exporter les données</button><button @click="chooseImport">Importer des données</button><input ref="fileInput" type="file" accept="application/json,.json" hidden @change="onImport"></div></section>
|
||||
<section v-else-if="view==='form'" class="form"><label>Nom de la classe<input v-model="form.nom" placeholder="ex. 3A" autofocus></label><label>Nombre d’élèves<input v-model.number="form.nombreEleves" type="number" min="1" step="1"></label><label>Pondération maximale<input v-model.number="form.ponderationMax" type="number" min="1" step="0.1"><small>1 = équiprobable. Plus la valeur est élevée, plus les élèves qui n’ont pas été tirés récemment sont favorisés.</small></label><button class="primary" @click="submit">Enregistrer</button></section>
|
||||
<section v-else><p class="muted">{{active.nombreEleves}} élèves · pondération max ×{{active.ponderationMax}}</p><div class="draw"><button class="primary" @click="draw">🎲 Tirer un élève au sort</button><strong v-if="active.tirages.length">n°{{active.tirages[0].numeroEleve}}</strong><button v-if="active.tirages.length" @click="redraw">🔁 Re-tirer (annule et retire)</button></div><h2>Historique des tirages</h2><div v-if="!active.tirages.length" class="empty small">Aucun tirage pour le moment.</div><div v-else class="history"><div v-for="(t,i) in active.tirages" :key="t.id"><b>{{t.numeroEleve}}</b><span>{{new Date(t.dateTirage).toLocaleString('fr-FR')}}</span><em v-if="i===0">dernier</em></div></div></section>
|
||||
</main>
|
||||
<div v-if="showHelp" class="overlay" @click.self="showHelp=false"><section class="help"><button class="close" @click="showHelp=false">×</button><h2>À propos et aide</h2><h3>Données privées et locales</h3><p>Les classes et l’historique sont stockés dans <b>le navigateur de votre appareil</b>, dans une base de données locale IndexedDB. Les données de tirage ne sont envoyées à aucun serveur.</p><p>Le serveur ne sert qu’à fournir les fichiers de l’application. Une fois la PWA installée et mise en cache, elle peut fonctionner hors connexion.</p><h3>Exporter les données</h3><p><b>Exporter</b> crée un fichier JSON contenant vos classes et leurs historiques. Gardez-le comme sauvegarde, notamment avant de changer de téléphone, de navigateur ou de supprimer les données du site.</p><p>L’export permet aussi de transférer vos données vers une autre installation grâce à <b>Importer des données</b>.</p><h3>Installer sur un mobile</h3><p><b>iPhone / iPad :</b> ouvrez l’application dans Safari, touchez <b>Partager</b>, puis <b>Sur l’écran d’accueil</b> et validez.</p><p><b>Android :</b> ouvrez l’application dans Chrome puis utilisez <b>Installer l’application</b> ou <b>Ajouter à l’écran d’accueil</b>, selon la version.</p><p class="note">L’installation nécessite une connexion HTTPS. Le nom et les menus peuvent légèrement varier selon le navigateur.</p><h3>Code source</h3><p><a href="https://git.spaier.fr/cspaier/tirages-classes" target="_blank" rel="noopener">Dépôt Git : git.spaier.fr/cspaier/tirages-classes</a></p></section></div></div></template>
|
||||
Reference in New Issue
Block a user