Fix Campaign log DB and rendering. Migrate mail rendering to virtual DOM API.

This commit is contained in:
Clément Pons
2025-11-19 17:14:45 +01:00
parent 7a40f8abac
commit c9f60d92ca
22 changed files with 237 additions and 177 deletions

View File

@@ -1,13 +1,13 @@
import { z } from "zod/v4";
import type { User } from "~/types/auth";
import type { Campaign } from "~/types/campaign";
import type { Campaign, CampaignLog } from "~/types/campaign";
import { div, dom, icon, span, svg, text } from "#shared/dom.util";
import { button, loading, tabgroup } from "#shared/components.util";
import { CharacterCompiler } from "#shared/character.util";
import { tooltip } from "#shared/floating.util";
import markdown from "#shared/markdown.util";
import { preview } from "./proses";
import { format } from "./general.util";
import { preview } from "#shared/proses";
import { format } from "#shared/general.util";
import { Socket } from "#shared/websocket.util";
export const CampaignValidation = z.object({
@@ -49,6 +49,13 @@ type PlayerState = {
dom: HTMLElement;
user: { id: number, username: string };
};
const logType: Record<CampaignLog['type'], string> = {
CHARACTER: ' a rencontré ',
FIGHT: ' a affronté ',
ITEM: ' a obtenu ',
PLACE: ' est arrivé ',
TEXT: ' ',
}
const activity = {
online: { class: 'absolute -bottom-1 -right-1 rounded-full w-3 h-3 block border-2 box-content bg-light-green dark:bg-dark-green border-light-green dark:border-dark-green', text: 'En ligne' },
afk: { class: 'absolute -bottom-1 -right-1 rounded-full w-3 h-3 block border-2 box-content bg-light-yellow dark:bg-dark-yellow border-light-yellow dark:border-dark-yellow', text: 'Inactif' },
@@ -128,6 +135,10 @@ export class CampaignSheet
]));
});
}
private logText(log: CampaignLog)
{
return `${log.target === 0 ? 'Le groupe' : this.players.find(e => e.user.id === log.target)?.user.username ?? 'Le groupe'}${logType[log.type]}${log.details}`;
}
private render()
{
const campaign = this.campaign;
@@ -147,9 +158,9 @@ export class CampaignSheet
]),
div('flex flex-1 flex-col items-center justify-center', [
div('border border-light-35 dark:border-dark-35 p-1 flex flex-row items-center gap-2', [
dom('pre', { class: 'ps-1 w-[400px] truncate' }, [ text(`https://d-any.com/campaign/join/${ encodeURIComponent(campaign.link) }`) ]),
dom('pre', { class: 'ps-1 w-[400px] truncate' }, [ text(`d-any.com/campaign/join/${ encodeURIComponent(campaign.link) }`) ]),
button(icon('radix-icons:clipboard', { width: 16, height: 16 }), () => {}, 'p-1'),
])
]),
]),
]),
div('flex flex-row gap-4 flex-1', [
@@ -158,7 +169,7 @@ export class CampaignSheet
...this.characters.map(e => e.container),
]),
div('flex h-full border-l border-light-40 dark:border-dark-40'),
div('flex flex-col w-full max-w-[900px] w-[900px]', [
div('flex flex-col', [
tabgroup([
{ id: 'campaign', title: [ text('Campagne') ], content: () => [
markdown(campaign.public_notes, '', { tags: { a: preview } }),
@@ -166,23 +177,43 @@ export class CampaignSheet
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [
] },
{ id: 'logs', title: [ text('Logs') ], content: () => [
campaign.logs.length > 0 ? div('flex flex-row ps-12 py-4', [
div('border-l-2 border-light-40 dark:border-dark-40'),
div('flex flex-col gap-8 py-4', campaign.logs.map(e => div('flex flex-row gap-2 items-center relative -left-4', [
div('w-3 h-3 border-2 rounded-full bg-light-40 dark:border-dark-40 border-light-0 dark:border-dark-0'),
div('flex flex-row items-center', [ svg('svg', { class: ' fill-light-40 dark:fill-dark-40', attributes: { width: "6", height: "9", viewBox: "0 0 6 9" } }, [svg('path', { attributes: { d: "M0 4.5L6 -4.15L6 9L0 4.5Z" } })]), span('px-4 py-2 bg-light-25 dark:bg-dark-25 border border-light-40 dark:border-dark-40', e.details) ]),
span('italic text-sm tracking-tight text-light-70 dark:text-dark-70', format(new Date(e.timestamp), 'hh:mm:ss')),
])))
]) : div('flex py-4 px-16', [ span('italic text-light-70 dark:text-darl-70', 'Aucune entrée pour le moment') ])
] },
{ id: 'logs', title: [ text('Logs') ], content: () => {
let lastDate: Date = new Date(0);
const logs = campaign.logs.flatMap(e => {
const date = new Date(e.timestamp), arr = [];
if(Math.floor(lastDate.getTime() / 86400000) < Math.floor(date.getTime() / 86400000))
{
lastDate = date;
arr.push(div('flex flex-row gap-2 items-center relative -left-2 mx-px', [
div('w-3 h-3 border-2 rounded-full bg-light-40 dark:bg-dark-40 border-light-0 dark:border-dark-0'),
div('flex flex-row gap-2 items-center flex-1', [
div('flex-1 border-t border-light-40 dark:border-dark-40 border-dashed'),
span('text-light-70 dark:text-dark-70 text-sm italic', format(date, 'dd MMMM yyyy')),
div('flex-1 border-t border-light-40 dark:border-dark-40 border-dashed'),
])
]))
}
arr.push(div('flex flex-row gap-2 items-center relative -left-2 mx-px', [
div('w-3 h-3 border-2 rounded-full bg-light-40 dark:bg-dark-40 border-light-0 dark:border-dark-0'),
div('flex flex-row items-center', [ svg('svg', { class: 'fill-light-40 dark:fill-dark-40', attributes: { width: "6", height: "9", viewBox: "0 0 6 9" } }, [svg('path', { attributes: { d: "M0 4.5L6 -4.15L6 9L0 4.5Z" } })]), span('px-4 py-2 bg-light-25 dark:bg-dark-25 border border-light-40 dark:border-dark-40', this.logText(e)) ]),
span('italic text-xs tracking-tight text-light-70 dark:text-dark-70', format(new Date(e.timestamp), 'HH:mm:ss')),
]));
return arr;
});
return [
campaign.logs.length > 0 ? div('flex flex-row ps-12 py-4', [
div('border-l-2 border-light-40 dark:border-dark-40 relative before:absolute before:block before:border-[6px] before:border-b-[12px] before:-left-px before:-translate-x-1/2 before:border-transparent before:border-b-light-40 dark:before:border-b-dark-40 before:-top-3'),
div('flex flex-col-reverse gap-8 py-4', logs),
]) : div('flex py-4 px-16', [ span('italic text-light-70 dark:text-darl-70', 'Aucune entrée pour le moment') ]),
]
} },
{ id: 'settings', title: [ text('Paramètres') ], content: () => [
] },
{ id: 'ressources', title: [ text('Ressources') ], content: () => [
] }
], { focused: 'campaign', })
], { focused: 'campaign', class: { container: 'max-w-[900px] w-[900px]' } }),
])
]))
}

106
shared/dom.virtual.util.ts Normal file
View File

@@ -0,0 +1,106 @@
import { iconLoaded, loadIcon, getIcon } from "iconify-icon";
import type { NodeProperties, Class } from "#shared/dom.util";
export type VirtualNode = string;
export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?: NodeProperties, children?: VirtualNode[]): VirtualNode
{
const node = [`<${tag}`];
if(properties?.attributes)
for(const [k, v] of Object.entries(properties.attributes))
if(typeof v === 'string' || typeof v === 'number') node.push(` ${k}="${v.toString(10).replaceAll('"', "'")}"`);
else if(typeof v === 'boolean' && v) node.push(` ${k.replaceAll('"', "'")}`);
if(properties?.class)
node.push(` class="${mergeClasses(properties.class).replaceAll('"', "'")}"`);
if(properties?.style)
{
if(typeof properties.style === 'string') node.push(` style="${properties.style.replaceAll('"', "'")}"`);
else node.push(` style="${Object.entries(properties.style).map(([k, v]) => { if(v !== undefined && v !== false) return `${k.replaceAll('"', "'")}: ${v.toString(10).replaceAll('"', "'")};` }).join('')}"`);
}
if(properties?.text)
{
children ??= [];
children?.push(properties.text);
}
if(children)
node.push(`>${children.filter(e => !!e).join('')}</${tag}>`);
else
node.push(`></${tag}>`)
return node.join('');
}
export function div(cls?: Class, children?: VirtualNode[]): VirtualNode
{
return dom("div", { class: cls }, children);
}
export function span(cls?: Class, text?: string): VirtualNode
{
return dom("span", { class: cls, text: text });
}
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: VirtualNode[]): VirtualNode
{
const node = [`<${tag}`];
if(properties?.attributes)
for(const [k, v] of Object.entries(properties.attributes))
if(typeof v === 'string' || typeof v === 'number') node.push(` ${k.replaceAll('"', "'")}="${v.toString(10).replaceAll('"', "'")}"`);
else if(typeof v === 'boolean' && v) node.push(` ${k.replaceAll('"', "'")}`);
if(properties?.class)
node.push(` class="${mergeClasses(properties.class).replaceAll('"', "'")}"`);
if(properties?.style)
{
if(typeof properties.style === 'string') node.push(` style="${properties.style.replaceAll('"', "'")}"`);
else node.push(` style="${Object.entries(properties.style).map(([k, v]) => { if(v !== undefined && v !== false) return `${k.replaceAll('"', "'")}: ${v.toString(10).replaceAll('"', "'")};"` }).join('')}"`);
}
if(children)
node.push(`>${children.filter(e => !!e).join('')}</${tag}>`);
else
node.push(`></${tag}>`)
return node.join(' ');
}
export function text(data: string): VirtualNode
{
return data;
}
export interface IconProperties
{
mode?: string;
inline?: boolean;
noobserver?: boolean;
width?: string|number;
height?: string|number;
flip?: string;
rotate?: number|string;
style?: Record<string, string | undefined> | string;
class?: Class;
}
export function icon(name: string, properties?: IconProperties): VirtualNode
{
return '';
}
export function mergeClasses(classes: Class): string
{
if(typeof classes === 'string')
{
return classes.trim();
}
else if(Array.isArray(classes))
{
return classes.map(e => mergeClasses(e)).join(' ');
}
else if(classes)
{
return Object.entries(classes).filter(e => e[1]).map(e => e[0].trim()).join(' ');
}
else
{
return '';
}
}

View File

@@ -75,8 +75,23 @@ export function padRight(text: string, pad: string, length: number): string
}
export function format(date: Date, template: string): string
{
const months = {
0: 'janvier',
1: 'fevrier',
2: 'mars',
3: 'avril',
4: 'mai',
5: 'juin',
6: 'juillet',
7: 'aout',
8: 'septembre',
9: 'octobre',
10: 'novembre',
11: 'decembre',
};
const transforms: Record<string, (date: Date) => string> = {
"yyyy": (date: Date) => date.getUTCFullYear().toString(),
"MMMM": (date: Date) => months[date.getUTCMonth() as keyof typeof months],
"MM": (date: Date) => padRight((date.getUTCMonth() + 1).toString(), '0', 2),
"dd": (date: Date) => padRight(date.getUTCDate().toString(), '0', 2),
"mm": (date: Date) => padRight(date.getUTCMinutes().toString(), '0', 2),