Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/data/anecdotes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# 29/06/2026 11:00
Anecdote 1
Anecdote 2
Anecdote 3
51 changes: 0 additions & 51 deletions src/markdown/anecdotes.md

This file was deleted.

48 changes: 25 additions & 23 deletions src/markdown/jeu.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
## Défi des secrets

_Associez les anecdotes aux bons sponsors, gagnez des **tickets**, et utilisez-les pour des **gourmandises** 🍿 ou pour participer à la **loterie** 🎁 !_
Associez les anecdotes aux bons sponsors, récoltez les 🦩 **Flamy** et tentez votre chance à la **loterie** 🎁 !

## Comment jouer ?

1. **Consultez [la liste des anecdotes](/anecdotes)**
1. **Prenez votre collecteur à 🦩 Flamy** — fournie avec votre bracelet, sinon disponible à l'accueil.
2. **Consultez [la liste des anecdotes](/anecdotes)** — ℹ️ elle évolue tout au long de la conférence.
3. **Discutez avec les sponsors** pour deviner à qui correspond chaque anecdote.
4. **Validez vos réponses** auprès du sponsor : une bonne réponse vous rapporte un 🦩 Flamy.
5. **Échangez vos Flamy à l'accueil** : 4 🦩 = 1 ticket 🎫 (jusqu'à 4 tickets au total).

> 📌 *[La liste](/anecdotes) peut évoluer tout au long de la conférence — pensez à la consulter régulièrement !*
<!-- Bug Astro: espace manquant entre une liste et un titre -->
<br>

2. **Discutez avec les sponsors**
## Loterie

> Échangez avec eux pour deviner à qui correspond chaque anecdote.
Deux tirages : **jeudi soir** (soirée networking) et **vendredi soir** (keynote de clôture).

3. **Validez vos réponses**
Les lots sont identiques pour chaque tirage :

> Pour chaque bonne réponse, vous remportez **un ticket** 🎫.
1. 🖨️ Une mini-imprimante 3D
2. 🎮 Une console portable rétro
3. 🎟️ Une place pour SunnyTech l'année prochaine

4. **Utilisez vos tickets** comme vous le souhaitez :
1. 🍿 **Échangez** les contre du popcorn ou de la barbe à papa (1 ticket = 1 popcorn ou 1 barbe à papa)
2. 🎁 **Participez à la loterie SunnyTech**.
<br/><br/>
## Points importants
<!-- Bug Astro: espace manquant entre une liste et un paragraphe -->
<br>

> ⚠️ La présence est obligatoire pour récupérer les lots physiques.

<!-- Bug Astro: espace manquant entre une quote et un paragraphe -->
<br>

Sur chaque ticket, notez vos **nom**, **email** et **téléphone** au dos, puis glissez-le dans l'urne à l'accueil.

* La [liste des anecdotes](/anecdotes) peut évoluer en cours de conférence. Restez à l'affût !
* Pour la loterie :
* Détachez chaque ticket.
* Inscrivez clairement vos **coordonnées** au dos :
* Nom
* Email
* Numéro de téléphone
* Glissez ensuite le ticket dans l’**urne à l'accueil**.
* Si vous avez des questions, n'hésitez pas à demander à l'un.e des bénévoles (tee-shirt jaune)
## Bonne chance !

Amusez-vous bien tout au long de SunnyTech, et que le plus curieux gagne ! 🦩
Que les plus curieux gagnent ! 🦩

Le règlement complet [est disponible ici](https://docs.google.com/document/d/16_iKaZ3iIz_SWdv_J0UHEzFao6zDDWZC6Vm71gtA-Bc/edit?usp=sharing).
_Des questions ? Les bénévoles en tee-shirt jaune sont là pour vous aider. — Règlement complet [est disponible ici](https://docs.google.com/document/d/16_iKaZ3iIz_SWdv_J0UHEzFao6zDDWZC6Vm71gtA-Bc/edit?usp=sharing)._
243 changes: 238 additions & 5 deletions src/pages/anecdotes.astro
Original file line number Diff line number Diff line change
@@ -1,11 +1,244 @@
---
import LayoutWithTitle from '../layouts/LayoutWithTitle.astro'
import MarkdownWrapper from '../components/ui-elements/MarkdownWrapper.astro'
import { Content } from '../markdown/anecdotes.md'
import rawAnecdotes from '../data/anecdotes.txt?raw'

const lines = rawAnecdotes.split('\n').map((line) => line.trim())
const lastUpdated = lines[0]?.startsWith('#') ? lines[0].slice(1).trim() : null
const anecdotes = lines.filter((line) => line && !line.startsWith('#'))
---

<LayoutWithTitle title="Anecdotes des Sponsors">
<MarkdownWrapper>
<Content />
</MarkdownWrapper>
<div class="anecdotes-page">
<p class="intro">
<em
>Voici les anecdotes qui ont été partagées par les sponsors, à toi de trouver à qui elles
correspondent&nbsp;! 🕵️</em
>
</p>
{lastUpdated && <p class="last-update text-subtle">Dernière mise à jour : {lastUpdated}</p>}

<p class="notice">ℹ️ Aucune de ces anecdotes ne correspond au sponsor SMAG.</p>

<div class="anecdotes-controls">
<span class="anecdotes-counter" id="anecdotes-counter" aria-live="polite">
{anecdotes.length} à trouver (0 / {anecdotes.length})
</span>
<button class="anecdotes-reset" id="anecdotes-reset" hidden>Réinitialiser</button>
</div>

<ul class="anecdote-list" id="anecdote-list">
{
anecdotes.map((text) => (
<li class="anecdote-item">
<label class="anecdote-label">
<input type="checkbox" class="anecdote-checkbox" />
<span class="anecdote-text">{text}</span>
</label>
</li>
))
}
</ul>
</div>
</LayoutWithTitle>

<script>
const STORAGE_KEY = 'sunnytech-anecdotes-2026'

function hashText(str: string): string {
let h = 5381
for (let i = 0; i < str.length; i++) {
h = ((h << 5) + h) ^ str.charCodeAt(i)
}
return (h >>> 0).toString(36)
}

function getChecked(): Set<string> {
try {
const raw = localStorage.getItem(STORAGE_KEY)
return new Set(raw ? JSON.parse(raw) : [])
} catch {
return new Set()
}
}

function saveChecked(checked: Set<string>): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify([...checked]))
}

function updateCounter(total: number, checkedCount: number): void {
const counter = document.getElementById('anecdotes-counter')
const resetBtn = document.getElementById('anecdotes-reset') as HTMLButtonElement | null
if (!counter) return
const remaining = total - checkedCount
counter.textContent = `${remaining} à trouver (${checkedCount} / ${total})`
if (resetBtn) {
resetBtn.hidden = checkedCount === 0
}
}

function init() {
const list = document.getElementById('anecdote-list')
if (!list) return

const items = Array.from(list.querySelectorAll<HTMLLIElement>('.anecdote-item'))
const checked = getChecked()
const total = items.length

items.forEach((item) => {
const textEl = item.querySelector<HTMLElement>('.anecdote-text')
const checkbox = item.querySelector<HTMLInputElement>('.anecdote-checkbox')
if (!textEl || !checkbox) return

const id = hashText(textEl.textContent?.trim() ?? '')
item.dataset.id = id

if (checked.has(id)) {
checkbox.checked = true
item.classList.add('is-checked')
}
})

// Sort: unchecked first, checked last — only on load
const unchecked = items.filter((el) => !el.classList.contains('is-checked'))
const checkedItems = items.filter((el) => el.classList.contains('is-checked'))
;[...unchecked, ...checkedItems].forEach((el) => list.appendChild(el))

updateCounter(total, checked.size)

list.addEventListener('change', (e) => {
const checkbox = e.target as HTMLInputElement
if (!checkbox.classList.contains('anecdote-checkbox')) return
const item = checkbox.closest<HTMLLIElement>('.anecdote-item')
if (!item) return

const id = item.dataset.id ?? ''
const currentChecked = getChecked()

if (checkbox.checked) {
currentChecked.add(id)
item.classList.add('is-checked')
} else {
currentChecked.delete(id)
item.classList.remove('is-checked')
}

saveChecked(currentChecked)
updateCounter(total, currentChecked.size)
})

const resetBtn = document.getElementById('anecdotes-reset')
resetBtn?.addEventListener('click', () => {
saveChecked(new Set())
items.forEach((item) => {
item.classList.remove('is-checked')
const cb = item.querySelector<HTMLInputElement>('.anecdote-checkbox')
if (cb) cb.checked = false
})
updateCounter(total, 0)
})
}

document.addEventListener('astro:page-load', init)
</script>

<style>
.anecdotes-page {
max-width: 70ch;
}

.intro {
line-height: 1.5;
margin-bottom: 0.4rem;
}

.last-update {
font-size: var(--fs-1);
margin-bottom: 1rem;
}

.notice {
font-size: var(--fs-1);
color: var(--text-subtle);
background: var(--component);
border-left: 3px solid var(--border);
border-radius: var(--border-radius);
padding: 0.5rem 0.75rem;
margin-bottom: 1.5rem;
}

.anecdotes-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.25rem;
padding: 0.6rem 1rem;
background: var(--component);
border-radius: var(--border-radius);
}

.anecdotes-counter {
font-size: var(--fs-1);
font-weight: 600;
color: var(--text-subtle);
}

.anecdotes-reset {
font-size: var(--fs-1);
background: none;
border: 1px solid var(--border);
border-radius: var(--border-radius);
padding: 0.2rem 0.6rem;
cursor: pointer;
color: var(--text-subtle);
transition: all var(--animation);

&:hover {
border-color: var(--text);
color: var(--text);
}
}

.anecdote-list {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}

.anecdote-item {
border-left: 3px solid var(--pink-2);
border-radius: var(--border-radius);
background: var(--component);
transition:
opacity var(--animation),
border-color var(--animation);
}

.anecdote-item.is-checked {
opacity: 0.45;
border-left-color: var(--grey-3);
}

.anecdote-label {
display: flex;
gap: 0.75rem;
align-items: flex-start;
padding: 0.9em 1em;
cursor: pointer;
}

.anecdote-checkbox {
flex-shrink: 0;
margin-top: 0.25em;
accent-color: var(--brand-solid);
width: 1rem;
height: 1rem;
cursor: pointer;
}

.anecdote-text {
line-height: 1.5;
}
</style>
Loading