Compare commits

..

No commits in common. "25bd165f1dd472c587410013134c59559a3f9847" and "feb2fb56c67be75e2df57e6d4da809774aa3a098" have entirely different histories.

14 changed files with 84 additions and 155 deletions

BIN
db.sqlite

Binary file not shown.

View File

@ -1,65 +1,20 @@
<template>
<div class="flex flex-row w-full max-w-full h-full max-h-full" style="--sidebar-width: 300px">
<div class="bg-light-0 dark:bg-dark-0 w-[var(--sidebar-width)] border-r border-light-30 dark:border-dark-30 flex flex-col gap-2">
<NuxtLink class="flex flex-row items-center justify-center group gap-2 my-2" aria-label="Accueil" :to="{ name: 'index', force: true }">
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
<Avatar src="/logo.light.svg" class="block dark:hidden" />
<span class="text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70">d[any]</span>
</NuxtLink>
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="treeParent"></div>
<div class="flex flex-col my-4 items-center justify-center gap-1 text-xs text-light-60 dark:text-dark-60">
<NuxtLink class="hover:underline" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
<NuxtLink class="hover:underline" :to="{ name: 'usage' }">Conditions d'utilisations</NuxtLink>
Copyright Peaceultime - 2025
</div>
</div>
<div class="flex flex-col flex-1 h-full w-[calc(100vw-var(--sidebar-width))]">
<div class="flex flex-row border-b border-light-30 dark:border-dark-30 justify-between px-8">
<div class="flex flex-row gap-16 items-center">
<NavigationMenuRoot class="relative">
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
<NavigationMenuItem>
<NavigationMenuTrigger>
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
</NavigationMenuTrigger>
<NavigationMenuContent class="absolute top-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30 py-2 z-20 flex flex-col">
<NuxtLink :href="{ name: 'character-list' }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<div class="absolute top-full left-0 flex w-full justify-center">
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
</div>
</NavigationMenuRoot>
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Campagnes</span></NuxtLink>
</div>
<div class="flex flex-row gap-16 items-center">
<template v-if="!loggedIn">
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">Se connecter</NuxtLink>
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70 max-md:hidden" :to="{ name: 'user-register' }">Créer un compte</NuxtLink>
</template>
<template v-else>
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
</template>
</div>
</div>
<slot></slot>
</div>
</div>
<div ref="container"></div>
<div ref="slotContainer"><slot></slot></div>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
import { TreeDOM } from '#shared/tree';
import { Content, iconByType } from '#shared/content.util';
import { dom, icon } from '#shared/dom.util';
import { unifySlug } from '#shared/general.util';
import { tooltip } from '#shared/floating.util';
import { link } from '#shared/components.util';
import { link, optionmenu } from '~/shared/components.util';
import { Content, iconByType } from '~/shared/content.util';
import { div, dom, icon, span, text } from '~/shared/dom.util';
import { popper, tooltip } from '~/shared/floating.util';
import { unifySlug } from '~/shared/general.util';
import { TreeDOM } from '~/shared/tree';
const container = useTemplateRef('container'), slots = useTemplateRef('slotContainer');
const open = ref(false);
const { loggedIn, user } = useUserSession();
const { loggedIn, user, clear: logout } = useUserSession();
const { fetch } = useContent();
await fetch(false);
@ -79,27 +34,68 @@ const tree = new TreeDOM((item, depth) => {
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
], { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined )]);
], { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined)]);
}, (item) => item.navigable);
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
const treeParent = useTemplateRef('treeParent');
const toggleByPath = (path: string | undefined) => (path?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
toggleByPath(path.value);
const unmount = useRouter().afterEach((to, from, failure) => {
if(failure)
return;
to.name === 'explore-path' && (unifySlug(to.params.path ?? '').split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
to.name === 'explore-path' && toggleByPath(unifySlug(to.params.path ?? ''));
});
watch(route, () => {
open.value = false;
});
const getUserDom = () => user.value ? [popper(link([ text(user.value.username), icon('radix-icons:caret-down', { width: 12, height: 12 }) ], { class: 'flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none' }, { name: 'user-profile' }), {
placement: 'bottom', delay: 0, content: () => [div('flex flex-1 flex-col', [
dom('span', { class: 'hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 cursor-pointer select-none', listeners: { click: logout } }, [ text('Se déconnecter') ], ),
])], class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full'
}).container] : [link([ text('S\'inscrire') ], { class: 'flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none' }, { name: 'user-register' }), link([ text('Se connecter') ], { class: 'flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none' }, { name: 'user-login' })];
const slotContainer = div('flex flex-1');
const content = () => div('flex flex-row w-full h-full', [
div('bg-light-0 dark:bg-dark-0 w-[300px] border-r border-light-30 dark:border-dark-30 flex flex-col gap-2', [
link([ dom('img', { attributes: { src: '/logo.dark.svg', width: 52, height: 41 } }), span('text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70', 'd[any]') ], { class: 'flex flex-row items-center justify-center group gap-2 my-2' }, { name: 'index' }),
div('flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden', [ tree.container ]),
div('flex flex-col my-4 items-center justify-center gap-1 text-xs text-light-60 dark:text-dark-60', [
link([ text('Mentions légales') ], { class: 'hover:underline' }, { name: 'legal' }),
link([ text('Conditions d\'utilisations') ], { class: 'hover:underline' }, { name: 'usage' }),
text('Copyright 2025 - Peaceultime & d[any]')
])
]),
div('flex flex-col flex-1 h-full', [
div('flex flex-row border-b border-light-30 dark:border-dark-30 justify-between px-8', [
div('flex flex-row gap-16 items-center', [
popper(link([ text('Personnages'), icon('radix-icons:caret-down', { width: 12, height: 12 }) ], { class: 'flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none' }, { name: 'character' }), {
placement: 'bottom', delay: 0, content: () => [div('flex flex-1 flex-col', [
link([ text('Personnages publics') ], { class: 'hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none' }, { name: 'character-list' }),
link([ text('Nouveau personnage') ], { class: 'hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none' }, { name: 'character-id-edit', params: { id: 'new' } })
])], class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full'
}).container,
link([ text('Campagnes') ], { class: 'flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none' }, { name: 'character' })
]),
div('flex flex-row gap-16 items-center', getUserDom())
]),
slotContainer,
])
]);
onMounted(() => {
if(treeParent.value)
treeParent.value.appendChild(tree.container);
if(container.value && slots.value)
{
slotContainer.replaceChildren(...slots.value.childNodes);
container.value!.replaceWith(content());
}
});
onUpdated(() => {
})
onUnmounted(() => {
unmount();
})
});
</script>

View File

@ -164,8 +164,8 @@ async function logout(user: User)
</Head>
<div class="flex flex-1 flex-col p-4">
<div class="flex flex-row justify-between items-center">
<h2 class="text-center flex-1 text-2xl font-bold">Administration</h2>
<NuxtLink :to="{ name: 'admin-jobs' }"><Button>Jobs</Button></NuxtLink>
<ProseH2 class="text-center flex-1">Administration</ProseH2>
<Button><NuxtLink :to="{ name: 'admin-jobs' }">Jobs</NuxtLink></Button>
</div>
<div class="flex flex-1 w-full justify-center items-stretch flex-row gap-4">
<div class="flex-1">

View File

@ -71,7 +71,7 @@ async function fetch()
<div class="flex flex-col justify-start items-center p-4">
<div class="flex flex-row justify-between items-center gap-8">
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
<h2 class="text-center flex-1 text-2xl font-bold">Administration</h2>
<ProseH2 class="text-center flex-1">Administration</ProseH2>
</div>
<div class="flex flex-row w-full gap-8">
<Select label="Job" v-model="job">

View File

@ -33,5 +33,5 @@ onMounted(() => {
</script>
<template>
<div class="flex flex-1 w-full h-full items-start justify-center" ref="container"></div>
<div class="flex h-full" ref="container"></div>
</template>

View File

@ -3,7 +3,7 @@
<Title>d[any] - Validation de votre adresse mail</Title>
</Head>
<div class="flex flex-col justify-center items-center">
<h2 class="text-2xl font-bold">Votre compte a été validé ! 🎉</h2>
<ProseH2>Votre compte a été validé ! 🎉</ProseH2>
<div class="flex flex-row gap-8">
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'user-login', replace: true }">Se connecter</NuxtLink></Button>
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'index', replace: true }">Retourner à l'accueil</NuxtLink></Button>

View File

@ -5,7 +5,7 @@
<div class="flex flex-1 flex-col justify-center items-center">
<div class="flex gap-8 items-center">
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
<h4 class="text-xl font-bold">Reinitialisation de mon mot de passe</h4>
<ProseH4>Reinitialisation de mon mot de passe</ProseH4>
</div>
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
<TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="email"/>

View File

@ -5,7 +5,7 @@
<div class="flex flex-1 flex-col justify-center items-center">
<div class="flex gap-8 items-center">
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
<h4 class="text-center flex-1 text-xl font-bold">Reinitialisation de mon mot de passe</h4>
<ProseH4>Reinitialisation de mon mot de passe</ProseH4>
</div>
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
<TextInput type="password" label="Nouveau mot de passe" autocomplete="newPassword" v-model="newPasswd" :class="{ '!border-light-red !dark:border-dark-red': error }"/>

View File

@ -5,7 +5,7 @@
<div class="flex flex-1 flex-col justify-center items-center">
<div class="flex gap-8 items-center">
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
<h4 class="text-center flex-1 text-xl font-bold">Modification de mon mot de passe</h4>
<ProseH4>Modification de mon mot de passe</ProseH4>
</div>
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
<TextInput type="password" label="Ancien mot de passe" name="old-password" autocomplete="current-password" v-model="oldPasswd"/>

View File

@ -5,7 +5,7 @@
<div class="flex flex-1 flex-col justify-center items-center">
<div class="flex gap-8 items-center">
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
<h4 class="text-xl font-bold">Connexion</h4>
<ProseH4>Connexion</ProseH4>
</div>
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
<TextInput type="text" label="Utilisateur ou email" name="username" autocomplete="username email" v-model="state.usernameOrEmail"/>

View File

@ -38,8 +38,8 @@ async function deleteUser()
<div class="flex gap-4">
<Avatar icon="radix-icons:person" :src="`/users/${user?.id}.medium.jpg`" class="w-32 h-32" />
<div class="flex flex-col items-start">
<h4 class="text-xl font-bold">{{ user.username }}</h4>
<h4 class="text-xl font-bold">{{ user.email }}</h4>
<ProseH5>{{ user.username }}</ProseH5>
<ProseH5>{{ user.email }}</ProseH5>
</div>
</div>
<div class="border-light-red dark:border-dark-red bg-light-redBack dark:bg-dark-redBack text-light-red dark:text-dark-red py-1 px-3 flex items-center justify-between flex-col md:flex-row"

View File

@ -5,7 +5,7 @@
<div class="flex flex-1 flex-col justify-center items-center">
<div class="flex gap-8 items-center">
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
<h4 class="text-xl font-bold">Inscription</h4>
<ProseH4>Inscription</ProseH4>
</div>
<form @submit.prevent="() => submit()" class="grid flex-1 p-4 grid-cols-2 md:grid-cols-1 gap-4 md:gap-0">
<TextInput type="text" label="Nom d'utilisateur" name="username" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/>

View File

@ -8,71 +8,6 @@ import { preview } from "#shared/proses";
import { SpatialGrid } from "#shared/physics.util";
import type { CanvasPreferences } from "~/types/general";
/*
stroke-light-red
stroke-light-orange
stroke-light-yellow
stroke-light-green
stroke-light-cyan
stroke-light-purple
dark:stroke-dark-red
dark:stroke-dark-orange
dark:stroke-dark-yellow
dark:stroke-dark-green
dark:stroke-dark-cyan
dark:stroke-dark-purple
fill-light-red
fill-light-orange
fill-light-yellow
fill-light-green
fill-light-cyan
fill-light-purple
dark:fill-dark-red
dark:fill-dark-orange
dark:fill-dark-yellow
dark:fill-dark-green
dark:fill-dark-cyan
dark:fill-dark-purple
bg-light-red
bg-light-orange
bg-light-yellow
bg-light-green
bg-light-cyan
bg-light-purple
dark:bg-dark-red
dark:bg-dark-orange
dark:bg-dark-yellow
dark:bg-dark-green
dark:bg-dark-cyan
dark:bg-dark-purple
border-light-red
border-light-orange
border-light-yellow
border-light-green
border-light-cyan
border-light-purple
dark:border-dark-red
dark:border-dark-orange
dark:border-dark-yellow
dark:border-dark-green
dark:border-dark-cyan
dark:border-dark-purple
outline-light-red
outline-light-orange
outline-light-yellow
outline-light-green
outline-light-cyan
outline-light-purple
dark:outline-dark-red
dark:outline-dark-orange
dark:outline-dark-yellow
dark:outline-dark-green
dark:outline-dark-cyan
dark:outline-dark-purple
*/
export type Direction = 'bottom' | 'top' | 'left' | 'right';
export type Position = { x: number, y: number };
export type Box = Position & { width: number, height: number };
@ -482,8 +417,6 @@ export class Canvas
]),
]), this.transform,
]);
console.log(this.nodes.length, this.edges.length);
}
protected computeLimits()
@ -534,11 +467,11 @@ export class Canvas
this.firstX = pos.x;
this.firstY = pos.y;
window.addEventListener('mouseup', dragEnd);
window.addEventListener('mousemove', dragMove);
window.addEventListener('mouseup', dragEnd, { passive: true });
window.addEventListener('mousemove', dragMove, { passive: true });
this.dragStart(e);
});
}, { passive: true });
this.container.addEventListener('wheel', (e) => {
if((this._zoom >= Canvas.maxZoom && e.deltaY < 0) || (this._zoom <= this.containZoom && e.deltaY > 0))
return;
@ -557,10 +490,10 @@ export class Canvas
this.lastDistance = distance(e.touches);
}
this.container.addEventListener('touchend', touchend);
this.container.addEventListener('touchcancel', touchcancel);
this.container.addEventListener('touchmove', touchmove);
});
this.container.addEventListener('touchend', touchend, { passive: true });
this.container.addEventListener('touchcancel', touchcancel, { passive: true });
this.container.addEventListener('touchmove', touchmove, { passive: true });
}, { passive: true });
const touchend = (e: TouchEvent) => {
if(e.touches.length > 1)
{

View File

@ -406,7 +406,7 @@ export class CharacterCompiler
if(feature.action === 'add' && !this._result.lists[feature.list]!.includes(feature.item))
this._result.lists[feature.list]!.push(feature.item);
else if(feature.action === 'remove')
this._result.lists[feature.list]!.splice(this._result.lists[feature.list]!.findIndex((e: string) => e === feature.item), 1);
this._result.lists[feature.list] = this._result.lists[feature.list]!.splice(this._result.lists[feature.list]!.findIndex((e: string) => e !== feature.item), 1);
return;
case "value":
@ -443,7 +443,7 @@ export class CharacterCompiler
if(feature.action === 'remove' && !this._result.lists[feature.list]!.includes(feature.item))
this._result.lists[feature.list]!.push(feature.item);
else if(feature.action === 'add')
this._result.lists[feature.list]!.splice(this._result.lists[feature.list]!.findIndex((e: string) => e === feature.item), 1)
this._result.lists[feature.list] = this._result.lists[feature.list]!.splice(this._result.lists[feature.list]!.findIndex((e: string) => e !== feature.item), 1)
return;
case "value":
@ -1233,7 +1233,7 @@ export class CharacterSheet
{
user: ComputedRef<User | null>;
character?: CharacterCompiler;
container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-center');
container: HTMLElement = div('flex h-full');
tabs?: HTMLDivElement & { refresh: () => void };
constructor(id: string, user: ComputedRef<User | null>)
{
@ -1300,8 +1300,8 @@ export class CharacterSheet
div('flex flex-col gap-2', [ span('text-lg font-bold', 'Notes privés'), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ privateNotes.dom ]) ]),
])
] },
], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px] h-full', content: 'overflow-auto' } });
this.container.replaceChildren(div('flex flex-col justify-start gap-1 h-full', [
], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px]' } });
this.container.replaceChildren(div('flex flex-col justify-center gap-1', [
div("flex flex-row gap-4 justify-between", [
div(),
@ -1424,7 +1424,7 @@ export class CharacterSheet
]),
]),
div("flex flex-1 flex-row items-stretch justify-center py-2 gap-4 h-0", [
div("flex flex-1 flex-row items-stretch justify-center py-2 gap-4", [
div("flex flex-col gap-4 py-1 w-60", [
div("flex flex-col py-1 gap-4", [
div("flex flex-row items-center justify-center gap-4", [