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] })]);
|
}, (table) => [primaryKey({ columns: [table.id, table.character] })]);
|
||||||
export const campaignLogsTable = table("campaign_logs", {
|
export const campaignLogsTable = table("campaign_logs", {
|
||||||
id: int().references(() => campaignTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
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(),
|
timestamp: int({ mode: 'timestamp_ms' }).notNull(),
|
||||||
type: text({ enum: ['ITEM', 'CHARACTER', 'PLACE', 'EVENT', 'FIGHT', 'TEXT'] }),
|
type: text({ enum: ['ITEM', 'CHARACTER', 'PLACE', 'EVENT', 'FIGHT', 'TEXT'] }),
|
||||||
details: text().notNull(),
|
details: text().notNull(),
|
||||||
|
|
@ -166,5 +166,4 @@ export const campaignCharacterRelation = relations(campaignCharactersTable, ({ o
|
||||||
}));
|
}));
|
||||||
export const campaignLogsRelation = relations(campaignLogsTable, ({ one }) => ({
|
export const campaignLogsRelation = relations(campaignLogsTable, ({ one }) => ({
|
||||||
campaign: one(campaignTable, { fields: [campaignLogsTable.id], references: [campaignTable.id], }),
|
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 = {
|
export type CampaignLog = {
|
||||||
target: number;
|
target: number;
|
||||||
timestamp: Serialize<Date>;
|
timestamp: Serialize<Date>;
|
||||||
type: 'ITEM' | 'CHARACTER' | 'PLACE' | 'EVENT' | 'FIGHT' | 'TEXT';
|
type: 'ITEM' | 'CHARACTER' | 'PLACE' | 'FIGHT' | 'TEXT';
|
||||||
details: string;
|
details: string;
|
||||||
};
|
};
|
||||||
|
|
@ -6,7 +6,7 @@ CREATE TABLE `campaign_logs` (
|
||||||
`details` text NOT NULL,
|
`details` text NOT NULL,
|
||||||
PRIMARY KEY(`id`, `from`, `timestamp`),
|
PRIMARY KEY(`id`, `from`, `timestamp`),
|
||||||
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
|
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
|
--> statement-breakpoint
|
||||||
ALTER TABLE `campaign` ADD `status` text DEFAULT 'PREPARING';--> 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,
|
`details` text NOT NULL,
|
||||||
PRIMARY KEY(`id`, `from`, `timestamp`),
|
PRIMARY KEY(`id`, `from`, `timestamp`),
|
||||||
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
|
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
|
--> statement-breakpoint
|
||||||
INSERT INTO `__new_campaign_logs`("id", "from", "timestamp", "type", "details") SELECT "id", "from", "timestamp", "type", "details" FROM `campaign_logs`;--> 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,
|
`details` text NOT NULL,
|
||||||
PRIMARY KEY(`id`, `target`, `timestamp`),
|
PRIMARY KEY(`id`, `target`, `timestamp`),
|
||||||
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
|
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
|
--> statement-breakpoint
|
||||||
INSERT INTO `__new_campaign_logs`("id", "target", "timestamp", "type", "details") SELECT "id", "target", "timestamp", "type", "details" FROM `campaign_logs`;--> 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",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "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": {
|
"compositePrimaryKeys": {
|
||||||
|
|
|
||||||
|
|
@ -116,19 +116,6 @@
|
||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "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": {
|
"compositePrimaryKeys": {
|
||||||
|
|
|
||||||
|
|
@ -116,19 +116,6 @@
|
||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "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": {
|
"compositePrimaryKeys": {
|
||||||
|
|
|
||||||
|
|
@ -116,19 +116,6 @@
|
||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "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": {
|
"compositePrimaryKeys": {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
import vuePlugin from 'rollup-plugin-vue'
|
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
|
|
@ -136,10 +135,7 @@ export default defineNuxtConfig({
|
||||||
usePolling: true,
|
usePolling: true,
|
||||||
},
|
},
|
||||||
rollupConfig: {
|
rollupConfig: {
|
||||||
external: ['bun'],
|
external: ['bun']
|
||||||
plugins: [
|
|
||||||
vuePlugin({ include: /\.vue$/, target: 'node' })
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
|
|
@ -168,7 +164,7 @@ export default defineNuxtConfig({
|
||||||
xssValidator: false,
|
xssValidator: false,
|
||||||
},
|
},
|
||||||
sitemap: {
|
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']
|
sources: ['/api/__sitemap__/urls']
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
|
@ -190,6 +186,13 @@ export default defineNuxtConfig({
|
||||||
cert: fs.readFileSync(path.resolve(__dirname, 'localhost+1.pem')).toString('utf-8'),
|
cert: fs.readFileSync(path.resolve(__dirname, 'localhost+1.pem')).toString('utf-8'),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
hmr: {
|
||||||
|
clientPort: 3000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
vue: {
|
vue: {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
isCustomElement: (tag) => tag === 'iconify-icon',
|
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;' }, [
|
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('div', { style: 'margin-left: auto; margin-right: auto; text-align: center;' }, [
|
||||||
dom('a', { style: 'display: inline-block;', attributes: { href: 'https://d-any.com' } }, [
|
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]' })
|
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 { createSSRApp, h } from 'vue';
|
||||||
import { renderToString } from 'vue/server-renderer';
|
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 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 config = useRuntimeConfig();
|
||||||
const [domain, selector, dkim] = config.mail.dkim.split(":");
|
const [domain, selector, dkim] = config.mail.dkim.split(":");
|
||||||
|
|
||||||
export const templates: Record<string, { component: any, subject: string }> = {
|
export const templates: Record<string, { component: (data: any) => string[], subject: string }> = {
|
||||||
"registration": { component: Registration, subject: 'Bienvenue sur d[any] 😎' },
|
"registration": { component: registration, subject: 'Bienvenue sur d[any] 😎' },
|
||||||
"reset-password": { component: ResetPassword, subject: 'Réinitialisation de votre mot de passe' },
|
"reset-password": { component: reset_password, subject: 'Réinitialisation de votre mot de passe' },
|
||||||
};
|
};
|
||||||
|
|
||||||
import type Mail from 'nodemailer/lib/mailer';
|
import type Mail from 'nodemailer/lib/mailer';
|
||||||
|
|
@ -76,11 +80,13 @@ export default defineTask({
|
||||||
throw new Error(`Modèle de mail ${mailPayload.template} inconnu`);
|
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');
|
console.time('Generating HTML');
|
||||||
const mail: Mail.Options = {
|
const mail: Mail.Options = {
|
||||||
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
|
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
|
||||||
to: mailPayload.to,
|
to: mailPayload.to,
|
||||||
html: await render(template.component, mailPayload.data),
|
html: `<html><body>${base(template.component(mailPayload.data))}</body></html>`,
|
||||||
subject: template.subject,
|
subject: template.subject,
|
||||||
textEncoding: 'quoted-printable',
|
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;
|
sessionConfig = runtimeConfig.session;
|
||||||
}
|
}
|
||||||
return useSession<UserSession>(event, sessionConfig);
|
return useSession<UserSession>(event, sessionConfig ?? {});
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import type { User } from "~/types/auth";
|
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 { div, dom, icon, span, svg, text } from "#shared/dom.util";
|
||||||
import { button, loading, tabgroup } from "#shared/components.util";
|
import { button, loading, tabgroup } from "#shared/components.util";
|
||||||
import { CharacterCompiler } from "#shared/character.util";
|
import { CharacterCompiler } from "#shared/character.util";
|
||||||
import { tooltip } from "#shared/floating.util";
|
import { tooltip } from "#shared/floating.util";
|
||||||
import markdown from "#shared/markdown.util";
|
import markdown from "#shared/markdown.util";
|
||||||
import { preview } from "./proses";
|
import { preview } from "#shared/proses";
|
||||||
import { format } from "./general.util";
|
import { format } from "#shared/general.util";
|
||||||
import { Socket } from "#shared/websocket.util";
|
import { Socket } from "#shared/websocket.util";
|
||||||
|
|
||||||
export const CampaignValidation = z.object({
|
export const CampaignValidation = z.object({
|
||||||
|
|
@ -49,6 +49,13 @@ type PlayerState = {
|
||||||
dom: HTMLElement;
|
dom: HTMLElement;
|
||||||
user: { id: number, username: string };
|
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 = {
|
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' },
|
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' },
|
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()
|
private render()
|
||||||
{
|
{
|
||||||
const campaign = this.campaign;
|
const campaign = this.campaign;
|
||||||
|
|
@ -147,9 +158,9 @@ export class CampaignSheet
|
||||||
]),
|
]),
|
||||||
div('flex flex-1 flex-col items-center justify-center', [
|
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', [
|
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'),
|
button(icon('radix-icons:clipboard', { width: 16, height: 16 }), () => {}, 'p-1'),
|
||||||
])
|
]),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
div('flex flex-row gap-4 flex-1', [
|
div('flex flex-row gap-4 flex-1', [
|
||||||
|
|
@ -158,7 +169,7 @@ export class CampaignSheet
|
||||||
...this.characters.map(e => e.container),
|
...this.characters.map(e => e.container),
|
||||||
]),
|
]),
|
||||||
div('flex h-full border-l border-light-40 dark:border-dark-40'),
|
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([
|
tabgroup([
|
||||||
{ id: 'campaign', title: [ text('Campagne') ], content: () => [
|
{ id: 'campaign', title: [ text('Campagne') ], content: () => [
|
||||||
markdown(campaign.public_notes, '', { tags: { a: preview } }),
|
markdown(campaign.public_notes, '', { tags: { a: preview } }),
|
||||||
|
|
@ -166,23 +177,43 @@ export class CampaignSheet
|
||||||
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [
|
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [
|
||||||
|
|
||||||
] },
|
] },
|
||||||
{ id: 'logs', title: [ text('Logs') ], content: () => [
|
{ id: 'logs', title: [ text('Logs') ], content: () => {
|
||||||
campaign.logs.length > 0 ? div('flex flex-row ps-12 py-4', [
|
let lastDate: Date = new Date(0);
|
||||||
div('border-l-2 border-light-40 dark:border-dark-40'),
|
const logs = campaign.logs.flatMap(e => {
|
||||||
div('flex flex-col gap-8 py-4', campaign.logs.map(e => div('flex flex-row gap-2 items-center relative -left-4', [
|
const date = new Date(e.timestamp), arr = [];
|
||||||
div('w-3 h-3 border-2 rounded-full bg-light-40 dark:border-dark-40 border-light-0 dark:border-dark-0'),
|
if(Math.floor(lastDate.getTime() / 86400000) < Math.floor(date.getTime() / 86400000))
|
||||||
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')),
|
lastDate = date;
|
||||||
])))
|
arr.push(div('flex flex-row gap-2 items-center relative -left-2 mx-px', [
|
||||||
]) : div('flex py-4 px-16', [ span('italic text-light-70 dark:text-darl-70', 'Aucune entrée pour le moment') ])
|
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: 'settings', title: [ text('Paramètres') ], content: () => [
|
||||||
|
|
||||||
] },
|
] },
|
||||||
{ id: 'ressources', title: [ text('Ressources') ], 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
|
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> = {
|
const transforms: Record<string, (date: Date) => string> = {
|
||||||
"yyyy": (date: Date) => date.getUTCFullYear().toString(),
|
"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),
|
"MM": (date: Date) => padRight((date.getUTCMonth() + 1).toString(), '0', 2),
|
||||||
"dd": (date: Date) => padRight(date.getUTCDate().toString(), '0', 2),
|
"dd": (date: Date) => padRight(date.getUTCDate().toString(), '0', 2),
|
||||||
"mm": (date: Date) => padRight(date.getUTCMinutes().toString(), '0', 2),
|
"mm": (date: Date) => padRight(date.getUTCMinutes().toString(), '0', 2),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue