Fix Campaign log DB and rendering. Migrate mail rendering to virtual DOM API.
This commit is contained in:
parent
7a40f8abac
commit
c9f60d92ca
|
|
@ -103,7 +103,7 @@ export const campaignCharactersTable = table("campaign_characters", {
|
|||
}, (table) => [primaryKey({ columns: [table.id, table.character] })]);
|
||||
export const campaignLogsTable = table("campaign_logs", {
|
||||
id: int().references(() => campaignTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
target: int().references(() => campaignCharactersTable.character, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
target: int(),
|
||||
timestamp: int({ mode: 'timestamp_ms' }).notNull(),
|
||||
type: text({ enum: ['ITEM', 'CHARACTER', 'PLACE', 'EVENT', 'FIGHT', 'TEXT'] }),
|
||||
details: text().notNull(),
|
||||
|
|
@ -166,5 +166,4 @@ export const campaignCharacterRelation = relations(campaignCharactersTable, ({ o
|
|||
}));
|
||||
export const campaignLogsRelation = relations(campaignLogsTable, ({ one }) => ({
|
||||
campaign: one(campaignTable, { fields: [campaignLogsTable.id], references: [campaignTable.id], }),
|
||||
character: one(campaignCharactersTable, { fields: [campaignLogsTable.target], references: [campaignCharactersTable.character], }),
|
||||
}));
|
||||
|
|
@ -21,6 +21,6 @@ export type Campaign = {
|
|||
export type CampaignLog = {
|
||||
target: number;
|
||||
timestamp: Serialize<Date>;
|
||||
type: 'ITEM' | 'CHARACTER' | 'PLACE' | 'EVENT' | 'FIGHT' | 'TEXT';
|
||||
type: 'ITEM' | 'CHARACTER' | 'PLACE' | 'FIGHT' | 'TEXT';
|
||||
details: string;
|
||||
};
|
||||
|
|
@ -6,7 +6,7 @@ CREATE TABLE `campaign_logs` (
|
|||
`details` text NOT NULL,
|
||||
PRIMARY KEY(`id`, `from`, `timestamp`),
|
||||
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`from`) REFERENCES `campaign_characters`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
FOREIGN KEY (`from`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `campaign` ADD `status` text DEFAULT 'PREPARING';--> statement-breakpoint
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ CREATE TABLE `__new_campaign_logs` (
|
|||
`details` text NOT NULL,
|
||||
PRIMARY KEY(`id`, `from`, `timestamp`),
|
||||
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`from`) REFERENCES `campaign_characters`(`character`) ON UPDATE cascade ON DELETE cascade
|
||||
FOREIGN KEY (`from`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_campaign_logs`("id", "from", "timestamp", "type", "details") SELECT "id", "from", "timestamp", "type", "details" FROM `campaign_logs`;--> statement-breakpoint
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ CREATE TABLE `__new_campaign_logs` (
|
|||
`details` text NOT NULL,
|
||||
PRIMARY KEY(`id`, `target`, `timestamp`),
|
||||
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`target`) REFERENCES `campaign_characters`(`character`) ON UPDATE cascade ON DELETE cascade
|
||||
FOREIGN KEY (`target`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_campaign_logs`("id", "target", "timestamp", "type", "details") SELECT "id", "target", "timestamp", "type", "details" FROM `campaign_logs`;--> statement-breakpoint
|
||||
|
|
|
|||
|
|
@ -116,19 +116,6 @@
|
|||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
},
|
||||
"campaign_logs_from_campaign_characters_id_fk": {
|
||||
"name": "campaign_logs_from_campaign_characters_id_fk",
|
||||
"tableFrom": "campaign_logs",
|
||||
"tableTo": "campaign_characters",
|
||||
"columnsFrom": [
|
||||
"from"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
|
|
|
|||
|
|
@ -116,19 +116,6 @@
|
|||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
},
|
||||
"campaign_logs_from_campaign_characters_character_fk": {
|
||||
"name": "campaign_logs_from_campaign_characters_character_fk",
|
||||
"tableFrom": "campaign_logs",
|
||||
"tableTo": "campaign_characters",
|
||||
"columnsFrom": [
|
||||
"from"
|
||||
],
|
||||
"columnsTo": [
|
||||
"character"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
|
|
|
|||
|
|
@ -116,19 +116,6 @@
|
|||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
},
|
||||
"campaign_logs_from_campaign_characters_character_fk": {
|
||||
"name": "campaign_logs_from_campaign_characters_character_fk",
|
||||
"tableFrom": "campaign_logs",
|
||||
"tableTo": "campaign_characters",
|
||||
"columnsFrom": [
|
||||
"from"
|
||||
],
|
||||
"columnsTo": [
|
||||
"character"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
|
|
|
|||
|
|
@ -116,19 +116,6 @@
|
|||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
},
|
||||
"campaign_logs_target_campaign_characters_character_fk": {
|
||||
"name": "campaign_logs_target_campaign_characters_character_fk",
|
||||
"tableFrom": "campaign_logs",
|
||||
"tableTo": "campaign_characters",
|
||||
"columnsFrom": [
|
||||
"target"
|
||||
],
|
||||
"columnsTo": [
|
||||
"character"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
import vuePlugin from 'rollup-plugin-vue'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
|
|
@ -136,10 +135,7 @@ export default defineNuxtConfig({
|
|||
usePolling: true,
|
||||
},
|
||||
rollupConfig: {
|
||||
external: ['bun'],
|
||||
plugins: [
|
||||
vuePlugin({ include: /\.vue$/, target: 'node' })
|
||||
]
|
||||
external: ['bun']
|
||||
},
|
||||
},
|
||||
runtimeConfig: {
|
||||
|
|
@ -168,7 +164,7 @@ export default defineNuxtConfig({
|
|||
xssValidator: false,
|
||||
},
|
||||
sitemap: {
|
||||
exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated', '/user/changing-password', '/user/reset-password'],
|
||||
exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated', '/user/changing-password', '/user/reset-password', '/character/manage', '/campaign/create'],
|
||||
sources: ['/api/__sitemap__/urls']
|
||||
},
|
||||
experimental: {
|
||||
|
|
@ -190,6 +186,13 @@ export default defineNuxtConfig({
|
|||
cert: fs.readFileSync(path.resolve(__dirname, 'localhost+1.pem')).toString('utf-8'),
|
||||
}
|
||||
},
|
||||
vite: {
|
||||
server: {
|
||||
hmr: {
|
||||
clientPort: 3000,
|
||||
}
|
||||
}
|
||||
},
|
||||
vue: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag === 'iconify-icon',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { dom } from '#shared/dom';
|
||||
import { dom, type VirtualNode } from "#shared/dom.virtual.util";
|
||||
|
||||
export default function(content: HTMLElement[])
|
||||
export default function(content: VirtualNode[])
|
||||
{
|
||||
return [dom('div', { style: 'margin-left: auto; margin-right: auto; width: 75%; font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; line-height: 1.5rem; color: #171717;' }, [
|
||||
dom('div', { style: 'margin-left: auto; margin-right: auto; text-align: center;' }, [
|
||||
dom('a', { style: 'display: inline-block;', attributes: { href: 'https://d-any.com' } }, [
|
||||
dom('img', { style: 'display: block; height: 4rem; width: 4rem; margin-left: auto; margin-right: auto;', attributes: { src: 'https://d-any.com/logo.light.png', alt: 'Logo', title: 'd[any] logo', width: '64', height: '64' } })
|
||||
dom('img', { style: 'display: block; height: 4rem; width: 4rem; margin-left: auto; margin-right: auto;', attributes: { src: 'https://d-any.com/logo.light.png', alt: 'Logo', title: 'd[any] logo', width: '64', height: '64' } }),
|
||||
dom('span', { style: `margin-inline-end: 1rem; font-size: 1.5rem; color: black; text-decoration: none; line-height: 2rem; font-weight: 700; font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;`, text: 'd[any]' })
|
||||
])
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
<template>
|
||||
<div style='margin-left: auto; margin-right: auto; width: 75%; font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; line-height: 1.5rem; color: #171717;'>
|
||||
<div style="margin-left: auto; margin-right: auto; text-align: center;">
|
||||
<a style="display: inline-block;" href="https://d-any.com">
|
||||
<img src="https://d-any.com/logo.light.png" alt="Logo" title="d[any] logo" width="64" height="64" style="display: block; height: 4rem; width: 4rem; margin-left: auto; margin-right: auto;" />
|
||||
<span style="margin-inline-end: 1rem; font-size: 1.5rem; color: black; text-decoration: none; line-height: 2rem; font-weight: 700; font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;">d[any]</span>
|
||||
</a>
|
||||
</div>
|
||||
<div style="padding: 1rem;">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background-color: #171717;">
|
||||
<p style="padding-top: 1rem; padding-bottom: 1rem; text-align: center; font-size: 0.75rem; line-height: 1rem; color: #fff;">Copyright Peaceultime / d[any] - 2024 / 2025</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { dom, text } from "#shared/dom.virtual.util";
|
||||
|
||||
export default function(data: any)
|
||||
{
|
||||
const hash = Bun.hash('1' + data.userId.toString(), data.timestamp);
|
||||
return [dom('div', { style: 'max-width: 800px; margin-left: auto; margin-right: auto;' }, [
|
||||
dom('p', { style: 'font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;' }, [ text(`Bienvenue sur d[any], `), dom('span', { style: '' }, [ text(data.username) ]) ]),
|
||||
dom('p', { style: '' }, [ text(`Nous vous invitons à valider votre compte afin de profiter de toutes les fonctionnalités de d[any].`) ]),
|
||||
dom('div', { style: 'padding-top: 1rem; padding-bottom: 1rem; text-align: center;' }, [
|
||||
dom('a', { attributes: { href: `https://d-any.com/user/mailvalidation?u=${data.userId}&i=${data.id}&t=${data.timestamp}&h=${hash}`, target: '_blank' } }, [ dom('span', { style: 'display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;' }, [ text('Je valide mon compte') ]) ]),
|
||||
dom('span', { style: 'display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;' }, [ text('Ce lien est valable 1 heure.') ])
|
||||
]),
|
||||
dom('div', { style: '' }, [
|
||||
dom('span', { style: '' }, [ text('Vous pouvez egalement copier le lien suivant pour valider votre compte: ') ]),
|
||||
dom('pre', { style: 'display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;' }, [ text(`https://d-any.com/user/mailvalidation?u=${data.userId}&i=${data.id}&t=${data.timestamp}&h=${hash}`) ])
|
||||
])
|
||||
])];
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<template>
|
||||
<div style="max-width: 800px; margin-left: auto; margin-right: auto;">
|
||||
<p style="font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;">Bienvenue sur d[any], <span>{{ username }}</span>.</p>
|
||||
<p>Nous vous invitons à valider votre compte afin de profiter de toutes les fonctionnalités de d[any].</p>
|
||||
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
|
||||
<a :href="`https://d-any.com/user/mailvalidation?u=${userId}&i=${id}&t=${timestamp}&h=${hash}`" target="_blank"><span style="display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;">Je valide mon compte</span></a>
|
||||
<span style="display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;">Ce lien est valable 1 heure.</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Vous pouvez egalement copier le lien suivant pour valider votre compte: </span>
|
||||
<pre style="display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;">{{ `https://d-any.com/user/mailvalidation?u=${userId}&i=${id}&t=${timestamp}&h=${hash}` }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import Bun from 'bun';
|
||||
|
||||
const { id, userId, username, timestamp } = defineProps<{
|
||||
id: number
|
||||
userId: number
|
||||
username: string
|
||||
timestamp: number
|
||||
}>();
|
||||
const hash = computed(() => Bun.hash('1' + userId.toString(), timestamp));
|
||||
</script>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { dom, text } from "#shared/dom.virtual.util";
|
||||
|
||||
export default function(data: any)
|
||||
{
|
||||
const hash = Bun.hash('1' + data.userId.toString(), data.timestamp);
|
||||
return [dom('div', { style: 'max-width: 800px; margin-left: auto; margin-right: auto;' }, [
|
||||
dom('p', { style: 'font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;' }, [ text(`Bonjour `), dom('span', { style: '' }, [ text(data.username) ]) ]),
|
||||
dom('p', { style: '' }, [ text(`Vous avez demandé à réinitialiser votre mot de passe aujourd'hui à ${ format(new Date(data.timestamp), 'HH:mm') }.`) ]),
|
||||
dom('div', { style: 'padding-top: 1rem; padding-bottom: 1rem; text-align: center;' }, [
|
||||
dom('a', { attributes: { href: `https://d-any.com/user/resetting-password?u=${data.userId}&i=${data.id}&t=${data.timestamp}&h=${hash}`, target: '_blank' } }, [ dom('span', { style: 'display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;' }, [ text('Je change mon mot de passe.') ]) ]),
|
||||
dom('span', { style: 'display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;' }, [ text('Ce lien est valable 1 heure.') ])
|
||||
]),
|
||||
dom('div', { style: '' }, [
|
||||
dom('span', { style: '' }, [ text('Vous pouvez egalement copier le lien suivant pour valider votre compte: ') ]),
|
||||
dom('pre', { style: 'display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;' }, [ text(`https://d-any.com/user/mailvalidation?u=${data.userId}&i=${data.id}&t=${data.timestamp}&h=${hash}`) ])
|
||||
]),
|
||||
dom('span', { style: '', text: 'Si vous n\'êtes pas à l\'origine de cette demande, vous n\'avez pas à modifier votre mot de passe, ce dernier est conservé tant que vous n\'interagissez pas avec le lien ci-dessus.' }),
|
||||
])];
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<template>
|
||||
<div style="max-width: 800px; margin-left: auto; margin-right: auto;">
|
||||
<p style="font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;">Bonjour <span>{{ username }}</span>.</p>
|
||||
<p>Vous avez demandé à réinitialiser votre mot de passe aujourd'hui à {{ format(new Date(timestamp), 'HH:mm') }}.</p>
|
||||
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
|
||||
<a :href="`https://d-any.com/user/resetting-password?u=${userId}&i=${id}&t=${timestamp}&h=${hash}`" target="_blank"><span style="display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;">Je change mon mot de passe.</span></a>
|
||||
<span style="display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;">Ce lien est valable 1 heure.</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Vous pouvez egalement copier le lien suivant pour changer votre mot de passe: </span>
|
||||
<pre style="display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;">{{ `https://d-any.com/user/resetting-password?u=${userId}&i=${id}&t=${timestamp}&h=${hash}` }}</pre>
|
||||
</div>
|
||||
<span>Si vous n'êtes pas à l'origine de cette demande, vous n'avez pas à modifier votre mot de passe, ce dernier est conservé tant que vous n'interagissez pas avec le lien ci-dessus.</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import Bun from 'bun';
|
||||
import { format } from '#shared/general.util';
|
||||
|
||||
const { id, userId, username, timestamp } = defineProps<{
|
||||
id: number
|
||||
userId: number
|
||||
username: string
|
||||
timestamp: number
|
||||
}>();
|
||||
const hash = computed(() => Bun.hash('2' + userId.toString(), timestamp));
|
||||
</script>
|
||||
|
|
@ -2,16 +2,20 @@ import nodemailer from 'nodemailer';
|
|||
import { createSSRApp, h } from 'vue';
|
||||
import { renderToString } from 'vue/server-renderer';
|
||||
|
||||
import base from '../components/mail/base.vue';
|
||||
import base from '../components/mail/base';
|
||||
import registration from '../components/mail/registration';
|
||||
import reset_password from '../components/mail/reset-password';
|
||||
|
||||
/* import base from '../components/mail/base.vue';
|
||||
import Registration from '../components/mail/registration.vue';
|
||||
import ResetPassword from '../components/mail/reset-password.vue';
|
||||
import ResetPassword from '../components/mail/reset-password.vue'; */
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const [domain, selector, dkim] = config.mail.dkim.split(":");
|
||||
|
||||
export const templates: Record<string, { component: any, subject: string }> = {
|
||||
"registration": { component: Registration, subject: 'Bienvenue sur d[any] 😎' },
|
||||
"reset-password": { component: ResetPassword, subject: 'Réinitialisation de votre mot de passe' },
|
||||
export const templates: Record<string, { component: (data: any) => string[], subject: string }> = {
|
||||
"registration": { component: registration, subject: 'Bienvenue sur d[any] 😎' },
|
||||
"reset-password": { component: reset_password, subject: 'Réinitialisation de votre mot de passe' },
|
||||
};
|
||||
|
||||
import type Mail from 'nodemailer/lib/mailer';
|
||||
|
|
@ -76,11 +80,13 @@ export default defineTask({
|
|||
throw new Error(`Modèle de mail ${mailPayload.template} inconnu`);
|
||||
}
|
||||
|
||||
console.log(`<html><body>${base(template.component(mailPayload.data))}</body></html>`);
|
||||
|
||||
console.time('Generating HTML');
|
||||
const mail: Mail.Options = {
|
||||
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
|
||||
to: mailPayload.to,
|
||||
html: await render(template.component, mailPayload.data),
|
||||
html: `<html><body>${base(template.component(mailPayload.data))}</body></html>`,
|
||||
subject: template.subject,
|
||||
textEncoding: 'quoted-printable',
|
||||
};
|
||||
|
|
@ -107,16 +113,3 @@ export default defineTask({
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function render(component: any, data: Record<string, any>): Promise<string>
|
||||
{
|
||||
const app = createSSRApp({
|
||||
render(){
|
||||
return h(base, null, { default: () => h(component, data, { default: () => null }) });
|
||||
}
|
||||
});
|
||||
|
||||
const html = await renderToString(app);
|
||||
|
||||
return (`<html><body><div>${html}</div></body></html>`);
|
||||
}
|
||||
|
|
@ -107,5 +107,5 @@ function _useSession(event: H3Event | CompatEvent) {
|
|||
|
||||
sessionConfig = runtimeConfig.session;
|
||||
}
|
||||
return useSession<UserSession>(event, sessionConfig);
|
||||
return useSession<UserSession>(event, sessionConfig ?? {});
|
||||
}
|
||||
|
|
@ -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]' } }),
|
||||
])
|
||||
]))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in New Issue