import { render } from "solid-js/web"; import { Tooltip } from "@kobalte/core/tooltip"; import { For, Show, createEffect, createResource, createSignal, onMount, onCleanup, } from "solid-js"; import { customElement } from "solid-element"; import { SolidMarkdown } from "solid-markdown"; import { SunIcon, MoonIcon, HomeIcon, TrophyIcon, SwordsIcon, UsersIcon, UserIcon, LogInIcon, LogOutIcon, } from "lucide-solid"; import toast, { Toaster } from "solid-toast"; export { toast } from "solid-toast"; import { FluentBundle, FluentResource } from "@fluent/bundle"; import { negotiateLanguages } from "@fluent/langneg"; import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "./command"; function navigate(url: string) { // @ts-ignore htmx.ajax("GET", url, { select: "#screen", target: "#screen", swap: "outerHTML", history: "push", headers: { "HX-Boosted": true, }, }); history.pushState({ htmx: true }, "", url); } const CommandMenu = () => { const [open, setOpen] = createSignal(false); const [data] = createResource( async () => (await (await fetch("/command-palette")).json()) as CommandPaletteData, ); onMount(() => { const down = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen((open) => !open); } }; // @ts-ignore window.openCommandPalette = () => setOpen(true); document.addEventListener("keydown", down); onCleanup(() => document.removeEventListener("keydown", down)); }); return ( {translate("no-results")} { navigate("/"); setOpen(false); }} > {translate("home")} { navigate("/challenges"); setOpen(false); }} > {translate("challenges")} { navigate("/team"); setOpen(false); }} > {translate("team")} { navigate("/account"); setOpen(false); }} > {translate("account")} { navigate("/signin"); setOpen(false); }} > {translate("sign-in")} {(category) => { const category_name = category[0]; const challenges = category[1]; return ( {(challenge) => { return ( { navigate(`/challenges#${challenge}`); setOpen(false); }} > {challenge} ); }} ); }} {(division) => { const id = division[0]; const name = division[1]; return ( { navigate(`/scoreboard/${id}`); setOpen(false); }} > {translate("scoreboard", { scoreboard: name })} ); }} { // @ts-ignore window.location = "/signin/discord"; }} > {translate("sign-in-with-discord")} { navigate("/signout"); setOpen(false); }} > {translate("sign-out")} { // @ts-ignore setTheme(false); setOpen(false); }} > {translate("light-theme")} { // @ts-ignore setTheme(true); setOpen(false); }} > {translate("dark-theme")} ); }; const localizations: Record = { en: ` type-to-search = Type a command or search... no-results = No results found. pages = Pages home = Home challenges = Challenges team = Team account = Account sign-in = Sign In scoreboard-title = Division Scoreboards scoreboard = {$scoreboard} Scoreboard sign-in-with-discord = Sign In with Discord sign-out = Sign Out theme = Theme light-theme = Light Theme dark-theme = Dark Theme healthy = Challenge is up. Last checked {$last_checked -> [one] {$last_checked} minute *[other] {$last_checked} minutes } ago unhealthy = Challenge is down. Last checked {$last_checked -> [one] {$last_checked} minute *[other] {$last_checked} minutes } ago solved-by = Solved by {$name} authored-by = Authored by {$name} solves = {$solves -> [one] {$solves} solve *[other] {$solves} solves } points = {$points -> [one] {$points} point *[other] {$points} points } solves-points = {solves} / {points} `.trim(), de: ` type-to-search = Befehl oder Suche eingeben... no-results = Keine Ergebnisse gefunden. pages = Seiten home = Startseite challenges = Challenges team = Team account = Konto sign-in = Anmelden scoreboard-title = Teilung Anzeigetafeln scoreboard = {$scoreboard} Anzeigetafel sign-in-with-discord = Mit Discord anmelden sign-out = Abmelden theme = Farbdesign light-theme = Helles Farbdesign dark-theme = Dunkles Farbdesign healthy = Challenge ist online und wurde vor {$last_checked -> [one] {$last_checked} Minute *[other] {$last_checked} Minuten } überprüft unhealthy = Challenge ist offline und wurde vor {$last_checked -> [one] {$last_checked} Minute *[other] {$last_checked} Minuten } überprüft solved-by = Gelöst von {$name} authored-by = Geschrieben von {$name} solves = {$solves -> [one] {$solves} Lösung *[other] {$solves} Lösungen } points = {$points -> [one] {$points} Punkt *[other] {$points} Punkte } solves-points = {solves} / {points} `.trim(), }; const negotiatedLocales = negotiateLanguages( navigator.languages, Object.keys(localizations), { defaultLocale: Object.keys(localizations)[0], }, ); const bundles = negotiatedLocales.map((locale) => { const bundle = new FluentBundle(locale); bundle.addResource(new FluentResource(localizations[locale])); return bundle; }); export function translate(key: string, args?: Record) { for (const bundle of bundles) { const message = bundle.getMessage(key); if (message) { return bundle.formatPattern(message.value, args); } } return key; } customElement("rhombus-tooltip", (_props, { element }) => { const anchor = document.querySelector("dialog"); // @ts-ignore let children: HTMLCollection = element.renderRoot.host.children; let content: Element = undefined; let trigger: Element = undefined; for (let i = 0; i < children.length; i++) { const slot = children[i].slot; if (slot === "content") { content = children[i]; } else { trigger = children[i]; } } return ( {content} {trigger} ); }); const ChallengesComponent = ({ challenge_json, }: { challenge_json: ChallengesData; }) => { const [shouldFetch, setShouldFetch] = createSignal(); const [data, { refetch }] = createResource( shouldFetch, async () => (await ( await fetch("/challenges.json", { headers: { accept: "application/json" }, }) ).json()) as ChallengesData, { initialValue: challenge_json }, ); (window as any).challengeRefetchHandler = () => { setShouldFetch(true); refetch(); }; createEffect(() => { data(); // @ts-ignore htmx.process(document.body); }); return ( {(category) => { const challenges = data() .challenges.filter( (challenge) => challenge.category_id === category.id, ) .sort( (a, b) => a.division_points[0].points - b.division_points[0].points, ); return ( {category.name} { challenges.filter( (challenge) => data().team.solves[challenge.id] !== undefined, ).length }{" "} / {challenges.length} {(challenge) => { const author = data().authors[challenge.author_id]; const solve = data().team.solves[challenge.id]; return ( {category.name} / {challenge.name} {challenge.health.healthy ? ( ) : ( )} {translate("solved-by", { name: data().team.users[solve.user_id] .name, })} {challenge.division_points.map( (division_points) => ( { data().divisions[ division_points.division_id ].name } {translate("solves", { solves: division_points.solves, })} {translate("points", { points: division_points.points, })} ), )} {translate("solves-points", { solves: challenge.division_points[0].solves, points: challenge.division_points[0].points, })} {translate("authored-by", { name: author.name, })} 0}> {(attachment) => ( {attachment.name} )} ); }} ); }} ); }; type CommandPaletteData = { challenges?: Record; divisions: Record; }; type ChallengesData = { ticket_enabled: boolean; challenges: { id: number; name: string; description: string; health: { healthy: boolean; last_checked: string; } | null; category_id: number; author_id: number; division_points: { division_id: number; points: number; solves: number; }[]; attachments: { name: string; url: string; }[]; }[]; categories: { id: number; name: string; color: string; }[]; authors: Record< number, { name: string; avatar_url: string; } >; divisions: Record< number, { name: string; } >; team: { users: Record< number, { name: string; avatar_url: string; } >; solves: Record< number, { solved_at: Date; user_id: number; } >; }; }; export function renderChallenges( element: HTMLElement, challenge_json: ChallengesData, ) { element.innerHTML = ""; render( () => , element, ); } document.addEventListener("DOMContentLoaded", () => { render( () => , document.querySelector("#toaster"), ); render(() => , document.body); document.body.addEventListener("pageRefresh", () => { location.reload(); }); document.body.addEventListener( "toast", ( evt: Event & { detail: { kind: "success" | "error"; message: string } }, ) => { if (evt.detail.kind === "success") { toast.success(evt.detail.message); } else if (evt.detail.kind === "error") { toast.error(evt.detail.message); } else { console.log("Unknown toast kind", evt.detail.kind); } }, ); });