Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123481d748 | ||
|
|
11031eb5a3 | ||
|
|
e5850a448b | ||
|
|
7162be1f49 | ||
|
|
3413c4f94a | ||
|
|
9d741b8516 | ||
|
|
bb13614d1e | ||
|
|
29ed46aa7f | ||
|
|
ab81a1c4be | ||
|
|
4399d1b434 | ||
|
|
428842affe | ||
|
|
514bb0ac47 | ||
|
|
3bc18baa30 | ||
|
|
3adcdcbd78 | ||
|
|
e6675c0d90 | ||
|
|
fb84c8229c | ||
|
|
0858a3f1e7 | ||
|
|
ce399ce258 | ||
|
|
5a5cc814c3 | ||
|
|
2948e5dabc | ||
|
|
1e874b790d | ||
|
|
8d883b9416 | ||
|
|
74ac5b5ef7 | ||
|
|
7bee9526fc |
No files matched your search
+11
-7
@@ -1,8 +1,12 @@
|
|||||||
node_modules/*
|
# Nix build result
|
||||||
lib/*
|
|
||||||
css/*
|
|
||||||
/api/resultats.json
|
|
||||||
/index.html
|
|
||||||
/main-built.js
|
|
||||||
/api
|
|
||||||
result
|
result
|
||||||
|
|
||||||
|
# .NET
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.db
|
||||||
|
publish/
|
||||||
|
|
||||||
|
.direnv/
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
image: nixos/nix
|
|
||||||
|
|
||||||
workflow:
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH
|
|
||||||
|
|
||||||
pages:
|
|
||||||
stage: deploy
|
|
||||||
script:
|
|
||||||
- mkdir -p "$HOME/.config/nix"
|
|
||||||
- echo 'experimental-features = nix-command flakes' > "$HOME/.config/nix/nix.conf"
|
|
||||||
- nix build
|
|
||||||
- cp -r result/ public
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- public
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == "main"
|
|
||||||
environment: production
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Phenix.Models;
|
||||||
|
using Phenix.Services;
|
||||||
|
|
||||||
|
namespace Phenix.Controllers;
|
||||||
|
|
||||||
|
public record CreateGameRequest(
|
||||||
|
string Date, string Time, string Home, string Away,
|
||||||
|
int? HomeScore = null, int? AwayScore = null,
|
||||||
|
bool Practice = false, bool Rain = false
|
||||||
|
);
|
||||||
|
|
||||||
|
[Route("/api")]
|
||||||
|
public class ApiController : Controller
|
||||||
|
{
|
||||||
|
private readonly IGameService _games;
|
||||||
|
|
||||||
|
public ApiController(IGameService games)
|
||||||
|
{
|
||||||
|
_games = games;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("results/{year}.json")]
|
||||||
|
public async Task<IActionResult> ResultsJson(int year)
|
||||||
|
{
|
||||||
|
var games = await _games.GetAllGames();
|
||||||
|
var results = games
|
||||||
|
.Where(g => g.Year == year)
|
||||||
|
.Select(g => new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["id"] = g.Id,
|
||||||
|
["date"] = g.Date,
|
||||||
|
["time"] = g.Time,
|
||||||
|
["home"] = g.Home,
|
||||||
|
["away"] = g.Away,
|
||||||
|
["year"] = g.Year,
|
||||||
|
[g.Home] = g.HomeScore,
|
||||||
|
[g.Away] = g.AwayScore,
|
||||||
|
["practice"] = g.Practice,
|
||||||
|
["rain"] = g.Rain,
|
||||||
|
});
|
||||||
|
return Json(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("available-years.json")]
|
||||||
|
public async Task<IActionResult> AvailableYearsJson()
|
||||||
|
{
|
||||||
|
var games = await _games.GetAllGames();
|
||||||
|
var years = games.Select(g => g.Year).Distinct().OrderByDescending(y => y);
|
||||||
|
return Json(years);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("games")]
|
||||||
|
public async Task<IActionResult> CreateGame([FromBody] CreateGameRequest req)
|
||||||
|
{
|
||||||
|
var (game, error) = await _games.CreateNewGame(req.Date, req.Time, req.Home, req.Away);
|
||||||
|
if (game is null)
|
||||||
|
return error switch
|
||||||
|
{
|
||||||
|
"game already exists at this date/time" => Conflict(new { error }),
|
||||||
|
_ => BadRequest(new { error }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await _games.UpdateGame(game.Id, req.HomeScore, req.AwayScore, req.Rain, req.Practice);
|
||||||
|
return Created($"/api/games/{game.Id}", game);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("games")]
|
||||||
|
public async Task<IActionResult> ClearGames()
|
||||||
|
{
|
||||||
|
await _games.ClearAllGames();
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Phenix.Services;
|
||||||
|
|
||||||
|
namespace Phenix.Controllers;
|
||||||
|
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
[HttpGet("/")]
|
||||||
|
public IActionResult Index() => View();
|
||||||
|
|
||||||
|
[HttpGet("/calendrier")]
|
||||||
|
public IActionResult Calendrier() => View();
|
||||||
|
|
||||||
|
[HttpGet("/nouvelles")]
|
||||||
|
public IActionResult Nouvelles() => View();
|
||||||
|
|
||||||
|
[HttpGet("/contact")]
|
||||||
|
public IActionResult Contact() => View();
|
||||||
|
|
||||||
|
[HttpGet("/reglement")]
|
||||||
|
public IActionResult Reglement() => View();
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Phenix.Services;
|
||||||
|
|
||||||
|
namespace Phenix.Controllers;
|
||||||
|
|
||||||
|
[Route("/membre")]
|
||||||
|
public class MembreController : Controller
|
||||||
|
{
|
||||||
|
private readonly IGameService _games;
|
||||||
|
private readonly LoginService _login;
|
||||||
|
private const string SessionCookie = "phenix-session";
|
||||||
|
private const string SessionSignCookie = "phenix-session-sign";
|
||||||
|
|
||||||
|
public MembreController(IGameService games, LoginService login)
|
||||||
|
{
|
||||||
|
_games = games;
|
||||||
|
_login = login;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("")]
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
ViewBag.IsLoggedIn = IsLoggedIn();
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("/login")]
|
||||||
|
public IActionResult Login(string? username, string? password)
|
||||||
|
{
|
||||||
|
if (username is null || password is null || !_login.ValidateCredentials(username, password))
|
||||||
|
return StatusCode(403);
|
||||||
|
|
||||||
|
var expiresAt = DateTimeOffset.UtcNow.AddDays(90).ToUnixTimeSeconds();
|
||||||
|
Response.Cookies.Append(SessionCookie, expiresAt.ToString(), new CookieOptions
|
||||||
|
{
|
||||||
|
MaxAge = TimeSpan.FromDays(90),
|
||||||
|
});
|
||||||
|
Response.Cookies.Append(SessionSignCookie, _login.SignSession(expiresAt), new CookieOptions
|
||||||
|
{
|
||||||
|
MaxAge = TimeSpan.FromDays(90),
|
||||||
|
});
|
||||||
|
|
||||||
|
return Redirect("/membre/games-editor/today");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("games-editor/today")]
|
||||||
|
public async Task<IActionResult> GamesEditorToday()
|
||||||
|
{
|
||||||
|
if (!IsLoggedIn()) return Redirect("/membre");
|
||||||
|
var games = await _games.GetAllGames();
|
||||||
|
var today = DateOnly.FromDateTime(TimeZoneInfo.ConvertTime(
|
||||||
|
DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("America/New_York")));
|
||||||
|
|
||||||
|
var lastGameDay = games
|
||||||
|
.Where(g => DateOnly.Parse(g.Date) <= today)
|
||||||
|
.Select(g => g.Date)
|
||||||
|
.OrderByDescending(d => d)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
ViewBag.SelectedYear = (int?)null;
|
||||||
|
ViewBag.IsToday = true;
|
||||||
|
ViewBag.Years = games.Select(g => g.Year).Distinct().OrderByDescending(y => y).ToList();
|
||||||
|
ViewBag.Games = games.Where(g => g.Date == lastGameDay).ToList();
|
||||||
|
return PartialView("GamesEditor");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("games-editor/{year:int}")]
|
||||||
|
public async Task<IActionResult> GamesEditorByYear(int year)
|
||||||
|
{
|
||||||
|
if (!IsLoggedIn()) return Redirect("/membre");
|
||||||
|
var games = await _games.GetAllGames();
|
||||||
|
ViewBag.SelectedYear = year;
|
||||||
|
ViewBag.IsToday = false;
|
||||||
|
ViewBag.Years = games.Select(g => g.Year).Distinct().OrderByDescending(y => y).ToList();
|
||||||
|
ViewBag.Games = games.Where(g => g.Year == year).ToList();
|
||||||
|
return PartialView("GamesEditor");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("new-game-editor")]
|
||||||
|
public IActionResult NewGameEditor()
|
||||||
|
{
|
||||||
|
if (!IsLoggedIn()) return Redirect("/membre");
|
||||||
|
return PartialView();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("new-game")]
|
||||||
|
public async Task<IActionResult> NewGame(string gameDate, string gameTime, string homeTeam, string awayTeam)
|
||||||
|
{
|
||||||
|
if (!IsLoggedIn()) return Redirect("/membre");
|
||||||
|
|
||||||
|
var (game, error) = await _games.CreateNewGame(gameDate, gameTime, homeTeam, awayTeam);
|
||||||
|
|
||||||
|
if (error != "")
|
||||||
|
{
|
||||||
|
ViewBag.Error = error;
|
||||||
|
return StatusCode(500, PartialView("NewGameError"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Response.Headers["game-id"] = game!.Id;
|
||||||
|
return PartialView("SingleGameEditor", game);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("games/{gameId}")]
|
||||||
|
public async Task<IActionResult> DeleteGame(string gameId)
|
||||||
|
{
|
||||||
|
if (!IsLoggedIn()) return Redirect("/membre");
|
||||||
|
var ok = await _games.DeleteGameById(gameId);
|
||||||
|
return Content(ok ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("games/{gameId}")]
|
||||||
|
public async Task<IActionResult> UpdateGame(string gameId, int? awayScore, int? homeScore, string? gameType)
|
||||||
|
{
|
||||||
|
if (!IsLoggedIn()) return Redirect("/membre");
|
||||||
|
var success = await _games.UpdateGame(gameId, homeScore, awayScore,
|
||||||
|
rain: gameType == "rain", practice: gameType == "practice");
|
||||||
|
return Content(success
|
||||||
|
? "<i class=\"fa fa-floppy-o\" aria-hidden=\"true\"></i>"
|
||||||
|
: "<i class=\"fa fa-exclamation-triangle\" aria-hidden=\"true\"></i>");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("games/{gameId}/switch")]
|
||||||
|
public async Task<IActionResult> SwitchVisitors(string gameId)
|
||||||
|
{
|
||||||
|
if (!IsLoggedIn()) return Redirect("/membre");
|
||||||
|
var success = await _games.SwitchVisitors(gameId);
|
||||||
|
if (!success)
|
||||||
|
return Content("<i class=\"fa fa-exclamation-triangle\" aria-hidden=\"true\"></i>");
|
||||||
|
|
||||||
|
var game = await _games.GetGameById(gameId);
|
||||||
|
Response.Headers["game-id"] = gameId;
|
||||||
|
return PartialView("SingleGameEditor", game);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsLoggedIn()
|
||||||
|
{
|
||||||
|
Request.Cookies.TryGetValue(SessionCookie, out var expires);
|
||||||
|
Request.Cookies.TryGetValue(SessionSignCookie, out var sig);
|
||||||
|
return _login.VerifySession(expires, sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Phenix.Models;
|
||||||
|
|
||||||
|
public class Game
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string Date { get; set; } = "";
|
||||||
|
public string Time { get; set; } = "";
|
||||||
|
public string Home { get; set; } = "";
|
||||||
|
public string Away { get; set; } = "";
|
||||||
|
public int Year { get; set; }
|
||||||
|
public int? HomeScore { get; set; }
|
||||||
|
public int? AwayScore { get; set; }
|
||||||
|
public bool Practice { get; set; }
|
||||||
|
public bool Rain { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Phenix.Models;
|
||||||
|
|
||||||
|
public class TeamSelectorModel
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string SelectedTeam { get; set; } = "";
|
||||||
|
public bool IsDisabled { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<RootNamespace>Phenix</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Npgsql" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
using Phenix.Services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
|
||||||
|
{
|
||||||
|
Args = args,
|
||||||
|
WebRootPath = "static",
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddControllersWithViews()
|
||||||
|
.AddRazorOptions(o =>
|
||||||
|
{
|
||||||
|
o.ViewLocationFormats.Clear();
|
||||||
|
o.ViewLocationFormats.Add("/Views/{1}/{0}.cshtml");
|
||||||
|
o.ViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
|
||||||
|
o.ViewLocationFormats.Add("/Views/{0}.cshtml");
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IGameService>(sp =>
|
||||||
|
{
|
||||||
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
|
var connStr = config.GetConnectionString("Default")
|
||||||
|
?? "Data Source=phenix.db";
|
||||||
|
var provider = config["DatabaseProvider"] ?? "Sqlite";
|
||||||
|
|
||||||
|
IGameService svc = provider.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"postgres" or "postgresql" => new PostgresGameService(connStr),
|
||||||
|
_ => new SqliteGameService(connStr),
|
||||||
|
};
|
||||||
|
svc.Init().GetAwaiter().GetResult();
|
||||||
|
return svc;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton(sp =>
|
||||||
|
{
|
||||||
|
var key = sp.GetRequiredService<IConfiguration>()["SessionKey"] ?? "dev-only";
|
||||||
|
return new LoginService(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -1,42 +1,39 @@
|
|||||||
# Site Web de Balle Phénix
|
# Site Web de [Balle Phénix](http://ballephenix.com)
|
||||||

|
|
||||||
|
|
||||||
|
## 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
|
## Développement
|
||||||
en lui-même est accessible au http://ballephenix.com ou au
|
|
||||||
https://ballephenix.gitlab.io/website/.
|
|
||||||
|
|
||||||
### Structure du site web.
|
```shell
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
Premièrement, mille excuses pour mes choix hétéroclites. J'ai décidé d'explorer
|
Base de données SQLite par défaut (`phenix.db`). La config se règle dans `appsettings.json`
|
||||||
de nouvels technologies avec ce projet. En conséquence, le code est un peu
|
(ou par variables d'environnement en prod, voir le module NixOS dans `flake.nix`).
|
||||||
difficile à comprendre à première vue mais il est __tellement__ plus cool!
|
|
||||||
|
|
||||||
|
Pour peupler une instance locale depuis la prod:
|
||||||
|
|
||||||
#### Choix
|
```shell
|
||||||
|
python dev/populate-db/populate.py --source https://ballephenix.com --target http://localhost:5000 --force
|
||||||
|
```
|
||||||
|
|
||||||
* mithril.js comme framework dans le fureteur
|
## Déploiement
|
||||||
* require.js pour les dépendances
|
|
||||||
* uikit avec sass comme framework css
|
|
||||||
* gulp pour la compilation
|
|
||||||
* gitlab pour le deployment
|
|
||||||
|
|
||||||
---
|
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`.
|
||||||
|
|
||||||
# English Version
|
## Outils d'administration
|
||||||
|
|
||||||
All the source code is in english. The source code is for a static web
|
- `debut_saison.ts` — script Deno qui crée le calendrier de la saison: se connecte
|
||||||
side for The softball league 'Balle Phénix'.
|
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.
|
||||||
|
|
||||||
## How to build
|
```shell
|
||||||
|
deno run -A debut_saison.ts
|
||||||
$ npm install
|
```
|
||||||
$ node_modules/.bin/gulp help
|
|
||||||
|
|
||||||
# How it is made
|
|
||||||
|
|
||||||
* mithril.js as client side framework
|
|
||||||
* uikit as css framework
|
|
||||||
* coffee and love
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Phenix.Models;
|
||||||
|
|
||||||
|
namespace Phenix.Services;
|
||||||
|
|
||||||
|
public interface IGameService
|
||||||
|
{
|
||||||
|
Task Init();
|
||||||
|
Task<List<Game>> GetAllGames();
|
||||||
|
Task<Game?> GetGameById(string id);
|
||||||
|
Task<(Game? Game, string Error)> CreateNewGame(string date, string time, string home, string away);
|
||||||
|
Task<bool> UpdateGame(string id, int? homeScore, int? awayScore, bool? rain, bool? practice);
|
||||||
|
Task<bool> DeleteGameById(string id);
|
||||||
|
Task<bool> SwitchVisitors(string id);
|
||||||
|
Task ClearAllGames();
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Phenix.Services;
|
||||||
|
|
||||||
|
public class LoginService
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, string> _users;
|
||||||
|
private readonly string _sessionKey;
|
||||||
|
|
||||||
|
private static readonly (string User, string Hash)[] OfficialUsers =
|
||||||
|
[
|
||||||
|
("marqueur", "34373780cc542a1eddb6a08488ec149323b94d66025421acd98ffc20eb5055a0:6f2fa05777d45a2da9db79b7fd11e3bc69eb1807642ccb53d41a0b9eb71a3311"),
|
||||||
|
("comite", "8649feff48a50f53448e59ce480db97b5549eef1d92c886c49b5a14ee22648e5:a0017b879c8834d2f34938e7a4fa3f4c7be8f70545b9bb22a28cf56ab2eb26d9"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public LoginService(string sessionKey)
|
||||||
|
{
|
||||||
|
_users = OfficialUsers.ToDictionary(x => x.User, x => x.Hash);
|
||||||
|
_sessionKey = sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ValidateCredentials(string username, string password)
|
||||||
|
{
|
||||||
|
if (!_users.TryGetValue(username, out var hashWithSalt))
|
||||||
|
return false;
|
||||||
|
return VerifyPassword(password, hashWithSalt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SignSession(long expiresAt)
|
||||||
|
{
|
||||||
|
return ShittyHmac(expiresAt.ToString(), _sessionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool VerifySession(string? expiresAt, string? signature)
|
||||||
|
{
|
||||||
|
if (expiresAt is null || signature is null)
|
||||||
|
return false;
|
||||||
|
if (!long.TryParse(expiresAt, out var ts))
|
||||||
|
return false;
|
||||||
|
if (signature != ShittyHmac(ts.ToString(), _sessionKey))
|
||||||
|
return false;
|
||||||
|
return DateTimeOffset.UtcNow.ToUnixTimeSeconds() < ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool VerifyPassword(string password, string hashWithSalt)
|
||||||
|
{
|
||||||
|
var parts = hashWithSalt.Split(':');
|
||||||
|
if (parts.Length != 2) return false;
|
||||||
|
var salt = parts[0];
|
||||||
|
var expected = parts[1];
|
||||||
|
return ShittyHmac(password, salt) == expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HashMessage(string message)
|
||||||
|
{
|
||||||
|
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(message));
|
||||||
|
return Convert.ToHexStringLower(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ShittyHmac(string message, string salt)
|
||||||
|
{
|
||||||
|
var h1 = HashMessage(message + salt);
|
||||||
|
var h2 = HashMessage(salt + message);
|
||||||
|
return HashMessage(h1 + h2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
using System.Data.Common;
|
||||||
|
using Npgsql;
|
||||||
|
using Phenix.Models;
|
||||||
|
|
||||||
|
namespace Phenix.Services;
|
||||||
|
|
||||||
|
public class PostgresGameService : IGameService
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
public PostgresGameService(string connectionString)
|
||||||
|
{
|
||||||
|
_connectionString = connectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Init()
|
||||||
|
{
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
CREATE TABLE IF NOT EXISTS games (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
time TEXT NOT NULL,
|
||||||
|
home TEXT NOT NULL,
|
||||||
|
away TEXT NOT NULL,
|
||||||
|
year INTEGER NOT NULL,
|
||||||
|
home_score INTEGER,
|
||||||
|
away_score INTEGER,
|
||||||
|
practice BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
rain BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
UNIQUE(date, time)
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Game>> GetAllGames()
|
||||||
|
{
|
||||||
|
var games = new List<Game>();
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT * FROM games ORDER BY date, time";
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
games.Add(ReadGame(reader));
|
||||||
|
return games;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Game?> GetGameById(string id)
|
||||||
|
{
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT * FROM games WHERE id = @id";
|
||||||
|
cmd.Parameters.AddWithValue("@id", id);
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
if (await reader.ReadAsync())
|
||||||
|
return ReadGame(reader);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(Game? Game, string Error)> CreateNewGame(string date, string time, string home, string away)
|
||||||
|
{
|
||||||
|
date = CleanDate(date);
|
||||||
|
time = CleanTime(time);
|
||||||
|
|
||||||
|
if (!DateOnly.TryParse(date, out _))
|
||||||
|
return (null, $"Invalide date format {date}");
|
||||||
|
if (!TimeOnly.TryParse(time, out _))
|
||||||
|
return (null, $"Invalide time format {time}");
|
||||||
|
if (!ValidTeams.Contains(away))
|
||||||
|
return (null, $"Invalide away format {away}");
|
||||||
|
if (!ValidTeams.Contains(home))
|
||||||
|
return (null, $"Invalide home format {home}");
|
||||||
|
if (home == away)
|
||||||
|
return (null, $"Home and away teams must be different (received {home} for both)");
|
||||||
|
|
||||||
|
var year = DateOnly.Parse(date).Year;
|
||||||
|
var game = new Game
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
Date = date,
|
||||||
|
Time = time,
|
||||||
|
Home = home,
|
||||||
|
Away = away,
|
||||||
|
Year = year,
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
INSERT INTO games (id, date, time, home, away, year)
|
||||||
|
VALUES (@id, @date, @time, @home, @away, @year)
|
||||||
|
""";
|
||||||
|
cmd.Parameters.AddWithValue("@id", game.Id);
|
||||||
|
cmd.Parameters.AddWithValue("@date", game.Date);
|
||||||
|
cmd.Parameters.AddWithValue("@time", game.Time);
|
||||||
|
cmd.Parameters.AddWithValue("@home", game.Home);
|
||||||
|
cmd.Parameters.AddWithValue("@away", game.Away);
|
||||||
|
cmd.Parameters.AddWithValue("@year", game.Year);
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
catch (PostgresException ex) when (ex.SqlState == "23505")
|
||||||
|
{
|
||||||
|
return (null, "game already exists at this date/time");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (game, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateGame(string id, int? homeScore, int? awayScore, bool? rain, bool? practice)
|
||||||
|
{
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
UPDATE games SET
|
||||||
|
home_score = COALESCE(@homeScore, home_score),
|
||||||
|
away_score = COALESCE(@awayScore, away_score),
|
||||||
|
rain = COALESCE(@rain, rain),
|
||||||
|
practice = COALESCE(@practice, practice)
|
||||||
|
WHERE id = @id
|
||||||
|
""";
|
||||||
|
cmd.Parameters.AddWithValue("@id", id);
|
||||||
|
cmd.Parameters.AddWithValue("@homeScore", (object?)homeScore ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@awayScore", (object?)awayScore ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@rain", rain ?? (object?)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@practice", practice ?? (object?)DBNull.Value);
|
||||||
|
return await cmd.ExecuteNonQueryAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteGameById(string id)
|
||||||
|
{
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "DELETE FROM games WHERE id = @id";
|
||||||
|
cmd.Parameters.AddWithValue("@id", id);
|
||||||
|
return await cmd.ExecuteNonQueryAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearAllGames()
|
||||||
|
{
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "DELETE FROM games";
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SwitchVisitors(string id)
|
||||||
|
{
|
||||||
|
var game = await GetGameById(id);
|
||||||
|
if (game is null) return false;
|
||||||
|
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
UPDATE games SET home = @home, away = @away,
|
||||||
|
home_score = @homeScore, away_score = @awayScore
|
||||||
|
WHERE id = @id
|
||||||
|
""";
|
||||||
|
cmd.Parameters.AddWithValue("@id", id);
|
||||||
|
cmd.Parameters.AddWithValue("@home", game.Away);
|
||||||
|
cmd.Parameters.AddWithValue("@away", game.Home);
|
||||||
|
cmd.Parameters.AddWithValue("@homeScore", (object?)game.AwayScore ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@awayScore", (object?)game.HomeScore ?? DBNull.Value);
|
||||||
|
return await cmd.ExecuteNonQueryAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Game ReadGame(DbDataReader r) => new()
|
||||||
|
{
|
||||||
|
Id = r.GetString(r.GetOrdinal("id")),
|
||||||
|
Date = r.GetString(r.GetOrdinal("date")),
|
||||||
|
Time = r.GetString(r.GetOrdinal("time")),
|
||||||
|
Home = r.GetString(r.GetOrdinal("home")),
|
||||||
|
Away = r.GetString(r.GetOrdinal("away")),
|
||||||
|
Year = r.GetInt32(r.GetOrdinal("year")),
|
||||||
|
HomeScore = r.IsDBNull(r.GetOrdinal("home_score")) ? null : r.GetInt32(r.GetOrdinal("home_score")),
|
||||||
|
AwayScore = r.IsDBNull(r.GetOrdinal("away_score")) ? null : r.GetInt32(r.GetOrdinal("away_score")),
|
||||||
|
Practice = r.GetBoolean(r.GetOrdinal("practice")),
|
||||||
|
Rain = r.GetBoolean(r.GetOrdinal("rain")),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] ValidTeams =
|
||||||
|
["jaune", "rouge", "bleu", "gris", "vert", "noir", "orange", "mauve"];
|
||||||
|
|
||||||
|
private static string CleanDate(string raw)
|
||||||
|
{
|
||||||
|
var parts = raw.Split('-');
|
||||||
|
if (parts.Length != 3) return raw;
|
||||||
|
return $"{parts[0]}-{parts[1].PadLeft(2, '0')}-{parts[2].PadLeft(2, '0')}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CleanTime(string raw)
|
||||||
|
{
|
||||||
|
var sep = raw.Contains('h') ? 'h' : ':';
|
||||||
|
var parts = raw.Split(sep);
|
||||||
|
if (parts.Length != 2) return raw;
|
||||||
|
return $"{parts[0].PadLeft(2, '0')}:{parts[1].PadLeft(2, '0')}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
using System.Data.Common;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Phenix.Models;
|
||||||
|
|
||||||
|
namespace Phenix.Services;
|
||||||
|
|
||||||
|
public class SqliteGameService : IGameService
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
public SqliteGameService(string connectionString)
|
||||||
|
{
|
||||||
|
_connectionString = connectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Init()
|
||||||
|
{
|
||||||
|
await using var conn = new SqliteConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
CREATE TABLE IF NOT EXISTS games (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
time TEXT NOT NULL,
|
||||||
|
home TEXT NOT NULL,
|
||||||
|
away TEXT NOT NULL,
|
||||||
|
year INTEGER NOT NULL,
|
||||||
|
home_score INTEGER,
|
||||||
|
away_score INTEGER,
|
||||||
|
practice INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rain INTEGER NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE(date, time)
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Game>> GetAllGames()
|
||||||
|
{
|
||||||
|
var games = new List<Game>();
|
||||||
|
await using var conn = new SqliteConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT * FROM games ORDER BY date, time";
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
games.Add(ReadGame(reader));
|
||||||
|
return games;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Game?> GetGameById(string id)
|
||||||
|
{
|
||||||
|
await using var conn = new SqliteConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT * FROM games WHERE id = @id";
|
||||||
|
cmd.Parameters.AddWithValue("@id", id);
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
if (await reader.ReadAsync())
|
||||||
|
return ReadGame(reader);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(Game? Game, string Error)> CreateNewGame(string date, string time, string home, string away)
|
||||||
|
{
|
||||||
|
date = CleanDate(date);
|
||||||
|
time = CleanTime(time);
|
||||||
|
|
||||||
|
if (!DateOnly.TryParse(date, out _))
|
||||||
|
return (null, $"Invalide date format {date}");
|
||||||
|
if (!TimeOnly.TryParse(time, out _))
|
||||||
|
return (null, $"Invalide time format {time}");
|
||||||
|
if (!ValidTeams.Contains(away))
|
||||||
|
return (null, $"Invalide away format {away}");
|
||||||
|
if (!ValidTeams.Contains(home))
|
||||||
|
return (null, $"Invalide home format {home}");
|
||||||
|
if (home == away)
|
||||||
|
return (null, $"Home and away teams must be different (received {home} for both)");
|
||||||
|
|
||||||
|
var year = DateOnly.Parse(date).Year;
|
||||||
|
var game = new Game
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
Date = date,
|
||||||
|
Time = time,
|
||||||
|
Home = home,
|
||||||
|
Away = away,
|
||||||
|
Year = year,
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var conn = new SqliteConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
INSERT INTO games (id, date, time, home, away, year)
|
||||||
|
VALUES (@id, @date, @time, @home, @away, @year)
|
||||||
|
""";
|
||||||
|
cmd.Parameters.AddWithValue("@id", game.Id);
|
||||||
|
cmd.Parameters.AddWithValue("@date", game.Date);
|
||||||
|
cmd.Parameters.AddWithValue("@time", game.Time);
|
||||||
|
cmd.Parameters.AddWithValue("@home", game.Home);
|
||||||
|
cmd.Parameters.AddWithValue("@away", game.Away);
|
||||||
|
cmd.Parameters.AddWithValue("@year", game.Year);
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
catch (SqliteException ex) when (ex.SqliteErrorCode == 19)
|
||||||
|
{
|
||||||
|
return (null, "game already exists at this date/time");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (game, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateGame(string id, int? homeScore, int? awayScore, bool? rain, bool? practice)
|
||||||
|
{
|
||||||
|
await using var conn = new SqliteConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
UPDATE games SET
|
||||||
|
home_score = COALESCE(@homeScore, home_score),
|
||||||
|
away_score = COALESCE(@awayScore, away_score),
|
||||||
|
rain = COALESCE(@rain, rain),
|
||||||
|
practice = COALESCE(@practice, practice)
|
||||||
|
WHERE id = @id
|
||||||
|
""";
|
||||||
|
cmd.Parameters.AddWithValue("@id", id);
|
||||||
|
cmd.Parameters.AddWithValue("@homeScore", (object?)homeScore ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@awayScore", (object?)awayScore ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@rain", rain ?? (object?)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@practice", practice ?? (object?)DBNull.Value);
|
||||||
|
return await cmd.ExecuteNonQueryAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteGameById(string id)
|
||||||
|
{
|
||||||
|
await using var conn = new SqliteConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "DELETE FROM games WHERE id = @id";
|
||||||
|
cmd.Parameters.AddWithValue("@id", id);
|
||||||
|
return await cmd.ExecuteNonQueryAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearAllGames()
|
||||||
|
{
|
||||||
|
await using var conn = new SqliteConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "DELETE FROM games";
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SwitchVisitors(string id)
|
||||||
|
{
|
||||||
|
var game = await GetGameById(id);
|
||||||
|
if (game is null) return false;
|
||||||
|
|
||||||
|
await using var conn = new SqliteConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
UPDATE games SET home = @home, away = @away,
|
||||||
|
home_score = @homeScore, away_score = @awayScore
|
||||||
|
WHERE id = @id
|
||||||
|
""";
|
||||||
|
cmd.Parameters.AddWithValue("@id", id);
|
||||||
|
cmd.Parameters.AddWithValue("@home", game.Away);
|
||||||
|
cmd.Parameters.AddWithValue("@away", game.Home);
|
||||||
|
cmd.Parameters.AddWithValue("@homeScore", (object?)game.AwayScore ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@awayScore", (object?)game.HomeScore ?? DBNull.Value);
|
||||||
|
return await cmd.ExecuteNonQueryAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Game ReadGame(DbDataReader r) => new()
|
||||||
|
{
|
||||||
|
Id = r.GetString(r.GetOrdinal("id")),
|
||||||
|
Date = r.GetString(r.GetOrdinal("date")),
|
||||||
|
Time = r.GetString(r.GetOrdinal("time")),
|
||||||
|
Home = r.GetString(r.GetOrdinal("home")),
|
||||||
|
Away = r.GetString(r.GetOrdinal("away")),
|
||||||
|
Year = r.GetInt32(r.GetOrdinal("year")),
|
||||||
|
HomeScore = r.IsDBNull(r.GetOrdinal("home_score")) ? null : r.GetInt32(r.GetOrdinal("home_score")),
|
||||||
|
AwayScore = r.IsDBNull(r.GetOrdinal("away_score")) ? null : r.GetInt32(r.GetOrdinal("away_score")),
|
||||||
|
Practice = r.GetBoolean(r.GetOrdinal("practice")),
|
||||||
|
Rain = r.GetBoolean(r.GetOrdinal("rain")),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] ValidTeams =
|
||||||
|
["jaune", "rouge", "bleu", "gris", "vert", "noir", "orange", "mauve"];
|
||||||
|
|
||||||
|
private static string CleanDate(string raw)
|
||||||
|
{
|
||||||
|
var parts = raw.Split('-');
|
||||||
|
if (parts.Length != 3) return raw;
|
||||||
|
return $"{parts[0]}-{parts[1].PadLeft(2, '0')}-{parts[2].PadLeft(2, '0')}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CleanTime(string raw)
|
||||||
|
{
|
||||||
|
var sep = raw.Contains('h') ? 'h' : ':';
|
||||||
|
var parts = raw.Split(sep);
|
||||||
|
if (parts.Length != 2) return raw;
|
||||||
|
return $"{parts[0].PadLeft(2, '0')}:{parts[1].PadLeft(2, '0')}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
{{ layout "templates/container.vto" }}
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData["Tab"] = "calendrier";
|
||||||
|
}
|
||||||
|
|
||||||
<div id="mithril-content"></div>
|
<div id="mithril-content"></div>
|
||||||
|
|
||||||
<script type=module src="scripts/calendar.js"></script>
|
<script type=module src="scripts/calendar.js"></script>
|
||||||
|
|
||||||
{{ /layout }}
|
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
{{ layout "templates/container.vto" }}
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData["Tab"] = "contact";
|
||||||
|
}
|
||||||
|
|
||||||
<div class="uk-width-large-2-5 uk-width-medium-4-5 uk-width-small-1-1 uk-container-center">
|
<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-grid uk-margin">
|
||||||
@@ -31,5 +34,3 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ /layout }}
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData["Tab"] = "home";
|
||||||
|
}
|
||||||
|
|
||||||
{{ layout "templates/container.vto" }}
|
|
||||||
<script type="module" src="scripts/little-calendar.js"></script>
|
<script type="module" src="scripts/little-calendar.js"></script>
|
||||||
|
|
||||||
<div class="uk-grid">
|
<div class="uk-grid">
|
||||||
@@ -17,7 +20,7 @@
|
|||||||
<i class="fa fa-newspaper-o" style="font-size: 1.7em;"></i>
|
<i class="fa fa-newspaper-o" style="font-size: 1.7em;"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ include "templates/includes/home-news.vto" }}
|
<partial name="HomeNews" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,19 +35,18 @@
|
|||||||
<div class="uk-panel uk-panel-box">
|
<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>
|
<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>
|
<h3 class="uk-panel-title">Parc Walter-Stewart</h3>
|
||||||
{{ include "templates/includes/walter-stewart-map.vto" }}
|
<partial name="WalterStewartMap" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- start -->
|
|
||||||
<div class="uk-visible-medium">
|
<div class="uk-visible-medium">
|
||||||
<div class="uk-grid uk-grid-match">
|
<div class="uk-grid uk-grid-match">
|
||||||
<div class="uk-width-medium-1-2">
|
<div class="uk-width-medium-1-2">
|
||||||
<div class="uk-panel uk-panel-box">
|
<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 class="uk-panel-badge uk-badge"><i class="fa fa-newspaper-o" style="font-size: 1.7em;"></i></div>
|
||||||
<div>
|
<div>
|
||||||
{{ include "templates/includes/home-news.vto" }}
|
<partial name="HomeNews" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,23 +63,19 @@
|
|||||||
<div class="uk-panel uk-panel-box">
|
<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>
|
<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>
|
<h3 class="uk-panel-title">Parc Walter-Stewart</h3>
|
||||||
{{ include "templates/includes/walter-stewart-map.vto" }}
|
<partial name="WalterStewartMap" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- end -->
|
|
||||||
<div class="uk-visible-small">
|
<div class="uk-visible-small">
|
||||||
<div class="uk-grid">
|
<div class="uk-grid">
|
||||||
<div class="uk-width-small-1-1">
|
<div class="uk-width-small-1-1">
|
||||||
<div class="uk-panel uk-panel-box">
|
<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 class="uk-panel-badge uk-badge"><i class="fa fa-newspaper-o" style="font-size: 1.7em;"></i></div>
|
||||||
<div>
|
<div>
|
||||||
{{ include "templates/includes/home-news.vto" }}
|
<partial name="HomeNews" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,12 +94,10 @@
|
|||||||
<div class="uk-panel uk-panel-box">
|
<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>
|
<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>
|
<h3 class="uk-panel-title">Parc Walter-Stewart</h3>
|
||||||
{{ include "templates/includes/walter-stewart-map.vto" }}
|
<partial name="WalterStewartMap" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ /layout }}
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData["Tab"] = "nouvelles";
|
||||||
|
}
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
{{ layout "templates/container.vto" }}
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData["Tab"] = "reglement";
|
||||||
|
}
|
||||||
|
|
||||||
<div class="uk-margin-top uk-margin-bottom uk-grid">
|
<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-width-large-3-5 uk-width-medium-4-5 uk-width-large-1-1 uk-container-center">
|
||||||
@@ -39,4 +41,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ /layout }}
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@{
|
||||||
|
var isToday = ViewBag.IsToday == true;
|
||||||
|
var years = (List<int>)ViewBag.Years;
|
||||||
|
var games = (List<Phenix.Models.Game>)ViewBag.Games;
|
||||||
|
var selectedYear = (int?)ViewBag.SelectedYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
<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 class="@(isToday ? "uk-active" : "")">
|
||||||
|
<a hx-get="/membre/games-editor/today" hx-target="#content">Aujourd'hui</a>
|
||||||
|
</li>
|
||||||
|
@foreach (var year in years)
|
||||||
|
{
|
||||||
|
<li class="@(year == selectedYear ? "uk-active" : "")">
|
||||||
|
<a hx-get="/membre/games-editor/@year" hx-target="#content">Saison @year</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (var game in games)
|
||||||
|
{
|
||||||
|
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-margin">
|
||||||
|
<partial name="GameEditor" model="game" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<button class="uk-button"
|
||||||
|
hx-get="/membre/new-game-editor"
|
||||||
|
hx-target="#content">
|
||||||
|
Nouvelle Partie
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
{{ layout "templates/container.vto" }}
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
ViewData["Tab"] = "membre";
|
||||||
|
}
|
||||||
|
|
||||||
<div id="content"
|
<div id="content"
|
||||||
{{ if isLoggedIn }}
|
@if (ViewBag.IsLoggedIn == true)
|
||||||
hx-get="/membre/games-editor/today"
|
{
|
||||||
hx-trigger="load"
|
@:hx-get="/membre/games-editor/today"
|
||||||
hx-target="#content"
|
@:hx-trigger="load"
|
||||||
{{ /if }}
|
@:hx-target="#content"
|
||||||
>
|
}>
|
||||||
<div class="uk-grid uk-margin">
|
<div class="uk-grid uk-margin">
|
||||||
<div class="uk-width-1-1">
|
<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 style="height: 250px; width: 250px; background-size: 250px; background-image: url(images/logosansfond.JPG);" class="uk-cover-background uk-container-center"></div>
|
||||||
@@ -15,7 +18,6 @@
|
|||||||
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-width-large-1-3 uk-container-center uk-margin">
|
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-width-large-1-3 uk-container-center uk-margin">
|
||||||
<form action=""
|
<form action=""
|
||||||
class="uk-text-center"
|
class="uk-text-center"
|
||||||
|
|
||||||
hx-get="/login"
|
hx-get="/login"
|
||||||
hx-target="#content">
|
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>Nom d'utilisateur <input type="text" name="username" class="uk-form-width-small"></label></div>
|
||||||
@@ -23,6 +25,4 @@
|
|||||||
<div class="uk-form-row"><button class="uk-button"><i class="fa fa-mars-double"></i> Pénétrez</button></div>
|
<div class="uk-form-row"><button class="uk-button"><i class="fa fa-mars-double"></i> Pénétrez</button></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{ /layout }}
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
<div class="uk-width-large-2-3 uk-container-center uk-margin">
|
@{
|
||||||
|
Layout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="uk-width-large-2-3 uk-container-center uk-margin">
|
||||||
<div class="uk-margin"></div>
|
<div class="uk-margin"></div>
|
||||||
|
|
||||||
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-margin">
|
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-margin">
|
||||||
@@ -7,25 +10,17 @@
|
|||||||
hx-target="#created-game"
|
hx-target="#created-game"
|
||||||
hx-swap="afterbegin">
|
hx-swap="afterbegin">
|
||||||
<div class="uk-form-row" style="text-align: center; font-size: 2em;">
|
<div class="uk-form-row" style="text-align: center; font-size: 2em;">
|
||||||
|
<partial name="TeamSelector" model='new Phenix.Models.TeamSelectorModel { Name = "awayTeam" }' />
|
||||||
{{ include "templates/includes/team-selector.vto" {name: "away-team"} }}
|
<label>Vs.</label>
|
||||||
<label>
|
<partial name="TeamSelector" model='new Phenix.Models.TeamSelectorModel { Name = "homeTeam" }' />
|
||||||
Vs.
|
|
||||||
</label>
|
|
||||||
{{ include "templates/includes/team-selector.vto" {name: "home-team"}}}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="uk-form-row">
|
<div class="uk-form-row">
|
||||||
<label>
|
<label>Date: <input type=date name="gameDate" required></label>
|
||||||
Date: <input type=date name="game-date" required>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="uk-form-row">
|
<div class="uk-form-row">
|
||||||
<label>
|
<label>Heure: <input type="time" name="gameTime"></label>
|
||||||
Heure: <input type="time" name="game-time">
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
@@ -33,13 +28,8 @@
|
|||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<button class="uk-button">Creer</button>
|
<button class="uk-button">Creer</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<form>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="created-game"></div>
|
<div id="created-game"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
@{
|
||||||
|
Layout = null;
|
||||||
|
}
|
||||||
|
|
||||||
<div class="uk-width-large-2-3 uk-container-center uk-margin">
|
<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">
|
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-margin">
|
||||||
<pre>{{ error }}</pre>
|
<pre>@ViewBag.Error</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
@model Phenix.Models.Game
|
||||||
|
|
||||||
<div class="uk-width-large-2-3 uk-container-center uk-margin">
|
<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">
|
<div class="uk-panel uk-panel-box uk-panel-box-secondary uk-margin">
|
||||||
{{ include "templates/includes/game-editor.vto" {game} }}
|
<partial name="GameEditor" model="Model" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
@model Phenix.Models.Game
|
||||||
|
@{
|
||||||
|
var prettyDate = DateOnly.Parse(Model.Date).ToString("d MMMM yyyy", new System.Globalization.CultureInfo("fr-CA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
<div id="game-@Model.Id">
|
||||||
|
<h3>@Model.Away vs @Model.Home | @prettyDate | @Model.Time</h3>
|
||||||
|
<form style="font-size: 1.1em;"
|
||||||
|
hx-trigger="input delay:250ms"
|
||||||
|
hx-post="/membre/games/@Model.Id"
|
||||||
|
hx-target="#saving-result">
|
||||||
|
<div class="uk-form-row">
|
||||||
|
<label>
|
||||||
|
Points @Model.Away : <input type=number min=0 name="awayScore"
|
||||||
|
value="@(Model.AwayScore ?? 0)"
|
||||||
|
disabled="@(Model.Rain || Model.Practice ? true : null)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="uk-form-row">
|
||||||
|
<label>
|
||||||
|
Points @Model.Home : <input type=number min=0 name="homeScore"
|
||||||
|
value="@(Model.HomeScore ?? 0)"
|
||||||
|
disabled="@(Model.Rain || Model.Practice ? true : null)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="uk-form-row">
|
||||||
|
<label>
|
||||||
|
Partie reguliere <input type=radio name="gameType" value="regular-game"
|
||||||
|
checked="@(!Model.Practice && !Model.Rain ? true : null)">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Pluie <input type=radio name="gameType" value="rain"
|
||||||
|
checked="@(Model.Rain ? true : null)">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Pratique <input type=radio name="gameType" value="practice"
|
||||||
|
checked="@(Model.Practice && !Model.Rain ? true : null)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="saving-result"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="text-align: right">
|
||||||
|
<button class="uk-button"
|
||||||
|
hx-post="/membre/games/@Model.Id/switch"
|
||||||
|
hx-target="#game-@Model.Id">Inverser visiteur/receveur</button>
|
||||||
|
<button class="uk-button-danger"
|
||||||
|
hx-delete="/membre/games/@Model.Id"
|
||||||
|
hx-confirm="Certain de voulez supprimer la partie?"
|
||||||
|
hx-target="#game-@Model.Id"
|
||||||
|
hx-swap="delete">Supprimer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<h3 class="uk-panel-title">
|
||||||
|
<a href="/nouvelles" class="uk-link-muted">
|
||||||
|
<h2 id="pre-inscription">Pré-inscription</h2>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
@model Phenix.Models.TeamSelectorModel
|
||||||
|
@{
|
||||||
|
var teams = new[] { "jaune", "rouge", "bleu", "gris", "vert", "noir", "orange", "mauve" };
|
||||||
|
}
|
||||||
|
|
||||||
|
<select name="@Model.Name" class="uk-select" disabled="@(Model.IsDisabled)">
|
||||||
|
@foreach (var team in teams)
|
||||||
|
{
|
||||||
|
<option value="@team" selected="@(Model.SelectedTeam == team)">@team</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
@@ -5,9 +5,8 @@
|
|||||||
frameborder="0"
|
frameborder="0"
|
||||||
scrolling="no"
|
scrolling="no"
|
||||||
marginheight="0"
|
marginheight="0"
|
||||||
src="//www.openstreetmap.org/export/embed.html?bbox=-73.55936229228975%2C45.53102667048665%2C-73.55230271816255%2C45.53444249585723&layer=mapnik"; // "//www.openstreetmap.org/export/embed.html?bbox=-73.56355190277101%2C45.5294483312637%2C-73.54943275451662%2C45.53627996625677&layer=mapnik"; // "//www.openstreetmap.org/export/embed.html?bbox=-73.56355190277101%2C45.5294483312637%2C-73.54943275451662%2C45.53627996625677&layer=mapnik"
|
src="//www.openstreetmap.org/export/embed.html?bbox=-73.55936229228975%2C45.53102667048665%2C-73.55230271816255%2C45.53444249585723&layer=mapnik">
|
||||||
></iframe>
|
</iframe>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<small>
|
<small>
|
||||||
<a href="http://www.openstreetmap.org/#map=17/45.53216/-73.55692">Voir la carte</a>
|
<a href="http://www.openstreetmap.org/#map=17/45.53216/-73.55692">Voir la carte</a>
|
||||||
@@ -1,69 +1,54 @@
|
|||||||
|
@{
|
||||||
|
Layout = null;
|
||||||
|
}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Balle Phenix</title>
|
<title>Balle Phenix</title>
|
||||||
|
<link rel="shortcut icon" href="/images/favicon.ico">
|
||||||
<link rel="shortcut icon" href="https://www.ballephenix.com/favicon.ico">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<script src="scripts/jquery-3.7.1.min.js"></script>
|
<script src="/scripts/jquery-3.7.1.min.js"></script>
|
||||||
<script src="scripts/uikit.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>
|
<script src="https://unpkg.com/htmx.org@2.0.0"></script>
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<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">
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<nav class="uk-navbar uk-navbar-attached">
|
<nav class="uk-navbar uk-navbar-attached">
|
||||||
<ul class="uk-navbar-nav uk-visible-large uk-navbar-content uk-navbar-center">
|
<ul class="uk-navbar-nav uk-visible-large uk-navbar-content uk-navbar-center">
|
||||||
<li class="uk-parent">
|
<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>
|
<div class="uk-navbar-brand"><a href="https://www.facebook.com/SportsPhenixMtl"><i class="fa fa-facebook-square"></i></a></div>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ if it.tab === 'home'}}uk-active{{ /if }}">
|
<li class="@((ViewData["Tab"] as string) == "home" ? "uk-active" : "")">
|
||||||
<a href="/">
|
<a href="/"><div><i class="fa fa-home"></i> Accueil</div></a>
|
||||||
<div><i class="fa fa-home"></i> Accueil</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ if it.tab === 'nouvelles'}}uk-active{{ /if }}">
|
<li class="@((ViewData["Tab"] as string) == "nouvelles" ? "uk-active" : "")">
|
||||||
<a href="/nouvelles">
|
<a href="/nouvelles"><div><i class="fa fa-newspaper-o"></i> Nouvelles</div></a>
|
||||||
<div><i class="fa fa-newspaper-o"></i> Nouvelles</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ if it.tab === 'calendrier'}}uk-active{{ /if }}">
|
<li class="@((ViewData["Tab"] as string) == "calendrier" ? "uk-active" : "")">
|
||||||
<a href="/calendrier">
|
<a href="/calendrier"><div><i class="fa fa-calendar"></i> Calendrier</div></a>
|
||||||
<div><i class="fa fa-calendar"></i> Calendrier</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ if it.tab === 'reglement'}}uk-active{{ /if }}">
|
<li class="@((ViewData["Tab"] as string) == "reglement" ? "uk-active" : "")">
|
||||||
<a href="/reglement">
|
<a href="/reglement"><div><i class="fa fa-legal"></i> Règlement</div></a>
|
||||||
<div><i class="fa fa-legal"></i> Règlement</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ if it.tab === 'membre'}}uk-active{{ /if }}">
|
<li class="@((ViewData["Tab"] as string) == "membre" ? "uk-active" : "")">
|
||||||
<a href="/membre">
|
<a href="/membre"><div><i class="fa fa-lock"></i> Membre</div></a>
|
||||||
<div><i class="fa fa-lock"></i> Membre</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ if it.tab === 'contact'}}uk-active{{ /if }}">
|
<li class="@((ViewData["Tab"] as string) == "contact" ? "uk-active" : "")">
|
||||||
<a href="/contact">
|
<a href="/contact"><div><i class="fa fa-phone"></i> Nous Joindre</div></a>
|
||||||
<div><i class="fa fa-phone"></i> Nous Joindre</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="uk-navbar-nav uk-visible-medium">
|
<ul class="uk-navbar-nav uk-visible-medium">
|
||||||
<li class="uk-parent">
|
<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>
|
<div class="uk-navbar-brand"><a href="https://www.facebook.com/SportsPhenixMtl"><i class="fa fa-facebook-square"></i></a></div>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ if tab === 'home'}}uk-active{{ /if }}"><a href="/"><i class="fa fa-home"></i> Accueil </a></li>
|
<li class="@((ViewData["Tab"] as string) == "home" ? "uk-active" : "")"><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="@((ViewData["Tab"] as string) == "nouvelles" ? "uk-active" : "")"><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="@((ViewData["Tab"] as string) == "calendrier" ? "uk-active" : "")"><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="@((ViewData["Tab"] as string) == "reglement" ? "uk-active" : "")"><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="@((ViewData["Tab"] as string) == "membre" ? "uk-active" : "")"><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>
|
<li class="@((ViewData["Tab"] as string) == "contact" ? "uk-active" : "")"><a href="/contact"><i class="fa fa-phone"></i> Nous Joindre</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="uk-navbar-nav uk-visible-small">
|
<ul class="uk-navbar-nav uk-visible-small">
|
||||||
<li class="uk-parent">
|
<li class="uk-parent">
|
||||||
@@ -84,7 +69,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{{ content }}
|
@RenderBody()
|
||||||
|
|
||||||
<footer class="uk-grid ph-footer" style="padding: 40px;">
|
<footer class="uk-grid ph-footer" style="padding: 40px;">
|
||||||
<div class="uk-container uk-container-center uk-text-center">
|
<div class="uk-container uk-container-center uk-text-center">
|
||||||
@@ -98,5 +83,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"DatabaseProvider": "Sqlite",
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Default": "Data Source=phenix.db"
|
||||||
|
},
|
||||||
|
"SessionKey": "dev-only"
|
||||||
|
}
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
const URL = "https://ballephenix.com";
|
||||||
|
const USERNAME = "XXXXXX";
|
||||||
|
const PASSWORD = "XXXXXX";
|
||||||
|
|
||||||
|
type PreGame = {
|
||||||
|
date: string;
|
||||||
|
time: string;
|
||||||
|
home: string;
|
||||||
|
away: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class WeekDay {
|
||||||
|
readonly year: number;
|
||||||
|
readonly month: number;
|
||||||
|
readonly day: number;
|
||||||
|
|
||||||
|
constructor(iso: string) {
|
||||||
|
const [y, m, d] = iso.split("-").map(Number);
|
||||||
|
this.year = y;
|
||||||
|
this.month = m;
|
||||||
|
this.day = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
next(): WeekDay {
|
||||||
|
const d = new Date(this.year, this.month - 1, this.day + 7);
|
||||||
|
return new WeekDay(
|
||||||
|
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
saturday(): WeekDay {
|
||||||
|
const d = new Date(this.year, this.month - 1, this.day + 2);
|
||||||
|
return new WeekDay(
|
||||||
|
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `${this.year}-${String(this.month).padStart(2, "0")}-${String(this.day).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const w1 = new WeekDay("2026-05-14");
|
||||||
|
const w2 = w1.next();
|
||||||
|
const w3 = w2.next();
|
||||||
|
const w4 = w3.next();
|
||||||
|
// skip Saint-Jean
|
||||||
|
const w5 = w4.next().next();
|
||||||
|
const w6 = w5.next();
|
||||||
|
const w7 = w6.next();
|
||||||
|
const w8 = w7.next();
|
||||||
|
const w9 = w8.next();
|
||||||
|
const w10 = w9.next();
|
||||||
|
const w11 = w10.next();
|
||||||
|
const w12 = w11.next();
|
||||||
|
// const tournament = w12.saturday();
|
||||||
|
const w13 = w12.next();
|
||||||
|
const w14 = w13.next();
|
||||||
|
|
||||||
|
const games: PreGame[] = [
|
||||||
|
{ date: `${w1}`, time: "18:45", home: "gris", away: "bleu" },
|
||||||
|
{ date: `${w1}`, time: "20:15", home: "mauve", away: "jaune" },
|
||||||
|
|
||||||
|
{ date: `${w2}`, time: "18:45", home: "jaune", away: "gris" },
|
||||||
|
{ date: `${w2}`, time: "20:15", home: "bleu", away: "mauve" },
|
||||||
|
|
||||||
|
{ date: `${w3}`, time: "18:45", home: "gris", away: "mauve" },
|
||||||
|
{ date: `${w3}`, time: "20:15", home: "jaune", away: "bleu" },
|
||||||
|
|
||||||
|
{ date: `${w4}`, time: "18:45", home: "mauve", away: "bleu" },
|
||||||
|
{ date: `${w4}`, time: "20:15", home: "gris", away: "jaune" },
|
||||||
|
|
||||||
|
{ date: `${w5}`, time: "18:45", home: "bleu", away: "jaune" },
|
||||||
|
{ date: `${w5}`, time: "20:15", home: "mauve", away: "gris" },
|
||||||
|
|
||||||
|
{ date: `${w6}`, time: "18:45", home: "jaune", away: "mauve" },
|
||||||
|
{ date: `${w6}`, time: "20:15", home: "bleu", away: "gris" },
|
||||||
|
|
||||||
|
{ date: `${w7}`, time: "18:45", home: "gris", away: "bleu" },
|
||||||
|
{ date: `${w7}`, time: "20:15", home: "mauve", away: "jaune" },
|
||||||
|
|
||||||
|
{ date: `${w8}`, time: "18:45", home: "jaune", away: "gris" },
|
||||||
|
{ date: `${w8}`, time: "20:15", home: "bleu", away: "mauve" },
|
||||||
|
|
||||||
|
{ date: `${w9}`, time: "18:45", home: "gris", away: "mauve" },
|
||||||
|
{ date: `${w9}`, time: "20:15", home: "jaune", away: "bleu" },
|
||||||
|
|
||||||
|
{ date: `${w10}`, time: "18:45", home: "mauve", away: "bleu" },
|
||||||
|
{ date: `${w10}`, time: "20:15", home: "gris", away: "jaune" },
|
||||||
|
{ date: `${w10}`, time: "21:35", home: "jaune", away: "gris" },
|
||||||
|
|
||||||
|
{ date: `${w11}`, time: "18:45", home: "bleu", away: "jaune" },
|
||||||
|
{ date: `${w11}`, time: "20:15", home: "mauve", away: "gris" },
|
||||||
|
|
||||||
|
{ date: `${w12}`, time: "18:45", home: "jaune", away: "mauve" },
|
||||||
|
{ date: `${w12}`, time: "20:15", home: "bleu", away: "gris" },
|
||||||
|
|
||||||
|
// { date: `${tournament}`, time: "09:00", home: "gris", away: "bleu" },
|
||||||
|
// { date: `${tournament}`, time: "10:30", home: "mauve", away: "jaune" },
|
||||||
|
// { date: `${tournament}`, time: "15:30", home: "bleu", away: "mauve" },
|
||||||
|
|
||||||
|
{ date: `${w13}`, time: "18:45", home: "gris", away: "mauve" },
|
||||||
|
{ date: `${w13}`, time: "20:15", home: "jaune", away: "bleu" },
|
||||||
|
|
||||||
|
{ date: `${w14}`, time: "18:45", home: "gris", away: "bleu" },
|
||||||
|
{ date: `${w14}`, time: "20:15", home: "mauve", away: "jaune" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function extractCookies(headers: Headers): string {
|
||||||
|
const cookies: string[] = [];
|
||||||
|
for (const value of headers.getSetCookie()) {
|
||||||
|
cookies.push(value.split(";")[0]);
|
||||||
|
}
|
||||||
|
return cookies.join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(): Promise<string> {
|
||||||
|
const res = await fetch(
|
||||||
|
`${URL}/login?username=${encodeURIComponent(USERNAME)}&password=${encodeURIComponent(PASSWORD)}`,
|
||||||
|
{ redirect: "manual" },
|
||||||
|
);
|
||||||
|
if (res.status !== 302) {
|
||||||
|
throw new Error(`Login failed with status ${res.status}`);
|
||||||
|
}
|
||||||
|
return extractCookies(res.headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createGame(cookie: string, game: PreGame): Promise<void> {
|
||||||
|
const body = new URLSearchParams({
|
||||||
|
"game-date": game.date,
|
||||||
|
"game-time": game.time,
|
||||||
|
"home-team": game.home,
|
||||||
|
"away-team": game.away,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch(`${URL}/membre/new-game`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Cookie: cookie,
|
||||||
|
},
|
||||||
|
body: body.toString(),
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status >= 400) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(
|
||||||
|
`Failed to create ${game.date} ${game.time} ${game.home} vs ${game.away}: ${text}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await res.body?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("Logging in...");
|
||||||
|
const cookie = await login();
|
||||||
|
console.log("Logged in.\n");
|
||||||
|
|
||||||
|
for (const game of games) {
|
||||||
|
console.log(
|
||||||
|
`Creating: ${game.date} ${game.time} ${game.home} vs ${game.away}`,
|
||||||
|
);
|
||||||
|
await createGame(cookie, game);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nDone. ${games.length} games created.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"deploy": {
|
|
||||||
"project": "7a93d03e-eabe-4db1-b78e-5f1bbcb460b0",
|
|
||||||
"exclude": [
|
|
||||||
"**/node_modules"
|
|
||||||
],
|
|
||||||
"include": [],
|
|
||||||
"entrypoint": "server.ts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "3",
|
|
||||||
"packages": {
|
|
||||||
"specifiers": {
|
|
||||||
"jsr:@oak/commons@0.11": "jsr:@oak/commons@0.11.0",
|
|
||||||
"jsr:@oak/oak": "jsr:@oak/oak@16.1.0",
|
|
||||||
"jsr:@std/assert@0.223": "jsr:@std/assert@0.223.0",
|
|
||||||
"jsr:@std/assert@0.226": "jsr:@std/assert@0.226.0",
|
|
||||||
"jsr:@std/assert@^0.223.0": "jsr:@std/assert@0.223.0",
|
|
||||||
"jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0",
|
|
||||||
"jsr:@std/bytes@0.223": "jsr:@std/bytes@0.223.0",
|
|
||||||
"jsr:@std/bytes@0.224": "jsr:@std/bytes@0.224.0",
|
|
||||||
"jsr:@std/bytes@^0.223.0": "jsr:@std/bytes@0.223.0",
|
|
||||||
"jsr:@std/crypto@0.223": "jsr:@std/crypto@0.223.0",
|
|
||||||
"jsr:@std/crypto@0.224": "jsr:@std/crypto@0.224.0",
|
|
||||||
"jsr:@std/encoding@1.0.0-rc.2": "jsr:@std/encoding@1.0.0-rc.2",
|
|
||||||
"jsr:@std/encoding@^0.223.0": "jsr:@std/encoding@0.223.0",
|
|
||||||
"jsr:@std/http@0.223": "jsr:@std/http@0.223.0",
|
|
||||||
"jsr:@std/http@0.224": "jsr:@std/http@0.224.5",
|
|
||||||
"jsr:@std/io@0.223": "jsr:@std/io@0.223.0",
|
|
||||||
"jsr:@std/media-types@0.223": "jsr:@std/media-types@0.223.0",
|
|
||||||
"jsr:@std/media-types@0.224": "jsr:@std/media-types@0.224.1",
|
|
||||||
"jsr:@std/path@0.223": "jsr:@std/path@0.223.0",
|
|
||||||
"npm:path-to-regexp@6.2.1": "npm:path-to-regexp@6.2.1"
|
|
||||||
},
|
|
||||||
"jsr": {
|
|
||||||
"@oak/commons@0.11.0": {
|
|
||||||
"integrity": "07702bfe5c07cd8144c422022994da1f9fea466b185824f4be63a2b1b1a65125",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@std/assert@0.226",
|
|
||||||
"jsr:@std/bytes@0.224",
|
|
||||||
"jsr:@std/crypto@0.224",
|
|
||||||
"jsr:@std/http@0.224",
|
|
||||||
"jsr:@std/media-types@0.224"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@oak/oak@16.1.0": {
|
|
||||||
"integrity": "ab21506555fffeb08dc8f45ff5d28607e8087949ff58bd2964b27df65994480b",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@oak/commons@0.11",
|
|
||||||
"jsr:@std/assert@0.223",
|
|
||||||
"jsr:@std/bytes@0.223",
|
|
||||||
"jsr:@std/crypto@0.223",
|
|
||||||
"jsr:@std/http@0.223",
|
|
||||||
"jsr:@std/io@0.223",
|
|
||||||
"jsr:@std/media-types@0.223",
|
|
||||||
"jsr:@std/path@0.223",
|
|
||||||
"npm:path-to-regexp@6.2.1"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@std/assert@0.223.0": {
|
|
||||||
"integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24"
|
|
||||||
},
|
|
||||||
"@std/assert@0.224.0": {
|
|
||||||
"integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f"
|
|
||||||
},
|
|
||||||
"@std/assert@0.226.0": {
|
|
||||||
"integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3"
|
|
||||||
},
|
|
||||||
"@std/bytes@0.223.0": {
|
|
||||||
"integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8"
|
|
||||||
},
|
|
||||||
"@std/bytes@0.224.0": {
|
|
||||||
"integrity": "a2250e1d0eb7d1c5a426f21267ab9bdeac2447fa87a3d0d1a467d3f7a6058e49"
|
|
||||||
},
|
|
||||||
"@std/crypto@0.223.0": {
|
|
||||||
"integrity": "1aa9555ff56b09e197ad988ea200f84bc6781fd4fd83f3a156ee44449af93000",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@std/assert@^0.223.0",
|
|
||||||
"jsr:@std/encoding@^0.223.0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@std/crypto@0.224.0": {
|
|
||||||
"integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@std/assert@^0.224.0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@std/encoding@0.223.0": {
|
|
||||||
"integrity": "2b5615a75e00337ce113f34cf2f9b8c18182c751a8dcc8b1a2c2fc0e117bef00"
|
|
||||||
},
|
|
||||||
"@std/encoding@1.0.0-rc.2": {
|
|
||||||
"integrity": "160d7674a20ebfbccdf610b3801fee91cf6e42d1c106dd46bbaf46e395cd35ef"
|
|
||||||
},
|
|
||||||
"@std/http@0.223.0": {
|
|
||||||
"integrity": "15ab8a0c5a7e9d5be017a15b01600f20f66602ceec48b378939fa24fcec522aa",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@std/assert@^0.223.0",
|
|
||||||
"jsr:@std/encoding@^0.223.0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@std/http@0.224.5": {
|
|
||||||
"integrity": "b03b5d1529f6c423badfb82f6640f9f2557b4034cd7c30655ba5bb447ff750a4",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@std/encoding@1.0.0-rc.2"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@std/io@0.223.0": {
|
|
||||||
"integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@std/bytes@^0.223.0"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@std/media-types@0.223.0": {
|
|
||||||
"integrity": "84684680c2eb6bc6d9369c6d6f26a49decaf2c7603ff531862dda575d9d6776e"
|
|
||||||
},
|
|
||||||
"@std/media-types@0.224.1": {
|
|
||||||
"integrity": "9e69a5daed37c5b5c6d3ce4731dc191f80e67f79bed392b0957d1d03b87f11e1"
|
|
||||||
},
|
|
||||||
"@std/path@0.223.0": {
|
|
||||||
"integrity": "593963402d7e6597f5a6e620931661053572c982fc014000459edc1f93cc3989",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@std/assert@^0.223.0"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"npm": {
|
|
||||||
"path-to-regexp@6.2.1": {
|
|
||||||
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==",
|
|
||||||
"dependencies": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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",
|
|
||||||
"https://esm.sh/lodash-es@4.17.21": "460848e64379750386465df2bfcbe530dc5be584cabb39816e0dec105e5f9af2",
|
|
||||||
"https://esm.sh/v135/lodash-es@4.17.21/denonext/lodash-es.mjs": "ca6d37e6a1271d1bd8920da5ff11f7d05465f5d7961d5d09b44db331838981d7"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
exec deno run -A https://deno.land/x/deploy/deployctl.ts deploy --prod --project=phenix server.ts
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{ fetchNuGet }: [
|
||||||
|
(fetchNuGet { pname = "Microsoft.Data.Sqlite"; version = "9.0.0"; sha256 = "0m58j9cdrkk8m0yl24z8jy3iafxfbwmf3v01cwhwnqs0cbq1ara8"; })
|
||||||
|
(fetchNuGet { pname = "Microsoft.Data.Sqlite.Core"; version = "9.0.0"; sha256 = "11s3pf12x4ghi1xn2dv6dsj3ckxq062m82dcinn2l42z590ny5fg"; })
|
||||||
|
(fetchNuGet { pname = "Microsoft.Extensions.DependencyInjection.Abstractions"; version = "8.0.0"; sha256 = "1zw0bpp5742jzx03wvqc8csnvsbgdqi0ls9jfc5i2vd3cl8b74pg"; })
|
||||||
|
(fetchNuGet { pname = "Microsoft.Extensions.Logging.Abstractions"; version = "8.0.0"; sha256 = "1klcqhg3hk55hb6vmjiq2wgqidsl81aldw0li2z98lrwx26msrr6"; })
|
||||||
|
(fetchNuGet { pname = "Npgsql"; version = "8.0.0"; sha256 = "1x3jlin44frkhd7d50q0vmy0gapxqhmd4jv1nvsnkndp3xjsaynd"; })
|
||||||
|
(fetchNuGet { pname = "SQLitePCLRaw.bundle_e_sqlite3"; version = "2.1.10"; sha256 = "1irrg46sv2w12nh6jc810clbi06fgclnbky14b8kamfkgy61d4li"; })
|
||||||
|
(fetchNuGet { pname = "SQLitePCLRaw.core"; version = "2.1.10"; sha256 = "03q02p1cajqzcbk9112mgw3qq733l4gd2vi21dq2am4911imr5l2"; })
|
||||||
|
(fetchNuGet { pname = "SQLitePCLRaw.lib.e_sqlite3"; version = "2.1.10"; sha256 = "0xqa3l4p68vm3p5n4h3d2gs97vrpnvhwfv366i6fv5x80m2zcswv"; })
|
||||||
|
(fetchNuGet { pname = "SQLitePCLRaw.provider.e_sqlite3"; version = "2.1.10"; sha256 = "0n7cdwix5h9k578a07g00sw9c03sv4liy91qzpj9wb8k4673gfrh"; })
|
||||||
|
(fetchNuGet { pname = "System.Memory"; version = "4.5.3"; sha256 = "0naqahm3wljxb5a911d37mwjqjdxv9l0b49p5dmfyijvni2ppy8a"; })
|
||||||
|
]
|
||||||
Executable
+90
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_json(api_root, path):
|
||||||
|
url = f"{api_root}{path}"
|
||||||
|
resp = requests.get(url, timeout=30)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Populate a local instance from production data via REST"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--source",
|
||||||
|
default=os.environ.get("PHENIX_SOURCE_URL", "https://ballephenix.com"),
|
||||||
|
help="Source API root (default: https://ballephenix.com)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--target",
|
||||||
|
default=os.environ.get("PHENIX_TARGET_URL", "http://localhost:5000"),
|
||||||
|
help="Target API root (default: http://localhost:5000)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Clear existing games before inserting",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
source_root = args.source.rstrip("/")
|
||||||
|
target_root = args.target.rstrip("/")
|
||||||
|
|
||||||
|
years = fetch_json(source_root, "/api/available-years.json")
|
||||||
|
print(f"Years on source: {len(years)} ({min(years)}–{max(years)})")
|
||||||
|
|
||||||
|
all_games = []
|
||||||
|
for year in years:
|
||||||
|
games = fetch_json(source_root, f"/api/results/{year}.json")
|
||||||
|
all_games.extend(games)
|
||||||
|
print(f"Total games fetched: {len(all_games)}")
|
||||||
|
|
||||||
|
if args.force:
|
||||||
|
resp = requests.delete(f"{target_root}/api/games", timeout=30)
|
||||||
|
if resp.status_code not in (204, 200):
|
||||||
|
print(f"Failed to clear games: {resp.status_code} {resp.text}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print("Cleared existing games")
|
||||||
|
|
||||||
|
inserted = 0
|
||||||
|
skipped = 0
|
||||||
|
errors = 0
|
||||||
|
|
||||||
|
for g in all_games:
|
||||||
|
payload = {
|
||||||
|
"date": g.get("date", ""),
|
||||||
|
"time": g.get("time", ""),
|
||||||
|
"home": g.get("home", ""),
|
||||||
|
"away": g.get("away", ""),
|
||||||
|
"homeScore": g.get("homeScore"),
|
||||||
|
"awayScore": g.get("awayScore"),
|
||||||
|
"practice": bool(g.get("practice")),
|
||||||
|
"rain": bool(g.get("rain")),
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(f"{target_root}/api/games", json=payload, timeout=30)
|
||||||
|
if resp.status_code == 201:
|
||||||
|
inserted += 1
|
||||||
|
elif resp.status_code == 409:
|
||||||
|
skipped += 1
|
||||||
|
else:
|
||||||
|
print(f"Error on {g.get('date')} {g.get('time')}: {resp.status_code} {resp.text}", file=sys.stderr)
|
||||||
|
errors += 1
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"Request error on {g.get('date')} {g.get('time')}: {e}", file=sys.stderr)
|
||||||
|
errors += 1
|
||||||
|
|
||||||
|
print(f"Inserted: {inserted}, skipped: {skipped}, errors: {errors}")
|
||||||
|
print(f"Target: {target_root}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,32 +1,127 @@
|
|||||||
{
|
{
|
||||||
description = "Phenix website";
|
description = "Balle Phenix website — ASP.NET + SQLite";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }:
|
outputs = { self, nixpkgs, flake-utils, ... }:
|
||||||
|
let
|
||||||
|
nixosModule = { config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.services.all-dressed-programming.phenix-website;
|
||||||
|
phenix = self.packages.${pkgs.system}.website;
|
||||||
|
in {
|
||||||
|
options.services.all-dressed-programming.phenix-website = {
|
||||||
|
enable = lib.mkEnableOption "Balle Phenix website";
|
||||||
|
domain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "ballephenix.com";
|
||||||
|
};
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 8000;
|
||||||
|
};
|
||||||
|
dataDir = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = "/var/lib/phenix";
|
||||||
|
};
|
||||||
|
databaseUrl = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
};
|
||||||
|
sessionKey = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
systemd.services.phenix-website = {
|
||||||
|
description = "Balle Phenix website";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
ExecStart = "${phenix}/lib/Phenix";
|
||||||
|
WorkingDirectory = "${phenix}/lib/";
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = 5;
|
||||||
|
User = "phenix";
|
||||||
|
Group = "phenix";
|
||||||
|
};
|
||||||
|
|
||||||
|
environment =
|
||||||
|
{
|
||||||
|
ASPNETCORE_URLS = "http://0.0.0.0:${toString cfg.port}";
|
||||||
|
ASPNETCORE_ENVIRONMENT = "Production";
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.databaseUrl == "") {
|
||||||
|
ConnectionStrings__Default = "Data Source=${cfg.dataDir}/phenix.db";
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.databaseUrl != "") {
|
||||||
|
DatabaseProvider = "Postgres";
|
||||||
|
ConnectionStrings__Default = cfg.databaseUrl;
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.sessionKey != "") {
|
||||||
|
SessionKey = cfg.sessionKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
postStart = ''
|
||||||
|
mkdir -p ${cfg.dataDir}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.phenix = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "phenix";
|
||||||
|
home = cfg.dataDir;
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.phenix = {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
build-static-files = pkgs.stdenv.mkDerivation {
|
dotnet = pkgs.dotnetCorePackages.sdk_9_0;
|
||||||
name = "phenix-website-static-assets";
|
pythonEnv = pkgs.python3.withPackages (ps: [ ps.requests ]);
|
||||||
src = ./static;
|
|
||||||
buildPhase = ''
|
|
||||||
find . -type f -exec ${pkgs.brotli}/bin/brotli -9 -f -k {} \;
|
|
||||||
'';
|
|
||||||
installPhase = ''
|
|
||||||
cp -r . $out
|
|
||||||
|
|
||||||
|
phenix-website = pkgs.buildDotnetModule {
|
||||||
|
name = "phenix-website";
|
||||||
|
src = ./.;
|
||||||
|
projectFile = "Phenix.csproj";
|
||||||
|
selfContainedBuild = true;
|
||||||
|
dotnet-sdk = dotnet;
|
||||||
|
dotnet-runtime = pkgs.dotnetCorePackages.runtime_9_0;
|
||||||
|
runtimeIdentifiers = "linux-x64";
|
||||||
|
nugetDeps = ./deps.nix;
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
cp -r static $out/lib/static/
|
||||||
|
cp appsettings.json $out/lib/
|
||||||
'';
|
'';
|
||||||
|
meta.mainProgram = "Phenix";
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
packages = { static = build-static-files; };
|
packages = {
|
||||||
|
default = phenix-website;
|
||||||
|
website = phenix-website;
|
||||||
|
};
|
||||||
|
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
name = "Phenix-Website";
|
name = "Phenix-Website";
|
||||||
buildInputs = with pkgs; [ deno ];
|
buildInputs = [ dotnet pkgs.nuget-to-nix pythonEnv ];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
export PATH="$PWD/dev/populate-db:$PATH"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}) // {
|
||||||
|
nixosModules.x86_64-linux = nixosModule;
|
||||||
};
|
};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
hello {{ title }}
|
|
||||||
@@ -1,218 +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>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
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 });
|
|
||||||
@@ -1,323 +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 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();
|
|
||||||
});
|
|
||||||
@@ -1,111 +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:
|
|
||||||
"5603007580f73044864e7a0b66744f405406eaaf28fa514d724ccf11d010bd89:070b1a477e6a877d44f31961dc0402a9f3258465eed73ab6a9110c461ac68714",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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([]);
|
|
||||||
const passwordHashWithSalt = await service.hashPassword("qwerty");
|
|
||||||
assert(await service.verifyPassword("qwerty", passwordHashWithSalt));
|
|
||||||
assertFalse(await service.verifyPassword("ytrewq", passwordHashWithSalt));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
|
After Width: | Height: | Size: 219 KiB |
File diff suppressed because it is too large.
Load diff
|
After Width: | Height: | Size: 379 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
@@ -1,13 +1,12 @@
|
|||||||
// alert("ok")
|
// alert("ok")
|
||||||
import * as util from "./util.js";
|
import * as util from "./util.js";
|
||||||
import * as resultParser from './resultParser.js'
|
import * as resultParser from "./resultParser.js";
|
||||||
import * as teams from './teams.js'
|
import * as teams from "./teams.js";
|
||||||
import m from "https://esm.sh/mithril@2.2.2"
|
import m from "https://esm.sh/mithril@2.2.2";
|
||||||
import { rainSvg } from "./images.js";
|
import { rainSvg } from "./images.js";
|
||||||
import _ from "https://esm.sh/lodash-es@4.17.21"
|
import _ from "https://esm.sh/lodash-es@4.17.21";
|
||||||
|
|
||||||
import {statsView, tabView, scheduleTeamTags } from './schedule.js'
|
|
||||||
|
|
||||||
|
import { statsView, tabView, scheduleTeamTags } from "./schedule.js";
|
||||||
|
|
||||||
const nextGameDaySchedule = {};
|
const nextGameDaySchedule = {};
|
||||||
|
|
||||||
@@ -16,55 +15,68 @@ nextGameDaySchedule._toDay = function(){
|
|||||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
};
|
};
|
||||||
|
|
||||||
nextGameDaySchedule.oninit = async function(){
|
const currentYear = new Date();
|
||||||
|
|
||||||
|
nextGameDaySchedule.oninit = async function () {
|
||||||
const result = await m.request({
|
const result = await m.request({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `/api/results/2024.json`
|
url: `/api/results/${new Date().getFullYear()}.json`,
|
||||||
});
|
});
|
||||||
const nightGames = resultParser.getNightGames(result);
|
const nightGames = resultParser.getNightGames(result);
|
||||||
nextGameDaySchedule.games = nextGameDaySchedule._getNextNightGame(nightGames, nextGameDaySchedule._toDay());
|
nextGameDaySchedule.games = nextGameDaySchedule._getNextNightGame(
|
||||||
|
nightGames,
|
||||||
|
nextGameDaySchedule._toDay(),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
nextGameDaySchedule._getNextNightGame = function (games, date) {
|
nextGameDaySchedule._getNextNightGame = function (games, date) {
|
||||||
return _(games)
|
return _(games)
|
||||||
.filter(g => g.jsDate.getTime() >= date.getTime())
|
.filter((g) => g.jsDate.getTime() >= date.getTime())
|
||||||
|
.sortBy("jsDate")
|
||||||
|
.reverse()
|
||||||
.last()
|
.last()
|
||||||
.games
|
.games.sort((g1, g2) => g1.jsDate - g2.jsDate);
|
||||||
.sort((g1, g2) => g1.jsDate - g2.jsDate);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nextGameDaySchedule._getTableHead = function () {
|
nextGameDaySchedule._getTableHead = function () {
|
||||||
return m('thead', m('tr', [
|
return m(
|
||||||
m('td', 'Date'),
|
"thead",
|
||||||
m('td', 'Heure'),
|
m("tr", [
|
||||||
m('td', 'Receveur'),
|
m("td", "Date"),
|
||||||
m('td', 'Visiteur')
|
m("td", "Heure"),
|
||||||
]));
|
m("td", "Visiteur"),
|
||||||
|
m("td", "Receveur"),
|
||||||
|
]),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
nextGameDaySchedule._responsoveText = function (largeText, smallText, game) {
|
nextGameDaySchedule._responsoveText = function (largeText, smallText, game) {
|
||||||
var ret = [];
|
var ret = [];
|
||||||
if (game.rain) {
|
if (game.rain) {
|
||||||
ret.push(rainSvg('1.5em'));
|
ret.push(rainSvg("1.5em"));
|
||||||
ret.push(' ');
|
ret.push(" ");
|
||||||
} else if (game.practice) {
|
} else if (game.practice) {
|
||||||
ret.push(m('span.uk-badge.uk-badge-success', 'pratique!'));
|
ret.push(m("span.uk-badge.uk-badge-success", "pratique!"));
|
||||||
ret.push(' ');
|
ret.push(" ");
|
||||||
}
|
}
|
||||||
ret.push(m('span.uk-visible-large', largeText));
|
ret.push(m("span.uk-visible-large", largeText));
|
||||||
ret.push(m('span.uk-hidden-large', smallText));
|
ret.push(m("span.uk-hidden-large", smallText));
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
nextGameDaySchedule._getGamesRow = function (game) {
|
nextGameDaySchedule._getGamesRow = function (game) {
|
||||||
return m('tr', [
|
return m("tr", [
|
||||||
m('td', nextGameDaySchedule._responsoveText(util.formatDate(game.jsDate),
|
m(
|
||||||
|
"td",
|
||||||
|
nextGameDaySchedule._responsoveText(
|
||||||
|
util.formatDate(game.jsDate),
|
||||||
util.formatDateSmall(game.jsDate),
|
util.formatDateSmall(game.jsDate),
|
||||||
game)),
|
game,
|
||||||
m('td', game.time),
|
),
|
||||||
m('td', scheduleTeamTags.getHomeTeamTag(game)),
|
),
|
||||||
m('td',scheduleTeamTags.getAwayTeamTag(game))
|
m("td", game.time),
|
||||||
|
m("td", scheduleTeamTags.getAwayTeamTag(game)),
|
||||||
|
m("td", scheduleTeamTags.getHomeTeamTag(game)),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,26 +85,26 @@ nextGameDaySchedule._getGamesRows = function(games){
|
|||||||
};
|
};
|
||||||
|
|
||||||
nextGameDaySchedule.view = function () {
|
nextGameDaySchedule.view = function () {
|
||||||
return m('table.uk-table.uk-table-hover.uk-table-striped.uk-table-condensed',[
|
return m(
|
||||||
|
"table.uk-table.uk-table-hover.uk-table-striped.uk-table-condensed",
|
||||||
|
[
|
||||||
nextGameDaySchedule._getTableHead(),
|
nextGameDaySchedule._getTableHead(),
|
||||||
m('tbody',
|
m("tbody", nextGameDaySchedule._getGamesRows(nextGameDaySchedule.games)),
|
||||||
nextGameDaySchedule._getGamesRows(nextGameDaySchedule.games)
|
],
|
||||||
)]);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LittleCalendar = {};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const LittleCalendar = {}
|
|
||||||
|
|
||||||
LittleCalendar.view = function () {
|
LittleCalendar.view = function () {
|
||||||
return m(nextGameDaySchedule);
|
return m(nextGameDaySchedule);
|
||||||
}
|
};
|
||||||
|
|
||||||
const containerIds = ['large-mithril-container',
|
const containerIds = [
|
||||||
'medium-mithril-container',
|
"large-mithril-container",
|
||||||
'small-mithril-container']
|
"medium-mithril-container",
|
||||||
|
"small-mithril-container",
|
||||||
|
];
|
||||||
|
|
||||||
for (const id of containerIds) {
|
for (const id of containerIds) {
|
||||||
m.mount(document.getElementById(id), LittleCalendar);
|
m.mount(document.getElementById(id), LittleCalendar);
|
||||||
|
|||||||
@@ -1,68 +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-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>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<h3 class="uk-panel-title">
|
|
||||||
<a href="#/nouvelles" class="uk-link-muted">
|
|
||||||
<h2 id="saison-2023">Saison 2023</h2>
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<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><strong><a href="#/nouvelles">lire la suite ...</a></strong></p>
|
|
||||||
</p>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,20 +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="saison-2023">Saison 2023</h2>
|
|
||||||
</h1>
|
|
||||||
<p class="uk-article-meta">
|
|
||||||
<p>Dimanche 10 Juillet 2023</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 }}
|
|
||||||
@@ -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="/member/new-game-editor"
|
|
||||||
hx-target="#content">
|
|
||||||
Nouvelle Partie
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
Reference in new issue
Block a user