You've already forked obsidian-visualiser
Fix Campaign log DB and rendering. Migrate mail rendering to virtual DOM API.
This commit is contained in:
@@ -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
106
shared/dom.virtual.util.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user