This commit is contained in:
didier committed 2026-08-08 09:17:05 -04:00
1 parent 11031eb5a3
commit 123481d748
24 files changed
+29 -1587

No files matched your search

+2 -9
View File
@@ -1,11 +1,4 @@
# Deno
node_modules/*
lib/*
css/*
/api/resultats.json
/index.html
/main-built.js
/api
# Nix build result
result
# .NET
@@ -16,4 +9,4 @@ obj/
*.db
publish/
.direnv/
.direnv/
+27 -36
View File
@@ -1,48 +1,39 @@
# Site Web de [Balle Phénix](http://ballephenix.com)
![Image Alt](https://gitlab.com/ballephenix/website/raw/master/images/phenix_logo.png)
## Stack
## introduction
- **ASP.NET Core 9** — Razor Views (`Views/*.cshtml`), contrôleurs dans `Controllers/`
- **SQLite** par défaut (`phenix.db`), Postgres au choix via `DatabaseProvider`
- **Nix** — build et déploiement via `flake.nix` (service NixOS, domaine `ballephenix.com`)
- Les assets statiques vivent dans `static/` et sont servis par `app.config.WebRootPath = "static"`
Vous trouverez le code source du site web pour les Phénix ici. Le site web
en lui-même est accessible au http://ballephenix.com ou au
https://ballephenix.gitlab.io/website/.
### Structure du site web.
Premièrement, mille excuses pour mes choix hétéroclites. J'ai décidé d'explorer
de nouvels technologies avec ce projet. En conséquence, le code est un peu
difficile à comprendre à première vue mais il est __tellement__ plus cool!
#### Choix
* mithril.js comme framework dans le fureteur
* require.js pour les dépendances
* uikit avec sass comme framework css
* gulp pour la compilation
* gitlab pour le deployment
---
# English Version
All the source code is in english. The source code is for a static web
side for The softball league 'Balle Phénix'.
## How to build
No build, it uses [deno](https://deno.land)
# How to run it
## Développement
```shell
deno run -A --unstable-vk server.ts
dotnet run
```
Base de données SQLite par défaut (`phenix.db`). La config se règle dans `appsettings.json`
(ou par variables d'environnement en prod, voir le module NixOS dans `flake.nix`).
## How to deploy the website
Pour peupler une instance locale depuis la prod:
```shell
./deploy
python dev/populate-db/populate.py --source https://ballephenix.com --target http://localhost:5000 --force
```
## Déploiement
Le build et le service NixOS sont décrits dans `flake.nix` (`packages.website` et
`nixosModules.x86_64-linux`). Le module déploie le binaire `Phenix` avec `static/`
et une base de données dans `/var/lib/phenix`.
## Outils d'administration
- `debut_saison.ts` — script Deno qui crée le calendrier de la saison: se connecte
via `GET /login` puis POST chaque partie à `/membre/new-game`. Mettre à jour les
identifiants en haut du fichier et les dates avant de le lancer.
```shell
deno run -A debut_saison.ts
```
-10
View File
@@ -1,10 +0,0 @@
{
"deploy": {
"project": "7a93d03e-eabe-4db1-b78e-5f1bbcb460b0",
"exclude": [
"**/node_modules"
],
"include": [],
"entrypoint": "server.ts"
}
}
Generated
-224
View File
@@ -1,224 +0,0 @@
{
"version": "4",
"specifiers": {
"jsr:@oak/commons@1": "1.0.0",
"jsr:@oak/oak@*": "17.1.4",
"jsr:@std/assert@1": "1.0.13",
"jsr:@std/bytes@1": "1.0.5",
"jsr:@std/crypto@1": "1.0.4",
"jsr:@std/encoding@1": "1.0.10",
"jsr:@std/encoding@^1.0.10": "1.0.10",
"jsr:@std/http@1": "1.0.15",
"jsr:@std/media-types@1": "1.1.0",
"jsr:@std/path@1": "1.0.9",
"npm:path-to-regexp@^6.3.0": "6.3.0"
},
"jsr": {
"@oak/commons@1.0.0": {
"integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac",
"dependencies": [
"jsr:@std/assert",
"jsr:@std/bytes",
"jsr:@std/crypto",
"jsr:@std/encoding@1",
"jsr:@std/http",
"jsr:@std/media-types"
]
},
"@oak/oak@17.1.4": {
"integrity": "60530b582bf276ff741e39cc664026781aa08dd5f2bc5134d756cc427bf2c13e",
"dependencies": [
"jsr:@oak/commons",
"jsr:@std/assert",
"jsr:@std/bytes",
"jsr:@std/http",
"jsr:@std/media-types",
"jsr:@std/path",
"npm:path-to-regexp"
]
},
"@std/assert@1.0.13": {
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29"
},
"@std/bytes@1.0.5": {
"integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e"
},
"@std/crypto@1.0.4": {
"integrity": "cee245c453bd5366207f4d8aa25ea3e9c86cecad2be3fefcaa6cb17203d79340"
},
"@std/encoding@1.0.10": {
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
},
"@std/http@1.0.15": {
"integrity": "435a4934b4e196e82a8233f724da525f7b7112f3566502f28815e94764c19159",
"dependencies": [
"jsr:@std/encoding@^1.0.10"
]
},
"@std/media-types@1.1.0": {
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
},
"@std/path@1.0.9": {
"integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e"
}
},
"npm": {
"path-to-regexp@6.3.0": {
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="
}
},
"remote": {
"https://deno.land/std@0.205.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee",
"https://deno.land/std@0.205.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56",
"https://deno.land/std@0.205.0/html/entities.ts": "43f208131bb847e6e222c9dd50322066491da93ed945a93d778923ef7e975398",
"https://deno.land/std@0.205.0/html/mod.ts": "3f8c71781e32037ab63bd417759d15d31fb9606c6615c85dcabcc515986494ba",
"https://deno.land/std@0.205.0/path/_common/assert_path.ts": "061e4d093d4ba5aebceb2c4da3318bfe3289e868570e9d3a8e327d91c2958946",
"https://deno.land/std@0.205.0/path/_common/basename.ts": "0d978ff818f339cd3b1d09dc914881f4d15617432ae519c1b8fdc09ff8d3789a",
"https://deno.land/std@0.205.0/path/_common/common.ts": "9e4233b2eeb50f8b2ae10ecc2108f58583aea6fd3e8907827020282dc2b76143",
"https://deno.land/std@0.205.0/path/_common/constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
"https://deno.land/std@0.205.0/path/_common/dirname.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397",
"https://deno.land/std@0.205.0/path/_common/format.ts": "11aa62e316dfbf22c126917f5e03ea5fe2ee707386555a8f513d27ad5756cf96",
"https://deno.land/std@0.205.0/path/_common/from_file_url.ts": "ef1bf3197d2efbf0297a2bdbf3a61d804b18f2bcce45548ae112313ec5be3c22",
"https://deno.land/std@0.205.0/path/_common/glob_to_reg_exp.ts": "5c3c2b79fc2294ec803d102bd9855c451c150021f452046312819fbb6d4dc156",
"https://deno.land/std@0.205.0/path/_common/is_glob.ts": "567dce5c6656bdedfc6b3ee6c0833e1e4db2b8dff6e62148e94a917f289c06ad",
"https://deno.land/std@0.205.0/path/_common/normalize.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397",
"https://deno.land/std@0.205.0/path/_common/normalize_string.ts": "88c472f28ae49525f9fe82de8c8816d93442d46a30d6bb5063b07ff8a89ff589",
"https://deno.land/std@0.205.0/path/_common/relative.ts": "1af19d787a2a84b8c534cc487424fe101f614982ae4851382c978ab2216186b4",
"https://deno.land/std@0.205.0/path/_common/strip_trailing_separators.ts": "7ffc7c287e97bdeeee31b155828686967f222cd73f9e5780bfe7dfb1b58c6c65",
"https://deno.land/std@0.205.0/path/_common/to_file_url.ts": "a8cdd1633bc9175b7eebd3613266d7c0b6ae0fb0cff24120b6092ac31662f9ae",
"https://deno.land/std@0.205.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
"https://deno.land/std@0.205.0/path/_os.ts": "30b0c2875f360c9296dbe6b7f2d528f0f9c741cecad2e97f803f5219e91b40a2",
"https://deno.land/std@0.205.0/path/basename.ts": "04bb5ef3e86bba8a35603b8f3b69537112cdd19ce64b77f2522006da2977a5f3",
"https://deno.land/std@0.205.0/path/common.ts": "f4d061c7d0b95a65c2a1a52439edec393e906b40f1caf4604c389fae7caa80f5",
"https://deno.land/std@0.205.0/path/dirname.ts": "88a0a71c21debafc4da7a4cd44fd32e899462df458fbca152390887d41c40361",
"https://deno.land/std@0.205.0/path/extname.ts": "2da4e2490f3b48b7121d19fb4c91681a5e11bd6bd99df4f6f47d7a71bb6ecdf2",
"https://deno.land/std@0.205.0/path/format.ts": "3457530cc85d1b4bab175f9ae73998b34fd456c830d01883169af0681b8894fb",
"https://deno.land/std@0.205.0/path/from_file_url.ts": "e7fa233ea1dff9641e8d566153a24d95010110185a6f418dd2e32320926043f8",
"https://deno.land/std@0.205.0/path/glob.ts": "9c77cf47db1d786e2ebf66670824d03fd84ecc7c807cac24441eb9d5cb6a2986",
"https://deno.land/std@0.205.0/path/is_absolute.ts": "67232b41b860571c5b7537f4954c88d86ae2ba45e883ee37d3dec27b74909d13",
"https://deno.land/std@0.205.0/path/join.ts": "98d3d76c819af4a11a81d5ba2dbb319f1ce9d63fc2b615597d4bcfddd4a89a09",
"https://deno.land/std@0.205.0/path/mod.ts": "2d62a0a8b78a60e8e6f485d881bac6b61d58573b11cf585fb7c8fc50d9b20d80",
"https://deno.land/std@0.205.0/path/normalize.ts": "aa95be9a92c7bd4f9dc0ba51e942a1973e2b93d266cd74f5ca751c136d520b66",
"https://deno.land/std@0.205.0/path/parse.ts": "d87ff0deef3fb495bc0d862278ff96da5a06acf0625ca27769fc52ac0d3d6ece",
"https://deno.land/std@0.205.0/path/posix/_util.ts": "ecf49560fedd7dd376c6156cc5565cad97c1abe9824f4417adebc7acc36c93e5",
"https://deno.land/std@0.205.0/path/posix/basename.ts": "a630aeb8fd8e27356b1823b9dedd505e30085015407caa3396332752f6b8406a",
"https://deno.land/std@0.205.0/path/posix/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b",
"https://deno.land/std@0.205.0/path/posix/dirname.ts": "f48c9c42cc670803b505478b7ef162c7cfa9d8e751b59d278b2ec59470531472",
"https://deno.land/std@0.205.0/path/posix/extname.ts": "ee7f6571a9c0a37f9218fbf510c440d1685a7c13082c348d701396cc795e0be0",
"https://deno.land/std@0.205.0/path/posix/format.ts": "b94876f77e61bfe1f147d5ccb46a920636cd3cef8be43df330f0052b03875968",
"https://deno.land/std@0.205.0/path/posix/from_file_url.ts": "b97287a83e6407ac27bdf3ab621db3fccbf1c27df0a1b1f20e1e1b5acf38a379",
"https://deno.land/std@0.205.0/path/posix/glob.ts": "86c3f06d1c98303613c74650961c3e24bdb871cde2a97c3ae7f0f6d4abbef445",
"https://deno.land/std@0.205.0/path/posix/is_absolute.ts": "159900a3422d11069d48395568217eb7fc105ceda2683d03d9b7c0f0769e01b8",
"https://deno.land/std@0.205.0/path/posix/join.ts": "0c0d84bdc344876930126640011ec1b888e6facf74153ffad9ef26813aa2a076",
"https://deno.land/std@0.205.0/path/posix/mod.ts": "6bfa8a42d85345b12dbe8571028ca2c62d460b6ef968125e498602b43b6cf6b6",
"https://deno.land/std@0.205.0/path/posix/normalize.ts": "11de90a94ab7148cc46e5a288f7d732aade1d616bc8c862f5560fa18ff987b4b",
"https://deno.land/std@0.205.0/path/posix/parse.ts": "199208f373dd93a792e9c585352bfc73a6293411bed6da6d3bc4f4ef90b04c8e",
"https://deno.land/std@0.205.0/path/posix/relative.ts": "e2f230608b0f083e6deaa06e063943e5accb3320c28aef8d87528fbb7fe6504c",
"https://deno.land/std@0.205.0/path/posix/resolve.ts": "51579d83159d5c719518c9ae50812a63959bbcb7561d79acbdb2c3682236e285",
"https://deno.land/std@0.205.0/path/posix/separator.ts": "0b6573b5f3269a3164d8edc9cefc33a02dd51003731c561008c8bb60220ebac1",
"https://deno.land/std@0.205.0/path/posix/to_file_url.ts": "08d43ea839ee75e9b8b1538376cfe95911070a655cd312bc9a00f88ef14967b6",
"https://deno.land/std@0.205.0/path/posix/to_namespaced_path.ts": "c9228a0e74fd37e76622cd7b142b8416663a9b87db643302fa0926b5a5c83bdc",
"https://deno.land/std@0.205.0/path/relative.ts": "23d45ede8b7ac464a8299663a43488aad6b561414e7cbbe4790775590db6349c",
"https://deno.land/std@0.205.0/path/resolve.ts": "5b184efc87155a0af9fa305ff68a109e28de9aee81fc3e77cd01380f19daf867",
"https://deno.land/std@0.205.0/path/separator.ts": "40a3e9a4ad10bef23bc2cd6c610291b6c502a06237c2c4cd034a15ca78dedc1f",
"https://deno.land/std@0.205.0/path/to_file_url.ts": "edaafa089e0bce386e1b2d47afe7c72e379ff93b28a5829a5885e4b6c626d864",
"https://deno.land/std@0.205.0/path/to_namespaced_path.ts": "cf8734848aac3c7527d1689d2adf82132b1618eff3cc523a775068847416b22a",
"https://deno.land/std@0.205.0/path/windows/_util.ts": "f32b9444554c8863b9b4814025c700492a2b57ff2369d015360970a1b1099d54",
"https://deno.land/std@0.205.0/path/windows/basename.ts": "8a9dbf7353d50afbc5b221af36c02a72c2d1b2b5b9f7c65bf6a5a2a0baf88ad3",
"https://deno.land/std@0.205.0/path/windows/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b",
"https://deno.land/std@0.205.0/path/windows/dirname.ts": "5c2aa541384bf0bd9aca821275d2a8690e8238fa846198ef5c7515ce31a01a94",
"https://deno.land/std@0.205.0/path/windows/extname.ts": "07f4fa1b40d06a827446b3e3bcc8d619c5546b079b8ed0c77040bbef716c7614",
"https://deno.land/std@0.205.0/path/windows/format.ts": "343019130d78f172a5c49fdc7e64686a7faf41553268961e7b6c92a6d6548edf",
"https://deno.land/std@0.205.0/path/windows/from_file_url.ts": "d53335c12b0725893d768be3ac6bf0112cc5b639d2deb0171b35988493b46199",
"https://deno.land/std@0.205.0/path/windows/glob.ts": "0286fb89ecd21db5cbf3b6c79e2b87c889b03f1311e66fb769e6b905d4142332",
"https://deno.land/std@0.205.0/path/windows/is_absolute.ts": "245b56b5f355ede8664bd7f080c910a97e2169972d23075554ae14d73722c53c",
"https://deno.land/std@0.205.0/path/windows/join.ts": "e6600bf88edeeef4e2276e155b8de1d5dec0435fd526ba2dc4d37986b2882f16",
"https://deno.land/std@0.205.0/path/windows/mod.ts": "c3d1a36fbf9f6db1320bcb4fbda8de011d25461be3497105e15cbea1e3726198",
"https://deno.land/std@0.205.0/path/windows/normalize.ts": "9deebbf40c81ef540b7b945d4ccd7a6a2c5a5992f791e6d3377043031e164e69",
"https://deno.land/std@0.205.0/path/windows/parse.ts": "120faf778fe1f22056f33ded069b68e12447668fcfa19540c0129561428d3ae5",
"https://deno.land/std@0.205.0/path/windows/relative.ts": "026855cd2c36c8f28f1df3c6fbd8f2449a2aa21f48797a74700c5d872b86d649",
"https://deno.land/std@0.205.0/path/windows/resolve.ts": "5ff441ab18a2346abadf778121128ee71bda4d0898513d4639a6ca04edca366b",
"https://deno.land/std@0.205.0/path/windows/separator.ts": "ae21f27015f10510ed1ac4a0ba9c4c9c967cbdd9d9e776a3e4967553c397bd5d",
"https://deno.land/std@0.205.0/path/windows/to_file_url.ts": "8e9ea9e1ff364aa06fa72999204229952d0a279dbb876b7b838b2b2fea55cce3",
"https://deno.land/std@0.205.0/path/windows/to_namespaced_path.ts": "e0f4d4a5e77f28a5708c1a33ff24360f35637ba6d8f103d19661255ef7bfd50d",
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e",
"https://deno.land/std@0.84.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58",
"https://deno.land/std@0.84.0/_util/os.ts": "e282950a0eaa96760c0cf11e7463e66babd15ec9157d4c9ed49cc0925686f6a7",
"https://deno.land/std@0.84.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853",
"https://deno.land/std@0.84.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4",
"https://deno.land/std@0.84.0/path/_util.ts": "f4fa69aa3cbbd8568763bfc43c7236875015ba343602d8bafd332b4b4243681b",
"https://deno.land/std@0.84.0/path/common.ts": "eaf03d08b569e8a87e674e4e265e099f237472b6fd135b3cbeae5827035ea14a",
"https://deno.land/std@0.84.0/path/glob.ts": "4a524c1c9da3e79a9fdabdc6e850cd9e41bdf31e442856ffa19c5b123268ca95",
"https://deno.land/std@0.84.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12",
"https://deno.land/std@0.84.0/path/posix.ts": "1408f8ba482a4dc5fc0a7cd7be28bbbff9608d2b3b5ffdcf288ae1228d959add",
"https://deno.land/std@0.84.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c",
"https://deno.land/std@0.84.0/path/win32.ts": "6ca052f54500f00cd7a5172fde62900626ab620dcd5bdcf4e6f5695d001ddef6",
"https://deno.land/std@0.99.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58",
"https://deno.land/std@0.99.0/_util/os.ts": "e282950a0eaa96760c0cf11e7463e66babd15ec9157d4c9ed49cc0925686f6a7",
"https://deno.land/std@0.99.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853",
"https://deno.land/std@0.99.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4",
"https://deno.land/std@0.99.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b",
"https://deno.land/std@0.99.0/path/common.ts": "eaf03d08b569e8a87e674e4e265e099f237472b6fd135b3cbeae5827035ea14a",
"https://deno.land/std@0.99.0/path/glob.ts": "314ad9ff263b895795208cdd4d5e35a44618ca3c6dd155e226fb15d065008652",
"https://deno.land/std@0.99.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12",
"https://deno.land/std@0.99.0/path/posix.ts": "f56c3c99feb47f30a40ce9d252ef6f00296fa7c0fcb6dd81211bdb3b8b99ca3b",
"https://deno.land/std@0.99.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c",
"https://deno.land/std@0.99.0/path/win32.ts": "77f7b3604e0de40f3a7c698e8a79e7f601dc187035a1c21cb1e596666ce112f8",
"https://deno.land/x/media_types@v2.7.1/db.ts": "1d95aa3fc1fe076638550640609394f0e157b18b430800e419aa7ece5c151d9c",
"https://deno.land/x/media_types@v2.7.1/deps.ts": "cd4c31e4b5ed8c4d8c61bd2fb38a82d68d3fd2dcf6d43f22511cbe6d9d310e51",
"https://deno.land/x/media_types@v2.7.1/mod.ts": "d63583b978d32eff8b76e1ae5d83cba2fb27baa90cc1bcb0ad15a06122ea8c19",
"https://deno.land/x/static_files@1.1.6/mod.ts": "8a9bb05599e53864c54eb22931fe98507818517e0d69f26e099a1d8f4ef2b638",
"https://deno.land/x/static_files@1.1.6/src/deps.ts": "fb62be7a780339157bf692a619d6d02ea9662490deca275f488f9f9db917fc4a",
"https://deno.land/x/static_files@1.1.6/src/types.ts": "ef201288626294e9e1b8ee85cdfe2cdfb282e4d78af445df1726050dfa0c0988",
"https://deno.land/x/static_files@1.1.6/src/utils.ts": "9e6172dc97fed01e70a6b063c5afff40e20fe726b3bae0c5f0a962bad74b7107",
"https://deno.land/x/vento@v0.9.1/deps.ts": "1fb4acd6b3b6261c308280b09e8feadac7b0fac1e01bd0e0f1e75527a2181a52",
"https://deno.land/x/vento@v0.9.1/mod.ts": "ab690911075158ee3762bd7aefe209cdcb6ead7b492f14b0693d7025244db8e6",
"https://deno.land/x/vento@v0.9.1/plugins/echo.ts": "f7c064fb6d34b29852f46f6e01583ed87656dcbbc5cae51c8f29198d6951d0cf",
"https://deno.land/x/vento@v0.9.1/plugins/escape.ts": "8115ec84c84c13811f4a43263b8954e912df94df6565d6ff9ce7faea7daa50be",
"https://deno.land/x/vento@v0.9.1/plugins/export.ts": "ef54811ed0205aaeef6ad09557c94b8ae98e1457234898da5a1c5461a7dcf424",
"https://deno.land/x/vento@v0.9.1/plugins/for.ts": "6313e139a23ed17ca8b0852031f8d9e11aeb7520b228e68b9233418d25745414",
"https://deno.land/x/vento@v0.9.1/plugins/function.ts": "cdf610a98493e4a093c53473abdad7d47cbcff40aa8362d280e39910620cb8d6",
"https://deno.land/x/vento@v0.9.1/plugins/if.ts": "9de295f8675cacaec7866ad21068b6b089bfcadf72d3c62e1df50ca378c04279",
"https://deno.land/x/vento@v0.9.1/plugins/import.ts": "406204ab9d785db206da5d5cbcffa9a111cef2a39a19574e630cdea89b0691c5",
"https://deno.land/x/vento@v0.9.1/plugins/include.ts": "a9c65ce90354766220c0c9f7b21ea2db97bed99d9ffbe28f8a5ac035d488b163",
"https://deno.land/x/vento@v0.9.1/plugins/js.ts": "2c50d60061e9c5b6280a145266e617181534b8767e91127d59d5cf5febdcfb03",
"https://deno.land/x/vento@v0.9.1/plugins/layout.ts": "b9970b762ba73cf5605e63e585792803a48e326621c6cb2ec216502ba21a90cd",
"https://deno.land/x/vento@v0.9.1/plugins/set.ts": "016235a906f99cbfab78d29dfdfe8ca0925db7a46458fd873adacd0d7c6ed82d",
"https://deno.land/x/vento@v0.9.1/plugins/unescape.ts": "946512835a409815d6ad98ac4529440e9a368f42ca499791d0f25ee0ecc4c4d6",
"https://deno.land/x/vento@v0.9.1/src/environment.ts": "9c28fbf05176297adb93fc688711125b295c69089554d4211cd38a5b04028ec5",
"https://deno.land/x/vento@v0.9.1/src/loader.ts": "eb01b0dca7ea8bcdcdfd30b1f90f2fad28fb7f2cfd943900322a85bfaa86130c",
"https://deno.land/x/vento@v0.9.1/src/tokenizer.ts": "e9918ac58f941e048fe08cea10e65fb6e19c00fe7ed31fb050663445f11c6e9d"
}
}
-1
View File
@@ -1 +0,0 @@
exec deno run -A jsr:@deno/deployctl deploy --prod --project=phenix server.ts
-237
View File
@@ -1,237 +0,0 @@
import { Application } from "jsr:@oak/oak/application";
import { Router } from "jsr:@oak/oak/router";
import { GameService } from "./server/game-service.ts";
import { Game } from "./server/types.ts";
import staticFiles from "https://deno.land/x/static_files@1.1.6/mod.ts";
import vento from "https://deno.land/x/vento@v0.9.1/mod.ts";
import { LoginService } from "./server/login-service.ts";
const env = vento();
setInterval(() => env.cache.clear(), 1000);
const gameService = new GameService(await Deno.openKv());
const loginService = LoginService.officialLoginService();
const router = new Router();
router.get("/", async (ctx) => {
const template = await env.load("templates/home.vto");
const result = await template({ tab: "home" });
ctx.response.body = result.content;
});
router.get("/calendrier", async (ctx) => {
const template = await env.load("templates/calendrier.vto");
const result = await template({
tab: "calendrier",
});
ctx.response.body = result.content;
});
router.get("/nouvelles", async (ctx) => {
const template = await env.load("templates/nouvelles.vto");
const result = await template({ tab: "nouvelles" });
ctx.response.body = result.content;
});
router.get("/contact", async (ctx) => {
const template = await env.load("templates/contact.vto");
const result = await template({ tab: "contact" });
ctx.response.body = result.content;
});
router.get("/reglement", async (ctx) => {
const template = await env.load("templates/reglement.vto");
const result = await template({ tab: "reglement" });
ctx.response.body = result.content;
});
router.get("/login", async (ctx) => {
const username = ctx.request.url.searchParams.get("username") || "";
const password = ctx.request.url.searchParams.get("password") || "";
if (!(await loginService.validateCredentials(username, password))) {
ctx.response.status = 403;
return;
}
await loginService.login(ctx);
ctx.response.redirect("/membre/games-editor/today");
});
const memberRouter = new Router();
memberRouter.use(async (ctx, next) => {
const isLoggedIn = await loginService.isLoggedIn(ctx);
if (ctx.request.url.pathname !== "/membre" && !isLoggedIn) {
ctx.response.redirect("/membre");
return;
}
ctx.params.isLoggedIn = isLoggedIn;
await next();
});
memberRouter.get("/", async (ctx) => {
const template = await env.load("templates/membre.vto");
const result = await template({
tab: "membre",
isLoggedIn: ctx.params.isLoggedIn,
});
ctx.response.body = result.content;
});
function today(): Date {
const date = new Date().toLocaleDateString("fr-CA", {
timeZone: "America/New_York",
});
return new Date(date);
}
memberRouter.get("/games-editor/today", async (ctx) => {
const template = await env.load("templates/partials/games-editor.vto");
const games = await gameService.getAllGames();
const lastGameDay = games
.map((g) => g.date)
.filter((d) => new Date(d) <= today())
.sort()
.reverse()[0];
const result = await template({
tab: "membre",
selectedYear: null,
isToday: true,
games: games.filter((g) => g.date === lastGameDay),
years: [...new Set(games.map((x) => x.year))].sort().reverse(),
});
ctx.response.body = result.content;
});
memberRouter.get("/games-editor/:year", async (ctx) => {
const template = await env.load("templates/partials/games-editor.vto");
const games = await gameService.getAllGames();
const selectedYear = parseInt(ctx.params.year);
const result = await template({
tab: "membre",
selectedYear: selectedYear,
games: games.filter((g) => g.year === selectedYear),
years: [...new Set(games.map((x) => x.year))].sort().reverse(),
});
ctx.response.body = result.content;
});
memberRouter.get("/new-game-editor", async (ctx) => {
const template = await env.load("templates/partials/new-games-editor.vto");
const result = await template({
tab: "membre",
});
ctx.response.body = result.content;
});
memberRouter.post("/new-game", async (ctx) => {
const payload = await ctx.request.body.form();
const preGame = {
date: payload.get("game-date"),
time: payload.get("game-time"),
away: payload.get("away-team"),
home: payload.get("home-team"),
};
const [game, error] = await gameService.createNewGame(preGame);
if (error !== "") {
const template = await env.load("templates/partials/new-game-error.vto");
const result = await template({
tab: "membre",
error: error,
});
ctx.response.status = 500;
ctx.response.body = result.content;
return;
}
const template = await env.load("templates/partials/single-game-editor.vto");
const result = await template({
tab: "membre",
game: game,
});
ctx.response.headers.set("game-id", game?.id ?? "");
ctx.response.body = result.content;
});
memberRouter.delete("/games/:gameId", async (ctx) => {
if (!(await loginService.isLoggedIn(ctx))) {
ctx.response.redirect("/membre");
return;
}
const success = await gameService.deleteGameById(ctx.params.gameId);
ctx.response.body = `${success}`;
});
memberRouter.post("/games/:gameId", async (ctx) => {
if (!(await loginService.isLoggedIn(ctx))) {
ctx.response.redirect("/membre");
return;
}
const payload = await ctx.request.body.form();
let preGame = {
awayScore: payload.get("away-score") ?? null,
homeScore: payload.get("home-score") ?? null,
rain: payload.get("game-type") === "rain",
practice: payload.get("game-type") === "practice",
};
preGame = Object.fromEntries(
Object.entries(preGame).filter(([_, v]) => v !== null),
) as Partial<Game>;
const success = await gameService.updateGame(ctx.params.gameId, preGame);
ctx.response.body = success
? `<i class="fa fa-floppy-o" aria-hidden="true"></i>`
: `<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>`;
});
memberRouter.post("/games/:gameId/switch", async (ctx) => {
if (!(await loginService.isLoggedIn(ctx))) {
ctx.response.redirect("/membre");
return;
}
const success = await gameService.switchVisitors(ctx.params.gameId);
if (!success) {
ctx.response.body = `<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>`;
}
const template = await env.load("templates/partials/single-game-editor.vto");
const game = await gameService.getGameById(ctx.params.gameId);
const result = await template({
tab: "membre",
game: game,
});
ctx.response.headers.set("game-id", game?.id ?? "");
ctx.response.body = result.content;
});
const apiRouter = new Router();
apiRouter.get("/results/:year.json", async (ctx) => {
ctx.response.json = true;
const games = await gameService.getAllGames();
ctx.response.body = games
.filter((g) => g.year == ctx.params.year)
.map((g) => {
g[g.home] = parseInt(g.homeScore);
g[g.away] = parseInt(g.awayScore);
return g;
});
});
apiRouter.get("/available-years.json", async (ctx) => {
ctx.response.json = true;
const games = await gameService.getAllGames();
ctx.response.body = [...new Set(games.map((g) => g.year))];
});
router.use("/membre", memberRouter.routes());
router.use("/api", apiRouter.routes());
const app = new Application();
app.use(router.routes());
app.use(staticFiles("static", { brotli: true, fetch: true }));
await app.listen({ port: 8000 });
-340
View File
@@ -1,340 +0,0 @@
import { Game, PreGame } from "./types.ts";
import {
assertEquals,
assertNotEquals,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
type GamePrimaryKey = ["phenix", "games", string];
type date = [number, number, number];
type time = [number, number];
type DateIndex = ["phenix", "indexes", "game-date", ...date, ...time];
export class GameService {
readonly kv: Deno.Kv;
private readonly DATE_VALIDATOR =
/^(?<year>\d{4})-(?<month>\d{1,2})-(?<day>\d{1,2})$/;
private readonly TIME_VALIDATOR = /^(?<hour>\d{1,2})(:|h)(?<minute>\d{1,2})$/;
private readonly VALID_TEAMS: string[] = [
"jaune",
"rouge",
"bleu",
"gris",
"vert",
"noir",
"orange",
"mauve",
];
constructor(kv: Deno.Kv) {
this.kv = kv;
}
private chronologicalGameOrder(game1: Game, game2: Game): number {
if (game1.date < game2.date) {
return -1;
}
if (game1.date > game2.date) {
return 1;
}
if (game1.time < game2.time) {
return -1;
}
if (game1.time > game2.time) {
return 1;
}
return 0;
}
async getAllGames(): Promise<Game[]> {
const ite = this.kv.list<Game>({ prefix: ["phenix", "games"] });
return (await Array.fromAsync(ite))
.map((e) => e.value)
.sort(this.chronologicalGameOrder);
}
private gamePrimaryKey(id: string): GamePrimaryKey {
return ["phenix", "games", id];
}
private dateIndex(date: string, time: string): DateIndex | null {
let destructedDate = this.DATE_VALIDATOR.exec(date)?.groups;
let destructedTime = this.TIME_VALIDATOR.exec(time)?.groups;
if (!destructedDate || !destructedTime) {
return null;
}
return [
"phenix",
"indexes",
"game-date",
parseInt(destructedDate.year),
parseInt(destructedDate.month),
parseInt(destructedDate.day),
parseInt(destructedTime.hour),
parseInt(destructedTime.minute),
];
}
private cleanTime(raw: string): string {
const groups = this.TIME_VALIDATOR.exec(raw)!.groups!;
return `${groups.hour.padStart(2, "0")}:${groups.minute.padStart(2, "0")}`;
}
private cleanDate(raw: string): string {
const groups = this.DATE_VALIDATOR.exec(raw)!.groups!;
return `${groups.year}-${groups.month.padStart(2, "0")}-${groups.day.padStart(2, "0")}`;
}
private extractYearFromDate(date: string): number {
return parseInt(this.DATE_VALIDATOR.exec(date)!.groups!.year);
}
private createGame(preGame: PreGame): Game {
const ret = {
...preGame,
id: crypto.randomUUID(),
practice: false,
rain: false,
year: this.extractYearFromDate(preGame.date),
homeScore: null,
awayScore: null,
};
ret.time = this.cleanTime(ret.time);
ret.date = this.cleanDate(ret.date);
return ret;
}
private validatePregate(preGame: PreGame): string {
if (!this.DATE_VALIDATOR.test(preGame.date)) {
return `Invalide date format ${preGame.date}`;
}
if (!this.TIME_VALIDATOR.test(preGame.time)) {
return `Invalide time format ${preGame.time}`;
}
if (!this.VALID_TEAMS.includes(preGame.away)) {
return `Invalide away format ${preGame.away}`;
}
if (!this.VALID_TEAMS.includes(preGame.home)) {
return `Invalide home format ${preGame.home}`;
}
if (preGame.home === preGame.away) {
return `Home and away teams must be different (received ${preGame.home} for both)`;
}
return "";
}
async createNewGame(preGame: PreGame): Promise<[Game | null, string]> {
if (this.validatePregate(preGame) !== "") {
return [null, this.validatePregate(preGame)];
}
const game = this.createGame(preGame);
const dateIndex = this.dateIndex(game.date, game.time);
if (!dateIndex) {
return [null, "Cound not parse date or time"];
}
const precondition = await this.kv.get<string>(dateIndex);
if (precondition.value !== null) {
return [null, "game already exists " + precondition.value];
}
const res = await this.kv
.atomic()
.check(precondition)
.set(this.gamePrimaryKey(game.id), game)
.set(dateIndex, game.id)
.commit();
if (!res.ok) {
return [null, "Transaction failed"];
}
return [game, ""];
}
async getGameById(id: string): Promise<Game | null> {
const res = await this.kv.get<Game>(this.gamePrimaryKey(id));
return res.value;
}
async deleteGameById(id: string): Promise<boolean> {
const precond = await this.kv.get<Game>(this.gamePrimaryKey(id));
if (precond.value === null) {
return false;
}
const res = await this.kv
.atomic()
.check(precond)
.delete(this.gamePrimaryKey(precond.value.id))
.delete(this.dateIndex(precond.value.date, precond.value.time)!)
.commit();
return res.ok;
}
async switchVisitors(id: string): Promise<boolean> {
const game = await this.getGameById(id);
if (game === null) {
return false;
}
game.id = "";
const [home, away] = [game.home, game.away];
const [homeScore, awayScore] = [game.homeScore, game.awayScore];
game.home = away;
game.away = home;
game.homeScore = awayScore;
game.awayScore = homeScore;
console.log(`game ${game}`);
return this.updateGame(id, game);
}
async updateGame(id: string, partial: Partial<Game>): Promise<boolean> {
delete partial.id;
const precond = await this.kv.get<Game>(this.gamePrimaryKey(id));
if (precond.value === null) {
return false;
}
const res = await this.kv
.atomic()
.check(precond)
.delete(this.gamePrimaryKey(precond.value.id))
.delete(this.dateIndex(precond.value.date, precond.value.time)!)
.set(
this.dateIndex(precond.value.date, precond.value.time)!,
precond.value.id,
)
.set(this.gamePrimaryKey(precond.value.id), {
...precond.value,
...partial,
})
.commit();
return res.ok;
}
}
Deno.test("saving simple games", async (t) => {
const kv = await Deno.openKv();
const service = new GameService(kv);
async function deleteAllKeys() {
const keys = kv.list({ prefix: [] });
for await (const entry of keys) {
await kv.delete(entry.key);
}
}
function validePreGame(seed: number): PreGame {
let year = ((645 * seed + 162) % 10000) + 1;
let month = ((335 * seed + 71) % 12) + 1;
let day = ((72625 * seed + 176) % 28) + 1;
return {
date: `${(year + "").padStart(4, "0")}-${(month + "").padStart(2, "0")}-${(day + "").padStart(2, "0")}`,
time: "18:45",
away: "jaune",
home: "rouge",
};
}
await deleteAllKeys();
await t.step("creating games", async () => {
const preGame = validePreGame(7276);
const [game, error] = await service.createNewGame(preGame);
assertEquals(error, "");
const ret = await service.getGameById(game!.id);
assertEquals(game, ret);
});
await deleteAllKeys();
await t.step("creating games with old date format", async () => {
const preGame = validePreGame(7276);
preGame.date = "2017-8-5";
const [game, error] = await service.createNewGame(preGame);
assertEquals(error, "");
assertEquals(game?.date, "2017-08-05");
});
await deleteAllKeys();
await t.step("creating games with old time format", async () => {
const preGame = validePreGame(7276);
preGame.time = "9:00";
const [game, error] = await service.createNewGame(preGame);
assertEquals(error, "");
assertEquals(game?.time, "09:00");
});
await deleteAllKeys();
await t.step(
"saving two games in the same timeslot should fail",
async () => {
const preGame = validePreGame(7276);
let [_, error] = await service.createNewGame(preGame);
assertEquals(error, "");
[_, error] = await service.createNewGame(preGame);
assertNotEquals(error, "");
},
);
await deleteAllKeys();
await t.step("saving games with bad fields", async () => {
let i = 0;
for (const key of Object.keys(validePreGame(7276))) {
let [_, error] = await service.createNewGame({
...validePreGame(7262 + i),
[key]: `Broken ${key}`,
});
assertNotEquals(error, "");
i++;
}
});
await t.step("saving games with the same home and away teams", async () => {
const preGame = validePreGame(7276);
preGame.home = preGame.away;
let [_, error] = await service.createNewGame(preGame);
assertNotEquals(error, "");
});
await deleteAllKeys();
await t.step("listing games", async () => {
for (let i = 0; i < 10; i++) {
await service.createNewGame(validePreGame(666 + i));
}
assertEquals((await service.getAllGames()).length, 10);
});
await deleteAllKeys();
await t.step("deleting games", async () => {
let [game, error] = await service.createNewGame(validePreGame(8992));
await service.deleteGameById(game!.id);
assertEquals((await service.getAllGames()).length, 0);
[game, error] = await service.createNewGame(validePreGame(8992));
assertEquals(error, "");
});
await deleteAllKeys();
await t.step("updating game scores", async () => {
let [game, _] = await service.createNewGame(validePreGame(8992));
const ret = await service.updateGame(game!.id, {
homeScore: 10,
awayScore: 32,
});
assertEquals(ret, true);
game = await service.getGameById(game!.id);
assertEquals(game!.homeScore, 10);
assertEquals(game!.awayScore, 32);
});
await deleteAllKeys();
await kv.close();
});
-116
View File
@@ -1,116 +0,0 @@
import {
assert,
assertFalse,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
type User = { username: string; passwordHashWithSalt: string };
const officialUsers: User[] = [
{
username: "marqueur",
passwordHashWithSalt:
"34373780cc542a1eddb6a08488ec149323b94d66025421acd98ffc20eb5055a0:6f2fa05777d45a2da9db79b7fd11e3bc69eb1807642ccb53d41a0b9eb71a3311",
},
{
username: "comite",
passwordHashWithSalt:
"8649feff48a50f53448e59ce480db97b5549eef1d92c886c49b5a14ee22648e5:a0017b879c8834d2f34938e7a4fa3f4c7be8f70545b9bb22a28cf56ab2eb26d9",
},
];
export class LoginService {
private readonly users: User[];
private readonly sessionKey: string;
constructor(users: User[], sessionKey: string) {
this.users = users;
this.sessionKey = sessionKey;
}
public static officialLoginService(): LoginService {
return new LoginService(
officialUsers,
Deno.env.get("SESSION_KEY") || "dev-only",
);
}
private async hashMessage(message: string): Promise<string> {
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8); // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join(""); // convert bytes to hex string
return hashHex;
}
private async shittyHmac(message: string, salt: string): Promise<string> {
const h1 = await this.hashMessage(message + salt);
const h2 = await this.hashMessage(salt + message);
return await this.hashMessage(h1 + h2);
}
async hashPassword(password: string): Promise<string> {
const salt = await this.hashMessage(`${Math.random()}`);
const hash = await this.shittyHmac(password, salt);
return `${salt}:${hash}`;
}
async verifyPassword(
password: string,
passwordHashWithSalt: string,
): Promise<boolean> {
const [passwordSalt, passwordHash] = passwordHashWithSalt.split(":");
return passwordHash === (await this.shittyHmac(password, passwordSalt));
}
async validateCredentials(
username: string,
password: string,
): Promise<boolean> {
for (const user of this.users) {
if (user.username === username) {
return this.verifyPassword(password, user.passwordHashWithSalt);
}
}
return false;
}
async login(ctx: any): Promise<void> {
const nDays = 90;
const maxAgesInSecond = nDays * 24 * 3600;
const maxUnixEpoch = Math.floor(Date.now() / 1000) + maxAgesInSecond;
await ctx.cookies.set("phenix-session", `${maxUnixEpoch}`, {
maxAge: maxAgesInSecond,
});
await ctx.cookies.set(
"phenix-session-sign",
await this.shittyHmac(`${maxUnixEpoch}`, this.sessionKey),
{
maxAge: maxAgesInSecond,
},
);
}
async isLoggedIn(ctx: any): Promise<boolean> {
const expiration = await ctx.cookies.get("phenix-session");
const signature = await ctx.cookies.get("phenix-session-sign");
if (signature !== (await this.shittyHmac(expiration, this.sessionKey))) {
return false;
}
if (!expiration || !/^\d+$/.test(expiration)) {
return false;
}
return Date.now() < parseInt(expiration) * 1000;
}
}
Deno.test("login service", async (t) => {
await t.step("hashing password", async () => {
const service = new LoginService([], "ss");
const passwordHashWithSalt = await service.hashPassword("qwerty");
assert(await service.verifyPassword("qwerty", passwordHashWithSalt));
assertFalse(await service.verifyPassword("ytrewq", passwordHashWithSalt));
});
});
-19
View File
@@ -1,19 +0,0 @@
export type Game = {
id: string;
date: string;
time: string;
home: string;
away: string;
year: number;
homeScore: number | null;
awayScore: number | null;
practice: boolean;
rain: boolean;
};
export type PreGame = {
date: string;
time: string;
home: string;
away: string;
};
-8
View File
@@ -1,8 +0,0 @@
{{ layout "templates/container.vto" }}
<div id="mithril-content"></div>
<script type=module src="scripts/calendar.js"></script>
{{ /layout }}
-35
View File
@@ -1,35 +0,0 @@
{{ layout "templates/container.vto" }}
<div class="uk-width-large-2-5 uk-width-medium-4-5 uk-width-small-1-1 uk-container-center">
<div class="uk-grid uk-margin">
<div class="uk-width-1-1">
<div style="height: 300px; width: 300px; background-size: 300px; background-image: url(images/logosansfond.JPG);" class="uk-cover-background uk-container-center"></div>
</div>
</div>
<div class="div uk-panel uk-panel-box uk-panel-box-secondary uk-container-center uk-margin">
<table class="uk-table">
<tr>
<td class="uk-text-right"><i class="fa fa-desktop"></i></td>
<td><a href="mailto:sportsphenixmtl@gmail.com">sportsphenixmtl@gmail.com</a></td>
</tr>
<tr>
<td class="uk-text-right"><i class="fa fa-phone"></i></td>
<td> (514) 838-8950</td>
</tr>
<tr>
<td class="uk-text-right"><i class="fa fa-envelope"></i></td>
<td>
<p> 10454 Sylvain-Garneau<br>Montréal, Québec H1C 0A2</p>
</td>
</tr>
<tr>
<td class="uk-text-right"><i class="fa fa-gitlab"></i></td>
<td>
<p> Le site web est ouvert et accessible sur <a href="https://gitlab.com/ballephenix/website">gitlab.</a></p>
</td>
</tr>
</table>
</div>
</div>
{{ /layout }}
-102
View File
@@ -1,102 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Balle Phenix</title>
<link rel="shortcut icon" href="/images/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="scripts/jquery-3.7.1.min.js"></script>
<script src="scripts/uikit.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/htmx.org@2.0.0"></script>
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<nav class="uk-navbar uk-navbar-attached">
<ul class="uk-navbar-nav uk-visible-large uk-navbar-content uk-navbar-center">
<li class="uk-parent">
<div class="uk-navbar-brand"><a href="https://www.facebook.com/SportsPhenixMtl"><i class="fa fa-facebook-square"></i></a></div>
</li>
<li class="{{ if it.tab === 'home'}}uk-active{{ /if }}">
<a href="/">
<div><i class="fa fa-home"></i> Accueil</div>
</a>
</li>
<li class="{{ if it.tab === 'nouvelles'}}uk-active{{ /if }}">
<a href="/nouvelles">
<div><i class="fa fa-newspaper-o"></i> Nouvelles</div>
</a>
</li>
<li class="{{ if it.tab === 'calendrier'}}uk-active{{ /if }}">
<a href="/calendrier">
<div><i class="fa fa-calendar"></i> Calendrier</div>
</a>
</li>
<li class="{{ if it.tab === 'reglement'}}uk-active{{ /if }}">
<a href="/reglement">
<div><i class="fa fa-legal"></i> Règlement</div>
</a>
</li>
<li class="{{ if it.tab === 'membre'}}uk-active{{ /if }}">
<a href="/membre">
<div><i class="fa fa-lock"></i> Membre</div>
</a>
</li>
<li class="{{ if it.tab === 'contact'}}uk-active{{ /if }}">
<a href="/contact">
<div><i class="fa fa-phone"></i> Nous Joindre</div>
</a>
</li>
</ul>
<ul class="uk-navbar-nav uk-visible-medium">
<li class="uk-parent">
<div class="uk-navbar-brand"><a href="https://www.facebook.com/SportsPhenixMtl"><i class="fa fa-facebook-square"></i></a></div>
</li>
<li class="{{ if tab === 'home'}}uk-active{{ /if }}"><a href="/"><i class="fa fa-home"></i> Accueil </a></li>
<li class="{{ if tab === 'nouvelles'}}uk-active{{ /if }}"><a href="/nouvelles"><i class="fa fa-newspaper-o"></i> Nouvelles</a></li>
<li class="{{ if it.tab === 'calendrier'}}uk-active{{ /if }}"><a href="/calendrier"><i class="fa fa-calendar"></i> Calendrier</a></li>
<li class="{{ if it.tab === 'reglement'}}uk-active{{ /if }}"><a href="/reglement"><i class="fa fa-legal"></i> Règlement</a></li>
<li class="{{ if it.tab === 'membre'}}uk-active{{ /if }}"><a href="/membre"><i class="fa fa-lock"></i> Membre</a></li>
<li class="{{ if it.tab === 'contact'}}uk-active{{ /if }}"><a href="/contact"><i class="fa fa-phone"></i> Nous Joindre</a></li>
</ul>
<ul class="uk-navbar-nav uk-visible-small">
<li class="uk-parent">
<div class="uk-navbar-brand"><a href="https://www.facebook.com/SportsPhenixMtl"><i class="fa fa-facebook-square"></i></a></div>
</li>
<li data-uk-dropdown="" aria-expanded="" class="uk-parent"><a><i class="fa fa-bars"></i> Menu</a>
<div class="uk-dropdown">
<ul class="uk-nav uk-nav-navbar uk-dropdown-navbar">
<li class="uk-active"><a href="/"><i class="fa fa-home"></i> Accueil</a></li>
<li class=""><a href="/nouvelles"><i class="fa fa-newspaper-o"></i> Nouvelles</a></li>
<li class=""><a href="/calendrier"><i class="fa fa-calendar"></i> Calendrier</a></li>
<li class=""><a href="/reglement"><i class="fa fa-legal"></i> Règlement</a></li>
<li class=""><a href="/membre"><i class="fa fa-lock"></i> Membre</a></li>
<li class=""><a href="/contact"><i class="fa fa-phone"></i> Nous Joindre</a></li>
</ul>
</div>
</li>
</ul>
</nav>
{{ content }}
<footer class="uk-grid ph-footer" style="padding: 40px;">
<div class="uk-container uk-container-center uk-text-center">
<div class="uk-panel"><span class="uk-hidden-small">Ligue de balle les Phénix... pour le plaisir de jouer!</span>
<h4 class="uk-visible-small">Ligue de balle les Phénix</h4>
</div>
<ul class="uk-subnav uk-subnav-line uk-flex-center">
<li><a href="mailto:sportsphenixmtl@gmail.com">sportsphenixmtl@gmail.com</a></li>
</ul>
<h1><a href="https://www.facebook.com/SportsPhenixMtl"><i class="fa fa-facebook-square"></i></a></h1>
</div>
</footer>
</body>
</html>
-108
View File
@@ -1,108 +0,0 @@
{{ layout "templates/container.vto" }}
<script type="module" src="scripts/little-calendar.js"></script>
<div class="uk-grid">
<div class="uk-large-1-3 uk-medium-2-3 uk-container-center">
<div style="height:300px; width: 300px; background-size: 300px; background-image: url(images/logosansfond.JPG);" class="uk-cover-background uk-container-center uk-margin-left uk-margin-top"></div>
</div>
</div>
<div class="uk-margin-top uk-margin-bottom uk-grid">
<div class="uk-width-large-3-5 uk-width-medium-4-5 uk-width-small-1-1 uk-container-center">
<div class="uk-grid uk-visible-large uk-grid-match">
<div class="uk-width-large-1-3 uk-width-medium-1-3">
<div class="uk-panel uk-panel-box">
<div class="uk-panel-badge uk-badge">
<i class="fa fa-newspaper-o" style="font-size: 1.7em;"></i>
</div>
<div>
{{ include "templates/includes/home-news.vto" }}
</div>
</div>
</div>
<div class="uk-width-large-1-3 uk-width-medium-1-3">
<div class="uk-panel uk-panel-box">
<div class="uk-panel-badge uk-badge"><i class="fa fa-calendar" style="font-size: 1.7em;"></i></div>
<h3 class="uk-panel-title"><a href="#/calendrier" class="uk-link-muted">Calendrier</a></h3>
<div id="large-mithril-container"></div>
</div>
</div>
<div class="uk-width-large-1-3 uk-width-medium-1-3">
<div class="uk-panel uk-panel-box">
<div class="uk-panel-badge uk-badge"><i class="fa fa-map-o" style="font-size: 1.7em;"></i></div>
<h3 class="uk-panel-title">Parc Walter-Stewart</h3>
{{ include "templates/includes/walter-stewart-map.vto" }}
</div>
</div>
</div>
<!-- start -->
<div class="uk-visible-medium">
<div class="uk-grid uk-grid-match">
<div class="uk-width-medium-1-2">
<div class="uk-panel uk-panel-box">
<div class="uk-panel-badge uk-badge"><i class="fa fa-newspaper-o" style="font-size: 1.7em;"></i></div>
<div>
{{ include "templates/includes/home-news.vto" }}
</div>
</div>
</div>
<div class="uk-width-medium-1-2">
<div class="uk-panel uk-panel-box">
<div class="uk-panel-badge uk-badge"><i class="fa fa-calendar" style="font-size: 1.7em;"></i></div>
<h3 class="uk-panel-title"><a href="#/calendrier" class="uk-link-muted">Calendrier</a></h3>
<div id="medium-mithril-container"></div>
</div>
</div>
</div>
<div class="uk-grid">
<div class="uk-width-medium-1-1">
<div class="uk-panel uk-panel-box">
<div class="uk-panel-badge uk-badge"><i class="fa fa-map-o" style="font-size: 1.7em;"></i></div>
<h3 class="uk-panel-title">Parc Walter-Stewart</h3>
{{ include "templates/includes/walter-stewart-map.vto" }}
</div>
</div>
</div>
</div>
<!-- end -->
<div class="uk-visible-small">
<div class="uk-grid">
<div class="uk-width-small-1-1">
<div class="uk-panel uk-panel-box">
<div class="uk-panel-badge uk-badge"><i class="fa fa-newspaper-o" style="font-size: 1.7em;"></i></div>
<div>
{{ include "templates/includes/home-news.vto" }}
</div>
</div>
</div>
</div>
<div class="uk-grid">
<div class="uk-width-small-1-1">
<div class="uk-panel uk-panel-box">
<div class="uk-panel-badge uk-badge"><i class="fa fa-calendar" style="font-size: 1.7em;"></i></div>
<h3 class="uk-panel-title"><a href="#/calendrier" class="uk-link-muted">Calendrier</a></h3>
<div id="small-mithril-container">
</div>
</div>
</div>
</div>
<div class="uk-grid">
<div class="uk-width-small-1-1">
<div class="uk-panel uk-panel-box">
<div class="uk-panel-badge uk-badge"><i class="fa fa-map-o" style="font-size: 1.7em;"></i></div>
<h3 class="uk-panel-title">Parc Walter-Stewart</h3>
{{ include "templates/includes/walter-stewart-map.vto" }}
</div>
</div>
</div>
</div>
</div>
</div>
{{ /layout }}
-72
View File
@@ -1,72 +0,0 @@
<div id="game-{{ game.id }}">
{{set prettyGameDate = new Date(game.date).toLocaleString("fr-CA", { month: 'long', year: 'numeric', day: 'numeric', timeZone: 'UTC'}) }}
<h3> {{ game.away }} vs {{ game.home }} | {{ prettyGameDate }} | {{ game.time }}</h3>
<form style="font-size: 1.1em;"
hx-trigger="input delay:250ms"
hx-post="/membre/games/{{game.id}}"
hx-target="#saving-result">
<div class="uk-form-row">
<label>
Points {{ game.away }} : <input type=number
min=0
name="away-score"
value="{{ game.awayScore || 0 }}"
{{if game.rain || game.practice }}disabled{{ /if}}>
</label>
</div>
<div class="uk-form-row">
<label>
Points {{ game.home }} : <input type=number
min=0
name="home-score"
value="{{ game.homeScore || 0 }}"
{{if game.rain || game.practice }}disabled{{ /if}}>
</label>
</div>
<div class="uk-form-row">
<label>
Partie reguliere <input type=radio
name="game-type"
value="regular-game"
{{if ! game.practice && ! game.rain }} checked {{/if}}>
</label>
<label>
Pluie <input type=radio
name="game-type"
value="rain"
{{if game.rain }}checked{{/if}}>
</label>
<label>
Pratique <input
type=radio
name="game-type"
value="practice"
{{if game.practice && ! game.rain }}checked{{/if}}>
</label>
</div>
<div id="saving-result">
</div>
</form>
<div style="text-align: right">
<button class="uk-button"
hx-post="/membre/games/{{game.id}}/switch"
hx-target="#game-{{ game.id }}"
>Inverser visiteur/receveur</button>
<button class="uk-button-danger"
hx-delete="/membre/games/{{game.id}}"
hx-confirm="Certain de vouloir supprimer la partie?"
hx-target="#game-{{ game.id }}"
hx-swap="delete"
>Supprimer</button>
</div>
</div>
-10
View File
@@ -1,10 +0,0 @@
<h3 class="uk-panel-title">
<a href="#/nouvelles" class="uk-link-muted">
<h2 id="pre-inscription">Pré-inscription</h2>
</a>
</h3>
<p>
<p>Pré-inscription pour la balle-molle Phénix, ça ne coûte rien et c'est sans prix! <a href="https://forms.gle/Mv2wCVdbP83uiMdC7">https://forms.gle/Mv2wCVdbP83uiMdC7</a></p>
<p><strong><a href="#/nouvelles">lire la suite ...</a></strong></p>
</p>
-10
View File
@@ -1,10 +0,0 @@
{{ set selectedTeam = selectedTeam || "" }}
{{set isDisabled = isDisabled || false }}
{{set teams = ['jaune', 'rouge', 'bleu', 'gris', 'vert', 'noir', 'orange', 'mauve'] }}
<select name="{{- name -}}" class="uk-select" {{if isDisabled}} disabled {{ /if }}>
{{ for team of teams }}
<option name="{{team}}" {{if selectedTeam == team}} selected {{ /if }}>{{ team }}</option>
{{ /for }}
</select>
-15
View File
@@ -1,15 +0,0 @@
<div>
<iframe
style="border: 1px solid black; min-height: 300px;"
width="100%"
frameborder="0"
scrolling="no"
marginheight="0"
src="//www.openstreetmap.org/export/embed.html?bbox=-73.55936229228975%2C45.53102667048665%2C-73.55230271816255%2C45.53444249585723&amp;layer=mapnik"; // "//www.openstreetmap.org/export/embed.html?bbox=-73.56355190277101%2C45.5294483312637%2C-73.54943275451662%2C45.53627996625677&amp;layer=mapnik"; // "//www.openstreetmap.org/export/embed.html?bbox=-73.56355190277101%2C45.5294483312637%2C-73.54943275451662%2C45.53627996625677&amp;layer=mapnik"
></iframe>
<br>
<small>
<a href="http://www.openstreetmap.org/#map=17/45.53216/-73.55692">Voir la carte</a>
</small>
</div>
-28
View File
@@ -1,28 +0,0 @@
{{ layout "templates/container.vto" }}
<div id="content"
{{ if isLoggedIn }}
hx-get="/membre/games-editor/today"
hx-trigger="load"
hx-target="#content"
{{ /if }}
>
<div class="uk-grid uk-margin">
<div class="uk-width-1-1">
<div style="height: 250px; width: 250px; background-size: 250px; background-image: url(images/logosansfond.JPG);" class="uk-cover-background uk-container-center"></div>
</div>
</div>
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-width-large-1-3 uk-container-center uk-margin">
<form action=""
class="uk-text-center"
hx-get="/login"
hx-target="#content">
<div class="uk-form-row"><label>Nom d'utilisateur <input type="text" name="username" class="uk-form-width-small"></label></div>
<div class="uk-form-row"><label>Mot de passe <input type="password" name="password" class="uk-form-width-small"></label></div>
<div class="uk-form-row"><button class="uk-button"><i class="fa fa-mars-double"></i> Pénétrez</button></div>
</form>
</div>
</div>
{{ /layout }}
-69
View File
@@ -1,69 +0,0 @@
{{ layout "templates/container.vto" }}
<div class="uk-width-large-2-5 uk-width-medium-3-5 uk-width-small-1-1 uk-container-center">
<div>
<article class="uk-article uk-margin-">
<h1 class="uk-article-title">
<h2 id="pre-inscription">Pré-inscription</h2>
</h1>
<p class="uk-article-meta">
<p>Samedi 8 Août 2026</p>
</p>
<p>
La pré-inscription pour la balle-molle Phénix est maintenant ouverte. Ça ne coûte rien et c'est sans prix!
</p>
<p>
<a href="https://forms.gle/Mv2wCVdbP83uiMdC7">https://forms.gle/Mv2wCVdbP83uiMdC7</a>
</p>
</article>
</div>
</div>
<div class="uk-width-large-2-5 uk-width-medium-3-5 uk-width-small-1-1 uk-container-center">
<div>
<article class="uk-article uk-margin-">
<h1 class="uk-article-title">
<h2 id="saison-2025">Saison 2026</h2>
</h1>
<p class="uk-article-meta">
<p>Lundi 9 Mars 2026</p>
</p>
<p>
La saison de la balle annonce à grand pas et les inscriptions débuteront lundi prochain (16 mars 2026). Le coût de la saison sera le même que l'an passé: 155$ pour la saison + 20$ de dépôt remboursable pour le prêt du chandail.
</p><p>
Comme à l'habitude, nous jouerons au parc walter-stewart les jeudis soir. La ligue comptera quatre équipes pour un grand total de 44 joueurs.
</p><p>
Les inscriptions en présentiel auront lieu le vendredi 20 mars au bar fullum entre 18h00 et 21h00. Pour ceux qui veulle venir prendre une petite bière avec nous ou payer en argent compten.
</p><p>
On vous tiens au courrant!
</p>
</article>
</div>
</div>
<div class="uk-width-large-2-5 uk-width-medium-3-5 uk-width-small-1-1 uk-container-center">
<div>
<article class="uk-article uk-margin-">
<h1 class="uk-article-title">
<h2 id="saison-2025">Saison 2025</h2>
</h1>
<p class="uk-article-meta">
<p>Vendredi 25 Avril 2025</p>
</p>
<p>Bonne saison à tous et toutes!</p>
<p>N'oubliez pas de consulter la page facebook: <a href="https://www.facebook.com/SportsPhenixMtl/">https://www.facebook.com/SportsPhenixMtl/</a> pour toutes mises à jour, y compris les annulations due à la météo.</p>
<p>Votre Comité 2023</p>
</article>
</div>
</div>
{{ /layout }}
-37
View File
@@ -1,37 +0,0 @@
{{set isToday = !!isToday }}
<div class="uk-width-large-2-3 uk-container-center uk-margin">
<div class="uk-margin" style="margin-top: 1em">
<ul class="uk-tab">
<li {{if isToday}} class="uk-active" {{ /if}}>
<a hx-get="/membre/games-editor/today"
hx-target="#content">
Aujourd'hui
</a>
</li>
{{ for year of years }}
<li {{if year === selectedYear}} class="uk-active" {{ /if}}>
<a hx-get="/membre/games-editor/{{year}}"
hx-target="#content">
Saison {{year}}
</a>
</li>
{{ /for }}
</ul>
</div>
{{ for game of games }}
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-margin">
{{ include "templates/includes/game-editor.vto" {game} }}
</div>
{{ /for }}
<hr>
<button class="uk-button"
hx-get="/membre/new-game-editor"
hx-target="#content">
Nouvelle Partie
</button>
</div>
-7
View File
@@ -1,7 +0,0 @@
<div class="uk-width-large-2-3 uk-container-center uk-margin">
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-margin">
<pre>{{ error }}</pre>
</div>
</div>
-45
View File
@@ -1,45 +0,0 @@
<div class="uk-width-large-2-3 uk-container-center uk-margin">
<div class="uk-margin"></div>
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-margin">
<form hx-post="/membre/new-game"
hx-target="#created-game"
hx-swap="afterbegin">
<div class="uk-form-row" style="text-align: center; font-size: 2em;">
{{ include "templates/includes/team-selector.vto" {name: "away-team"} }}
<label>
Vs.
</label>
{{ include "templates/includes/team-selector.vto" {name: "home-team"}}}
</div>
<div class="uk-form-row">
<label>
Date: <input type=date name="game-date" required>
</label>
</div>
<div class="uk-form-row">
<label>
Heure: <input type="time" name="game-time">
</label>
</div>
<br>
<div style="text-align: right;">
<button class="uk-button">Creer</button>
</div>
<form>
</div>
<div id="created-game"></div>
</div>
@@ -1,7 +0,0 @@
<div class="uk-width-large-2-3 uk-container-center uk-margin">
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-margin">
{{ include "templates/includes/game-editor.vto" {game} }}
</div>
</div>
-42
View File
@@ -1,42 +0,0 @@
{{ layout "templates/container.vto" }}
<div class="uk-margin-top uk-margin-bottom uk-grid">
<div class="uk-width-large-3-5 uk-width-medium-4-5 uk-width-large-1-1 uk-container-center">
<div class="uk-grid">
<div class="uk-width-large-1-3 uk-width-small-1-1">
<div style="height: 250px; width: 250px; background-size: 250px; background-image: url(images/logosansfond.JPG);" class="uk-cover-background uk-container-center"></div>
</div>
<div class="uk-width-large-2-3 uk-width-small-1-1">
<div class="uk-panel uk-panel-box uk-panel-box-secondary">
<h2>Règlements 2021</h2>
<ul>
<li>
<h3><a href="files/regle_de_jeux_2021.pdf">Règles de jeu</a></h3>
</li>
<li>
<h3><a href="files/regles_et_procedures_2021.pdf">Règles et procédures</a></h3>
</li>
<li>
<h3><a href="files/charte_organisationnelle_2021.pdf">Charte Organisationnelle</a></h3>
</li>
<li>
<h3><a href="files/formulaire_inscription_2021.pdf">Formulaire d'inscription</a></h3>
</li>
<li>
<h3><a href="files/code_ethique_2021.pdf">Code d'éthique</a></h3>
</li>
<li>
<h3><a href="http://www.asbm.ca/">Association des Sports de Balle à Montréal (ASBN)</a></h3>
</li>
</ul>
</div>
</div>
</div>
<div class="uk-panel uk-panel-box uk-panel-box-secondary">
<h2>Mission</h2>
<p>La ligue de balle Les Phénix a été fondée en 2004 afin d'offrir aux membres de la communauté gaie et lesbienne et à leurs amis hétéros l'opportunité de s'amuser et de pratiquer un sport dans une ambiance amicale et décontractée. L'ouverture d'esprit, la camaraderie et le plaisir de pratiquer un sport à l'extérieur caractérisent cette ligue.</p>
</div>
</div>
</div>
{{ /layout }}