40 Commits

Author SHA1 Message Date
Clément Pons
25bd165f1d Merge branch 'dev' into HEAD 2025-10-21 17:26:16 +02:00
Clément Pons
5c1f41b0b7 Fix ProseH remains, rollback layout rendering and add proper scrolling to the character sheet tabs 2025-10-21 17:22:46 +02:00
feb2fb56c6 New default layout without vuejs rendering (still needs some fixes) 2025-10-19 23:35:11 +02:00
Clément Pons
df9ae95890 Note tab in character sheet 2025-10-15 17:01:23 +02:00
Clément Pons
72843f2425 Fix registration email and add no character friendly messages 2025-10-15 14:58:59 +02:00
Clément Pons
443612cc58 Floater pinned true handler, SQL schema update to handle private/public notes on character, fix Canvas zoom debounce on move. 2025-10-15 14:34:12 +02:00
Clément Pons
a577e3ccfc Checkbox and item panel improvements 2025-10-14 17:57:34 +02:00
Clément Pons
48e767944a Progress on ItemEditor interface and rendering 2025-10-13 17:56:22 +02:00
d187957915 Start implementing ItemEditor 2025-10-13 13:19:50 +02:00
Clément Pons
16cc3ee438 Floater imrprovement with parametrable show and hide events, title and minimization. 2025-10-10 16:57:36 +02:00
Clément Pons
26aa0847d9 Fix comrpessing bug on null buffers, make pinned floaters resizable and optimize a few things here and there 2025-10-06 17:42:16 +02:00
b19d2d1b41 Updated legal stuff, added floating popup that can be pin and move. Fix character compiler modifier updates not dirtying all dependents. 2025-10-05 23:54:37 +02:00
Clément Pons
89c4476ffb Merge branch 'dev' of https://git.peaceultime.com/Peaceultime/obsidian-visualiser into dev 2025-10-01 17:59:30 +02:00
Clément Pons
3113d8b0f3 Feature choice UI rework, feature editor fixes, new character manage page UI with tabgroup and action config 2025-10-01 17:59:14 +02:00
2b39f26722 Merge branch 'dev' of https://git.peaceultime.com/peaceultime/obsidian-visualiser into dev 2025-09-30 21:50:59 +02:00
d2a807694b Fix compression error 2025-09-30 21:36:40 +02:00
Clément Pons
eb0c33deae New ability display, sereval Character compile and creation fixes 2025-09-30 18:03:38 +02:00
Clément Pons
61d2d144b7 Spell UI, variables saving and mail server fixes (finally working in prod !!!) 2025-09-30 17:15:49 +02:00
Clément Pons
1642cd513f Work in progress: CharacterSheet implementation and FeatureChoice rework 2025-09-29 17:53:41 +02:00
Clément Pons
b1ac379f1a Work in progress: CharacterSheet implementation and FeatureChoice rework 2025-09-29 17:53:39 +02:00
81f191d5f6 Compress middleware 2025-09-14 20:46:48 +02:00
Clément Pons
423df7bc42 Add spell picker in the character sheet 2025-09-01 17:53:07 +02:00
c93cc4078c New Toaster class, Ability and Resistance removed from config file and choices improvement 2025-08-31 23:52:11 +02:00
Clément Pons
17bc232602 Changes tooltips reference, update character sheet UI, getID now embed the ID_SIZE, new ability max option in feature effect. 2025-08-29 17:46:08 +02:00
Clément Pons
042d4479ee Ajust database schema to recent changes 2025-08-26 17:34:34 +02:00
Clément Pons
da93fcd82d Homebrew manager completed ! 2025-08-26 15:27:47 +02:00
Clément Pons
80a94bee86 Remove unused components, change zod to v4 and cahnge a few character properties 2025-08-26 13:21:42 +02:00
Clément Pons
5387dc66c3 Character BuilderTabs rework 2025-08-26 10:18:07 +02:00
Clément Pons
6fe3746df4 Alignment handled as string instead of objects 2025-08-26 10:17:46 +02:00
893247e1eb Aspect and Spell editor, multiselect component. 2025-08-26 00:17:08 +02:00
Clément Pons
69ee62c08e Convert list texts to a separate i18n text, allowing translation and fixing action/passive/... removal. Character sheet now use the character compiler. 2025-08-25 17:35:15 +02:00
247b14b2c8 Various fixes to select, combobox and feature editor. 2025-08-24 23:35:57 +02:00
Clément Pons
658499749d Nearly finished FeatureEditor for choices 2025-08-20 22:25:47 +02:00
Clément Pons
06276b3fbc Progress on option rendering 2025-08-18 17:42:07 +02:00
Clément Pons
72982a4ea9 Impoved FloatingUI components and create a PickableFeature class 2025-08-13 17:39:58 +02:00
Clément Pons
4e5ea504ea Add combobox groups 2025-08-11 17:52:53 +02:00
920ce2e1b6 Feature Builder panel progress 2025-08-11 09:39:41 +02:00
Clément Pons
86556ec604 Completed first people effects 2025-07-23 18:09:13 +02:00
Clément Pons
7d6f9162ed Finalize CharacterBuilder 2025-07-22 17:46:16 +02:00
3ef98df5d2 Add LevelPicker 2025-07-22 00:05:06 +02:00
128 changed files with 18000 additions and 11681 deletions

25
app.vue
View File

@@ -1,26 +1,23 @@
<template> <template>
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden"> <div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
<NuxtRouteAnnouncer/> <NuxtRouteAnnouncer/>
<TooltipProvider>
<NuxtLayout> <NuxtLayout>
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative" id="mainContainer"> <div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative" id="mainContainer">
<NuxtPage /> <NuxtPage />
</div> </div>
</NuxtLayout> </NuxtLayout>
<Toaster v-model="list" />
</TooltipProvider>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Content } from './shared/content.util'; import { Content } from '#shared/content.util';
import * as Floating from '#shared/floating.util'; import * as Floating from '#shared/floating.util';
import { Toaster } from '#shared/components.util';
provideToaster();
onBeforeMount(() => { onBeforeMount(() => {
Content.init(); Content.init();
Floating.init(); Floating.init();
Toaster.init();
const unmount = useRouter().afterEach((to, from, failure) => { const unmount = useRouter().afterEach((to, from, failure) => {
if(failure) return; if(failure) return;
@@ -33,11 +30,23 @@ onBeforeMount(() => {
unmount(); unmount();
}) })
}); });
const { list } = useToast();
</script> </script>
<style> <style>
.ToastRoot[data-type='error'] {
@apply border-light-red;
@apply dark:border-dark-red;
@apply bg-light-red;
@apply dark:bg-dark-red;
@apply !bg-opacity-50;
}
.ToastRoot[data-type='success'] {
@apply border-light-green;
@apply dark:border-dark-green;
@apply bg-light-green;
@apply dark:bg-dark-green;
@apply !bg-opacity-50;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 12px; width: 12px;
height: 12px; height: 12px;

View File

@@ -39,6 +39,7 @@
"remark-rehype": "^11.1.2", "remark-rehype": "^11.1.2",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-vue": "^6.0.0", "rollup-plugin-vue": "^6.0.0",
"strip-markdown": "^6.0.0",
"unified": "^11.0.5", "unified": "^11.0.5",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vue": "^3.5.17", "vue": "^3.5.17",
@@ -1953,6 +1954,8 @@
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
"strip-markdown": ["strip-markdown@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-mSa8FtUoX3ExJYDkjPUTC14xaBAn4Ik5GPQD45G5E2egAmeV3kHgVSTfIoSDggbF6Pk9stahVgqsLCNExv6jHw=="],
"strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], "strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],
"structured-clone-es": ["structured-clone-es@1.0.0", "", {}, "sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ=="], "structured-clone-es": ["structured-clone-es@1.0.0", "", {}, "sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ=="],

View File

@@ -1,932 +0,0 @@
<script lang="ts">
import { bezier, getBbox, opposite, posFromDir, rotation, type Box, type Direction, type Path, type Position } from '#shared/canvas.util';
import type CanvasNodeEditor from './canvas/CanvasNodeEditor.vue';
import type CanvasEdgeEditor from './canvas/CanvasEdgeEditor.vue';
import { SnapFinder, type SnapHint } from '#shared/physics.util';
import type { CanvasPreferences } from '~/types/general';
export type Element = { type: 'node' | 'edge', id: string };
interface ActionMap {
remove: CanvasNode | CanvasEdge | undefined;
create: CanvasNode | CanvasEdge | undefined;
property: CanvasNode | CanvasEdge;
}
type Action = keyof ActionMap;
interface HistoryEvent<T extends Action = Action>
{
event: T;
actions: HistoryAction<T>[];
}
interface HistoryAction<T extends Action>
{
element: Element;
from: ActionMap[T];
to: ActionMap[T];
}
type NodeEditor = InstanceType<typeof CanvasNodeEditor>;
type EdgeEditor = InstanceType<typeof CanvasEdgeEditor>;
const cancelEvent = (e: Event) => e.preventDefault();
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
function center(touches: TouchList): Position
{
const pos = { x: 0, y: 0 };
for(const touch of touches)
{
pos.x += touch.clientX;
pos.y += touch.clientY;
}
pos.x /= touches.length;
pos.y /= touches.length;
return pos;
}
function distance(touches: TouchList): number
{
const [A, B] = touches;
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
}
</script>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
import { clamp, getID, ID_SIZE } from '#shared/general.util';
import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas';
const canvas = defineModel<CanvasContent>({ required: true });
const props = defineProps<{
path: string,
}>();
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5), spacing = ref<number | undefined>(32);
const focusing = ref<Element>(), editing = ref<Element>();
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef'), patternRef = useTemplateRef('patternRef'), toolbarRef = useTemplateRef('toolbarRef'), viewportSize = useElementBounding(canvasRef);
const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges');
const canvasSettings = useCookie<CanvasPreferences>('canvasPreference', { default: () => ({ gridSnap: true, neighborSnap: true, spacing: 32 }) });
const hints = ref<SnapHint[]>([]);
const viewport = computed<Box>(() => {
const width = viewportSize.width.value / zoom.value, height = viewportSize.height.value / zoom.value;
const movementX = viewportSize.width.value - width, movementY = viewportSize.height.value - height;
return { x: -dispX.value + movementX / 2, y: -dispY.value + movementY / 2, width, height };
});
const updateScaleVar = useDebounceFn(() => {
if(transformRef.value)
{
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
}
if(canvasRef.value)
{
canvasRef.value.style.setProperty('--zoom-multiplier', (1 / Math.pow(zoom.value, 0.7)).toFixed(3));
}
}, 100);
type DragOrigin = { type: 'edge', id: string, destination: 'from' | 'to', node: string } | { type: 'node', id: string };
const fakeEdge = ref<{ from?: Position, fromSide?: Direction, to?: Position, toSide?: Direction, path?: Path, style?: { stroke: string, fill: string }, hex?: string, drag?: DragOrigin, snapped?: { node: string, side: Direction } }>({});
const focused = computed(() => focusing.value ? focusing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === focusing.value!.id) : edges.value?.find(e => !!e && e.id === focusing.value!.id) : undefined), edited = computed(() => editing.value ? editing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === editing.value!.id) : edges.value?.find(e => !!e && e.id === editing.value!.id) : undefined);
let snapFinder: SnapFinder;
const history = ref<HistoryEvent[]>([]);
const historyPos = ref(-1);
const historyCursor = computed(() => history.value.length > 0 && historyPos.value > -1 ? history.value[historyPos.value] : undefined);
watch(props, () => {
snapFinder = new SnapFinder(hints, viewport, { gridSize: 512, preferences: canvasSettings.value, threshold: 16, cellSize: 64 });
canvas.value.nodes?.forEach((e) => {
snapFinder.update(e);
});
focusing.value = undefined;
editing.value = undefined;
dispX.value = 0;
dispY.value = 0;
zoom.value = 0.5;
history.value = [];
historyPos.value = -1;
fakeEdge.value = {};
}, { immediate: true });
watch(canvas, () => {
updateToolbarTransform();
}, { immediate: true, deep: true, });
const reset = (_: MouseEvent) => {
zoom.value = minZoom.value;
dispX.value = 0;
dispY.value = 0;
updateTransform();
}
function addAction<T extends Action = Action>(event: T, actions: HistoryAction<T>[])
{
historyPos.value++;
history.value.splice(historyPos.value, history.value.length - historyPos.value);
history.value[historyPos.value] = { event, actions };
}
onMounted(() => {
let lastX = 0, lastY = 0, lastDistance = 0;
const dragMove = (e: MouseEvent) => {
dispX.value = dispX.value - (lastX - e.clientX) / zoom.value;
dispY.value = dispY.value - (lastY - e.clientY) / zoom.value;
lastX = e.clientX;
lastY = e.clientY;
updateTransform();
};
const dragEnd = (e: MouseEvent) => {
window.removeEventListener('mouseup', dragEnd);
window.removeEventListener('mousemove', dragMove);
};
canvasRef.value?.addEventListener('mouseenter', () => {
window.addEventListener('wheel', cancelEvent, { passive: false });
document.addEventListener('gesturestart', cancelEvent);
document.addEventListener('gesturechange', cancelEvent);
canvasRef.value?.addEventListener('mouseleave', () => {
window.removeEventListener('wheel', cancelEvent);
document.removeEventListener('gesturestart', cancelEvent);
document.removeEventListener('gesturechange', cancelEvent);
});
})
canvasRef.value?.addEventListener('mousedown', (e) => {
if(e.button === 1)
{
lastX = e.clientX;
lastY = e.clientY;
window.addEventListener('mouseup', dragEnd, { passive: true });
window.addEventListener('mousemove', dragMove, { passive: true });
}
}, { passive: true });
canvasRef.value?.addEventListener('wheel', (e) => {
if((zoom.value >= 3 && e.deltaY < 0) || (zoom.value <= minZoom.value && e.deltaY > 0))
return;
const diff = Math.exp(e.deltaY * -0.001);
const centerX = (viewportSize.x.value + viewportSize.width.value / 2), centerY = (viewportSize.y.value + viewportSize.height.value / 2);
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value);
dispY.value = dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value);
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
spacing.value = canvasSettings.value.gridSnap ? canvasSettings.value.spacing ?? 32 : undefined;
updateTransform();
}, { passive: true });
canvasRef.value?.addEventListener('touchstart', (e) => {
({ x: lastX, y: lastY } = center(e.touches));
if(e.touches.length > 1)
{
lastDistance = distance(e.touches);
}
canvasRef.value?.addEventListener('touchend', touchend, { passive: true });
canvasRef.value?.addEventListener('touchcancel', touchcancel, { passive: true });
canvasRef.value?.addEventListener('touchmove', touchmove, { passive: true });
}, { passive: true });
const touchend = (e: TouchEvent) => {
if(e.touches.length > 1)
{
({ x: lastX, y: lastY } = center(e.touches));
}
canvasRef.value?.removeEventListener('touchend', touchend);
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
canvasRef.value?.removeEventListener('touchmove', touchmove);
};
const touchcancel = (e: TouchEvent) => {
if(e.touches.length > 1)
{
({ x: lastX, y: lastY } = center(e.touches));
}
canvasRef.value?.removeEventListener('touchend', touchend);
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
canvasRef.value?.removeEventListener('touchmove', touchmove);
};
const touchmove = (e: TouchEvent) => {
const pos = center(e.touches);
dispX.value = dispX.value - (lastX - pos.x) / zoom.value;
dispY.value = dispY.value - (lastY - pos.y) / zoom.value;
lastX = pos.x;
lastY = pos.y;
if(e.touches.length === 2)
{
const dist = distance(e.touches);
const diff = dist / lastDistance;
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
}
updateTransform();
};
updateTransform();
});
function updateTransform()
{
if(transformRef.value)
{
transformRef.value.style.transform = `scale3d(${zoom.value}, ${zoom.value}, 1) translate3d(${dispX.value}px, ${dispY.value}px, 0)`;
updateScaleVar();
}
if(patternRef.value && canvasSettings.value.gridSnap)
{
patternRef.value.parentElement?.classList.remove('hidden');
patternRef.value.setAttribute("x", (viewportSize.width.value / 2 + dispX.value % spacing.value! * zoom.value).toFixed(3));
patternRef.value.setAttribute("y", (viewportSize.height.value / 2 + dispY.value % spacing.value! * zoom.value).toFixed(3));
patternRef.value.setAttribute("width", (zoom.value * spacing.value!).toFixed(3));
patternRef.value.setAttribute("height", (zoom.value * spacing.value!).toFixed(3));
patternRef.value.children[0].setAttribute('cx', (zoom.value).toFixed(3));
patternRef.value.children[0].setAttribute('cy', (zoom.value).toFixed(3));
patternRef.value.children[0].setAttribute('r', (zoom.value).toFixed(3));
}
else if(patternRef.value && !canvasSettings.value.gridSnap)
{
patternRef.value.parentElement?.classList.remove('hidden');
}
}
function updateToolbarTransform()
{
const offsetY = -12;
if(toolbarRef.value)
{
if(!focusing.value)
{
toolbarRef.value.style.transform = '';
}
else if(focusing.value.type === 'node')
{
const node = canvas.value.nodes!.find(e => e.id === focusing.value!.id)!;
toolbarRef.value.style.transform = `translate(${node.x}px, ${node.y}px) translateY(-100%) translateY(${offsetY}px) translateX(-50%) translateX(${node.width / 2}px) scale(calc(1 / var(--tw-scale)))`;
}
else
{
const path = edges.value!.find(e => e.id === focusing.value!.id)!.path;
const x = path.from.x + (path.to.x - path.from.x) / 2, y = path.from.y;
toolbarRef.value.style.transform = `translate(${x}px, ${y}px) translateY(-100%) translateY(${offsetY}px) translateX(-50%) scale(calc(1 / var(--tw-scale)))`;
}
}
}
function moveNode(ids: string[], deltax: number, deltay: number)
{
if(ids.length === 0)
return;
const actions: HistoryAction<'property'>[] = [];
for(const id of ids)
{
const node = canvas.value.nodes!.find(e => e.id === id)!;
snapFinder.update(node);
actions.push({ element: { type: 'node', id }, from: { ...node, x: node.x - deltax, y: node.y - deltay }, to: { ...node } });
}
addAction('property', actions);
}
function resizeNode(ids: string[], deltax: number, deltay: number, deltaw: number, deltah: number)
{
if(ids.length === 0)
return;
const actions: HistoryAction<'property'>[] = [];
for(const id of ids)
{
const node = canvas.value.nodes!.find(e => e.id === id)!;
snapFinder.update(node);
actions.push({ element: { type: 'node', id }, from: { ...node, x: node.x - deltax, y: node.y - deltay, width: node.width - deltaw, height: node.height - deltah }, to: { ...node } });
}
addAction('property', actions);
}
function select(element: Element)
{
if(focusing.value && (focusing.value.id !== element.id || focusing.value.type !== element.type))
{
unselect();
}
focusing.value = element;
focused.value?.dom?.addEventListener('click', stopPropagation, { passive: true });
canvasRef.value?.addEventListener('click', unselect, { once: true });
updateToolbarTransform();
}
function edit(element: Element)
{
editing.value = element;
focused.value?.dom?.addEventListener('wheel', stopPropagation, { passive: true });
focused.value?.dom?.addEventListener('dblclick', stopPropagation, { passive: true });
canvasRef.value?.addEventListener('click', unselect, { once: true });
}
function createNode(e: MouseEvent)
{
const width = 250, height = 100;
const x = e.layerX / zoom.value - dispX.value - width / 2;
const y = e.layerY / zoom.value - dispY.value - height / 2;
const node: CanvasNode = { id: getID(ID_SIZE), x, y, width, height, type: 'text' };
if(!canvas.value.nodes)
canvas.value.nodes = [node];
else
canvas.value.nodes.push(node);
snapFinder.add(node);
addAction('create', [{ element: { type: 'node', id: node.id }, from: undefined, to: node }]);
}
function remove(elements: Element[])
{
if(elements.length === 0)
return;
const actions: HistoryAction<'remove'>[] = [];
focusing.value = undefined;
editing.value = undefined;
const c = canvas.value;
for(const element of elements)
{
if(element.type === 'node')
{
const edges = c.edges?.map((e, i) => ({ id: e.id, from: e.fromNode, to: e.toNode, index: i }))?.filter(e => e.from === element.id || e.to === element.id) ?? [];
for(let i = edges.length - 1; i >= 0; i--)
{
actions.push({ element: { type: 'edge', id: edges[i].id }, from: c.edges!.splice(edges[i].index, 1)[0], to: undefined });
}
const index = c.nodes!.findIndex(e => e.id === element.id);
const node = c.nodes!.splice(index, 1)[0];
snapFinder.remove(node);
actions.push({ element: { type: 'node', id: element.id }, from: node, to: undefined });
}
else if(element.type === 'edge' && !actions.find(e => e.element.type === 'edge' && e.element.id === element.id))
{
const index = c.edges!.findIndex(e => e.id === element.id);
actions.push({ element: { type: 'edge', id: element.id }, from: c.edges!.splice(index, 1)[0], to: undefined });
}
}
canvas.value = c;
addAction('remove', actions);
}
function dragEdgeTo(e: MouseEvent): void
{
(fakeEdge.value.to as Position).x += e.movementX / zoom.value;
(fakeEdge.value.to as Position).y += e.movementY / zoom.value;
const result = snapFinder.findEdgeSnapPosition(fakeEdge.value.drag!.id, fakeEdge.value.to!.x, fakeEdge.value.to!.y);
fakeEdge.value.snapped = result ? { node: result.node, side: result.direction } : undefined;
fakeEdge.value.path = bezier((fakeEdge.value.from as Position), fakeEdge.value.fromSide!, result ?? (fakeEdge.value.to as Position), result?.direction ?? fakeEdge.value.toSide!);
}
function dragEndEdgeTo(e: MouseEvent): void
{
window.removeEventListener('mousemove', dragEdgeTo);
window.removeEventListener('mouseup', dragEndEdgeTo);
if(fakeEdge.value.snapped)
{
const node = canvas.value.nodes!.find(e => e.id === fakeEdge.value.drag!.id)!;
const edge: CanvasEdge = { fromNode: fakeEdge.value.drag!.id, fromSide: fakeEdge.value.fromSide!, toNode: fakeEdge.value.snapped.node, toSide: fakeEdge.value.snapped.side, id: getID(ID_SIZE), color: node.color };
canvas.value.edges?.push(edge);
addAction('create', [{ from: undefined, to: edge, element: { id: edge.id, type: 'edge' } }]);
}
fakeEdge.value = {};
}
function dragStartEdgeTo(id: string, e: MouseEvent, direction: Direction): void
{
const node = canvas.value.nodes!.find(e => e.id === id)!;
fakeEdgeFromNode(node, direction);
window.addEventListener('mousemove', dragEdgeTo, { passive: true });
window.addEventListener('mouseup', dragEndEdgeTo, { passive: true });
}
function dragEdgeSide(e: MouseEvent): void
{
if(fakeEdge.value.drag?.type === 'node')
return;
const destination = fakeEdge.value.drag!.destination;
const pos = fakeEdge.value[destination]!;
pos.x += e.movementX / zoom.value;
pos.y += e.movementY / zoom.value;
const result = snapFinder.findEdgeSnapPosition(fakeEdge.value.drag!.node, pos.x, pos.y);
fakeEdge.value.snapped = result ? { node: result.node, side: result.direction } : undefined;
fakeEdge.value.path = bezier(destination === 'from' ? (result ?? pos) : fakeEdge.value.from!, destination === 'from' ? result?.direction ?? fakeEdge.value.fromSide! : fakeEdge.value.fromSide!, destination === 'to' ? (result ?? pos) : fakeEdge.value.to!, destination === 'to' ? result?.direction ?? fakeEdge.value.toSide! : fakeEdge.value.toSide!);
}
function dragEndEdgeSide(e: MouseEvent): void
{
if(fakeEdge.value.drag?.type === 'node')
return;
window.removeEventListener('mousemove', dragEdgeSide);
window.removeEventListener('mouseup', dragEndEdgeSide);
if(fakeEdge.value.snapped)
{
const edge = canvas.value.edges!.find(e => e.id === fakeEdge.value.drag?.id)!
const old = { ... edge };
const destination = fakeEdge.value.drag!.destination;
edge.fromNode = destination === 'to' ? fakeEdge.value.drag!.node : fakeEdge.value.snapped.node;
edge.fromSide = destination === 'to' ? fakeEdge.value.fromSide! : fakeEdge.value.snapped.side;
edge.toNode = destination === 'from' ? fakeEdge.value.drag!.node : fakeEdge.value.snapped.node;
edge.toSide = destination === 'from' ? fakeEdge.value.toSide! : fakeEdge.value.snapped.side;
addAction('property', [{ from: old, to: edge, element: { id: edge.id, type: 'edge' } }]);
}
fakeEdge.value = {};
}
function dragStartEdgeSide(id: string, e: MouseEvent, direction: 'from' | 'to'): void
{
const edge = canvas.value.edges!.find(e => e.id === id)!;
fakeEdgeFromEdge(edge, direction);
window.addEventListener('mousemove', dragEdgeSide, { passive: true });
window.addEventListener('mouseup', dragEndEdgeSide, { passive: true });
}
function fakeEdgeFromEdge(edge: CanvasEdge, direction: 'from' | 'to'): void
{
fakeEdge.value.drag = { type: 'edge', id: edge.id, destination: direction, node: direction === 'to' ? edge.fromNode : edge.toNode };
const destinationNode = direction === 'from' ? canvas.value.nodes!.find(e => e.id === edge.fromNode)! : canvas.value.nodes!.find(e => e.id === edge.toNode)!;
const otherNode = direction === 'from' ? canvas.value.nodes!.find(e => e.id === edge.toNode)! : canvas.value.nodes!.find(e => e.id === edge.fromNode)!;
const destinationPos = posFromDir(getBbox(destinationNode), direction === 'from' ? edge.fromSide : edge.toSide);
const otherPos = posFromDir(getBbox(otherNode), direction === 'from' ? edge.toSide : edge.fromSide);
fakeEdge.value.from = direction === 'from' ? destinationPos : otherPos;
fakeEdge.value.fromSide = edge.fromSide;
fakeEdge.value.to = direction === 'to' ? destinationPos : otherPos;
fakeEdge.value.toSide = edge.toSide;
fakeEdge.value.path = bezier(destinationPos, edge.fromSide, otherPos, edge.toSide);
fakeEdge.value.hex = edge.color?.hex;
fakeEdge.value.style = edge?.color ? edge.color?.class ?
{ fill: `fill-light-${edge.color?.class} dark:fill-dark-${edge.color?.class}`, stroke: `stroke-light-${edge.color?.class} dark:stroke-dark-${edge.color?.class}` } :
{ fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]` } :
{ stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40` };
}
function fakeEdgeFromNode(node: CanvasNode, direction: Direction): void
{
const pos = posFromDir(getBbox(node), direction);
fakeEdge.value.drag = { type: 'node', id: node.id };
fakeEdge.value.from = { ... pos };
fakeEdge.value.fromSide = direction;
fakeEdge.value.to = { ... pos };
fakeEdge.value.toSide = opposite[direction];
fakeEdge.value.path = bezier(pos, fakeEdge.value.fromSide!, pos, fakeEdge.value.toSide!);
fakeEdge.value.hex = node.color?.hex;
fakeEdge.value.style = node?.color ? node.color?.class ?
{ fill: `fill-light-${node.color?.class} dark:fill-dark-${node.color?.class}`, stroke: `stroke-light-${node.color?.class} dark:stroke-dark-${node.color?.class}` } :
{ fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]` } :
{ stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40` };
}
function editNodeProperty<T extends keyof CanvasNode>(ids: string[], property: T, value: CanvasNode[T])
{
if(ids.length === 0)
return;
const actions: HistoryAction<'property'>[] = [];
for(const id of ids)
{
const copy = JSON.parse(JSON.stringify(canvas.value.nodes!.find(e => e.id === id)!)) as CanvasNode;
canvas.value.nodes!.find(e => e.id === id)![property] = value;
actions.push({ element: { type: 'node', id }, from: copy, to: canvas.value.nodes!.find(e => e.id === id)! });
}
addAction('property', actions);
}
function editEdgeProperty<T extends keyof CanvasEdge>(ids: string[], property: T, value: CanvasEdge[T])
{
if(ids.length === 0)
return;
const actions: HistoryAction<'property'>[] = [];
for(const id of ids)
{
const copy = JSON.parse(JSON.stringify(canvas.value.edges!.find(e => e.id === id)!)) as CanvasEdge;
canvas.value.edges!.find(e => e.id === id)![property] = value;
actions.push({ element: { type: 'edge', id }, from: copy, to: canvas.value.edges!.find(e => e.id === id)! });
}
addAction('property', actions);
}
const unselect = () => {
if(focusing.value !== undefined)
{
focused.value?.dom?.removeEventListener('click', stopPropagation);
focused.value?.unselect();
updateToolbarTransform();
}
focusing.value = undefined;
if(editing.value !== undefined)
{
edited.value?.dom?.removeEventListener('wheel', stopPropagation);
edited.value?.dom?.removeEventListener('dblclick', stopPropagation);
edited.value?.dom?.removeEventListener('click', stopPropagation);
edited.value?.unselect();
}
editing.value = undefined;
};
const undo = () => {
if(!historyCursor.value)
return;
for(const action of historyCursor.value.actions)
{
if(action.element.type === 'node')
{
switch(historyCursor.value.event)
{
case 'create':
{
const a = action as HistoryAction<'create'>;
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
snapFinder.remove(canvas.value.nodes!.splice(index, 1)[0]);
break;
}
case 'remove':
{
const a = action as HistoryAction<'remove'>;
canvas.value.nodes!.push(a.from as CanvasNode);
snapFinder.add(a.from as CanvasNode);
break;
}
case 'property':
{
const a = action as HistoryAction<'property'>;
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
canvas.value.nodes![index] = a.from as CanvasNode;
snapFinder.update(a.from as CanvasNode);
break;
}
}
}
else if(action.element.type === 'edge')
{
switch(historyCursor.value.event)
{
case 'create':
{
const a = action as HistoryAction<'create'>;
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
canvas.value.edges!.splice(index, 1);
break;
}
case 'remove':
{
const a = action as HistoryAction<'remove'>;
canvas.value.edges!.push(a.from! as CanvasEdge);
break;
}
case 'property':
{
const a = action as HistoryAction<'property'>;
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
canvas.value.edges![index] = a.from as CanvasEdge;
break;
}
}
}
}
historyPos.value--;
};
const redo = () => {
if(!history.value || history.value.length - 1 <= historyPos.value)
return;
historyPos.value++;
if(!historyCursor.value)
{
historyPos.value--;
return;
}
for(const action of historyCursor.value.actions)
{
if(action.element.type === 'node')
{
switch(historyCursor.value.event)
{
case 'create':
{
const a = action as HistoryAction<'create'>;
canvas.value.nodes!.push(a.to as CanvasNode);
snapFinder.add(a.to as CanvasNode);
break;
}
case 'remove':
{
const a = action as HistoryAction<'remove'>;
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
snapFinder.remove(canvas.value.nodes!.splice(index, 1)[0]);
break;
}
case 'property':
{
const a = action as HistoryAction<'property'>;
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
canvas.value.nodes![index] = a.to as CanvasNode;
snapFinder.update(a.to as CanvasNode);
break;
}
}
}
else if(action.element.type === 'edge')
{
switch(historyCursor.value.event)
{
case 'create':
{
const a = action as HistoryAction<'create'>;
canvas.value.edges!.push(a.to as CanvasEdge);
break;
}
case 'remove':
{
const a = action as HistoryAction<'remove'>;
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
canvas.value.edges!.splice(index, 1);
break;
}
case 'property':
{
const a = action as HistoryAction<'property'>;
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
canvas.value.edges![index] = a.to as CanvasEdge;
break;
}
}
}
}
};
useShortcuts({
meta_z: undo,
meta_y: redo,
Delete: () => { if(focusing.value !== undefined) { remove([focusing.value]) } }
});
</script>
<template>
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" @dblclick.left="createNode">
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4" @click="stopPropagation" @dblclick="stopPropagation">
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
<Tooltip message="Zoom avant" side="right">
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:plus" />
</div>
</Tooltip>
<Tooltip message="Reset" side="right">
<div @click="zoom = 1; updateTransform();" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:reload" />
</div>
</Tooltip>
<Tooltip message="Tout contenir" side="right">
<div @click="reset" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:corners" />
</div>
</Tooltip>
<Tooltip message="Zoom arrière" side="right">
<div @click="zoom = clamp(zoom / 1.1, minZoom, 3); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:minus" />
</div>
</Tooltip>
</div>
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
<Tooltip message="Annuler (Ctrl+Z)" side="right">
<div @click="undo" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" :class="{ 'text-light-50 dark:text-dark-50 !cursor-default hover:bg-transparent dark:hover:bg-transparent': historyPos === -1 }">
<Icon icon="ph:arrow-bend-up-left" />
</div>
</Tooltip>
<Tooltip message="Retablir (Ctrl+Y)" side="right">
<div @click="redo" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" :class="{ 'text-light-50 dark:text-dark-50 !cursor-default hover:bg-transparent dark:hover:bg-transparent': historyPos === history.length - 1 }">
<Icon icon="ph:arrow-bend-up-right" />
</div>
</Tooltip>
</div>
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
<Tooltip message="Préférences" side="right">
<Dialog title="Préférences" iconClose>
<template #trigger>
<div class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:gear" />
</div>
</template>
<template #default>
<Switch v-model="canvasSettings.neighborSnap" label="S'accrocher aux voisins" @update:model-value="snapFinder.config.preferences = canvasSettings" />
<Switch v-model="canvasSettings.gridSnap" label="S'accrocher à la grille" @update:model-value="(v) => { canvasSettings.spacing = v ? 32 : undefined; snapFinder.config.preferences = canvasSettings }" />
<NumberPicker v-model="canvasSettings.spacing" label="Taille de la grille" :disabled="!canvasSettings.gridSnap" @update:model-value="(v) => { spacing = v; updateTransform(); snapFinder.config.preferences = canvasSettings}" />
</template>
</Dialog>
</Tooltip>
<Tooltip message="Aide" side="right">
<Dialog title="Aide" iconClose>
<template #trigger>
<div class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:question-mark-circled" />
</div>
</template>
<template #default>
<div class="flex flex-row justify-between px-4">
<div class="flex flex-col gap-2">
<ProseH4>Ordinateur</ProseH4>
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Selectionner</div>
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Modifier</div>
<div class="flex items-center"><Icon icon="ph:mouse-middle-click-fill" class="w-6 h-6"/>: Déplacer</div>
<div class="flex items-center"><Icon icon="ph:mouse-right-click-fill" class="w-6 h-6"/>: Menu</div>
</div>
<div class="flex flex-col gap-2">
<ProseH4>Mobile</ProseH4>
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/>: Selectionner</div>
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/><Icon icon="ph:hand-tap" class="w-6 h-6"/>: Modifier</div>
<div class="flex items-center"><Icon icon="mdi:gesture-pinch" class="w-6 h-6"/>: Zoomer</div>
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/> maintenu: Menu</div>
</div>
</div>
</template>
</Dialog>
</Tooltip>
</div>
</div>
<svg class="absolute top-0 left-0 w-full h-full pointer-events-none">
<pattern ref="patternRef" id="canvasPattern" patternUnits="userSpaceOnUse">
<circle cx="0.75" cy="0.75" r="0.75" class="fill-light-35 dark:fill-dark-35"></circle>
</pattern>
<rect x="0" y="0" width="100%" height="100%" fill="url(#canvasPattern)"></rect>
</svg>
<div ref="transformRef" :style="{
'transform-origin': 'center center',
}" class="h-full">
<div class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
<div class="absolute z-20 destination-bottom" ref="toolbarRef">
<template v-if="focusing">
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row" v-if="focusing.type === 'node'">
<PopoverRoot>
<PopoverTrigger asChild>
<div @click="stopPropagation">
<Tooltip message="Couleur" side="top">
<div class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="ph:palette" class="w-6 h-6" />
</div>
</Tooltip>
</div>
</PopoverTrigger>
<PopoverPortal disabled>
<PopoverContent align="center" side="top" class="bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 m-2">
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row *:cursor-pointer">
<div @click="editNodeProperty([focusing.id], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-40 dark:bg-dark-40 w-4 h-4 block"></span>
</div>
<div @click="editNodeProperty([focusing.id], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-red dark:bg-dark-red w-4 h-4 block"></span>
</div>
<div @click="editNodeProperty([focusing.id], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-orange dark:bg-dark-orange w-4 h-4 block"></span>
</div>
<div @click="editNodeProperty([focusing.id], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-yellow dark:bg-dark-yellow w-4 h-4 block"></span>
</div>
<div @click="editNodeProperty([focusing.id], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-green dark:bg-dark-green w-4 h-4 block"></span>
</div>
<div @click="editNodeProperty([focusing.id], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-cyan dark:bg-dark-cyan w-4 h-4 block"></span>
</div>
<div @click="editNodeProperty([focusing.id], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-purple dark:bg-dark-purple w-4 h-4 block"></span>
</div>
<label>
<div @click="stopPropagation" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block relative"></span><input @change="(e: Event) => editNodeProperty([focusing!.id], 'color', { hex: (e.target as HTMLInputElement).value })" type="color" class="appearance-none w-0 h-0 absolute" />
</div>
</label>
</div>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
<Tooltip message="Supprimer" side="top">
<div @click="remove([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:trash" class="text-light-red dark:text-dark-red w-6 h-6" />
</div>
</Tooltip>
</div>
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row" v-else>
<PopoverRoot>
<PopoverTrigger asChild>
<div @click="stopPropagation">
<Tooltip message="Couleur" side="top">
<div class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="ph:palette" class="w-6 h-6" />
</div>
</Tooltip>
</div>
</PopoverTrigger>
<PopoverPortal disabled>
<PopoverContent align="center" side="top" class="bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 m-2">
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row *:cursor-pointer">
<div @click="editEdgeProperty([focusing.id], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-40 dark:bg-dark-40 w-4 h-4 block"></span>
</div>
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-red dark:bg-dark-red w-4 h-4 block"></span>
</div>
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-orange dark:bg-dark-orange w-4 h-4 block"></span>
</div>
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-yellow dark:bg-dark-yellow w-4 h-4 block"></span>
</div>
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-green dark:bg-dark-green w-4 h-4 block"></span>
</div>
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-cyan dark:bg-dark-cyan w-4 h-4 block"></span>
</div>
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span class="bg-light-purple dark:bg-dark-purple w-4 h-4 block"></span>
</div>
<label>
<div @click="stopPropagation" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
<span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block relative"></span><input @change="(e: Event) => editEdgeProperty([focusing!.id], 'color', { hex: (e.target as HTMLInputElement).value })" type="color" class="appearance-none w-0 h-0 absolute" />
</div>
</label>
</div>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
<Tooltip message="Supprimer" side="top">
<div @click="remove([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:trash" class="text-light-red dark:text-dark-red w-6 h-6" />
</div>
</Tooltip>
</div>
</template>
</div>
<div>
<CanvasNodeEditor v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom"
@select="select" @edit="edit" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" @input="(id, text) => editNodeProperty([id], node.type === 'group' ? 'label' : 'text', text)" :snap="snapFinder.findNodeSnapPosition.bind(snapFinder)" @edge="dragStartEdgeTo" />
</div>
<div>
<CanvasEdgeEditor v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" @select="select" @edit="edit" @input="(id, text) => editEdgeProperty([id], 'label', text)" @drag="dragStartEdgeSide" />
<div v-if="fakeEdge.path" class="absolute overflow-visible">
<svg class="absolute top-0 overflow-visible h-px w-px">
<g :style="{'--canvas-color': fakeEdge.hex}" class="z-0">
<g :style="`transform: translate(${fakeEdge.path!.to.x}px, ${fakeEdge.path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[fakeEdge.path!.side]}deg);`">
<polygon :class="fakeEdge.style?.fill" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
</g>
<path :style="`stroke-width: calc(3px * var(--zoom-multiplier));`" style="stroke-linecap: butt;" :class="fakeEdge.style?.stroke" class="fill-none stroke-[4px]" :d="fakeEdge.path.path"></path>
</g>
</svg>
</div>
</div>
<svg class="absolute overflow-visible top-0 h-px w-px fill-accent-purple stroke-accent-purple stroke-1 z-50">
<g v-for="hint of hints">
<circle :cx="hint.start.x" :cy="hint.start.y" r="3" />
<circle v-if="hint.end" :cx="hint.end.x" :cy="hint.end.y" r="3" />
<line v-if="hint.end" :x1="hint.start.x" :x2="hint.end.x" :y1="hint.start.y" :y2="hint.end.y"/>
</g>
</svg>
</div>
</div>
</div>
</template>

View File

@@ -1,213 +0,0 @@
<script lang="ts">
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
import { Annotation, EditorState, RangeValue, SelectionRange, type Range } from '@codemirror/state';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { bracketMatching, defaultHighlightStyle, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
import { search, searchKeymap } from '@codemirror/search';
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { IterMode, Tree } from '@lezer/common';
import { tags } from '@lezer/highlight';
const External = Annotation.define<boolean>();
const Hidden = Decoration.mark({ class: 'hidden' });
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
const Blockquote = Decoration.line({ class: '*:hidden before:block !ps-4 relative before:absolute before:top-0 before:bottom-0 before:left-0 before:w-1 before:bg-none before:bg-light-30 dark:before:bg-dark-30' });
const TagTag = tags.special(tags.content);
const intersects = (a: {
from: number;
to: number;
}, b: {
from: number;
to: number;
}) => !(a.to < b.from || b.to < a.from);
const highlight = HighlightStyle.define([
{ tag: tags.heading1, class: 'text-5xl pt-4 pb-2 after:hidden' },
{ tag: tags.heading2, class: 'text-4xl pt-4 pb-2 ps-1 leading-loose after:hidden' },
{ tag: tags.heading3, class: 'text-2xl font-bold pt-1 after:hidden' },
{ tag: tags.heading4, class: 'text-xl font-semibold pt-1 after:hidden variant-cap' },
{ tag: tags.meta, color: "#404740" },
{ tag: tags.link, textDecoration: "underline" },
{ tag: tags.heading, textDecoration: "underline", fontWeight: "bold" },
{ tag: tags.emphasis, fontStyle: "italic" },
{ tag: tags.strong, fontWeight: "bold" },
{ tag: tags.strikethrough, textDecoration: "line-through" },
{ tag: tags.keyword, color: "#708" },
{ tag: TagTag, class: 'cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30' }
]);
class Decorator
{
static hiddenNodes: string[] = [
'HardBreak',
'LinkMark',
'EmphasisMark',
'CodeMark',
'CodeInfo',
'URL',
]
decorations: DecorationSet;
constructor(view: EditorView)
{
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, []), true);
}
update(update: ViewUpdate)
{
if(!update.docChanged && !update.viewportChanged && !update.selectionSet)
return;
this.decorations = this.decorations.update({
filter: (f, t, v) => false,
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges),
sort: true,
});
}
iterate(tree: Tree, visible: readonly {
from: number;
to: number;
}[], selection: readonly SelectionRange[]): Range<Decoration>[]
{
const decorations: Range<Decoration>[] = [];
for (let { from, to } of visible) {
tree.iterate({
from, to, mode: IterMode.IgnoreMounts,
enter: node => {
if(node.node.parent && selection.some(e => intersects(e, node.node.parent!)))
return true;
else if(node.name === 'HeaderMark')
decorations.push(Hidden.range(node.from, node.to + 1));
else if(Decorator.hiddenNodes.includes(node.name))
decorations.push(Hidden.range(node.from, node.to));
else if(node.matchContext(['BulletList', 'ListItem']) && node.name === 'ListMark')
decorations.push(Bullet.range(node.from, node.to + 1));
else if(node.matchContext(['Blockquote']))
decorations.push(Blockquote.range(node.from, node.to));
return true;
},
});
}
return decorations;
}
}
</script>
<script setup lang="ts">
const { autofocus = false } = defineProps<{
placeholder?: string
autofocus?: boolean
}>();
const model = defineModel<string>();
const editor = useTemplateRef('editor');
const view = ref<EditorView>();
onMounted(() => {
if(editor.value)
{
view.value = new EditorView({
doc: model.value,
parent: editor.value,
extensions: [
markdown({
base: markdownLanguage
}),
history(),
search(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(highlight),
bracketMatching(),
closeBrackets(),
crosshairCursor(),
EditorView.lineWrapping,
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
]),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
{
model.value = viewUpdate.state.doc.toString();
}
}),
EditorView.contentAttributes.of({spellcheck: "true"}),
ViewPlugin.fromClass(Decorator, {
decorations: e => e.decorations,
})
]
});
if(autofocus)
{
view.value.focus();
}
}
});
onBeforeUnmount(() => {
if (view.value)
{
view.value?.destroy();
view.value = undefined;
}
});
watchEffect(() => {
if (model.value === void 0) {
return;
}
const currentValue = view.value ? view.value.state.doc.toString() : "";
if (view.value && model.value !== currentValue) {
view.value.dispatch({
changes: { from: 0, to: currentValue.length, insert: model.value || "" },
annotations: [External.of(true)],
});
}
});
defineExpose({ focus: () => editor.value?.focus() });
</script>
<template>
<div ref="editor" class="flex flex-1 w-full justify-stretch items-stretch py-2 px-1.5 font-sans text-base"></div>
</template>
<style>
.variant-cap
{
font-variant: small-caps;
}
.cm-editor
{
@apply bg-transparent;
@apply flex-1 h-full;
@apply font-sans;
@apply text-light-100 dark:text-dark-100;
}
.cm-editor .cm-content
{
@apply caret-light-100 dark:caret-dark-100;
}
.cm-line
{
@apply text-base;
@apply font-sans;
}
</style>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import render, { type MDProperties } from '#shared/markdown.util'
const { content, filter, properties } = defineProps<{
content?: string,
filter?: string,
properties?: MDProperties
}>();
const container = useTemplateRef('container');
content && onMounted(() => {
queueMicrotask(() => {
container.value && content && container.value.replaceChildren(render(content, filter, properties));
})
})
</script>
<template>
<div ref="container"></div>
</template>

View File

@@ -1,39 +0,0 @@
<script setup lang="ts">
import type { CharacterConfig, MainStat, TrainingLevel } from '~/types/character';
import PreviewA from './prose/PreviewA.vue';
const { config } = defineProps<{
config: CharacterConfig
}>();
const selection = ref<{
stat: MainStat;
level: TrainingLevel;
option: number;
}>();
function focusTraining(stat: MainStat, level: TrainingLevel, option: number)
{
const s = selection.value;
if(s !== undefined && s.stat === stat && s.level === level && s.option === option)
{
selection.value = undefined;
}
else
{
selection.value = {
stat, level, option
};
}
}
</script>
<template>
<TrainingViewer :config="config" progress>
<template #default="{ stat, level, option }">
<div @click.capture="console.log" class="border border-light-40 dark:border-dark-40 hover:border-light-70 dark:hover:border-dark-70 cursor-pointer px-2 py-1 w-[400px]" :class="{ '!border-accent-blue': selection !== undefined && selection?.stat == stat && selection?.level == level && selection?.option == option }">
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="config.training[stat][level][option].description.map(e => e.text).join('\n')" />
</div>
</template>
</TrainingViewer>
</template>

View File

@@ -1,51 +0,0 @@
<script setup lang="ts">
import { MAIN_STATS, mainStatTexts } from '#shared/character.util';
import type { CharacterConfig, MainStat } from '~/types/character';
const { config } = defineProps<{
config: CharacterConfig
}>();
const position = ref(0);
</script>
<template>
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10 min-h-20">
<div class="flex flex-shrink gap-3 items-center relative w-48 ms-12">
<span v-for="(stat, i) of MAIN_STATS" :value="stat" class="block w-2.5 h-2.5 m-px outline outline-1 outline-transparent
hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer" @click="position = i"></span>
<span :style="{ 'left': position * 1.5 + 'em' }" :data-text="mainStatTexts[MAIN_STATS[position] as MainStat]" class="rounded-full w-3 h-3 bg-accent-blue absolute transition-[left]
after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center"></span>
</div>
<div class="flex-1 flex">
<slot name="addin" :stat="MAIN_STATS[position]"></slot>
</div>
<span></span>
</div>
<div class="flex flex-1 px-8 overflow-hidden max-w-full">
<div class="relative cursor-grab active:cursor-grabbing select-none transition-[left] flex flex-1 flex-row max-w-full" :style="{ 'left': `-${position * 100}%` }">
<div class="flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-20" v-for="(stat, name) in config.training">
<template v-for="(options, level) of stat">
<div class="w-full flex h-px"><div class="border-t border-dashed border-light-50 dark:border-dark-50 w-full"></div><span class="relative left-4">{{ level }}</span></div>
<div class="flex flex-row gap-4 justify-center">
<template v-for="(option, i) in options">
<slot :stat="name" :level="level" :option="i"></slot>
</template>
</div>
</template>
</div>
<!-- <div class="flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-20" v-for="(stat, name) in config.training" >
<div class="flex flex-row gap-2 justify-center relative" v-for="(options, level) in stat">
<template v-if="progress">
<div class="absolute left-0 right-0 -top-2 h-px border-t border-light-30 dark:border-dark-30 border-dashed">
<span class="absolute right-0 p-1 text-end">{{ level }}</span>
</div>
</template>
<template v-for="(option, i) in options">
<slot :stat="name" :level="level" :option="i"></slot>
</template>
</div>
</div> -->
</div>
</div>
</template>

View File

@@ -1,21 +0,0 @@
<template>
<ProgressRoot class="my-2 relative overflow-hidden bg-light-25 dark:bg-dark-25 w-48 h-3 data-[shape=thin]:h-1 data-[shape=large]:h-6" :data-shape="shape" style="transform: translateZ(0)" >
<ProgressIndicator class="bg-light-50 dark:bg-dark-50 h-full w-0 transition-[width] ease-linear" :style="`transition-duration: ${delay}ms; width: ${progress ? 100 : 0}%`" />
</ProgressRoot>
</template>
<script setup lang="ts">
const { delay = 1500, decreasing = false, shape = 'normal' } = defineProps<{
delay?: number
decreasing?: boolean
shape?: 'thin' | 'normal' | 'large'
}>();
const emit = defineEmits(['finish']);
const progress = ref(false);
nextTick(() => {
progress.value = true;
setTimeout(emit, delay, 'finish');
});
</script>

View File

@@ -1,94 +0,0 @@
<template>
<ToastProvider>
<ToastRoot v-for="toast in model" :key="toast.id" :duration="toast.duration" class="ToastRoot bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 group" :open="toast.state ?? true" @update:open="(state: boolean) => tryClose(toast, state)" :data-type="toast.type ?? 'info'">
<div class="grid grid-cols-8 px-3 pt-2 pb-2">
<ToastTitle v-if="toast.title" class="font-semibold text-xl col-span-7 text-light-70 dark:text-dark-70" asChild><h4>{{ toast.title }}</h4></ToastTitle>
<ToastClose v-if="toast.closeable" aria-label="Close" class="text-xl -translate-y-2 translate-x-4 cursor-pointer"><span aria-hidden>×</span></ToastClose>
<ToastDescription v-if="toast.content" class="text-sm col-span-8 text-light-100 dark:text-dark-100" asChild><span>{{ toast.content }}</span></ToastDescription>
</div>
<TimerProgress v-if="toast.timer" shape="thin" :delay="toast.duration" class="mb-0 mt-0 w-full
group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green
group-data-[type=error]:bg-light-red dark:group-data-[type=error]:bg-dark-red group-data-[type=success]:bg-light-green dark:group-data-[type=success]:bg-dark-green !bg-opacity-50"
@finish="() => tryClose(toast, false)" />
</ToastRoot>
<ToastViewport class="fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72" />
</ToastProvider>
</template>
<script setup lang="ts">
const model = defineModel<ExtraToastConfig[]>();
function tryClose(config: ExtraToastConfig, state: boolean)
{
if(!state)
{
const m = model.value;
if(m)
{
const idx = m?.findIndex(e => e.id === config.id);
m[idx].state = false;
model.value = m;
}
setTimeout(() => model.value?.splice(model.value?.findIndex(e => e.id === config.id), 1), 500);
}
}
</script>
<style>
.ToastRoot[data-type='error'] {
@apply border-light-red;
@apply dark:border-dark-red;
@apply bg-light-red;
@apply dark:bg-dark-red;
@apply !bg-opacity-50;
}
.ToastRoot[data-type='success'] {
@apply border-light-green;
@apply dark:border-dark-green;
@apply bg-light-green;
@apply dark:bg-dark-green;
@apply !bg-opacity-50;
}
.ToastRoot[data-state='open'] {
animation: slideIn .15s cubic-bezier(0.16, 1, 0.3, 1);
}
.ToastRoot[data-state='closed'] {
animation: hide .1s ease-in;
}
.ToastRoot[data-swipe='move'] {
transform: translateX(var(--radix-toast-swipe-move-x));
}
.ToastRoot[data-swipe='cancel'] {
transform: translateX(0);
transition: transform .2s ease-out;
}
.ToastRoot[data-swipe='end'] {
animation: swipeRight .1s ease-out;
}
@keyframes hide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideIn {
from {
transform: translateX(calc(100% + var(--viewport-padding)));
}
to {
transform: translateX(0);
}
}
@keyframes swipeRight {
from {
transform: translateX(var(--radix-toast-swipe-end-x));
}
to {
transform: translateX(100%);
}
}
</style>

View File

@@ -1,42 +0,0 @@
<template>
<div class="absolute overflow-visible">
<div v-if="edge.label" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20">{{ edge.label }}</div>
<svg class="absolute top-0 overflow-visible h-px w-px">
<g :style="{'--canvas-color': edge.color?.hex}" class="z-0">
<g :style="`transform: translate(${path!.to.x}px, ${path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[path!.side]}deg);`">
<polygon :class="style.fill" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
</g>
<path :style="`stroke-width: calc(3px * var(--zoom-multiplier));`" style="stroke-linecap: butt;" :class="style.stroke" class="fill-none stroke-[4px]" :d="path!.path"></path>
</g>
</svg>
</div>
</template>
<style>
.fill-colored
{
--tw-bg-opacity: 1;
fill: rgba(from var(--canvas-color) r g b / var(--tw-bg-opacity));
}
</style>
<script setup lang="ts">
import { getPath, labelCenter, rotation } from '#shared/canvas.util';
import type { CanvasEdge, CanvasNode } from '~/types/canvas';
const { edge, nodes } = defineProps<{
edge: CanvasEdge
nodes: CanvasNode[]
}>();
const from = computed(() => nodes!.find(f => f.id === edge.fromNode));
const to = computed(() => nodes!.find(f => f.id === edge.toNode));
const path = computed(() => getPath(from.value!, edge.fromSide, to.value!, edge.toSide));
const labelPos = computed(() => labelCenter(from.value!, edge.fromSide, to.value!, edge.toSide));
const style = computed(() => {
return edge.color ? edge.color?.class ?
{ fill: `fill-light-${edge.color?.class} dark:fill-dark-${edge.color?.class}`, stroke: `stroke-light-${edge.color?.class} dark:stroke-dark-${edge.color?.class}` } :
{ fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]` } :
{ stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40` }
});
</script>

View File

@@ -1,98 +0,0 @@
<template>
<div class="absolute overflow-visible group" :class="{ 'z-[1]': focusing }">
<input v-autofocus v-if="editing" @click="e => e.stopImmediatePropagation()" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20" v-model="edge.label" />
<div v-else-if="edge.label" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20" @click.left="select" @dblclick.left="edit">{{ edge.label }}</div>
<svg ref="dom" class="absolute top-0 overflow-visible h-px w-px">
<g :style="{'--canvas-color': edge.color?.hex}" class="z-0">
<g :style="`transform: translate(${path.to.x}px, ${path.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[path.side]}deg);`">
<polygon :class="style.fill" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
</g>
<path :style="`stroke-width: calc(${focusing ? 6 : 3}px * var(--zoom-multiplier));`" style="stroke-linecap: butt;" :class="style.stroke" class="transition-[stroke-width] fill-none stroke-[4px]" :d="path.path"></path>
<path style="stroke-width: calc(22px * var(--zoom-multiplier));" class="fill-none transition-opacity z-30 opacity-0 hover:opacity-25" :class="[style.stroke, { 'opacity-25': focusing }]" :d="path.path" @click="select" @dblclick="edit"></path>
</g>
</svg>
<span v-if="focusing && !editing" :style="`transform: translate(${path.from.x}px, ${path.from.y}px) translate(-50%, -50%) scale(var(--zoom-multiplier))`" @mousedown.left="(e) => dragEdge(e, 'from')" :class="style.fill" class="hidden group-hover:block z-[31] absolute rounded-full border-2 border-light-70 dark:border-dark-70 bg-light-30 dark:bg-dark-30 w-6 h-6"></span>
<span v-if="focusing && !editing" :style="`transform: translate(${path.to.x}px, ${path.to.y}px) translate(-50%, -50%) scale(var(--zoom-multiplier))`" @mousedown.left="(e) => dragEdge(e, 'to')" :class="style.fill" class="hidden group-hover:block z-[31] absolute rounded-full border-2 border-light-70 dark:border-dark-70 bg-light-30 dark:bg-dark-30 w-6 h-6"></span>
</div>
</template>
<style>
.fill-colored
{
--tw-bg-opacity: 1;
fill: rgba(from var(--canvas-color) r g b / var(--tw-bg-opacity));
}
</style>
<script setup lang="ts">
import { getPath, labelCenter, rotation } from '#shared/canvas.util';
import type { Element } from '../CanvasEditor.vue';
import type { CanvasEdge, CanvasNode } from '~/types/canvas';
const { edge, nodes } = defineProps<{
edge: CanvasEdge
nodes: CanvasNode[]
}>();
const emit = defineEmits<{
(e: 'select', id: Element): void,
(e: 'edit', id: Element): void,
(e: 'drag', id: string, _e: MouseEvent, origin: 'from' | 'to'): void,
(e: 'input', id: string, text?: string): void,
}>();
const dom = useTemplateRef('dom');
const focusing = ref(false), editing = ref(false);
const from = computed(() => nodes!.find(f => f.id === edge.fromNode));
const to = computed(() => nodes!.find(f => f.id === edge.toNode));
const path = computed(() => getPath(from.value!, edge.fromSide, to.value!, edge.toSide)!);
const labelPos = computed(() => labelCenter(from.value!, edge.fromSide, to.value!, edge.toSide));
let oldText = edge.label;
function select(e: Event) {
if(editing.value)
return;
focusing.value = true;
emit('select', { type: 'edge', id: edge.id });
}
function edit(e: Event) {
oldText = edge.label;
focusing.value = true;
editing.value = true;
e.stopImmediatePropagation();
emit('edit', { type: 'edge', id: edge.id });
}
function dragEdge(e: MouseEvent, origin: 'from' | 'to') {
e.stopImmediatePropagation();
emit('drag', edge.id, e, origin);
}
function unselect() {
if(editing.value)
{
const text = edge.label;
if(text !== oldText)
{
edge.label = oldText;
emit('input', edge.id, text);
}
}
focusing.value = false;
editing.value = false;
}
defineExpose({ unselect, dom, id: edge.id, path });
const style = computed(() => {
return edge.color ? edge.color?.class ?
{ fill: `fill-light-${edge.color?.class} dark:fill-dark-${edge.color?.class}`, stroke: `stroke-light-${edge.color?.class} dark:stroke-dark-${edge.color?.class}`, outline: `outline-light-${edge.color?.class} dark:outline-dark-${edge.color?.class}` } :
{ fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]`, outline: `outline-[color:var(--canvas-color)]` } :
{ stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40`, outline: `outline-light-40 dark:outline-dark-40` }
});
</script>

View File

@@ -1,35 +0,0 @@
<template>
<div class="absolute" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
<div :class="[style.border]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg">
<div v-if="node.text && node.text.length > 0" class="flex items-center">
<MarkdownRenderer :content="node.text" />
</div>
</div>
</div>
<div v-if="node.type === 'group' && node.label !== undefined" :class="style.border" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin">{{ node.label }}</div>
</div>
</template>
<style>
.bg-colored
{
--tw-bg-opacity: 1;
background-color: rgba(from var(--canvas-color) r g b / var(--tw-bg-opacity));
}
</style>
<script setup lang="ts">
import type { CanvasNode } from '~/types/canvas';
const { node } = defineProps<{
node: CanvasNode
zoom: number
}>();
const style = computed(() => {
return node.color ? node.color?.class ?
{ bg: `bg-light-${node.color?.class} dark:bg-dark-${node.color?.class}`, border: `border-light-${node.color?.class} dark:border-dark-${node.color?.class}` } :
{ bg: `bg-colored`, border: `border-[color:var(--canvas-color)]` } :
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` }
});
</script>

View File

@@ -1,190 +0,0 @@
<template>
<div class="absolute" ref="dom" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
<div v-if="!editing || node.type === 'group'" style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4 cursor-move': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg" @click.left="(e) => { if(node.type !== 'group') selectNode(e) }" @dblclick.left="(e) => { if(node.type !== 'group') editNode(e) }">
<div v-if="node.text && node.text.length > 0" class="flex items-center">
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
</div>
</div>
<div v-if="focusing">
<span @mousedown.left="(e) => resizeNode(e, 0, 1, 0, -1)" id="n " class="cursor-n-resize absolute -top-3 -right-3 -left-3 h-6 group">
<span @mousedown.left="(e) => dragEdge(e, 'top')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -top-1.5 left-1/2 -translate-x-3"></span>
</span> <!-- North -->
<span @mousedown.left="(e) => resizeNode(e, 0, 0, 0, 1)" id="s " class="cursor-s-resize absolute -bottom-3 -right-3 -left-3 h-6 group">
<span @mousedown.left="(e) => dragEdge(e, 'bottom')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -bottom-1.5 left-1/2 -translate-x-3"></span>
</span> <!-- South -->
<span @mousedown.left="(e) => resizeNode(e, 0, 0, 1, 0)" id="e " class="cursor-e-resize absolute -top-3 -bottom-3 -right-3 w-6 group">
<span @mousedown.left="(e) => dragEdge(e, 'right')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -right-1.5 top-1/2 -translate-y-3"></span>
</span> <!-- East -->
<span @mousedown.left="(e) => resizeNode(e, 1, 0, -1, 0)" id="w " class="cursor-w-resize absolute -top-3 -bottom-3 -left-3 w-6 group">
<span @mousedown.left="(e) => dragEdge(e, 'left')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -left-1.5 top-1/2 -translate-y-3"></span>
</span> <!-- West -->
<span @mousedown.left="(e) => resizeNode(e, 1, 1, -1, -1)" id="nw" class="cursor-nw-resize absolute -top-4 -left-4 w-8 h-8"></span> <!-- North West -->
<span @mousedown.left="(e) => resizeNode(e, 0, 1, 1, -1)" id="ne" class="cursor-ne-resize absolute -top-4 -right-4 w-8 h-8"></span> <!-- North East -->
<span @mousedown.left="(e) => resizeNode(e, 0, 0, 1, 1)" id="se" class="cursor-se-resize absolute -bottom-4 -right-4 w-8 h-8"></span> <!-- South East -->
<span @mousedown.left="(e) => resizeNode(e, 1, 0, -1, 1)" id="sw" class="cursor-sw-resize absolute -bottom-4 -left-4 w-8 h-8"></span> <!-- South West -->
</div>
</div>
<div v-else style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex py-2" >
<FramedEditor v-model="node.text" autofocus :gutters="false"/>
</div>
<div v-if="!editing && node.type === 'group' && node.label !== undefined" @click.left="(e) => selectNode(e)" @dblclick.left="(e) => editNode(e)" :class="style.border" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin">{{ node.label }}</div>
<input v-else-if="editing && node.type === 'group'" v-model="node.label" @click="e => e.stopImmediatePropagation()" v-autofocus :class="[style.border, style.outline]" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] appearance-none bg-transparent outline-4 mb-2 px-2 py-1 font-thin min-w-4" />
</div>
</template>
<style>
.bg-colored
{
--tw-bg-opacity: 1;
background-color: rgba(from var(--canvas-color) r g b / var(--tw-bg-opacity));
}
</style>
<script setup lang="ts">
import type { Box, Direction } from '#shared/canvas.util';
import type { Element } from '../CanvasEditor.vue';
import FakeA from '../prose/FakeA.vue';
import type { CanvasNode } from '~/types/canvas';
const { node, zoom, snap } = defineProps<{
node: CanvasNode
zoom: number,
snap: (activeNode: CanvasNode, resizeHandle?: Box) => Partial<Box>,
}>();
const emit = defineEmits<{
(e: 'select', id: Element): void,
(e: 'edit', id: Element): void,
(e: 'move', id: string, x: number, y: number): void,
(e: 'resize', id: string, x: number, y: number, w: number, h: number): void,
(e: 'input', id: string, text: string): void,
(e: 'edge', id: string, _e: MouseEvent, side: Direction): void,
}>();
const dom = useTemplateRef('dom');
const focusing = ref(false), editing = ref(false);
let oldText = node.type === 'group' ? node.label : node.text;
function selectNode(e: Event) {
if(editing.value)
return;
focusing.value = true;
emit('select', { type: 'node', id: node.id });
dom.value?.addEventListener('mousedown', dragstart, { passive: true });
}
function editNode(e: Event) {
focusing.value = true;
editing.value = true;
oldText = node.type === 'group' ? node.label : node.text;
e.stopImmediatePropagation();
dom.value?.removeEventListener('mousedown', dragstart);
emit('edit', { type: 'node', id: node.id });
}
function resizeNode(e: MouseEvent, x: number, y: number, width: number, height: number) {
e.stopImmediatePropagation();
const startx = node.x, starty = node.y, startw = node.width, starth = node.height;
let realx = node.x, realy = node.y, realw = node.width, realh = node.height;
const resizemove = (e: MouseEvent) => {
if(e.button !== 0)
return;
realx = realx + (e.movementX / zoom) * x;
realy = realy + (e.movementY / zoom) * y;
realw = Math.max(realw + (e.movementX / zoom) * width, 64);
realh = Math.max(realh + (e.movementY / zoom) * height, 64);
const result = e.altKey ? undefined : snap({ ...node, x: realx, y: realy, width: realw, height: realh }, { x, y, width, height });
node.x = result?.x ?? realx;
node.y = result?.y ?? realy;
node.width = result?.width ?? realw;
node.height = result?.height ?? realh;
};
const resizeend = (e: MouseEvent) => {
if(e.button !== 0)
return;
emit('resize', node.id, node.x - startx, node.y - starty, node.width - startw, node.height - starth);
window.removeEventListener('mousemove', resizemove);
window.removeEventListener('mouseup', resizeend);
}
window.addEventListener('mousemove', resizemove);
window.addEventListener('mouseup', resizeend);
}
function dragEdge(e: MouseEvent, direction: Direction) {
e.stopImmediatePropagation();
emit('edge', node.id, e, direction)
}
function unselect() {
if(editing.value)
{
const text = node.type === 'group' ? node.label! : node.text!;
if(text !== oldText)
{
if(node.type === 'group')
node.label = oldText;
else
node.text = oldText;
emit('input', node.id, text);
}
}
focusing.value = false;
editing.value = false;
dom.value?.removeEventListener('mousedown', dragstart);
}
let lastx = 0, lasty = 0;
let realx = 0, realy = 0;
const dragmove = (e: MouseEvent) => {
if(e.button !== 0)
return;
realx += e.movementX / zoom;
realy += e.movementY / zoom;
const result = e.altKey ? undefined : snap({ ...node, x: realx, y: realy });
node.x = result?.x ?? realx;
node.y = result?.y ?? realy;
};
const dragend = (e: MouseEvent) => {
if(e.button !== 0)
return;
window.removeEventListener('mousemove', dragmove);
window.removeEventListener('mouseup', dragend);
emit('move', node.id, node.x - lastx, node.y - lasty);
};
const dragstart = (e: MouseEvent) => {
if(e.button !== 0)
return;
lastx = node.x, lasty = node.y;
realx = node.x, realy = node.y;
window.addEventListener('mousemove', dragmove, { passive: true });
window.addEventListener('mouseup', dragend, { passive: true });
};
defineExpose({ unselect, dom, id: node.id });
const style = computed(() => {
return node.color ? node.color?.class ?
{ bg: `bg-light-${node.color?.class} dark:bg-dark-${node.color?.class}`, border: `border-light-${node.color?.class} dark:border-dark-${node.color?.class}`, outline: `outline-light-${node.color?.class} dark:outline-dark-${node.color?.class}` } :
{ bg: `bg-colored`, border: `border-[color:var(--canvas-color)]`, outline: `outline-[color:var(--canvas-color)]` } :
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40`, outline: `outline-light-40 dark:outline-dark-40` }
});
</script>

View File

@@ -1,38 +0,0 @@
<template>
<template v-if="model && model.people !== undefined">
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10">
<Label class="flex items-center justify-between gap-2">
<span class="pb-1 mx-2 md:p-0">Points restants</span>
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>
</Label>
<Button @click="emit('next')">Suivant</Button>
</div>
<div class="flex flex-row flex-wrap justify-center items-center flex-1 gap-12 mx-8 my-4 px-48">
<template v-for="ability of config.abilities">
<div class="flex flex-col border border-light-50 dark:border-dark-50 p-4 gap-2 w-[200px] relative">
<div class="flex justify-between">
<NumberFieldRoot :min="0" class="flex w-20 justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>
<Tooltip side="bottom" :message="`${mainStatTexts[ability.max[0]]} (0) + ${mainStatTexts[ability.max[1]]} (0) + 0`"><span class="text-lg text-end cursor-pointer">/ {{ 0 }}</span></Tooltip>
</div>
<span class="text-xl text-center font-bold">{{ ability.name }}</span>
<span class="absolute -bottom-px -left-px h-[3px] bg-accent-blue" :style="{ width: `200px` }"></span>
</div>
</template>
</div>
</template>
</template>
<script setup lang="ts">
import { mainStatTexts, type Character, type CharacterConfig } from '~/types/character';
const { config } = defineProps<{
config: CharacterConfig,
}>();
const model = defineModel<Character>({ required: true });
const emit = defineEmits(['next']);
</script>

View File

@@ -1,38 +0,0 @@
<template>
<template v-if="model && model.people !== undefined">
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10">
<Label class="flex items-center justify-between gap-2">
<span class="pb-1 mx-2 md:p-0">Physique</span>
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
</NumberFieldRoot>
</Label>
<Label class="flex items-center justify-between gap-2">
<span class="pb-1 mx-2 md:p-0">Mental</span>
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
</NumberFieldRoot>
</Label>
<Label class="flex items-center justify-between gap-2">
<span class="pb-1 mx-2 md:p-0">Caractère</span>
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
</NumberFieldRoot>
</Label>
<Button @click="emit('next')" :disabled="model.aspect === undefined">Enregistrer</Button>
</div>
<div class="flex flex-col flex-1 gap-4 mx-8 my-4">
</div>
</template>
</template>
<script setup lang="ts">
import type { Character, CharacterConfig } from '~/types/character';
const { config } = defineProps<{
config: CharacterConfig,
}>();
const model = defineModel<Character>({ required: true });
const emit = defineEmits(['next']);
</script>

View File

@@ -1,56 +0,0 @@
<template>
<template v-if="model && model.character && model.character.people !== undefined">
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10">
<Label class="flex items-center justify-between gap-2">
<span class="pb-1 mx-2 md:p-0">Niveau</span>
<NumberFieldRoot :min="1" :max="20" v-model="model.character.level" @update:model-value="val => model.updateLevel(val as Level)" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>
</Label>
<Label class="flex items-center justify-between gap-2">
<span class="pb-1 mx-2 md:p-0">Attributions restantes</span>
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
</NumberFieldRoot>
</Label>
<Label class="flex items-center justify-between gap-2">
<span class="pb-1 mx-2 md:p-0">Vie</span>
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
</NumberFieldRoot>
</Label>
<Label class="flex items-center justify-between gap-2">
<span class="pb-1 mx-2 md:p-0">Mana</span>
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
</NumberFieldRoot>
</Label>
<Button @click="emit('next')">Suivant</Button>
</div>
<div class="flex flex-col flex-1 gap-4 mx-8 my-4">
<template v-for="(level, index) of config.peoples[model.character.people!].options">
<div class="w-full flex h-px"><div class="border-t border-dashed border-light-50 dark:border-dark-50 w-full" :class="{ 'opacity-30': index > model.character.level }"></div><span class="sticky top-0">{{ index }}</span></div>
<div class="flex flex-row gap-4 justify-center" :class="{ 'opacity-30': index > model.character.level }">
<template v-for="(option, i) of level">
<div class="flex border border-light-50 dark:border-dark-50 px-4 py-2 w-[400px]" @click="model.toggleLevelOption(parseInt(index as unknown as string, 10) as Level, i)"
:class="{ 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer': index <= model.character.level, '!border-accent-blue bg-accent-blue bg-opacity-20': model.character.leveling?.some(e => e[0] == index && e[1] === i) ?? false }">
<span class="text-wrap whitespace-pre">{{ option.description }}</span>
</div>
</template>
</div>
</template>
</div>
</template>
</template>
<script setup lang="ts">
import type { CharacterBuilder } from '#shared/character.util';
import type { CharacterConfig, Level } from '~/types/character';
const { config } = defineProps<{
config: CharacterConfig,
}>();
const model = defineModel<CharacterBuilder>({ required: true });
const emit = defineEmits(['next']);
</script>

View File

@@ -1,30 +0,0 @@
<template>
<template v-if="model">
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center">
<TextInput label="Nom" v-model="model.character.name" class="flex-none"/>
<Switch label="Privé ?" :default-value="model.character.visibility === 'private'" @update:model-value="(e) => model!.character.visibility = e ? 'private' : 'public'" />
<Button @click="emit('next')">Suivant</Button>
</div>
<div class="flex flex-1 gap-4 p-2 overflow-x-auto justify-center">
<div v-for="(people, i) of config.peoples" @click="model.character.people = i" class="flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35
cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]" :class="{ '!border-accent-blue outline-2 outline outline-accent-blue': model.character.people === i }">
<Avatar :src="people.name" :text="`Image placeholder`" class="h-[320px]" />
<span class="text-xl font-bold text-center">{{ people.name }}</span>
<span class="w-full border-b border-light-50 dark:border-dark-50"></span>
<span class="text-wrap word-break">{{ people.description }}</span>
</div>
</div>
</template>
</template>
<script setup lang="ts">
import type { CharacterBuilder } from '#shared/character.util';
import type { CharacterConfig } from '~/types/character';
const { config } = defineProps<{
config: CharacterConfig,
}>();
const model = defineModel<CharacterBuilder>();
const emit = defineEmits(['next']);
</script>

View File

@@ -1,69 +0,0 @@
<template>
<TrainingViewer :config="config">
<template #addin="{ stat }">
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10">
<Label class="flex items-center justify-between gap-2">
<span class="pb-1 mx-2 md:p-0">Points restants</span>
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
</NumberFieldRoot>
</Label>
<Button @click="emit('next')">Suivant</Button>
</div>
</template>
<template #default="{ stat, level, option }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50" @click="toggleOption(stat, parseInt(level as unknown as string, 10) as TrainingLevel, option)" :class="{ /*'opacity-30': level > maxTraining[stat] + 1, 'hover:border-light-60 dark:hover:border-dark-60': level <= maxTraining[stat] + 1, */'!border-accent-blue bg-accent-blue bg-opacity-20': level == 0 || (model.training[stat]?.some(e => e[0] == level && e[1] === option) ?? false) }">
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="config.training[stat][level][option].description.map(e => e.text).join('\n')" />
</div>
</template>
</TrainingViewer>
</template>
<script setup lang="ts">
import PreviewA from '~/components/prose/PreviewA.vue';
import { MAIN_STATS, type Character, type CharacterConfig, type MainStat, type TrainingLevel } from '~/types/character';
const { config } = defineProps<{
config: CharacterConfig,
}>();
const model = defineModel<Character>({ required: true, });
const maxTraining = Object.fromEntries(MAIN_STATS.map(e => [e, 0]));
const emit = defineEmits(['next']);
function toggleOption(stat: MainStat, level: TrainingLevel, choice: number)
{
const character = model.value;
if(level == 0)
return;
for(let i = 1; i < level; i++) //Check previous levels as a requirement
{
if(!character.training[stat].some(e => e[0] == i))
return;
}
if(character.training[stat].some(e => e[0] == level))
{
if(character.training[stat].some(e => e[0] == level && e[1] === choice))
{
for(let i = 15; i >= level; i --) //Invalidate higher levels
{
const index = character.training[stat].findIndex(e => e[0] == i);
if(index !== -1)
character.training[stat].splice(index, 1);
}
}
else
character.training[stat].splice(character.training[stat].findIndex(e => e[0] == level), 1, [level, choice]);
}
else //if(trainingPoints.value && trainingPoints.value > 0)
{
character.training[stat].push([level, choice]);
}
model.value = character;
}
</script>

View File

@@ -1,109 +0,0 @@
<script lang="ts">
/*
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
*/
</script>
<script setup lang="ts">
import type { CanvasContent } from '~/types/content';
import { Canvas } from '#shared/canvas.util';
const { path } = defineProps<{
path: string
}>();
const { user } = useUserSession();
const { content, get } = useContent();
const overview = computed(() => content.value.find(e => e.path === path) as CanvasContent | undefined);
const isOwner = computed(() => user.value?.id === overview.value?.owner);
const element = useTemplateRef('element');
onMounted(() => {
mount();
});
if(overview.value && !overview.value.content)
{
await get(path);
mount();
}
const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined);
console.log(canvas.value);
function mount()
{
if(element.value && canvas.value)
{
const c = new Canvas(canvas.value);
element.value.appendChild(c.container);
c.mount();
}
updateScaleVar();
}
</script>
<template>
<div ref="element"></div>
</template>

View File

@@ -1,40 +0,0 @@
<script setup lang="ts">
import { hasPermissions } from '~/shared/auth.util';
const { path } = defineProps<{
path: string
filter?: string,
popover?: boolean
}>();
const { user } = useUserSession();
const { content, get } = useContent();
const overview = computed(() => content.value.find(e => e.path === path));
const isOwner = computed(() => user.value?.id === overview.value?.owner);
const loading = ref(false);
if(overview.value && !overview.value.content)
{
loading.value = true;
await get(path);
loading.value = false;
}
</script>
<template>
<div class="flex flex-1 justify-start items-start flex-col lg:px-16 xl:px-32 2xl:px-64 py-6">
<Loading v-if="loading" />
<template v-else-if="overview">
<div v-if="!popover" class="flex flex-1 flex-row justify-between items-center">
<ProseH1>{{ overview.title }}</ProseH1>
<div class="flex gap-4">
<NuxtLink :href="{ name: 'explore-edit', hash: '#' + overview.path }" v-if="isOwner || hasPermissions(user?.permissions ?? [], ['admin', 'editor'])"><Button>Modifier</Button></NuxtLink>
</div>
</div>
<MarkdownRenderer v-if="overview.content" :content="overview.content" :filter="filter" />
</template>
<template v-else>
<div><ProseH2>Impossible d'afficher le contenu demandé</ProseH2></div>
</template>
</div>
</template>

View File

@@ -1,27 +0,0 @@
<template>
<span>
<HoverCard trigger-key="Ctrl" nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
<template #content>
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="pathname" :filter="hash.substring(1)" popover />
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="pathname" /></div></template>
</template>
<span>
<span class="text-accent-blue inline-flex items-center cursor-pointer hover:text-opacity-85"><slot v-bind="$attrs"></slot></span>
</span>
</HoverCard>
</span>
</template>
<script setup lang="ts">
import { parseURL } from 'ufo';
const { href } = defineProps<{
href: string
class?: string
}>();
const { hash, pathname } = parseURL(href);
const { content } = useContent();
const overview = computed(() => content.value.find(e => e.path === pathname));
</script>

View File

@@ -1,30 +0,0 @@
<template>
<span class="text-accent-blue inline-flex items-center" :class="class">
<HoverCard nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
<template #content>
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="decodeURIComponent(pathname)" :filter="hash.substring(1)" popover />
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="decodeURIComponent(pathname)" /></div></template>
</template>
<span>
<slot v-bind="$attrs"></slot>
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :icon="iconByType[overview.type]" />
</span>
</HoverCard>
</span>
</template>
<script setup lang="ts">
import { parseURL } from 'ufo';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { iconByType } from '#shared/content.util';
const { href } = defineProps<{
href: string
class?: string
}>();
const { hash, pathname } = parseURL(href);
const { content } = useContent();
const overview = computed(() => content.value.find(e => e.path === decodeURIComponent(pathname)));
</script>

View File

@@ -1,36 +0,0 @@
<template>
<NuxtLink class="text-accent-blue inline-flex items-center" :to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: decodeURIComponent(hash) } : href" :class="class">
<HoverCard nuxt-client class="min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
<template #content>
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="decodeURIComponent(pathname)" :filter="hash.substring(1)" popover />
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="decodeURIComponent(pathname)" /></div></template>
</template>
<span>
<slot v-bind="$attrs"></slot>
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :icon="iconByType[overview.type]" />
</span>
</HoverCard>
</NuxtLink>
</template>
<script setup lang="ts">
import { parseURL } from 'ufo';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { iconByType } from '#shared/content.util';
const { href } = defineProps<{
href: string
class?: string
}>();
const { hash, pathname } = parseURL(href);
const { content } = useContent();
const overview = computed(() => content.value.find(e => e.path === decodeURIComponent(pathname)));
</script>
<style>
.cm-link {
@apply text-accent-blue inline-flex items-center cursor-pointer hover:text-opacity-85;
}
</style>

View File

@@ -1,28 +0,0 @@
<template>
<blockquote class="empty:before:hidden ps-4 my-4 relative before:absolute before:-top-1 before:-bottom-1 before:left-0 before:w-1 before:bg-light-30 dark:before:bg-dark-30" ref="el">
<slot />
</blockquote>
</template>
<style>
.HyperMD-quote
{
@apply before:hidden;
}
.HyperMD-quote.hmd-inactive-line
{
@apply before:block empty:before:!hidden !pb-2 !ps-4 !relative before:!absolute before:!-top-1 before:!-bottom-1 before:!left-0 before:!w-1 before:!bg-none before:!bg-light-30 dark:before:!bg-dark-30;
}
.HyperMD-quote.HyperMD-header
{
@apply before:!hidden;
}
.hmd-inactive-line .cm-formatting-quote
{
@apply !hidden;
}
.cm-quote
{
@apply text-light-100 dark:text-dark-100;
}
</style>

View File

@@ -1,45 +0,0 @@
<template>
<CollapsibleRoot :disabled="disabled" :defaultOpen="fold === true || fold === undefined" class="callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue" :data-type="type">
<CollapsibleTrigger>
<div :class="{ 'cursor-pointer': fold !== undefined }" class="flex flex-row items-center justify-start ps-2">
<Icon :icon="calloutIconByType[type] ?? defaultCalloutIcon" inline class="w-6 h-6 stroke-2 float-start me-2 flex-shrink-0" />
<span v-if="title" class="block font-bold text-start">{{ title }}</span>
<Icon icon="radix-icons:caret-right" v-if="fold !== undefined" class="transition-transform group-data-[state=open]:rotate-90 w-6 h-6 mx-6" />
</div>
</CollapsibleTrigger>
<CollapsibleContent class="overflow-hidden data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out] data-[state=closed]:h-0">
<div class="px-2">
<slot />
</div>
</CollapsibleContent>
</CollapsibleRoot>
</template>
<script lang="ts">
const calloutIconByType: Record<string, string> = {
note: 'radix-icons:pencil-1',
abstract: 'radix-icons:file-text',
info: 'radix-icons:info-circled',
todo: 'radix-icons:check-circled',
tip: 'radix-icons:star',
success: 'radix-icons:check',
question: 'radix-icons:question-mark-circled',
warning: 'radix-icons:exclamation-triangle',
failure: 'radix-icons:cross-circled',
danger: 'radix-icons:circle-backslash',
bug: 'solar:bug-linear',
example: 'radix-icons:list-bullet',
quote: 'radix-icons:quote',
};
const defaultCalloutIcon = 'radix-icons:info-circled';
</script>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
const { type, title, fold } = defineProps<{
type: string;
title?: string;
fold?: boolean;
}>();
const disabled = computed(() => fold === undefined);
</script>

View File

@@ -1,10 +0,0 @@
<template>
<code><slot /></code>
</template>
<style>
.cm-inline-code
{
@apply !border-none !bg-transparent !text-light-100 dark:!text-dark-100 !p-0;
}
</style>

View File

@@ -1,5 +0,0 @@
<template>
<em>
<slot />
</em>
</template>

View File

@@ -1,21 +0,0 @@
<template>
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2">
<slot />
</h1>
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.util';
const props = defineProps<{ id?: string }>()
</script>
<style>
.HyperMD-header-1
{
@apply text-5xl pt-4 pb-2 after:hidden;
}
.HyperMD-header-1 .cm-header
{
@apply font-thin;
}
</style>

View File

@@ -1,23 +0,0 @@
<template>
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2">
<slot />
</h2>
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.util';
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)
</script>
<style>
.HyperMD-header-2
{
@apply !text-4xl !pt-4 !pb-2 !ps-1 leading-loose after:hidden;
}
.HyperMD-header-2 .cm-header
{
@apply font-semibold;
}
</style>

View File

@@ -1,23 +0,0 @@
<template>
<h3 :id="parseId(id)" class="text-2xl font-bold mt-2 mb-4">
<slot />
</h3>
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.util';
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)
</script>
<style>
.HyperMD-header-3
{
@apply !text-2xl !font-bold !pt-1 after:!hidden;
}
.HyperMD-header-3 .cm-header
{
@apply font-bold;
}
</style>

View File

@@ -1,22 +0,0 @@
<template>
<h4 :id="parseId(id)" class="text-xl font-semibold my-2" style="font-variant: small-caps;">
<slot />
</h4>
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.util';
const props = defineProps<{ id?: string }>()
</script>
<style>
.HyperMD-header-4
{
@apply !text-xl font-semibold pt-1 after:hidden;
font-variant: small-caps;
}
.HyperMD-header-4 .cm-header
{
@apply font-semibold;
}
</style>

View File

@@ -1,12 +0,0 @@
<template>
<h5 :id="parseId(id)" class="text-lg font-semibold my-1">
<slot />
</h5>
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.util';
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)
</script>

View File

@@ -1,12 +0,0 @@
<template>
<h6 :id="parseId(id)">
<slot />
</h6>
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.util';
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)
</script>

View File

@@ -1,10 +0,0 @@
<template>
<Separator class="border-b border-light-35 dark:border-dark-35 m-4" />
</template>
<style>
.HyperMD-hr
{
@apply bg-light-35 dark:bg-dark-35 h-px;
}
</style>

View File

@@ -1,42 +0,0 @@
<template>
<img
:src="refinedSrc"
:alt="alt"
:width="width"
:height="height"
/>
</template>
<script setup lang="ts">
import { withTrailingSlash, withLeadingSlash, joinURL } from 'ufo'
import { useRuntimeConfig, computed, resolveComponent } from '#imports'
const props = defineProps({
src: {
type: String,
default: ''
},
alt: {
type: String,
default: ''
},
width: {
type: [String, Number],
default: undefined
},
height: {
type: [String, Number],
default: undefined
}
})
const refinedSrc = computed(() => {
if (props.src?.startsWith('/') && !props.src.startsWith('//')) {
const _base = withLeadingSlash(withTrailingSlash(useRuntimeConfig().app.baseURL))
if (_base !== '/' && !props.src.startsWith(_base)) {
return joinURL(_base, props.src)
}
}
return props.src
})
</script>

View File

@@ -1,22 +0,0 @@
<template>
<li class="before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4"><slot /></li>
</template>
<style>
.HyperMD-list-line
{
@apply !py-1;
}
.HyperMD-list-line.hmd-inactive-line > span
{
@apply before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4;
}
.hmd-inactive-line .cm-formatting-list
{
@apply hidden;
}
.cm-hmd-list-indent
{
@apply !hidden;
}
</style>

View File

@@ -1,5 +0,0 @@
<template>
<ol>
<slot />
</ol>
</template>

View File

@@ -1,3 +0,0 @@
<template>
<p><slot /></p>
</template>

View File

@@ -1,36 +0,0 @@
<template>
<pre :class="$props.class"><slot /></pre>
</template>
<script setup lang="ts">
defineProps({
code: {
type: String,
default: ''
},
language: {
type: String,
default: null
},
filename: {
type: String,
default: null
},
highlights: {
type: Array as () => number[],
default: () => []
},
meta: {
type: String,
default: null
},
class: {
type: String,
default: null
}
})
</script>
<style>
pre code .line{display:block}
</style>

View File

@@ -1,15 +0,0 @@
<template>
<div v-if="isDev">
Rendering the <code>script</code> element is dangerous and is disabled by default. Consider implementing your own <code>ProseScript</code> element to have control over script rendering.
</div>
</template>
<script setup lang="ts">
defineProps({
src: {
type: String,
default: ''
}
})
const isDev = import.meta.dev
</script>

View File

@@ -1,5 +0,0 @@
<template>
<small class="text-light-60 dark:text-dark-60 text-sm italic">
<slot />
</small>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<strong>
<slot />
</strong>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<table class="mx-4 my-8 border-collapse border border-light-35 dark:border-dark-35">
<slot />
</table>
</template>

View File

@@ -1,30 +0,0 @@
<template>
<HoverCard nuxt-client class="min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]">
<template #content>
<Markdown class="!px-6" path="tags" :filter="tag" popover />
</template>
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
<slot></slot>
</span>
</HoverCard>
</template>
<style>
.cm-hashtag.cm-hashtag-begin
{
@apply bg-accent-blue bg-opacity-10 text-accent-blue text-sm pb-0.5 ps-1 rounded-l-[12px] border border-r-0 border-accent-blue border-opacity-30;
}
.cm-hashtag.cm-hashtag-end
{
@apply bg-accent-blue bg-opacity-10 text-accent-blue text-sm pb-0.5 pe-1 rounded-r-[12px] !rounded-se-none border border-l-0 border-accent-blue border-opacity-30;
}
</style>
<script setup lang="ts">
const { tag } = defineProps<{
tag: string
}>();
const { content } = useContent();
const overview = computed(() => content.value.find(e => e.path === "tags"));
</script>

View File

@@ -1,5 +0,0 @@
<template>
<tbody>
<slot />
</tbody>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<td class="border border-light-35 dark:border-dark-35 py-1 px-2">
<slot />
</td>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<th class="border border-light-35 dark:border-dark-35 px-4 first:pt-0">
<slot />
</th>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<thead>
<slot />
</thead>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<tr>
<slot />
</tr>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<ul>
<slot />
</ul>
</template>

View File

@@ -2,7 +2,9 @@ import { Database } from "bun:sqlite";
import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite"; import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite";
import * as schema from '../db/schema'; import * as schema from '../db/schema';
let instance: BunSQLiteDatabase<typeof schema>; let instance: BunSQLiteDatabase<typeof schema> & {
$client: Database;
};
export default function useDatabase() export default function useDatabase()
{ {
if(!instance) if(!instance)
@@ -13,6 +15,7 @@ export default function useDatabase()
instance.run("PRAGMA journal_mode = WAL;"); instance.run("PRAGMA journal_mode = WAL;");
instance.run("PRAGMA foreign_keys = true;"); instance.run("PRAGMA foreign_keys = true;");
instance.run("PRAGMA optimize=0x10002;");
} }
return instance; return instance;

View File

@@ -7,11 +7,14 @@ import RemarkOfm from 'remark-ofm';
import RemarkGfm from 'remark-gfm'; import RemarkGfm from 'remark-gfm';
import RemarkBreaks from 'remark-breaks'; import RemarkBreaks from 'remark-breaks';
import RemarkFrontmatter from 'remark-frontmatter'; import RemarkFrontmatter from 'remark-frontmatter';
import StripMarkdown from 'strip-markdown';
import RemarkStringify from 'remark-stringify';
interface Parser interface Parser
{ {
parse: (md: string) => Promise<Root>; parse: (md: string) => Promise<Root>;
parseSync: (md: string) => Root parseSync: (md: string) => Root;
text: (md: string) => string;
} }
export default function useMarkdown(): Parser export default function useMarkdown(): Parser
{ {
@@ -39,5 +42,17 @@ export default function useMarkdown(): Parser
return processed; return processed;
} }
return { parse, parseSync }; const text = (markdown: string) => {
if (!processor)
{
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter ]);
processor.use(StripMarkdown, { remove: [ 'comment', 'tag', 'callout' ] });
processor.use(RemarkStringify);
}
const processed = processor.processSync(markdown);
return String(processed);
}
return { parse, parseSync, text };
} }

View File

@@ -181,7 +181,7 @@ export const _useShortcuts = () => {
return false return false
}) })
onMounted(() => { tryOnMounted(() => {
metaSymbol.value = macOS.value ? '⌘' : 'Ctrl' metaSymbol.value = macOS.value ? '⌘' : 'Ctrl'
}) })

View File

@@ -1,40 +0,0 @@
export interface ToastConfig
{
closeable?: boolean
duration: number
title?: string
content?: string
timer?: boolean
type?: ToastType
}
export type ToastType = 'info' | 'success' | 'error';
export type ExtraToastConfig = ToastConfig & { id: string, state: boolean };
let id = 0;
const [provideToaster, useToast] = createInjectionState(() => {
const list = ref<ExtraToastConfig[]>([]);
function add(config: ToastConfig)
{
list.value.push({ ...config, id: (++id).toString(), state: true, });
}
function clear(type?: ToastType)
{
list.value.forEach(e => { if(e.type !== type) { e.state = false; } });
}
return { list, add, clear }
}, { injectionKey: Symbol('toaster') });
export { provideToaster, useToastWithDefault as useToast };
function useToastWithDefault()
{
const toasts = useToast();
if(!toasts)
{
return { list: ref<ExtraToastConfig[]>([]), add: () => {}, clear: () => {} };
}
return toasts;
}

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,5 @@
import { relations } from 'drizzle-orm'; import { relations } from 'drizzle-orm';
import { int, text, sqliteTable as table, primaryKey, blob } from 'drizzle-orm/sqlite-core'; import { int, text, sqliteTable as table, primaryKey, blob } from 'drizzle-orm/sqlite-core';
import { ABILITIES, MAIN_STATS } from '../shared/character.util';
export const usersTable = table("users", { export const usersTable = table("users", {
id: int().primaryKey({ autoIncrement: true }), id: int().primaryKey({ autoIncrement: true }),
@@ -53,12 +52,12 @@ export const characterTable = table("character", {
id: int().primaryKey({ autoIncrement: true }), id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(), name: text().notNull(),
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
people: int().notNull(), people: text().notNull(),
level: int().notNull().default(1), level: int().notNull().default(1),
variables: text({ mode: 'json' }).notNull().default('{"health": 0,"mana": 0,"spells": [],"items": [],"exhaustion": 0,"sickness": [],"poisons": []}'),
aspect: int(), aspect: int(),
notes: text(), public_notes: text(),
health: int().notNull().default(0), private_notes: text(),
mana: int().notNull().default(0),
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'), visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
thumbnail: blob(), thumbnail: blob(),
@@ -66,7 +65,7 @@ export const characterTable = table("character", {
export const characterTrainingTable = table("character_training", { export const characterTrainingTable = table("character_training", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
stat: text({ enum: MAIN_STATS }).notNull(), stat: text({ enum: ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] }).notNull(),
level: int().notNull(), level: int().notNull(),
choice: int().notNull(), choice: int().notNull(),
}, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]); }, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]);
@@ -79,21 +78,16 @@ export const characterLevelingTable = table("character_leveling", {
export const characterAbilitiesTable = table("character_abilities", { export const characterAbilitiesTable = table("character_abilities", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
ability: text({ enum: ABILITIES }).notNull(), ability: text({ enum: ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] }).notNull(),
value: int().notNull().default(0), value: int().notNull().default(0),
max: int().notNull().default(0), max: int().notNull().default(0),
}, (table) => [primaryKey({ columns: [table.character, table.ability] })]); }, (table) => [primaryKey({ columns: [table.character, table.ability] })]);
export const characterModifiersTable = table("character_modifiers", { export const characterChoicesTable = table("character_choices", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
modifier: text({ enum: MAIN_STATS }).notNull(), id: text().notNull(),
value: int().notNull().default(0), choice: int().notNull(),
}, (table) => [primaryKey({ columns: [table.character, table.modifier] })]); }, (table) => [primaryKey({ columns: [table.character, table.id, table.choice] })]);
export const characterSpellsTable = table("character_spell", {
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
value: text().notNull(),
}, (table) => [primaryKey({ columns: [table.character, table.value] })]);
export const usersRelation = relations(usersTable, ({ one, many }) => ({ export const usersRelation = relations(usersTable, ({ one, many }) => ({
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }), data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
@@ -118,8 +112,7 @@ export const characterRelation = relations(characterTable, ({ one, many }) => ({
training: many(characterTrainingTable), training: many(characterTrainingTable),
levels: many(characterLevelingTable), levels: many(characterLevelingTable),
abilities: many(characterAbilitiesTable), abilities: many(characterAbilitiesTable),
modifiers: many(characterModifiersTable), choices: many(characterChoicesTable)
spells: many(characterSpellsTable)
})); }));
export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({ export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({
@@ -131,9 +124,6 @@ export const characterLevelingRelation = relations(characterLevelingTable, ({ on
export const characterAbilitiesRelation = relations(characterAbilitiesTable, ({ one }) => ({ export const characterAbilitiesRelation = relations(characterAbilitiesTable, ({ one }) => ({
character: one(characterTable, { fields: [characterAbilitiesTable.character], references: [characterTable.id] }) character: one(characterTable, { fields: [characterAbilitiesTable.character], references: [characterTable.id] })
})); }));
export const characterModifierRelation = relations(characterModifiersTable, ({ one }) => ({ export const characterChoicesRelation = relations(characterChoicesTable, ({ one }) => ({
character: one(characterTable, { fields: [characterModifiersTable.character], references: [characterTable.id] }) character: one(characterTable, { fields: [characterChoicesTable.character], references: [characterTable.id] })
}));
export const characterSpellsRelation = relations(characterSpellsTable, ({ one }) => ({
character: one(characterTable, { fields: [characterSpellsTable.character], references: [characterTable.id] })
})); }));

View File

@@ -0,0 +1,7 @@
CREATE TABLE `character_choices` (
`character` integer NOT NULL,
`id` text NOT NULL,
`choice` integer NOT NULL,
PRIMARY KEY(`character`, `id`, `choice`),
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
);

View File

@@ -0,0 +1,20 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_character` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`owner` integer NOT NULL,
`people` text NOT NULL,
`level` integer DEFAULT 1 NOT NULL,
`aspect` integer,
`notes` text,
`health` integer DEFAULT 0 NOT NULL,
`mana` integer DEFAULT 0 NOT NULL,
`visibility` text DEFAULT 'private' NOT NULL,
`thumbnail` blob,
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
INSERT INTO `__new_character`("id", "name", "owner", "people", "level", "aspect", "notes", "health", "mana", "visibility", "thumbnail") SELECT "id", "name", "owner", "people", "level", "aspect", "notes", "health", "mana", "visibility", "thumbnail" FROM `character`;--> statement-breakpoint
DROP TABLE `character`;--> statement-breakpoint
ALTER TABLE `__new_character` RENAME TO `character`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,5 @@
DROP TABLE `character_modifiers`;--> statement-breakpoint
DROP TABLE `character_spell`;--> statement-breakpoint
ALTER TABLE `character` ADD `variables` text DEFAULT '{"health": 0,"mana": 0,"spells": [],"equipment": [],"exhaustion": 0,"sickness": []}' NOT NULL;--> statement-breakpoint
ALTER TABLE `character` DROP COLUMN `health`;--> statement-breakpoint
ALTER TABLE `character` DROP COLUMN `mana`;

View File

@@ -0,0 +1,20 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_character` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`owner` integer NOT NULL,
`people` text NOT NULL,
`level` integer DEFAULT 1 NOT NULL,
`variables` text DEFAULT '{"health": 0,"mana": 0,"spells": [],"items": [],"exhaustion": 0,"sickness": [],"poisons": []}' NOT NULL,
`aspect` integer,
`public_notes` text,
`private_notes` text,
`visibility` text DEFAULT 'private' NOT NULL,
`thumbnail` blob,
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
);
--> statement-breakpoint
INSERT INTO `__new_character`("id", "name", "owner", "people", "level", "variables", "aspect", "public_notes", "private_notes", "visibility", "thumbnail") SELECT "id", "name", "owner", "people", "level", "variables", "aspect", "public_notes", "private_notes", "visibility", "thumbnail" FROM `character`;--> statement-breakpoint
DROP TABLE `character`;--> statement-breakpoint
ALTER TABLE `__new_character` RENAME TO `character`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,810 @@
{
"version": "6",
"dialect": "sqlite",
"id": "8f89d284-71da-46ae-a282-538f8a901294",
"prevId": "854c13bd-59bb-40bd-a046-69632b59557e",
"tables": {
"character_abilities": {
"name": "character_abilities",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ability": {
"name": "ability",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"max": {
"name": "max",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"character_abilities_character_character_id_fk": {
"name": "character_abilities_character_character_id_fk",
"tableFrom": "character_abilities",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_abilities_character_ability_pk": {
"columns": [
"character",
"ability"
],
"name": "character_abilities_character_ability_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_choices": {
"name": "character_choices",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_choices_character_character_id_fk": {
"name": "character_choices_character_character_id_fk",
"tableFrom": "character_choices",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_choices_character_id_choice_pk": {
"columns": [
"character",
"id",
"choice"
],
"name": "character_choices_character_id_choice_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_leveling": {
"name": "character_leveling",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_leveling_character_character_id_fk": {
"name": "character_leveling_character_character_id_fk",
"tableFrom": "character_leveling",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_leveling_character_level_pk": {
"columns": [
"character",
"level"
],
"name": "character_leveling_character_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_modifiers": {
"name": "character_modifiers",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"modifier": {
"name": "modifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"character_modifiers_character_character_id_fk": {
"name": "character_modifiers_character_character_id_fk",
"tableFrom": "character_modifiers",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_modifiers_character_modifier_pk": {
"columns": [
"character",
"modifier"
],
"name": "character_modifiers_character_modifier_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_spell": {
"name": "character_spell",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_spell_character_character_id_fk": {
"name": "character_spell_character_character_id_fk",
"tableFrom": "character_spell",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_spell_character_value_pk": {
"columns": [
"character",
"value"
],
"name": "character_spell_character_value_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character": {
"name": "character",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"people": {
"name": "people",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"aspect": {
"name": "aspect",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"health": {
"name": "health",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"mana": {
"name": "mana",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'private'"
},
"thumbnail": {
"name": "thumbnail",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_owner_users_id_fk": {
"name": "character_owner_users_id_fk",
"tableFrom": "character",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_training": {
"name": "character_training",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"stat": {
"name": "stat",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_training_character_character_id_fk": {
"name": "character_training_character_character_id_fk",
"tableFrom": "character_training",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_training_character_stat_level_pk": {
"columns": [
"character",
"stat",
"level"
],
"name": "character_training_character_stat_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"email_validation": {
"name": "email_validation",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_content": {
"name": "project_content",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_files": {
"name": "project_files",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"project_files_path_unique": {
"name": "project_files_path_unique",
"columns": [
"path"
],
"isUnique": true
}
},
"foreignKeys": {
"project_files_owner_users_id_fk": {
"name": "project_files_owner_users_id_fk",
"tableFrom": "project_files",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,810 @@
{
"version": "6",
"dialect": "sqlite",
"id": "6651137c-a198-4538-86be-7cb8b88ca998",
"prevId": "8f89d284-71da-46ae-a282-538f8a901294",
"tables": {
"character_abilities": {
"name": "character_abilities",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ability": {
"name": "ability",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"max": {
"name": "max",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"character_abilities_character_character_id_fk": {
"name": "character_abilities_character_character_id_fk",
"tableFrom": "character_abilities",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_abilities_character_ability_pk": {
"columns": [
"character",
"ability"
],
"name": "character_abilities_character_ability_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_choices": {
"name": "character_choices",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_choices_character_character_id_fk": {
"name": "character_choices_character_character_id_fk",
"tableFrom": "character_choices",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_choices_character_id_choice_pk": {
"columns": [
"character",
"id",
"choice"
],
"name": "character_choices_character_id_choice_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_leveling": {
"name": "character_leveling",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_leveling_character_character_id_fk": {
"name": "character_leveling_character_character_id_fk",
"tableFrom": "character_leveling",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_leveling_character_level_pk": {
"columns": [
"character",
"level"
],
"name": "character_leveling_character_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_modifiers": {
"name": "character_modifiers",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"modifier": {
"name": "modifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"character_modifiers_character_character_id_fk": {
"name": "character_modifiers_character_character_id_fk",
"tableFrom": "character_modifiers",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_modifiers_character_modifier_pk": {
"columns": [
"character",
"modifier"
],
"name": "character_modifiers_character_modifier_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_spell": {
"name": "character_spell",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_spell_character_character_id_fk": {
"name": "character_spell_character_character_id_fk",
"tableFrom": "character_spell",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_spell_character_value_pk": {
"columns": [
"character",
"value"
],
"name": "character_spell_character_value_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character": {
"name": "character",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"people": {
"name": "people",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"aspect": {
"name": "aspect",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"health": {
"name": "health",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"mana": {
"name": "mana",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'private'"
},
"thumbnail": {
"name": "thumbnail",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_owner_users_id_fk": {
"name": "character_owner_users_id_fk",
"tableFrom": "character",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_training": {
"name": "character_training",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"stat": {
"name": "stat",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_training_character_character_id_fk": {
"name": "character_training_character_character_id_fk",
"tableFrom": "character_training",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_training_character_stat_level_pk": {
"columns": [
"character",
"stat",
"level"
],
"name": "character_training_character_stat_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"email_validation": {
"name": "email_validation",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_content": {
"name": "project_content",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_files": {
"name": "project_files",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"project_files_path_unique": {
"name": "project_files_path_unique",
"columns": [
"path"
],
"isUnique": true
}
},
"foreignKeys": {
"project_files_owner_users_id_fk": {
"name": "project_files_owner_users_id_fk",
"tableFrom": "project_files",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,702 @@
{
"version": "6",
"dialect": "sqlite",
"id": "05b549e7-5b3f-40f4-9461-05db59391e20",
"prevId": "6651137c-a198-4538-86be-7cb8b88ca998",
"tables": {
"character_abilities": {
"name": "character_abilities",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ability": {
"name": "ability",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"max": {
"name": "max",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"character_abilities_character_character_id_fk": {
"name": "character_abilities_character_character_id_fk",
"tableFrom": "character_abilities",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_abilities_character_ability_pk": {
"columns": [
"character",
"ability"
],
"name": "character_abilities_character_ability_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_choices": {
"name": "character_choices",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_choices_character_character_id_fk": {
"name": "character_choices_character_character_id_fk",
"tableFrom": "character_choices",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_choices_character_id_choice_pk": {
"columns": [
"character",
"id",
"choice"
],
"name": "character_choices_character_id_choice_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_leveling": {
"name": "character_leveling",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_leveling_character_character_id_fk": {
"name": "character_leveling_character_character_id_fk",
"tableFrom": "character_leveling",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_leveling_character_level_pk": {
"columns": [
"character",
"level"
],
"name": "character_leveling_character_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character": {
"name": "character",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"people": {
"name": "people",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"variables": {
"name": "variables",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'{}'"
},
"aspect": {
"name": "aspect",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'private'"
},
"thumbnail": {
"name": "thumbnail",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_owner_users_id_fk": {
"name": "character_owner_users_id_fk",
"tableFrom": "character",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_training": {
"name": "character_training",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"stat": {
"name": "stat",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_training_character_character_id_fk": {
"name": "character_training_character_character_id_fk",
"tableFrom": "character_training",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_training_character_stat_level_pk": {
"columns": [
"character",
"stat",
"level"
],
"name": "character_training_character_stat_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"email_validation": {
"name": "email_validation",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_content": {
"name": "project_content",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_files": {
"name": "project_files",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"project_files_path_unique": {
"name": "project_files_path_unique",
"columns": [
"path"
],
"isUnique": true
}
},
"foreignKeys": {
"project_files_owner_users_id_fk": {
"name": "project_files_owner_users_id_fk",
"tableFrom": "project_files",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,711 @@
{
"version": "6",
"dialect": "sqlite",
"id": "153969ef-bcdb-4bbd-bd57-01fbd8004fc6",
"prevId": "05b549e7-5b3f-40f4-9461-05db59391e20",
"tables": {
"character_abilities": {
"name": "character_abilities",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ability": {
"name": "ability",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"max": {
"name": "max",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"character_abilities_character_character_id_fk": {
"name": "character_abilities_character_character_id_fk",
"tableFrom": "character_abilities",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_abilities_character_ability_pk": {
"columns": [
"character",
"ability"
],
"name": "character_abilities_character_ability_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_choices": {
"name": "character_choices",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_choices_character_character_id_fk": {
"name": "character_choices_character_character_id_fk",
"tableFrom": "character_choices",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_choices_character_id_choice_pk": {
"columns": [
"character",
"id",
"choice"
],
"name": "character_choices_character_id_choice_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_leveling": {
"name": "character_leveling",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_leveling_character_character_id_fk": {
"name": "character_leveling_character_character_id_fk",
"tableFrom": "character_leveling",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_leveling_character_level_pk": {
"columns": [
"character",
"level"
],
"name": "character_leveling_character_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character": {
"name": "character",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"people": {
"name": "people",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"variables": {
"name": "variables",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'{\"health\": 0,\"mana\": 0,\"spells\": [],\"items\": [],\"exhaustion\": 0,\"sickness\": [],\"poisons\": []}'"
},
"aspect": {
"name": "aspect",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"public_notes": {
"name": "public_notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"private_notes": {
"name": "private_notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'private'"
},
"thumbnail": {
"name": "thumbnail",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_owner_users_id_fk": {
"name": "character_owner_users_id_fk",
"tableFrom": "character",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"character_training": {
"name": "character_training",
"columns": {
"character": {
"name": "character",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"stat": {
"name": "stat",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"level": {
"name": "level",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"choice": {
"name": "choice",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"character_training_character_character_id_fk": {
"name": "character_training_character_character_id_fk",
"tableFrom": "character_training",
"tableTo": "character",
"columnsFrom": [
"character"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"character_training_character_stat_level_pk": {
"columns": [
"character",
"stat",
"level"
],
"name": "character_training_character_stat_level_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"email_validation": {
"name": "email_validation",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_content": {
"name": "project_content",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"project_files": {
"name": "project_files",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"project_files_path_unique": {
"name": "project_files_path_unique",
"columns": [
"path"
],
"isUnique": true
}
},
"foreignKeys": {
"project_files_owner_users_id_fk": {
"name": "project_files_owner_users_id_fk",
"tableFrom": "project_files",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lastTimestamp": {
"name": "lastTimestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {
"\"character\".\"notes\"": "\"character\".\"public_notes\""
}
},
"internal": {
"indexes": {}
}
}

View File

@@ -99,6 +99,34 @@
"when": 1753097020642, "when": 1753097020642,
"tag": "0013_wakeful_lake", "tag": "0013_wakeful_lake",
"breakpoints": true "breakpoints": true
},
{
"idx": 14,
"version": "6",
"when": 1753175811770,
"tag": "0014_careless_nick_fury",
"breakpoints": true
},
{
"idx": 15,
"version": "6",
"when": 1756214160038,
"tag": "0015_typical_blade",
"breakpoints": true
},
{
"idx": 16,
"version": "6",
"when": 1756221197092,
"tag": "0016_wild_the_anarchist",
"breakpoints": true
},
{
"idx": 17,
"version": "6",
"when": 1760531331328,
"tag": "0017_workable_scrambler",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,35 +1,40 @@
<template> <template>
<CollapsibleRoot class="flex flex-1 flex-col" v-model:open="open"> <div class="flex flex-row w-full max-w-full h-full max-h-full" style="--sidebar-width: 300px">
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2"> <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">
<div class="flex items-center px-2 gap-4"> <NuxtLink class="flex flex-row items-center justify-center group gap-2 my-2" aria-label="Accueil" :to="{ name: 'index', force: true }">
<CollapsibleTrigger asChild>
<Button icon class="!bg-transparent group md:hidden">
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
</Button>
</CollapsibleTrigger>
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-opacity-70 m-2 flex items-center gap-4" aria-label="Accueil" :to="{ path: '/', force: true }">
<Avatar src="/logo.dark.svg" class="dark:block hidden" /> <Avatar src="/logo.dark.svg" class="dark:block hidden" />
<Avatar src="/logo.light.svg" class="block dark:hidden" /> <Avatar src="/logo.light.svg" class="block dark:hidden" />
<span class="text-xl max-md:hidden">d[any]</span> <span class="text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70">d[any]</span>
</NuxtLink> </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>
<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"> <NavigationMenuRoot class="relative">
<NavigationMenuList class="flex items-center gap-8 max-md:hidden"> <NavigationMenuList class="flex items-center gap-8 max-md:hidden">
<NavigationMenuItem> <NavigationMenuItem>
<NavigationMenuTrigger> <NavigationMenuTrigger>
<NuxtLink :href="{ name: 'character' }" class="text-light-70 dark:text-dark-70" active-class="!text-accent-blue"><span class="pl-3 py-1 flex-1 truncate">Personnages</span></NuxtLink> <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> </NavigationMenuTrigger>
<NavigationMenuContent class="absolute top-0 left-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30"> <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="text-light-70 dark:text-dark-70" active-class="!text-accent-blue"><span class="py-2 px-3 flex-1 truncate">Tous les personnages</span></NuxtLink> <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> </NavigationMenuContent>
</NavigationMenuItem> </NavigationMenuItem>
</NavigationMenuList> </NavigationMenuList>
<div class="absolute top-full left-0 flex w-full justify-center my-4"> <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] overflow-hidden rounded-[10px] bg-white transition-[width,_height] duration-300 sm:w-[var(--radix-navigation-menu-viewport-width)]" /> <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> </div>
</NavigationMenuRoot> </NavigationMenuRoot>
<div class="flex items-center px-2 gap-4"> <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"> <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" :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> <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>
@@ -39,48 +44,22 @@
</template> </template>
</div> </div>
</div> </div>
<div class="flex flex-1 flex-row relative h-screen w-screen overflow-hidden">
<!-- <CollapsibleContent asChild forceMount> -->
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="treeParent">
<div v-if="user" class="flex flex-1 py-4 px-2 flex-row flex-1 justify-between items-center">
<NuxtLink v-if="hasPermissions(user.permissions, ['admin', 'editor'])" :to="{ name: 'explore-edit' }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink>
</div>
</div>
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60">
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
<p>Copyright Peaceultime - 2025</p>
</div>
</div>
<!-- </CollapsibleContent> -->
<slot></slot> <slot></slot>
</div> </div>
</CollapsibleRoot> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import type { DropdownOption } from '~/components/base/DropdownMenu.vue';
import { hasPermissions } from '#shared/auth.util';
import { TreeDOM } from '#shared/tree'; import { TreeDOM } from '#shared/tree';
import { Content, iconByType } from '#shared/content.util'; import { Content, iconByType } from '#shared/content.util';
import { dom, icon, text } from '#shared/dom.util'; import { dom, icon } from '#shared/dom.util';
import { unifySlug } from '#shared/general.util'; import { unifySlug } from '#shared/general.util';
import { popper } from '#shared/floating.util'; import { tooltip } from '#shared/floating.util';
import { link } from '#shared/proses'; import { link } from '#shared/components.util';
const options = ref<DropdownOption[]>([{
type: 'item',
label: 'Mon profil',
select: () => useRouter().push({ name: 'user-profile' }),
}, {
type: 'item',
label: 'Deconnexion',
select: () => clear(),
}]);
const open = ref(false); const open = ref(false);
const { loggedIn, user, clear } = useUserSession(); const { loggedIn, user } = useUserSession();
const { fetch } = useContent(); const { fetch } = useContent();
await fetch(false); await fetch(false);
@@ -90,26 +69,26 @@ const path = computed(() => route.value.params.path ? decodeURIComponent(unifySl
await Content.init(); await Content.init();
const tree = new TreeDOM((item, depth) => { const tree = new TreeDOM((item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [ return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }), icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 1.5 - 1}em` } }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }), dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
item.private ? popper(dom('span', { class: 'flex' }, [icon('radix-icons:lock-closed', { class: 'mx-1' })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50' }) : undefined, item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
])]); ])]);
}, (item, depth) => { }, (item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [link({ 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, [ return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [link([
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }), 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 } }), dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
item.private ? popper(dom('span', { class: 'flex' }, [icon('radix-icons:lock-closed', { class: 'mx-1' })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50' }) : undefined, 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 )]);
}, (item) => item.navigable); }, (item) => item.navigable);
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(e, true)); (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 treeParent = useTemplateRef('treeParent');
const unmount = useRouter().afterEach((to, from, failure) => { const unmount = useRouter().afterEach((to, from, failure) => {
if(failure) if(failure)
return; return;
to.name === 'explore-path' && (unifySlug(to.params.path).split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(e, true)); 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));
}); });
watch(route, () => { watch(route, () => {
@@ -118,9 +97,7 @@ watch(route, () => {
onMounted(() => { onMounted(() => {
if(treeParent.value) if(treeParent.value)
{
treeParent.value.appendChild(tree.container); treeParent.value.appendChild(tree.container);
}
}) })
onUnmounted(() => { onUnmounted(() => {
unmount(); unmount();

View File

@@ -120,7 +120,6 @@ export default defineNuxtConfig({
pageTransition: false, pageTransition: false,
layoutTransition: false layoutTransition: false
}, },
ssr: false,
components: [ components: [
{ {
path: '~/components', path: '~/components',
@@ -144,6 +143,7 @@ export default defineNuxtConfig({
}, },
runtimeConfig: { runtimeConfig: {
session: { session: {
maxAge: 60*60*24*31,
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013', password: '699c46bd-9aaa-4364-ad01-510ee4fe7013',
}, },
database: 'db.sqlite', database: 'db.sqlite',

View File

@@ -44,6 +44,7 @@
"remark-rehype": "^11.1.2", "remark-rehype": "^11.1.2",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-vue": "^6.0.0", "rollup-plugin-vue": "^6.0.0",
"strip-markdown": "^6.0.0",
"unified": "^11.0.5", "unified": "^11.0.5",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vue": "^3.5.17", "vue": "^3.5.17",

View File

@@ -31,9 +31,10 @@
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { format } from '~/shared/general.util'; import { format } from '#shared/general.util';
import { iconByType } from '~/shared/content.util'; import { iconByType } from '#shared/content.util';
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import { Toaster } from '#shared/components.util';
interface File interface File
{ {
@@ -69,8 +70,6 @@ definePageMeta({
rights: ['admin'], rights: ['admin'],
}); });
const toaster = useToast();
const { data: users } = useFetch('/api/admin/users', { const { data: users } = useFetch('/api/admin/users', {
transform: (users) => { transform: (users) => {
//@ts-ignore //@ts-ignore
@@ -125,13 +124,13 @@ async function editPermissions(user: User)
body: permissionCopy.value, body: permissionCopy.value,
}); });
user.permission = permissionCopy.value; user.permission = permissionCopy.value;
toaster.add({ Toaster.add({
duration: 10000, type: 'success', content: 'Permissions mises à jour.', timer: true, duration: 10000, type: 'success', content: 'Permissions mises à jour.', timer: true,
}); });
} }
catch(e) catch(e)
{ {
toaster.add({ Toaster.add({
duration: 10000, type: 'error', content: (e as any).message, timer: true, duration: 10000, type: 'error', content: (e as any).message, timer: true,
}); });
} }
@@ -146,13 +145,13 @@ async function logout(user: User)
user.session.length = 0; user.session.length = 0;
toaster.add({ Toaster.add({
duration: 10000, type: 'success', content: 'L\'utilisateur vient d\'être déconnecté.', timer: true, duration: 10000, type: 'success', content: 'L\'utilisateur vient d\'être déconnecté.', timer: true,
}); });
} }
catch(e) catch(e)
{ {
toaster.add({ Toaster.add({
duration: 10000, type: 'error', content: (e as any).message, timer: true, duration: 10000, type: 'error', content: (e as any).message, timer: true,
}); });
} }
@@ -165,8 +164,8 @@ async function logout(user: User)
</Head> </Head>
<div class="flex flex-1 flex-col p-4"> <div class="flex flex-1 flex-col p-4">
<div class="flex flex-row justify-between items-center"> <div class="flex flex-row justify-between items-center">
<ProseH2 class="text-center flex-1">Administration</ProseH2> <h2 class="text-center flex-1 text-2xl font-bold">Administration</h2>
<Button><NuxtLink :to="{ name: 'admin-jobs' }">Jobs</NuxtLink></Button> <NuxtLink :to="{ name: 'admin-jobs' }"><Button>Jobs</Button></NuxtLink>
</div> </div>
<div class="flex flex-1 w-full justify-center items-stretch flex-row gap-4"> <div class="flex flex-1 w-full justify-center items-stretch flex-row gap-4">
<div class="flex-1"> <div class="flex-1">

View File

@@ -13,15 +13,15 @@ const schemaList: Record<string, z.ZodObject<any> | null> = {
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { z } from 'zod'; import { z } from 'zod/v4';
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import { Toaster } from '#shared/components.util';
definePageMeta({ definePageMeta({
rights: ['admin'], rights: ['admin'],
}) })
const job = ref<string>(''); const job = ref<string>('');
const toaster = useToast();
const payload = reactive<Record<string, any>>({ const payload = reactive<Record<string, any>>({
data: JSON.stringify({ username: "Peaceultime", id: 1, timestamp: Date.now() }), data: JSON.stringify({ username: "Peaceultime", id: 1, timestamp: Date.now() }),
to: 'clem31470@gmail.com', to: 'clem31470@gmail.com',
@@ -51,7 +51,7 @@ async function fetch()
error.value = null; error.value = null;
success.value = true; success.value = true;
toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, }); Toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, });
} }
catch(e) catch(e)
{ {
@@ -59,7 +59,7 @@ async function fetch()
error.value = e as Error; error.value = e as Error;
success.value = false; success.value = false;
toaster.add({ duration: 10000, content: error.value.message, type: 'error', timer: true, }); Toaster.add({ duration: 10000, content: error.value.message, type: 'error', timer: true, });
} }
} }
</script> </script>
@@ -71,7 +71,7 @@ async function fetch()
<div class="flex flex-col justify-start items-center p-4"> <div class="flex flex-col justify-start items-center p-4">
<div class="flex flex-row justify-between items-center gap-8"> <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> <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>
<ProseH2 class="text-center flex-1">Administration</ProseH2> <h2 class="text-center flex-1 text-2xl font-bold">Administration</h2>
</div> </div>
<div class="flex flex-row w-full gap-8"> <div class="flex flex-row w-full gap-8">
<Select label="Job" v-model="job"> <Select label="Job" v-model="job">

View File

@@ -16,7 +16,7 @@ onMounted(() => {
useShortcuts({ useShortcuts({
"Meta_S": () => builder.save(false), "Meta_S": () => builder.save(false),
}) });
} }
}); });
}) })

View File

@@ -1,19 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import characterConfig from '#shared/character-config.json'; import characterConfig from '#shared/character-config.json';
import { Icon } from '@iconify/vue/dist/iconify.js'; import { unifySlug } from '#shared/general.util';
import PreviewA from '~/components/prose/PreviewA.vue';
import { clamp } from '#shared/general.util';
import type { SpellConfig } from '~/types/character';
import type { CharacterConfig } from '~/types/character'; import type { CharacterConfig } from '~/types/character';
import { CharacterSheet } from '#shared/character.util';
const config = characterConfig as CharacterConfig;
const id = useRouter().currentRoute.value.params.id;
const { user } = useUserSession();
const { add } = useToast();
const { data: character, status, error } = await useFetch(`/api/character/${id}/compiled`);
/* /*
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue
@@ -25,169 +14,24 @@ text-light-green dark:text-dark-green border-light-green dark:border-dark-green
text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow
text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple
*/ */
const config = characterConfig as CharacterConfig;
const id = useRouter().currentRoute.value.params.id ? unifySlug(useRouter().currentRoute.value.params.id!) : undefined;
const { user } = useUserSession();
const container = useTemplateRef('container');
onMounted(() => {
queueMicrotask(() => {
if(container.value && id)
{
const character = new CharacterSheet(id, user);
container.value.replaceWith(character.container);
}
});
});
</script> </script>
<template> <template>
<div v-if="status === 'pending'"> <div class="flex flex-1 w-full h-full items-start justify-center" ref="container"></div>
<Head>
<Title>d[any] - Chargement ...</Title>
</Head>
</div>
<div v-else-if="status === 'success' && character && !error">
<Head>
<Title>d[any] - {{ character.name }}</Title>
</Head>
<div class="flex flex-row gap-4 justify-between">
<div></div>
<div class="flex lg:flex-row flex-col gap-6 items-center justify-center">
<div class="flex gap-6 items-center">
<Avatar src="" icon="radix-icons:person" size="large" />
<div class="flex flex-col">
<span class="text-xl font-bold">{{ character.name }}</span>
<span class="text-sm">De {{ character.username }}</span>
</div>
<div class="flex flex-col">
<span class="font-bold">Niveau {{ character.level }}</span>
<span>{{ character.race === -1 ? "Race inconnue" : characterConfig.peoples[character.race].name }}</span>
</div>
</div>
<div class="flex flex-col lg:border-l border-light-30 dark:border-dark-30 py-4 ps-4">
<span class="flex flex-row items-center gap-2">PV: {{ character.health - character.values.health }}/{{ character.health }}</span>
<span class="flex flex-row items-center gap-2">Mana: {{ character.mana - character.values.mana }}/{{ character.mana }}</span>
</div>
</div>
<div class="self-center">
<Tooltip side="right" message="Modifier" v-if="user && user.id === character.owner"><NuxtLink :to="{ name: 'character-id-edit', params: { id: character.id } }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink></Tooltip>
</div>
</div>
<div class="flex flex-1 flex-col justify-center gap-4 *:py-2">
<div class="grid 2xl:grid-cols-10 grid-cols-1 gap-4 items-center border-b border-light-30 dark:border-dark-30 me-4 pe-4">
<div class="flex relative justify-between ps-4 gap-2 2xl:col-span-6">
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.strength }}</span><span class="text-sm 2xl:text-base">Force</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.dexterity }}</span><span class="text-sm 2xl:text-base">Dextérité</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.constitution }}</span><span class="text-sm 2xl:text-base">Constitution</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.intelligence }}</span><span class="text-sm 2xl:text-base">Intelligence</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.curiosity }}</span><span class="text-sm 2xl:text-base">Curiosité</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.charisma }}</span><span class="text-sm 2xl:text-base">Charisme</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.psyche }}</span><span class="text-sm 2xl:text-base">Psyché</span></div>
</div>
<div class="flex flex-1 relative 2xl:border-l border-light-30 dark:border-dark-30 ps-4 2xl:col-span-4 flex-row items-center justify-between">
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.initiative }}</span><span>Initiative</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">{{ character.speed === false ? "Aucun déplacement" : `${character.speed} cases` }}</span><span>Course</span></div>
<Icon icon="ph:shield-checkered" class="w-8 h-8" />
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.passivedodge + character.defense.passiveparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Passive</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.passivedodge + character.defense.activeparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Blocage</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.activedodge + character.defense.passiveparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Esquive</span></div>
</div>
</div>
<div class="flex flex-1 px-8">
<div class="flex flex-col pe-8 gap-4 py-8 w-80 border-r border-light-30 dark:border-dark-30">
<div class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'arme</span>
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes légères">Arme légère</PreviewA>
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes de jet">Arme de jet</PreviewA>
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes naturelles">Arme naturelle</PreviewA>
<PreviewA v-if="character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes">Arme standard</PreviewA>
<PreviewA v-if="character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes improvisées">Arme improvisée</PreviewA>
<PreviewA v-if="character.mastery.strength > 2" href="1. Règles/99. Annexes/4. Équipement#Les armes lourdes">Arme lourde</PreviewA>
<PreviewA v-if="character.mastery.strength > 3" href="1. Règles/99. Annexes/4. Équipement#Les armes à deux mains">Arme à deux mains</PreviewA>
<PreviewA v-if="character.mastery.dexterity > 0 && character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes maniables">Arme maniable</PreviewA>
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes à projectiles">Arme à projectiles</PreviewA>
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 2" href="1. Règles/99. Annexes/4. Équipement#Les armes longues">Arme longue</PreviewA>
<PreviewA v-if="character.mastery.shield > 0" href="1. Règles/99. Annexes/4. Équipement#Les boucliers">Bouclier</PreviewA>
<PreviewA v-if="character.mastery.shield > 0 && character.mastery.strength > 3" href="1. Règles/99. Annexes/4. Équipement#Les boucliers à deux mains">Bouclier à deux mains</PreviewA>
</div>
</div>
<div v-if="character.mastery.armor > 0" class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'armure</span>
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
<PreviewA v-if="character.mastery.armor > 0" href="1. Règles/99. Annexes/4. Équipement#Les armures légères">Armure légère</PreviewA>
<PreviewA v-if="character.mastery.armor > 1" href="1. Règles/99. Annexes/4. Équipement#Les armures">Armure standard</PreviewA>
<PreviewA v-if="character.mastery.armor > 2" href="1. Règles/99. Annexes/4. Équipement#Les armures lourdes">Armure lourde</PreviewA>
</div>
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise de sorts</span>
<span>Sorts de précision: <span class="font-bold">{{ character.spellranks.precision }}</span></span>
<span>Sorts de savoir: <span class="font-bold">{{ character.spellranks.knowledge }}</span></span>
<span>Sorts d'instinct: <span class="font-bold">{{ character.spellranks.instinct }}</span></span>
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30 mb-2">Compétences</span>
<div class="grid grid-cols-3 gap-1">
<div class="flex flex-col px-2 items-center text-sm text-light-70 dark:text-dark-70" v-for="(value, ability) of character.abilities"><span class="font-bold text-base text-light-100 dark:text-dark-100">+{{ value }}</span><span>{{ characterConfig.abilities[ability].name }}</span></div>
</div>
</div>
</div>
<TabsRoot default-value="features" class="w-[60rem]">
<TabsList class="flex flex-row relative px-4 gap-4">
<TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator>
<TabsTrigger value="features" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Aptitudes</TabsTrigger>
<TabsTrigger value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Sorts</TabsTrigger>
<TabsTrigger value="notes" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Notes</TabsTrigger>
</TabsList>
<TabsContent value="features">
<div class="flex flex-1 flex-col ps-8 gap-4 py-8">
<div class="grid grid-cols-3 gap-2">
<div class="flex flex-col">
<span class="text-lg font-semibold">Actions</span>
<span class="text-sm text-light-70 dark:text-dark-70">Attaquer - Saisir - Faire chuter - Déplacer - Courir - Pas de coté - Lancer un sort - S'interposer - Se transformer - Utiliser un objet - Anticiper une action - Improviser</span>
<MarkdownRenderer :content="character.features.action.join('\n')" />
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Réactions</span>
<span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span>
<MarkdownRenderer :content="character.features.reaction.join('\n')" />
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Actions libre</span>
<span class="text-sm text-light-70 dark:text-dark-70">Analyser une situation - Communiquer</span>
<MarkdownRenderer :content="character.features.freeaction.join('\n')" />
</div>
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Aptitudes</span>
<MarkdownRenderer :content="character.features.passive.map(e => `> ${e}`).join('\n\n')" />
</div>
</div>
</TabsContent>
<TabsContent v-if="character.spells.length > 0" value="spells">
<div class="flex flex-1 flex-col ps-8 gap-4 py-8">
<div class="flex flex-col">
<div class="pb-4 px-2 mt-4 border-b last:border-none border-light-30 dark:border-dark-30 flex flex-col" v-for="spell of character.spells.map(e => characterConfig.spells.find((f: SpellConfig) => f.id === e)).filter(e => !!e)">
<div class="flex flex-row justify-between">
<span class="text-lg font-bold">{{ spell.name }}</span>
<div class="flex flex-row items-center gap-6">
<div class="flex flex-row text-sm gap-2">
<span v-for="element of spell.elements" :class="elementTexts[element].class" class="border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px">{{ elementTexts[element].text }}</span>
</div>
<div class="flex flex-row text-sm gap-1">
<span class="">Rang {{ spell.rank }}</span><span>/</span>
<span class="">{{ spellTypeTexts[spell.type] }}</span><span>/</span>
<span class="">{{ spell.cost }} mana</span><span>/</span>
<span class="">{{ typeof spell.speed === 'string' ? spell.speed : `${spell.speed} minutes` }}</span>
</div>
</div>
</div>
<MarkdownRenderer :content="spell.effect" />
</div>
</div>
</div>
</TabsContent>
<TabsContent value="notes">
<div class="flex flex-1 flex-col ps-8 gap-4 py-8">
<MarkdownRenderer :content="character.notes" />
</div>
</TabsContent>
</TabsRoot>
</div>
</div>
</div>
<div v-else>
<Head>
<Title>d[any] - Erreur</Title>
</Head>
<div>Erreur de chargement</div>
</div>
</template> </template>

View File

@@ -1,20 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js'; import characterConfig from '#shared/character-config.json';
import { Toaster } from '#shared/components.util';
import type { CharacterConfig } from '~/types/character';
definePageMeta({ definePageMeta({
guestsGoesTo: '/user/login', guestsGoesTo: '/user/login',
}) })
const { add } = useToast();
const { user } = useUserSession();
const { data: characters, error, status } = await useFetch(`/api/character`); const { data: characters, error, status } = await useFetch(`/api/character`);
const config = characterConfig as CharacterConfig;
async function deleteCharacter(id: number) async function deleteCharacter(id: number)
{ {
status.value = "pending"; status.value = "pending";
await useRequestFetch()(`/api/character/${id}`, { method: 'delete' }); await useRequestFetch()(`/api/character/${id}`, { method: 'delete' });
status.value = "success"; status.value = "success";
add({ content: 'Personnage supprimé', type: 'info', duration: 25000, timer: true, }); Toaster.add({ content: 'Personnage supprimé', type: 'info', duration: 25000, timer: true, });
characters.value = characters.value?.filter(e => e.id !== id); characters.value = characters.value?.filter(e => e.id !== id);
} }
async function duplicateCharacter(id: number) async function duplicateCharacter(id: number)
@@ -22,7 +22,7 @@ async function duplicateCharacter(id: number)
status.value = "pending"; status.value = "pending";
const newId = await useRequestFetch()(`/api/character/${id}/duplicate`, { method: 'post' }); const newId = await useRequestFetch()(`/api/character/${id}/duplicate`, { method: 'post' });
status.value = "success"; status.value = "success";
add({ content: 'Personnage dupliqué', type: 'info', duration: 25000, timer: true, }); Toaster.add({ content: 'Personnage dupliqué', type: 'info', duration: 25000, timer: true, });
useRouter().push({ name: 'character-id', params: { id: newId } }); useRouter().push({ name: 'character-id', params: { id: newId } });
} }
</script> </script>
@@ -32,47 +32,35 @@ async function duplicateCharacter(id: number)
<Title>d[any] - Mes personnages</Title> <Title>d[any] - Mes personnages</Title>
</Head> </Head>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex align-center justify-center">
<NuxtLink v-if="user?.state === 1" :to="{ name: 'character-id-edit', params: { id: 'new' } }"><Button>Nouveau personnage</Button></NuxtLink>
<Tooltip v-else side="top" message="Veuillez valider votre email avant de pouvoir créer un personnage."><Button disabled>Nouveau personnage</Button></Tooltip>
</div>
<div v-if="status === 'pending'" class="flex flex-1 justify-center align-center"> <div v-if="status === 'pending'" class="flex flex-1 justify-center align-center">
<Loading size="large" /> <Loading size="large" />
</div> </div>
<div v-else-if="status === 'success'" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full"> <template v-else-if="status === 'success'">
<div class="border border-light-30 dark:border-dark-30 p-3 flex flex-row gap-4" v-for="character of characters"> <div v-if="characters && characters.length > 0" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
<Avatar size="large" icon="radix-icons:person" src="" /> <div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="character of characters">
<div class="flex flex-1 flex-shrink flex-col truncate"> <NuxtLink :to="{ name: 'character-id', params: { id: character.id } }" class="group bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2">
<NuxtLink class="text-xl font-bold hover:text-accent-blue truncate" :to="{ name: 'character-id', params: { id: character.id } }" :title="character.name">{{ character.name }}</NuxtLink> <div class="flex flex-row gap-8 ps-4 items-center">
<span class="text-sm truncate">Niveau {{ character.level }}</span> <div class="flex flex-1 flex-col gap-2 justify-center">
<span class="text-lg font-bold group-hover:text-accent-blue">{{ character.name }}</span>
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
<div class="flex flex-row flex-1 items-stretch gap-4">
<span class="text-sm">Niveau {{ character.level }}</span>
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
<span class="text-sm italic">{{ config.peoples[character.people!]?.name }}</span>
</div> </div>
</div>
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
</div>
</NuxtLink>
<div class="flex justify-around items-center py-2 px-4 gap-4">
<NuxtLink :to="{ name: 'character-id-edit', params: { id: character.id } }" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Editer</NuxtLink>
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
<NuxtLink @click="duplicateCharacter(character.id)" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Dupliquer</NuxtLink>
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
<AlertDialogRoot> <AlertDialogRoot>
<DropdownMenuRoot>
<DropdownMenuTrigger class="self-start">
<Button icon><Icon icon="radix-icons:dots-vertical" /></Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent align="end" side="bottom" class="z-50 outline-none bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade border border-light-35 dark:border-dark-35">
<DropdownMenuItem @select="useRouter().push({ name: 'character-id-edit', params: { id: character.id } })" class="cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-baseline py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
<Icon icon="radix-icons:pencil-1" class="absolute left-1.5" />
<span>Editer</span>
</DropdownMenuItem>
<DropdownMenuItem @select="duplicateCharacter(character.id)" class="cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
<Icon icon="radix-icons:clipboard-copy" class="absolute left-1.5" />
<span>Dupliquer</span>
</DropdownMenuItem>
<AlertDialogTrigger> <AlertDialogTrigger>
<DropdownMenuItem class="cursor-pointer text-base text-light-red dark:text-dark-red leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-red dark:data-[highlighted]:bg-dark-red data-[highlighted]:bg-opacity-30 dark:data-[highlighted]:bg-opacity-30"> <span class="text-sm font-bold text-light-red dark:text-dark-red">Supprimer</span>
<Icon icon="radix-icons:trash" class="absolute left-1.5" />
<span>Supprimer</span>
</DropdownMenuItem>
</AlertDialogTrigger> </AlertDialogTrigger>
<DropdownMenuArrow class="fill-light-35 dark:fill-dark-35" />
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenuRoot>
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" /> <AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
<AlertDialogContent <AlertDialogContent
@@ -87,6 +75,19 @@ async function duplicateCharacter(id: number)
</AlertDialogRoot> </AlertDialogRoot>
</div> </div>
</div> </div>
</div>
<div v-else class="flex flex-col gap-2 items-center flex-1">
<span class="text-lg font-bold">Vous n'avez pas encore de personnage</span>
<NuxtLink class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 py-2 px-4" :to="{ name: 'character-id-edit', params: { id: 'new' } }">Nouveau personnage</NuxtLink>
<NuxtLink class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 py-2 px-4" :to="{ name: 'character-list' }">Qu'ont fait les autres ?</NuxtLink>
</div>
</template>
<div v-else> <div v-else>
<span>Erreur de chargement</span> <span>Erreur de chargement</span>
<span>{{ error?.message }}</span> <span>{{ error?.message }}</span>

View File

@@ -1,5 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import characterConfig from '#shared/character-config.json';
import type { CharacterConfig } from '~/types/character';
const { data: characters, error, status } = await useFetch(`/api/character`, { params: { visibility: "public" } }); const { data: characters, error, status } = await useFetch(`/api/character`, { params: { visibility: "public" } });
const config = characterConfig as CharacterConfig;
</script> </script>
<template> <template>
@@ -10,15 +13,34 @@ const { data: characters, error, status } = await useFetch(`/api/character`, { p
<div v-if="status === 'pending'" class="flex flex-1 justify-center align-center"> <div v-if="status === 'pending'" class="flex flex-1 justify-center align-center">
<Loading size="large" /> <Loading size="large" />
</div> </div>
<div v-else-if="status === 'success'" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full"> <template v-else-if="status === 'success'">
<div class="border border-light-30 dark:border-dark-30 p-3 flex flex-row gap-4" v-for="character of characters"> <div v-if="characters && characters.length > 0" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
<Avatar size="large" icon="radix-icons:person" src="" /> <div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="character of characters">
<div class="flex flex-1 flex-shrink flex-col truncate"> <NuxtLink :to="{ name: 'character-id', params: { id: character.id } }" class="group bg-light-10 dark:bg-dark-10 p-2 flex flex-col gap-2">
<NuxtLink class="text-xl font-bold hover:text-accent-blue truncate" :to="{ name: 'character-id', params: { id: character.id } }" :title="character.name">{{ character.name }}</NuxtLink> <div class="flex flex-row gap-8 ps-4 items-center">
<span class="text-sm truncate">Niveau {{ character.progress.level }}</span> <div class="flex flex-1 flex-col gap-2 justify-center">
<span class="text-lg font-bold group-hover:text-accent-blue">{{ character.name }}</span>
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
<div class="flex flex-row flex-1 items-stretch gap-4">
<span class="text-sm">Niveau {{ character.level }}</span>
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
<span class="text-sm italic">{{ config.peoples[character.people!]?.name }}</span>
</div> </div>
</div> </div>
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
</div> </div>
</NuxtLink>
</div>
</div>
<div v-else class="flex flex-col gap-2 items-center flex-1">
<span class="text-lg font-bold">Il n'existe pas encore de personnage public</span>
Soyez le premier à partager vos créations !
<NuxtLink class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 py-2 px-4" :to="{ name: 'character-id-edit', params: { id: 'new' } }">Nouveau personnage</NuxtLink>
</div>
</template>
<div v-else> <div v-else>
<span>Erreur de chargement</span> <span>Erreur de chargement</span>
<span>{{ error?.message }}</span> <span>{{ error?.message }}</span>

View File

@@ -1,42 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import characterConfig from '#shared/character-config.json'; import { HomebrewBuilder } from '~/shared/feature.util';
import { Icon } from '@iconify/vue/dist/iconify.js';
import type { CharacterConfig } from '~/types/character';
//@ts-ignore
const config = ref<CharacterConfig>(characterConfig);
function copy() definePageMeta({
{ guestsGoesTo: '/user/login',
navigator.clipboard.writeText(JSON.stringify(config.value)); });
}
const container = useTemplateRef('container');
onMounted(() => {
queueMicrotask(() => {
if(container.value)
{
const builder = new HomebrewBuilder(container.value);
}
});
})
</script> </script>
<template> <template>
<Head> <Head>
<Title>d[any] - Edition de données</Title> <Title>d[any] - Edition de données</Title>
</Head> </Head>
<TabsRoot class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full" default-value="training"> <div ref="container" class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full"></div>
<TabsList class="flex flex-row gap-4 self-center relative px-4">
<TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator>
<TabsTrigger value="peoples" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Peuples</TabsTrigger>
<TabsTrigger value="training" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Entrainement</TabsTrigger>
<TabsTrigger value="abilities" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Compétences</TabsTrigger>
<TabsTrigger value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Sorts</TabsTrigger>
<Tooltip message="Copier le JSON" side="right"><Button icon @click="copy" class="p-2"><Icon icon="radix-icons:clipboard-copy" /></Button></Tooltip>
</TabsList>
<div class="flex-1 outline-none max-w-full w-full">
<TabsContent value="peoples">
</TabsContent>
<TabsContent value="training">
<TrainingConfigEditor :config="config" />
</TabsContent>
<TabsContent value="abilities">
</TabsContent>
<TabsContent value="spells">
</TabsContent>
</div>
</TabsRoot>
</template> </template>

View File

@@ -36,16 +36,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { Content, Editor } from '#shared/content.util'; import { Content, Editor } from '#shared/content.util';
import { button, loading } from '#shared/proses'; import { button, loading } from '#shared/components.util';
import { dom, icon, text } from '~/shared/dom.util'; import { dom, icon } from '#shared/dom.util';
import { modal, popper } from '~/shared/floating.util'; import { modal, tooltip } from '#shared/floating.util';
import { Toaster } from '#shared/components.util';
definePageMeta({ definePageMeta({
rights: ['admin', 'editor'], rights: ['admin', 'editor'],
layout: 'null', layout: 'null',
}); });
const toaster = useToast();
const { user } = useUserSession(); const { user } = useUserSession();
const tree = useTemplateRef('tree'), container = useTemplateRef('container'); const tree = useTemplateRef('tree'), container = useTemplateRef('container');
let editor: Editor; let editor: Editor;
@@ -53,9 +53,9 @@ let editor: Editor;
function pull() function pull()
{ {
Content.pull().then(e => { Content.pull().then(e => {
toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 }); Toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
}).catch(e => { }).catch(e => {
toaster.add({ type: 'success', content: 'Une erreur est survenue durant la récupération des données.', timer: true, duration: 7500 }); Toaster.add({ type: 'success', content: 'Une erreur est survenue durant la récupération des données.', timer: true, duration: 7500 });
console.error(e); console.error(e);
}); });
} }
@@ -64,10 +64,10 @@ function push()
const { close } = modal([dom('div', { class: 'flex flex-col gap-4 justify-center items-center' }, [ dom('div', { class: 'text-xl', text: 'Mise à jour des données' }), loading('large') ])], { priority: false, closeWhenOutside: true, }); const { close } = modal([dom('div', { class: 'flex flex-col gap-4 justify-center items-center' }, [ dom('div', { class: 'text-xl', text: 'Mise à jour des données' }), loading('large') ])], { priority: false, closeWhenOutside: true, });
Content.push().then(e => { Content.push().then(e => {
close(); close();
toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 }); Toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
}).catch(e => { }).catch(e => {
close(); close();
toaster.add({ type: 'success', content: 'Une erreur est survenue durant l\'enregistrement des données.', timer: true, duration: 7500 }); Toaster.add({ type: 'success', content: 'Une erreur est survenue durant l\'enregistrement des données.', timer: true, duration: 7500 });
console.error(e); console.error(e);
}); });
} }
@@ -79,8 +79,8 @@ onMounted(async () => {
tree.value.appendChild(load); tree.value.appendChild(load);
const content = dom('div', { class: 'flex flex-row justify-start items-center gap-4 p-2' }, [ const content = dom('div', { class: 'flex flex-row justify-start items-center gap-4 p-2' }, [
popper(button(icon('ph:cloud-arrow-down', { height: 20, width: 20 }), pull, 'p-1'), { placement: 'top', offset: 4, delay: 120, arrow: true, content: [text('Actualiser')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50' }), tooltip(button(icon('ph:cloud-arrow-down', { height: 20, width: 20 }), pull, 'p-1'), 'Actualiser', 'top'),
popper(button(icon('ph:cloud-arrow-up', { height: 20, width: 20 }), push, 'p-1'), { placement: 'top', offset: 4, delay: 120, arrow: true, content: [text('Enregistrer')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50' }), tooltip(button(icon('ph:cloud-arrow-up', { height: 20, width: 20 }), push, 'p-1'), 'Enregistrer', 'top'),
]) ])
tree.value.insertBefore(content, load); tree.value.insertBefore(content, load);
@@ -95,193 +95,4 @@ onMounted(async () => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
editor?.unmount(); editor?.unmount();
}); });
/* import { Icon } from '@iconify/vue/dist/iconify.js';
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
import type { FileType, LocalContent, TreeItem } from '#shared/content.util';
import { DEFAULT_CONTENT, iconByType, Content, getPath } from '#shared/content.util';
import type { Preferences } from '~/types/general';
import { fakeA as proseA } from '#shared/proses';
import { parsePath } from '~/shared/general.util';
import type { CanvasContent } from '~/types/canvas';
export type TreeItemEditable = LocalContent &
{
parent?: string;
name?: string;
customPath?: boolean;
children?: TreeItemEditable[];
}
definePageMeta({
rights: ['admin', 'editor'],
layout: 'null',
});
const { user } = useUserSession();
const router = useRouter();
const open = ref(true), topOpen = ref(true);
const toaster = useToast();
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
let navigation = Content.tree as TreeItemEditable[];
const selected = ref<TreeItemEditable>();
const preferences = useCookie<Preferences>('preferences', { default: () => ({ markdown: { editing: 'split' }, canvas: { gridSnap: true, neighborSnap: true, spacing: 32 } }), watch: true, maxAge: 60*60*24*31 });
watch(selected, async (value, old) => {
if(selected.value)
{
if(!selected.value.content && selected.value.path)
{
selected.value = await Content.content(selected.value.path);
}
router.replace({ hash: '#' + selected.value!.path || getPath(selected.value!) });
}
else
{
router.replace({ hash: '' });
}
})
const debouncedSave = useDebounceFn(save, 60000, { maxWait: 180000 });
useShortcuts({
//meta_s: { usingInput: true, handler: () => save(), prevent: true },
meta_n: { usingInput: true, handler: () => add('markdown'), prevent: true },
meta_shift_n: { usingInput: true, handler: () => add('folder'), prevent: true },
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore-path', params: { path: 'index' } }), prevent: true }
})
function add(type: FileType): void
{
if(!navigation)
{
return;
}
const news = [...tree.search(navigation, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i);
const title = `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`;
const item: TreeItemEditable = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: [], customPath: false, content: DEFAULT_CONTENT[type], owner: -1, timestamp: new Date(), visit: 0 };
if(!selected.value)
{
navigation = [...navigation, item];
}
else if(selected.value?.children)
{
item.parent = getPath(selected.value);
navigation = tree.insertChild(navigation, item.parent, item);
}
else
{
navigation = tree.insertAfter(navigation, getPath(selected.value), item);
}
}
function updateTree(instruction: Instruction, itemId: string, targetId: string) : TreeItemEditable[] | undefined {
if(!navigation)
return;
const item = tree.find(navigation, itemId);
const target = tree.find(navigation, targetId);
if(!item)
return;
if (instruction.type === 'reparent') {
const path = tree.getPathToItem({
current: navigation,
targetId: targetId,
});
if (!path) {
console.error(`missing ${path}`);
return;
}
const desiredId = path[instruction.desiredLevel];
let result = tree.remove(navigation, itemId);
result = tree.insertAfter(result, desiredId, item);
return result;
}
// the rest of the actions require you to drop on something else
if (itemId === targetId)
return navigation;
if (instruction.type === 'reorder-above') {
let result = tree.remove(navigation, itemId);
result = tree.insertBefore(result, targetId, item);
return result;
}
if (instruction.type === 'reorder-below') {
let result = tree.remove(navigation, itemId);
result = tree.insertAfter(result, targetId, item);
return result;
}
if (instruction.type === 'make-child') {
if(!target || target.type !== 'folder')
return;
let result = tree.remove(navigation, itemId);
result = tree.insertChild(result, targetId, item);
rebuildPath([item], targetId);
return result;
}
return navigation;
}
function transform(items: TreeItem[] | undefined): TreeItemEditable[] | undefined
{
return items?.map(e => ({
...e,
parent: e.path.substring(0, e.path.lastIndexOf('/')),
name: e.path.substring(e.path.lastIndexOf('/') + 1),
customPath: e.path.substring(e.path.lastIndexOf('/') + 1) !== parsePath(e.title),
children: transform(e.children)
})) as TreeItemEditable[] | undefined;
}
function flatten(items: TreeItemEditable[] | undefined): TreeItemEditable[]
{
return items?.flatMap(e => [e, ...flatten(e.children)]) ?? [];
}
function drop(instruction: Instruction, itemId: string, targetId: string)
{
navigation = updateTree(instruction, itemId, targetId) ?? navigation ?? [];
}
function rebuildPath(tree: TreeItemEditable[] | null | undefined, parentPath: string)
{
if(!tree)
return;
tree.forEach(e => {
e.parent = parentPath;
rebuildPath(e.children, getPath(e));
});
}
function save()
{
if(selected.value && selected.value.content)
{
selected.value.path = getPath(selected.value);
Content.save(selected.value);
}
}
const defaultExpanded = computed(() => {
if(router.currentRoute.value.hash)
{
const split = router.currentRoute.value.hash.substring(1).split('/');
split.forEach((e, i) => { if(i !== 0) split[i] = split[i - 1] + '/' + e });
return split;
}
});
/*watch(router.currentRoute, (value) => {
if(value && value.hash && navigation)
selected.value = tree.find(navigation, value.hash.substring(1));
else
selected.value = undefined;
}, { immediate: true }); */
</script> </script>

View File

@@ -3,8 +3,8 @@
<Title>d[any] - Mentions légales</Title> <Title>d[any] - Mentions légales</Title>
</Head> </Head>
<div class="flex flex-col max-w-[1200px] p-16"> <div class="flex flex-col max-w-[1200px] p-16">
<ProseH3>Mentions Légales</ProseH3> <h3 class="text-xl font-bold">Mentions Légales</h3>
<ProseH4>Collecte et Traitement des Données Personnelles</ProseH4> <h4 class="text-lg font-semibold">Collecte et Traitement des Données Personnelles</h4>
Ce site collecte des données personnelles durant l'inscription et des données anonymes durant la navigation sur Ce site collecte des données personnelles durant l'inscription et des données anonymes durant la navigation sur
le site dans un but de collecte statistiques.<br /> le site dans un but de collecte statistiques.<br />
@@ -12,21 +12,21 @@
suppression de vos données personnelles. <br /> suppression de vos données personnelles. <br />
Pour exercer ces droits, vous pouvez vous rendre dans votre profil et selectionner l'option "Supprimer mon Pour exercer ces droits, vous pouvez vous rendre dans votre profil et selectionner l'option "Supprimer mon
compte" qui garanti une suppression de l'intégralité de vos données personnelles. compte" qui garanti une suppression de l'intégralité de vos données personnelles.<br /><br />
<ProseH4>Utilisation des Cookies</ProseH4> <h4 class="text-lg font-semibold">Utilisation des Cookies</h4>
Ce site utilise des cookies uniquement pour maintenir la connexion des utilisateurs et faciliter leur navigation Ce site utilise des cookies uniquement pour maintenir la connexion des utilisateurs et faciliter leur navigation
lors de chaque visite. Aucune information de suivi ou de profilage n'est réalisée. Ces cookies sont essentiels lors de chaque visite. Aucune information de suivi ou de profilage n'est réalisée. Ces cookies sont essentiels
au fonctionnement du site et ne nécessitent pas de consentement préalable. <br /> au fonctionnement du site et ne nécessitent pas de consentement préalable. <br />
Vous pouvez gérer les cookies en configurant les paramètres de votre navigateur, mais la désactivation de ces Vous pouvez gérer les cookies en configurant les paramètres de votre navigateur, mais la désactivation de ces
cookies pourrait affecter votre expérience de navigation. cookies pourrait affecter votre expérience de navigation.<br /><br />
<ProseH4>Limitation de Responsabilité</ProseH4> <h4 class="text-lg font-semibold">Limitation de Responsabilité</h4>
Les informations publiées sur ce site sont fournies à titre indicatif et peuvent contenir des erreurs. <br /> Les informations publiées sur ce site sont fournies à titre indicatif et peuvent contenir des erreurs. <br />
L'éditeur décline toute responsabilité quant à l'usage qui pourrait être fait de ces informations. L'éditeur décline toute responsabilité quant à l'usage qui pourrait être fait de ces informations.<br /><br />
<ProseH4>Propriété Intellectuelle</ProseH4> <h4 class="text-lg font-semibold">Propriété Intellectuelle</h4>
Tous les contenus présents sur ce site (textes, images, logos, etc.) sont protégés par les lois en vigueur Tous les contenus présents sur ce site (textes, images, logos, etc.) sont protégés par les lois en vigueur
sur la propriété intellectuelle. Toute reproduction ou utilisation de ces contenus sans autorisation préalable sur la propriété intellectuelle. Toute reproduction ou utilisation de ces contenus sans autorisation préalable
est interdite. <br /><br /> est interdite. <br /><br />

View File

@@ -1,53 +0,0 @@
<template>
<Head>
<Title>d[any] - Roadmap</Title>
</Head>
<div class="flex flex-col justify-start p-6">
<ProseH2>Roadmap</ProseH2>
<div class="grid grid-cols-4 gap-x-2 gap-y-4">
<div v-if="loggedIn && user && hasPermissions(user.permissions, ['admin'])" class="flex flex-col gap-2 justify-start">
<ProseH3>Administration</ProseH3>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Dashboard de statistiques</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Editeur de permissions</span><ProseTag>prioritaire</ProseTag></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Synchro project <-> GIT</span><ProseTag>prioritaire</ProseTag></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Versionning automatisé, releases et newsletter</span></Label>
</div>
<div class="flex flex-col gap-2 justify-start">
<ProseH3>Editeur</ProseH3>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Edition de page</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Edition riche de page</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Edition live de page</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Raccourcis d'edition</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Affichage alternatif par page</span></Label>
</div>
<div class="flex flex-col gap-2 justify-start">
<ProseH3>Projet</ProseH3>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Edition du projet</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Déplacement des fichiers</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Configuration de droit du projet</span><ProseTag>prioritaire</ProseTag></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Theme par projet</span></Label>
</div>
<div class="flex flex-col gap-2 justify-start">
<ProseH3>Nouvelles features</ProseH3>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Historique des modifs</span><ProseTag>prioritaire</ProseTag></Label><!-- Objet release: key hash, timestamp, version, name, description?. Objet edit: key hash, key property, value, timestamp -->
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Commentaire par page</span></Label><!-- Object comment: key path, key comment_id, position, content, owner, following? -->
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Type de fichier: Timeline</span></Label><!-- Propriétés: array of (from, (to || ponctual), ((title, content) || dedicated page)) -->
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Type de fichier: Whiteboard</span></Label><!-- Tableau de données SVG -->
</div>
<div class="flex flex-col gap-2 justify-start">
<ProseH3>Utilisateur</ProseH3>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Validation du compte par mail<ProseTag>prioritaire</ProseTag></span></Label>
<!-- <Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Modification de profil</span></Label> -->
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Image de profil</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Préférence d'email</span></Label><!-- New features, newsletter et surveys -->
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
import { hasPermissions } from '~/shared/auth.util';
const { loggedIn, user } = useUserSession();
</script>

45
pages/usage.vue Normal file
View File

@@ -0,0 +1,45 @@
<template>
<Head>
<Title>d[any] - Mentions légales</Title>
</Head>
<div class="flex flex-col max-w-[1200px] p-16">
<h3 class="text-xl font-bold">Conditions Générales d'Utilisation du site d-any.com</h3>
<h4 class="text-lg font-semibold py-2">1. Objet</h4>
Le site d-any.com offre un service en ligne dédié au jeu de rôle comprenant une section de règles officielles maintenues par l'administrateur, une section permettant la création de personnages
publics ou privés et une section de campagnes visant à rassembler plusieurs joueurs pour faire interagir leurs personnages. L'utilisation du site implique l'acceptation pleine et entière des présentes conditions. <br/><br/>
<h4 class="text-lg font-semibold py-2">2. Accès et fonctionnement</h4>
L'accès au site est gratuit. L'interaction entre utilisateurs est strictement limitée aux personnages et joueurs participant à une même campagne partagée. Aucun contact direct ni interaction n'est possible en dehors de cette structure.<br/><br/>
<h4 class="text-lg font-semibold py-2">3. Création et gestion des personnages</h4>
Les utilisateurs peuvent créer des personnages publics, visibles par tous les membres des campagnes partagées, ou privés, visibles uniquement par leur créateur.
Les utilisateurs sont responsables du contenu des personnages qu'ils créent. Ils s'engagent à ne pas créer ou publier des personnages portant atteinte à la dignité, contenant des propos discriminatoires, diffamatoires, obscènes ou illicites.
L'administrateur du site se réserve le droit de supprimer ou masquer tout personnage en infraction avec ces règles.<br/><br/>
<h4 class="text-lg font-semibold py-2">4. Règles du jeu</h4>
Les règles officielles du jeu, rédigées et entretenues par l'administrateur, doivent être respectées par tous les utilisateurs dans la création et le déroulement des campagnes.<br/><br/>
<h4 class="text-lg font-semibold py-2">5. Interaction en campagne</h4>
Les communications et interactions entre joueurs et personnages sont strictement limitées aux campagnes partagées.
Toute interaction dans ces cadres doit respecter les règles de respect, de courtoisie et de fair-play.
Tout comportement abusif, harcèlement, propos haineux ou toute forme de contenu illicite est prohibé et pourra entraîner des sanctions, incluant la suppression de comptes ou personnages.<br/><br/>
<h4 class="text-lg font-semibold py-2">6. Propriété intellectuelle</h4>
Les règles, outils, et contenus hébergés sur le site sont la propriété de l'administrateur ou des auteurs respectifs.
Les personnages créés appartiennent à leurs auteurs, sous réserve du respect des droits d'auteur liés au jeu original et de la charte du site.<br/><br/>
<h4 class="text-lg font-semibold py-2">7. Données personnelles</h4>
Les données collectées se limitent à celles nécessaires au fonctionnement du site. Toute donnée personnelle est traitée conformément à la réglementation en vigueur et peut être modifiée ou supprimée sur demande.<br/><br/>
<h4 class="text-lg font-semibold py-2">8. Responsabilité</h4>
L'administrateur ne pourra être tenu responsable des usages faits par les utilisateurs des personnages publics ou des interactions au sein des campagnes. L'éditeur décline toute responsabilité en cas d'abus
entre joueurs ou de contenu illégal diffusé par un utilisateur.<br/><br/>
<h4 class="text-lg font-semibold py-2">9. Modification des conditions</h4>
Ces conditions peuvent être modifiées à tout moment par l'administrateur. Les utilisateurs seront informés des modifications via le site et l'usage continu vaudra acceptation des nouvelles conditions.<br/><br/>
<h4 class="text-lg font-semibold py-2">10. Droit applicable</h4>
Les présentes conditions sont soumises au droit français. Tout litige sera porté devant les tribunaux compétents.<br/><br/>
<div class="py-32"></div>
</div>
</template>

View File

@@ -3,7 +3,7 @@
<Title>d[any] - Validation de votre adresse mail</Title> <Title>d[any] - Validation de votre adresse mail</Title>
</Head> </Head>
<div class="flex flex-col justify-center items-center"> <div class="flex flex-col justify-center items-center">
<ProseH2>Votre compte a été validé ! 🎉</ProseH2> <h2 class="text-2xl font-bold">Votre compte a été validé ! 🎉</h2>
<div class="flex flex-row gap-8"> <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: '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> <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 flex-1 flex-col justify-center items-center">
<div class="flex gap-8 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> <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>
<ProseH4>Reinitialisation de mon mot de passe</ProseH4> <h4 class="text-xl font-bold">Reinitialisation de mon mot de passe</h4>
</div> </div>
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch"> <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"/> <TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="email"/>
@@ -25,8 +25,6 @@ definePageMeta({
usersGoesTo: '/user/profile', usersGoesTo: '/user/profile',
}); });
const toaster = useToast();
const email = ref(''), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'); const email = ref(''), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
async function submit() async function submit()

View File

@@ -5,7 +5,7 @@
<div class="flex flex-1 flex-col justify-center items-center"> <div class="flex flex-1 flex-col justify-center items-center">
<div class="flex gap-8 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> <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>
<ProseH4>Reinitialisation de mon mot de passe</ProseH4> <h4 class="text-center flex-1 text-xl font-bold">Reinitialisation de mon mot de passe</h4>
</div> </div>
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch"> <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 }"/> <TextInput type="password" label="Nouveau mot de passe" autocomplete="newPassword" v-model="newPasswd" :class="{ '!border-light-red !dark:border-dark-red': error }"/>
@@ -25,6 +25,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import { Toaster } from '#shared/components.util';
definePageMeta({ definePageMeta({
layout: 'login', layout: 'login',
@@ -33,7 +34,6 @@ definePageMeta({
const query = useRouter().currentRoute.value.query; const query = useRouter().currentRoute.value.query;
const toaster = useToast();
const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false); const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false);
const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref(''); const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref('');
@@ -70,7 +70,7 @@ async function submit()
{ {
status.value = 'success'; status.value = 'success';
toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' }); Toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' });
useRouter().push({ name: 'user-login' }); useRouter().push({ name: 'user-login' });
} }
else else
@@ -81,7 +81,7 @@ async function submit()
status.value = 'error'; status.value = 'error';
const err = e as any; const err = e as any;
toaster.add({ content: err?.data?.message ?? err?.message ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' }); Toaster.add({ content: err?.data?.message ?? err?.message ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' });
} }
} }
</script> </script>

View File

@@ -5,7 +5,7 @@
<div class="flex flex-1 flex-col justify-center items-center"> <div class="flex flex-1 flex-col justify-center items-center">
<div class="flex gap-8 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> <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>
<ProseH4>Modification de mon mot de passe</ProseH4> <h4 class="text-center flex-1 text-xl font-bold">Modification de mon mot de passe</h4>
</div> </div>
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch"> <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"/> <TextInput type="password" label="Ancien mot de passe" name="old-password" autocomplete="current-password" v-model="oldPasswd"/>
@@ -26,13 +26,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import { Toaster } from '#shared/components.util';
definePageMeta({ definePageMeta({
layout: 'login', layout: 'login',
guestsGoesTo: '/user/login', guestsGoesTo: '/user/login',
}); });
const toaster = useToast();
const { user } = useUserSession(); const { user } = useUserSession();
const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false); const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false);
const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref(''); const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref('');
@@ -70,19 +70,19 @@ async function submit()
{ {
status.value = 'success'; status.value = 'success';
toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' }); Toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' });
useRouter().push({ name: 'user-profile' }); useRouter().push({ name: 'user-profile' });
} }
else else
{ {
status.value = 'error'; status.value = 'error';
toaster.add({ content: result.error ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' }); Toaster.add({ content: result.error ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' });
} }
} catch(e) { } catch(e) {
status.value = 'error'; status.value = 'error';
toaster.add({ content: (e as Error).message ?? e, duration: 10000, timer: true, type: 'error' }); Toaster.add({ content: (e as Error).message ?? e, duration: 10000, timer: true, type: 'error' });
} }
} }
</script> </script>

View File

@@ -5,7 +5,7 @@
<div class="flex flex-1 flex-col justify-center items-center"> <div class="flex flex-1 flex-col justify-center items-center">
<div class="flex gap-8 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> <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>
<ProseH4>Connexion</ProseH4> <h4 class="text-xl font-bold">Connexion</h4>
</div> </div>
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch"> <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"/> <TextInput type="text" label="Utilisateur ou email" name="username" autocomplete="username email" v-model="state.usernameOrEmail"/>
@@ -18,17 +18,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ZodError } from 'zod'; import type { ZodError } from 'zod/v4';
import { schema, type Login } from '~/schemas/login'; import { schema, type Login } from '~/schemas/login';
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import { Toaster } from '#shared/components.util';
definePageMeta({ definePageMeta({
layout: 'login', layout: 'login',
usersGoesTo: '/user/profile', usersGoesTo: '/user/profile',
}); });
const { add: addToast, clear: clearToasts } = useToast();
const state = reactive<Login>({ const state = reactive<Login>({
usernameOrEmail: '', usernameOrEmail: '',
password: '' password: ''
@@ -47,9 +46,9 @@ const toastMessage = ref('');
async function submit() async function submit()
{ {
if(state.usernameOrEmail === "") if(state.usernameOrEmail === "")
return addToast({ content: 'Veuillez saisir un nom d\'utilisateur ou un email', timer: true, duration: 10000 }); return Toaster.add({ content: 'Veuillez saisir un nom d\'utilisateur ou un email', timer: true, duration: 10000 });
if(state.password === "") if(state.password === "")
return addToast({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 }); return Toaster.add({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 });
const data = schema.safeParse(state); const data = schema.safeParse(state);
@@ -64,8 +63,8 @@ async function submit()
} }
else if(status.value === 'success' && login.success) else if(status.value === 'success' && login.success)
{ {
clearToasts(); Toaster.clear();
addToast({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' }); Toaster.add({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
await navigateTo('/user/profile'); await navigateTo('/user/profile');
} }
} }
@@ -85,12 +84,12 @@ function handleErrors(error: Error | ZodError)
{ {
for(const err of (error as ZodError).issues) for(const err of (error as ZodError).issues)
{ {
return addToast({ content: err.message, timer: true, duration: 10000, type: 'error' }); return Toaster.add({ content: err.message, timer: true, duration: 10000, type: 'error' });
} }
} }
else else
{ {
return addToast({ content: error?.message ?? 'Une erreur est survenue', timer: true, duration: 10000, type: 'error' }); return Toaster.add({ content: error?.message ?? 'Une erreur est survenue', timer: true, duration: 10000, type: 'error' });
} }
} }
</script> </script>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { hasPermissions } from "#shared/auth.util"; import { hasPermissions } from "#shared/auth.util";
import { Toaster } from '#shared/components.util';
definePageMeta({ definePageMeta({
guestsGoesTo: '/user/login', guestsGoesTo: '/user/login',
}) })
const { user, clear } = useUserSession(); const { user, clear } = useUserSession();
const toaster = useToast();
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
async function revalidateUser() async function revalidateUser()
@@ -15,7 +15,7 @@ async function revalidateUser()
method: 'post' method: 'post'
}); });
loading.value = false; loading.value = false;
toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' }); Toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' });
} }
async function deleteUser() async function deleteUser()
{ {
@@ -38,8 +38,8 @@ async function deleteUser()
<div class="flex gap-4"> <div class="flex gap-4">
<Avatar icon="radix-icons:person" :src="`/users/${user?.id}.medium.jpg`" class="w-32 h-32" /> <Avatar icon="radix-icons:person" :src="`/users/${user?.id}.medium.jpg`" class="w-32 h-32" />
<div class="flex flex-col items-start"> <div class="flex flex-col items-start">
<ProseH5>{{ user.username }}</ProseH5> <h4 class="text-xl font-bold">{{ user.username }}</h4>
<ProseH5>{{ user.email }}</ProseH5> <h4 class="text-xl font-bold">{{ user.email }}</h4>
</div> </div>
</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" <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 flex-1 flex-col justify-center items-center">
<div class="flex gap-8 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> <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>
<ProseH4>Inscription</ProseH4> <h4 class="text-xl font-bold">Inscription</h4>
</div> </div>
<form @submit.prevent="() => submit()" class="grid flex-1 p-4 grid-cols-2 md:grid-cols-1 gap-4 md:gap-0"> <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"/> <TextInput type="text" label="Nom d'utilisateur" name="username" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/>
@@ -20,6 +20,7 @@
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span> <span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
</div> </div>
<TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/> <TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/>
<Label class="pb-2 col-span-2 md:col-span-1 flex flex-row gap-2 items-center"><CheckboxRoot v-model:checked="agreeOnRules" class="border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 w-5 h-5" ><CheckboxIndicator ><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span>J'ai lu et j'accepte les <NuxtLink class="text-accent-blue cursor-pointer" :to="{ name: 'usage' }" target="_blank">conditions d'utilisation</NuxtLink></span></Label>
<Button type="submit" class="border border-light-35 dark:border-dark-35 max-w-48 w-full order-9 col-span-2 md:col-span-1 m-auto" :loading="status === 'pending'">S'inscrire</Button> <Button type="submit" class="border border-light-35 dark:border-dark-35 max-w-48 w-full order-9 col-span-2 md:col-span-1 m-auto" :loading="status === 'pending'">S'inscrire</Button>
<span class="mt-4 order-10 flex justify-center items-center gap-4 col-span-2 md:col-span-1 m-auto">Vous avez déjà un compte ?<NuxtLink class="text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-login' }">Se connecter</NuxtLink></span> <span class="mt-4 order-10 flex justify-center items-center gap-4 col-span-2 md:col-span-1 m-auto">Vous avez déjà un compte ?<NuxtLink class="text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-login' }">Se connecter</NuxtLink></span>
</form> </form>
@@ -27,9 +28,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ZodError } from 'zod'; import { ZodError } from 'zod/v4';
import { schema, type Registration } from '~/schemas/registration'; import { schema, type Registration } from '~/schemas/registration';
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import { Toaster } from '#shared/components.util';
definePageMeta({ definePageMeta({
layout: 'login', layout: 'login',
@@ -42,7 +44,6 @@ const state = reactive<Registration>({
password: '' password: ''
}); });
const { add: addToast, clear: clearToasts } = useToast();
const confirmPassword = ref(""); const confirmPassword = ref("");
const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128); const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128);
@@ -50,6 +51,7 @@ const checkedLower = computed(() => state.password.toUpperCase() !== state.passw
const checkedUpper = computed(() => state.password.toLowerCase() !== state.password); const checkedUpper = computed(() => state.password.toLowerCase() !== state.password);
const checkedDigit = computed(() => /[0-9]/.test(state.password)); const checkedDigit = computed(() => /[0-9]/.test(state.password));
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => state.password.includes(e))); const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => state.password.includes(e)));
const agreeOnRules = ref<boolean>(false);
const { data: result, status, error, refresh } = await useFetch('/api/auth/register', { const { data: result, status, error, refresh } = await useFetch('/api/auth/register', {
body: state, body: state,
@@ -57,18 +59,20 @@ const { data: result, status, error, refresh } = await useFetch('/api/auth/regis
method: 'POST', method: 'POST',
watch: false, watch: false,
ignoreResponseError: true, ignoreResponseError: true,
}) });
async function submit() async function submit()
{ {
if(state.username === '') if(state.username === '')
return addToast({ content: 'Veuillez saisir un nom d\'utilisateur', timer: true, duration: 10000 }); return Toaster.add({ content: 'Veuillez saisir un nom d\'utilisateur', timer: true, duration: 10000 });
if(state.email === '') if(state.email === '')
return addToast({ content: 'Veuillez saisir une adresse mail', timer: true, duration: 10000 }); return Toaster.add({ content: 'Veuillez saisir une adresse mail', timer: true, duration: 10000 });
if(state.password === "") if(state.password === "")
return addToast({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 }); return Toaster.add({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 });
if(state.password !== confirmPassword.value) if(state.password !== confirmPassword.value)
return addToast({ content: 'Les deux mots de passe saisis ne correspondent pas', timer: true, duration: 10000 }); return Toaster.add({ content: 'Les deux mots de passe saisis ne correspondent pas', timer: true, duration: 10000 });
if(agreeOnRules.value !== true)
return Toaster.add({ content: 'Veuillez accepter des conditions d\'utilisations pour vous inscrire', timer: true, duration: 10000 });
const data = schema.safeParse(state); const data = schema.safeParse(state);
@@ -83,8 +87,8 @@ async function submit()
} }
else if(status.value === 'success' && login.success) else if(status.value === 'success' && login.success)
{ {
clearToasts(); Toaster.clear();
addToast({ duration: 10000, content: 'Vous avez été enregistré. Pensez à valider votre adresse mail.', timer: true, type: 'success' }); Toaster.add({ duration: 10000, content: 'Vous avez été enregistré. Pensez à valider votre adresse mail.', timer: true, type: 'success' });
await navigateTo('/user/profile'); await navigateTo('/user/profile');
} }
} }
@@ -104,12 +108,12 @@ function handleErrors(error: Error | ZodError)
{ {
for(const err of (error as ZodError).issues) for(const err of (error as ZodError).issues)
{ {
return addToast({ content: err.message, timer: true, duration: 10000, type: 'error' }); return Toaster.add({ content: err.message, timer: true, duration: 10000, type: 'error' });
} }
} }
else else
{ {
return addToast({ content: error?.message ?? 'Une erreur est survenue', timer: true, duration: 10000, type: 'error' }); return Toaster.add({ content: error?.message ?? 'Une erreur est survenue', timer: true, duration: 10000, type: 'error' });
} }
} }
</script> </script>

View File

@@ -4,7 +4,11 @@ declare module 'nitropack'
{ {
interface TaskPayload interface TaskPayload
{ {
type: string type: string;
}
interface TaskResult<RT = unknown>
{
error?: Error | string;
} }
} }
@@ -17,7 +21,7 @@ export default defineEventHandler(async (e) => {
return; return;
} }
const id = getRouterParam(e, 'id'); const id = getRouterParam(e, 'id');
const payload: Record<string, any> = await readBody(e); const body: Record<string, any> = await readBody(e);
if(!id) if(!id)
{ {
@@ -25,8 +29,11 @@ export default defineEventHandler(async (e) => {
return; return;
} }
payload.type = id; body.data = JSON.parse(body.data);
payload.data = JSON.parse(payload.data); const payload = {
type: id,
data: body,
}
const result = await runTask(id, { const result = await runTask(id, {
payload: payload payload: payload
@@ -36,7 +43,7 @@ export default defineEventHandler(async (e) => {
{ {
setResponseStatus(e, 500); setResponseStatus(e, 500);
if(result.error && (result.error as Error).message) if(result.error && result.error.message)
throw result.error; throw result.error;
else if(result.error) else if(result.error)
throw new Error(result.error); throw new Error(result.error);

View File

@@ -1,9 +1,9 @@
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { schema } from '~/schemas/login'; import { schema } from '~/schemas/login';
import type { UserSession, UserSessionRequired } from '~/types/auth'; import type { UserSession, UserSessionRequired } from '~/types/auth';
import { ZodError } from 'zod'; import { ZodError } from 'zod/v4';
import { checkSession, logSession } from '~/server/utils/user'; import { checkSession, logSession } from '~/server/utils/user';
import { usersDataTable, usersTable } from '~/db/schema'; import { usersTable } from '~/db/schema';
import { eq, or, sql } from 'drizzle-orm'; import { eq, or, sql } from 'drizzle-orm';
interface SuccessHandler interface SuccessHandler
@@ -50,7 +50,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
await clearUserSession(e); await clearUserSession(e);
setResponseStatus(e, 401); setResponseStatus(e, 401);
return { success: false, error: new ZodError([{ code: 'custom', path: ['username'], message: 'Identifiant inconnu' }]) }; return { success: false, error: new ZodError([{ code: 'custom', input: undefined, path: ['username'], message: 'Identifiant inconnu' }]) };
} }
const valid = await Bun.password.verify(body.data.password, id.hash); const valid = await Bun.password.verify(body.data.password, id.hash);
@@ -60,7 +60,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
await clearUserSession(e); await clearUserSession(e);
setResponseStatus(e, 401); setResponseStatus(e, 401);
return { success: false, error: new ZodError([{ code: 'custom', path: ['password'], message: 'Mot de passe incorrect' }]) }; return { success: false, error: new ZodError([{ code: 'custom', input: undefined, path: ['password'], message: 'Mot de passe incorrect' }]) };
} }
const user = db.query.usersTable.findFirst({ const user = db.query.usersTable.findFirst({

View File

@@ -1,11 +1,11 @@
import { count, eq, sql } from 'drizzle-orm'; import { count, eq, sql } from 'drizzle-orm';
import { ZodError, type ZodIssue } from 'zod'; import { ZodError } from 'zod/v4';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { usersDataTable, usersTable } from '~/db/schema'; import { usersDataTable, usersTable } from '~/db/schema';
import { schema } from '~/schemas/registration'; import { schema } from '~/schemas/registration';
import { checkSession, logSession } from '~/server/utils/user'; import { checkSession, logSession } from '~/server/utils/user';
import type { UserSession, UserSessionRequired } from '~/types/auth'; import type { UserSession, UserSessionRequired } from '~/types/auth';
import sendMail from '~/server/tasks/mail'; import type { $ZodIssue } from 'zod/v4/core';
interface SuccessHandler interface SuccessHandler
{ {
@@ -47,11 +47,11 @@ export default defineEventHandler(async (e): Promise<Return> => {
const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username }); const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username });
const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email }); const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email });
const errors: ZodIssue[] = []; const errors: $ZodIssue[] = [];
if(!checkUsername || checkUsername.count !== 0) if(!checkUsername || checkUsername.count !== 0)
errors.push({ code: 'custom', path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" }); errors.push({ code: 'custom', input: undefined, path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
if(!checkEmail || checkEmail.count !== 0) if(!checkEmail || checkEmail.count !== 0)
errors.push({ code: 'custom', path: ['email'], message: "Cette adresse mail est déjà utilisée" }); errors.push({ code: 'custom', input: undefined, path: ['email'], message: "Cette adresse mail est déjà utilisée" });
if(errors.length > 0) if(errors.length > 0)
{ {
@@ -83,7 +83,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
id: emailId, timestamp, id: emailId, timestamp,
} }
}); });
await sendMail({ await runTask('mail', {
payload: { payload: {
type: 'mail', type: 'mail',
to: [body.data.email], to: [body.data.email],

View File

@@ -1,6 +1,6 @@
import { hash } from 'bun'; import { hash } from 'bun';
import { eq, or } from 'drizzle-orm'; import { eq, or } from 'drizzle-orm';
import { z } from 'zod'; import { z } from 'zod/v4';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { usersTable } from '~/db/schema'; import { usersTable } from '~/db/schema';
import sendMail from '~/server/tasks/mail'; import sendMail from '~/server/tasks/mail';

View File

@@ -1,11 +1,12 @@
import { count, eq, sql } from 'drizzle-orm'; import { count, eq, sql } from 'drizzle-orm';
import { ZodError, type ZodIssue } from 'zod'; import { ZodError } from 'zod/v4';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { usersDataTable, usersTable } from '~/db/schema'; import { usersDataTable, usersTable } from '~/db/schema';
import { schema } from '~/schemas/registration'; import { schema } from '~/schemas/registration';
import { checkSession, logSession } from '~/server/utils/user'; import { checkSession, logSession } from '~/server/utils/user';
import type { UserSession, UserSessionRequired } from '~/types/auth'; import type { UserSession, UserSessionRequired } from '~/types/auth';
import sendMail from '~/server/tasks/mail'; import sendMail from '~/server/tasks/mail';
import type { $ZodIssue } from 'zod/v4/core';
interface SuccessHandler interface SuccessHandler
{ {
@@ -47,11 +48,11 @@ export default defineEventHandler(async (e): Promise<Return> => {
const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username }); const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username });
const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email }); const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email });
const errors: ZodIssue[] = []; const errors: $ZodIssue[] = [];
if(!checkUsername || checkUsername.count !== 0) if(!checkUsername || checkUsername.count !== 0)
errors.push({ code: 'custom', path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" }); errors.push({ code: 'custom', input: undefined, path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
if(!checkEmail || checkEmail.count !== 0) if(!checkEmail || checkEmail.count !== 0)
errors.push({ code: 'custom', path: ['email'], message: "Cette adresse mail est déjà utilisée" }); errors.push({ code: 'custom', input: undefined, path: ['email'], message: "Cette adresse mail est déjà utilisée" });
if(errors.length > 0) if(errors.length > 0)
{ {

View File

@@ -1,9 +1,9 @@
import { and, eq, SQL, sql, type Operators } from 'drizzle-orm'; import { eq, SQL, type Operators } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { characterTable, userPermissionsTable } from '~/db/schema'; import { characterTable, userPermissionsTable } from '~/db/schema';
import { hasPermissions } from '~/shared/auth.util'; import { hasPermissions } from '~/shared/auth.util';
import { group } from '~/shared/general.util'; import { group } from '~/shared/general.util';
import type { Character, DoubleIndex, Level, MainStat, TrainingLevel } from '~/types/character'; import type { Character, MainStat, TrainingLevel } from '~/types/character';
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
let { visibility } = getQuery(e) as { visibility?: "public" | "own" | "admin" }; let { visibility } = getQuery(e) as { visibility?: "public" | "own" | "admin" };
@@ -16,16 +16,16 @@ export default defineEventHandler(async (e) => {
let where: ((character: typeof characterTable._.config.columns, sql: Operators) => SQL | undefined) | undefined = undefined; let where: ((character: typeof characterTable._.config.columns, sql: Operators) => SQL | undefined) | undefined = undefined;
const db = useDatabase(); const db = useDatabase();
const session = await getUserSession(e);
if(visibility === "own") if(visibility === "own")
{ {
const session = await getUserSession(e);
if(!session.user) if(!session.user)
{ {
setResponseStatus(e, 401); setResponseStatus(e, 401);
return; return;
} }
where = (character, { eq, and }) => and(eq(character.owner, session.user!.id), eq(character.visibility, "private")); where = (character, { eq, and }) => and(eq(character.owner, session.user!.id));
} }
else if(visibility === 'public') else if(visibility === 'public')
{ {
@@ -33,7 +33,6 @@ export default defineEventHandler(async (e) => {
} }
else if(visibility === 'admin') else if(visibility === 'admin')
{ {
const session = await getUserSession(e);
if(!session.user) if(!session.user)
{ {
setResponseStatus(e, 401); setResponseStatus(e, 401);
@@ -55,9 +54,8 @@ export default defineEventHandler(async (e) => {
with: { with: {
abilities: true, abilities: true,
levels: true, levels: true,
modifiers: true,
spells: true,
training: true, training: true,
choices: true,
user: { user: {
columns: { username: true } columns: { username: true }
} }
@@ -74,15 +72,13 @@ export default defineEventHandler(async (e) => {
people: character.people, people: character.people,
level: character.level, level: character.level,
aspect: character.aspect, aspect: character.aspect,
notes: character.notes, notes: { public: character.public_notes, private: session.user?.id === character.owner ? character.private_notes : undefined },
health: character.health, variables: character.variables,
mana: character.mana,
training: character.training.reduce((p, v) => { if(!(v.stat in p)) p[v.stat] = []; p[v.stat].push([v.level as TrainingLevel, v.choice]); return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>), training: character.training.reduce((p, v) => { p[v.stat] ??= {}; p[v.stat][v.level as TrainingLevel] = v.choice; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex<Level>), leveling: group(character.levels, "level", "choice"),
abilities: group(character.abilities.map(e => ({ ...e, value: [e.value, e.max] as [number, number] })), "ability", "value"), abilities: group(character.abilities, "ability", "value"),
spells: character.spells.map(e => e.value), choices: character.choices.reduce((p, v) => { p[v.id] ??= []; p[v.id]?.push(v.choice); return p; }, {} as Record<string, number[]>),
modifiers: group(character.modifiers, "modifier", "value"),
owner: character.owner, owner: character.owner,
username: character.user.username, username: character.user.username,

View File

@@ -1,6 +1,6 @@
import { z } from 'zod'; import { z } from 'zod/v4';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema'; import { characterAbilitiesTable, characterLevelingTable, characterTable, characterTrainingTable } from '~/db/schema';
import { CharacterValidation } from '#shared/character.util'; import { CharacterValidation } from '#shared/character.util';
import { type Ability, type MainStat } from '~/types/character'; import { type Ability, type MainStat } from '~/types/character';
@@ -31,24 +31,19 @@ export default defineEventHandler(async (e) => {
people: body.data.people!, people: body.data.people!,
level: body.data.level, level: body.data.level,
aspect: body.data.aspect, aspect: body.data.aspect,
notes: body.data.notes, public_notes: body.data.notes.public,
health: body.data.health, private_notes: body.data.notes.private,
mana: body.data.mana, variables: body.data.variables,
visibility: body.data.visibility, visibility: body.data.visibility,
thumbnail: body.data.thumbnail, thumbnail: body.data.thumbnail,
}).returning({ id: characterTable.id }).get().id; }).returning({ id: characterTable.id }).get().id;
if(body.data.leveling.length > 0) tx.insert(characterLevelingTable).values(body.data.leveling.map(e => ({ character: id, level: e[0], choice: e[1] }))).run(); if(Object.keys(body.data.leveling).length > 0) tx.insert(characterLevelingTable).values(Object.entries(body.data.leveling).filter(e => e[1] !== undefined).map(e => ({ character: id, level: parseInt(e[0], 10), choice: e[1]! }))).run();
const training = Object.entries(body.data.training).flatMap(e => e[1].map(_e => ({ character: id, stat: e[0] as MainStat, level: _e[0], choice: _e[1] }))); const training = Object.entries(body.data.training).flatMap(e => Object.entries(e[1]).filter(e => e[1] !== undefined).map(_e => ({ character: id, stat: e[0] as MainStat, level: parseInt(_e[0], 10), choice: _e[1]! })));
if(training.length > 0) tx.insert(characterTrainingTable).values(training).run(); if(training.length > 0) tx.insert(characterTrainingTable).values(training).run();
const modifiers = Object.entries(body.data.modifiers).map((e) => ({ character: id, modifier: e[0] as MainStat, value: e[1] })); const abilities = Object.entries(body.data.abilities).filter(e => e[1] !== undefined).map(e => ({ character: id, ability: e[0] as Ability, value: e[1] }));
if(modifiers.length > 0) tx.insert(characterModifiersTable).values(modifiers).run();
if(body.data.spells.length > 0) tx.insert(characterSpellsTable).values(body.data.spells.map(e => ({ character: id, value: e }))).run();
const abilities = Object.entries(body.data.abilities).map(e => ({ character: id, ability: e[0] as Ability, value: e[1][0], max: e[1][1] }));
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run(); if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
return id; return id;

View File

@@ -1,8 +1,7 @@
import { and, eq, sql } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema'; import { characterTable } from '~/db/schema';
import { group } from '~/shared/general.util'; import { group } from '~/shared/general.util';
import type { Character, DoubleIndex, Level, MainStat, TrainingLevel } from '~/types/character'; import type { Character, CharacterVariables, Level, MainStat, TrainingLevel } from '~/types/character';
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id"); const id = getRouterParam(e, "id");
@@ -26,9 +25,8 @@ export default defineEventHandler(async (e) => {
with: { with: {
abilities: true, abilities: true,
levels: true, levels: true,
modifiers: true,
spells: true,
training: true, training: true,
choices: true,
user: { user: {
columns: { username: true } columns: { username: true }
} }
@@ -45,15 +43,13 @@ export default defineEventHandler(async (e) => {
people: character.people, people: character.people,
level: character.level, level: character.level,
aspect: character.aspect, aspect: character.aspect,
notes: character.notes, notes: { public: character.public_notes, private: session.user?.id === character.owner ? character.private_notes : undefined },
health: character.health, variables: character.variables,
mana: character.mana,
training: character.training.reduce((p, v) => { if(!(v.stat in p)) p[v.stat] = []; p[v.stat].push([v.level as TrainingLevel, v.choice]); return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>), training: character.training.reduce((p, v) => { p[v.stat] ??= {}; p[v.stat][v.level as TrainingLevel] = v.choice; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex<Level>), leveling: group(character.levels, "level", "choice"),
abilities: group(character.abilities.map(e => ({ ...e, value: [e.value, e.max] as [number, number] })), "ability", "value"), abilities: group(character.abilities, "ability", "value"),
spells: character.spells.map(e => e.value), choices: character.choices.reduce((p, v) => { p[v.id] ??= []; p[v.id]?.push(v.choice); return p; }, {} as Record<string, number[]>),
modifiers: group(character.modifiers, "modifier", "value"),
owner: character.owner, owner: character.owner,
username: character.user.username, username: character.user.username,

View File

@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema'; import { characterAbilitiesTable, characterChoicesTable, characterLevelingTable, characterTable, characterTrainingTable } from '~/db/schema';
import { CharacterValidation } from '#shared/character.util'; import { CharacterValidation } from '#shared/character.util';
import { type Ability, type MainStat } from '~/types/character'; import { type Ability, type MainStat } from '~/types/character';
@@ -42,35 +42,28 @@ export default defineEventHandler(async (e) => {
people: body.data.people!, people: body.data.people!,
level: body.data.level, level: body.data.level,
aspect: body.data.aspect, aspect: body.data.aspect,
notes: body.data.notes, public_notes: body.data.notes.public,
health: body.data.health, private_notes: body.data.notes.private,
mana: body.data.mana, variables: body.data.variables,
visibility: body.data.visibility, visibility: body.data.visibility,
thumbnail: body.data.thumbnail, thumbnail: body.data.thumbnail,
}).where(eq(characterTable.id, id)).run(); }).where(eq(characterTable.id, id)).run();
tx.delete(characterLevelingTable).where(eq(characterLevelingTable.character, id)).run(); tx.delete(characterLevelingTable).where(eq(characterLevelingTable.character, id)).run();
tx.delete(characterTrainingTable).where(eq(characterTrainingTable.character, id)).run(); tx.delete(characterTrainingTable).where(eq(characterTrainingTable.character, id)).run();
tx.delete(characterModifiersTable).where(eq(characterModifiersTable.character, id)).run();
tx.delete(characterSpellsTable).where(eq(characterSpellsTable.character, id)).run();
tx.delete(characterAbilitiesTable).where(eq(characterAbilitiesTable.character, id)).run(); tx.delete(characterAbilitiesTable).where(eq(characterAbilitiesTable.character, id)).run();
tx.delete(characterChoicesTable).where(eq(characterChoicesTable.character, id)).run();
if(body.data.leveling.length > 0) tx.insert(characterLevelingTable).values(body.data.leveling.map(e => ({ character: id, level: e[0], choice: e[1] }))).run(); const leveling = Object.entries(body.data.leveling).filter(e => e[1] !== undefined).map(e => ({ character: id, level: parseInt(e[0]), choice: e[1]! }));
if(leveling.length > 0) tx.insert(characterLevelingTable).values(leveling).run();
const training = Object.entries(body.data.training).flatMap(e => e[1].map(_e => ({ character: id, stat: e[0] as MainStat, level: _e[0], choice: _e[1] }))); const training = Object.entries(body.data.training).flatMap(e => Object.entries(e[1]).filter(_e => _e[1] !== undefined).map(_e => ({ character: id, stat: e[0] as MainStat, level: parseInt(_e[0]), choice: _e[1]! })));
if(training.length > 0) tx.insert(characterTrainingTable).values(training).run(); if(training.length > 0) tx.insert(characterTrainingTable).values(training).run();
const modifiers = Object.entries(body.data.modifiers).map((e) => ({ character: id, modifier: e[0] as MainStat, value: e[1] })); const abilities = Object.entries(body.data.abilities).filter(e => e[1] !== undefined).map(e => ({ character: id, ability: e[0] as Ability, value: e[1], max: 0 }));
if(modifiers.length > 0) tx.insert(characterModifiersTable).values(modifiers).run();
if(body.data.spells.length > 0) tx.insert(characterSpellsTable).values(body.data.spells.map(e => ({ character: id, value: e }))).run();
const abilities = Object.entries(body.data.abilities).map(e => ({ character: id, ability: e[0] as Ability, value: e[1][0], max: e[1][1] }));
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run(); if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
}); });
await useStorage('cache').removeItem(`nitro:functions:character:${id}.json`);
setResponseStatus(e, 200); setResponseStatus(e, 200);
return; return;
}); });

View File

@@ -1,149 +0,0 @@
import useDatabase from '~/composables/useDatabase';
import { type Character, type CharacterConfig, type CompiledCharacter, type DoubleIndex, type Level, type MainStat, type TrainingLevel, type TrainingOption } from '~/types/character';
import characterData from '#shared/character-config.json';
import { group } from '#shared/general.util';
import { defaultCharacter, MAIN_STATS } from '#shared/character.util';
export default defineCachedEventHandler(async (e) => {
const id = getRouterParam(e, "id");
if(!id)
{
setResponseStatus(e, 400);
return;
}
const db = useDatabase();
const character = db.query.characterTable.findFirst({
with: {
abilities: true,
levels: true,
modifiers: true,
spells: true,
training: true,
user: {
columns: { username: true }
}
},
where: (character, { eq }) => eq(character.id, parseInt(id, 10)),
}).sync();
if(character !== undefined)
{
return compileCharacter(Object.assign(defaultCharacter, {
id: character.id,
name: character.name,
people: character.people,
level: character.level,
aspect: character.aspect,
notes: character.notes,
health: character.health,
mana: character.mana,
training: character.training.reduce((p, v) => { if(!(v.stat in p)) p[v.stat] = []; p[v.stat].push([v.level as TrainingLevel, v.choice]); return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>),
leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex<Level>),
abilities: group(character.abilities.map(e => ({ ...e, value: [e.value, e.max] as [number, number] })), "ability", "value"),
spells: character.spells.map(e => e.value),
modifiers: group(character.modifiers, "modifier", "value"),
owner: character.owner,
username: character.user.username,
visibility: character.visibility,
} as Character) as Character);
}
setResponseStatus(e, 404);
return;
}, { name: "character", getKey: (e) => getRouterParam(e, "id") || 'error' });
function compileCharacter(character: Character & { username?: string }): CompiledCharacter
{
const config = characterData as CharacterConfig;
const race = character.people !== undefined ? config.peoples[character.people] : undefined;
const raceOptions = race ? character.leveling!.map(e => race.options[e[0]][e[1]]) : [];
const features = Object.entries(config.training).map(e => [e[0], getFeaturesOf(e[0] as MainStat, character.training[e[0] as MainStat])]) as [MainStat, TrainingOption[]][];
const compiled: CompiledCharacter = {
id: character.id,
owner: character.owner,
username: character.username,
name: character.name,
health: raceOptions.reduce((p, v) => p + (v.health ?? 0), 0),
mana: raceOptions.reduce((p, v) => p + (v.mana ?? 0), 0),
race: character.people!,
modifier: features.map(e => [e[0], Math.floor((e[1].length - 1) / 3) + (character.modifiers[e[0]] ?? 0)] as [MainStat, number]).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<MainStat, number>),
level: character.level,
values: {
health: character.health,
mana: character.mana
},
features: {
action: [],
reaction: [],
freeaction: [],
passive: [],
},
abilities: {
athletics: 0,
acrobatics: 0,
intimidation: 0,
sleightofhand: 0,
stealth: 0,
survival: 0,
investigation: 0,
history: 0,
religion: 0,
arcana: 0,
understanding: 0,
perception: 0,
performance: 0,
medecine: 0,
persuasion: 0,
animalhandling: 0,
deception: 0
},
spellslots: 0,
artslots: 0,
spellranks: {
instinct: 0,
knowledge: 0,
precision: 0,
arts: 0,
},
spells: character.spells ?? [],
speed: false,
defense: {
hardcap: Infinity,
static: 6,
activeparry: 0,
activedodge: 0,
passiveparry: 0,
passivedodge: 0,
},
mastery: {
strength: 0,
dexterity: 0,
shield: 0,
armor: 0,
multiattack: 1,
magicpower: 0,
magicspeed: 0,
magicelement: 0,
magicinstinct: 0,
},
resistance: {},//Object.fromEntries(MAIN_STATS.map(e => [e as MainStat, [0, 0]])) as Record<MainStat, [number, number]>,
initiative: 0,
aspect: "",
notes: character.notes ?? "",
};
//features.forEach(e => e[1].forEach(_e => _e.features?.forEach(f => applyFeature(compiled, f))));
return compiled;
}
export function getFeaturesOf(stat: MainStat, progression: DoubleIndex<TrainingLevel>[]): TrainingOption[]
{
const config = characterData as CharacterConfig;
return progression.map(e => config.training[stat][e[0]][e[1]]);
}

View File

@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema'; import { characterAbilitiesTable, characterLevelingTable, characterTable, characterTrainingTable } from '~/db/schema';
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id"); const id = getRouterParam(e, "id");
@@ -36,8 +36,7 @@ export default defineEventHandler(async (e) => {
level: old.level, level: old.level,
aspect: old.aspect, aspect: old.aspect,
notes: old.notes, notes: old.notes,
health: old.health, variables: old.variables,
mana: old.mana,
visibility: old.visibility, visibility: old.visibility,
thumbnail: old.thumbnail, thumbnail: old.thumbnail,
}).returning({ id: characterTable.id }).get().id; }).returning({ id: characterTable.id }).get().id;
@@ -48,12 +47,6 @@ export default defineEventHandler(async (e) => {
const training = tx.select().from(characterTrainingTable).where(eq(characterTrainingTable.character, parseInt(id, 10))).all(); const training = tx.select().from(characterTrainingTable).where(eq(characterTrainingTable.character, parseInt(id, 10))).all();
if(training.length > 0) tx.insert(characterTrainingTable).values(training.map(e => ({ character: _id, stat: e.stat, level: e.level, choice: e.choice }))).run(); if(training.length > 0) tx.insert(characterTrainingTable).values(training.map(e => ({ character: _id, stat: e.stat, level: e.level, choice: e.choice }))).run();
const modifiers = tx.select().from(characterModifiersTable).where(eq(characterModifiersTable.character, parseInt(id, 10))).all();
if(modifiers.length > 0) tx.insert(characterModifiersTable).values(modifiers.map(e => ({ character: _id, modifier: e.modifier, value: e.value }))).run();
const spells = tx.select().from(characterSpellsTable).where(eq(characterSpellsTable.character, parseInt(id, 10))).all();
if(spells.length > 0) tx.insert(characterSpellsTable).values(spells.map(e => ({ character: _id, value: e.value }))).run();
const abilities = tx.select().from(characterAbilitiesTable).where(eq(characterAbilitiesTable.character, parseInt(id, 10))).all(); const abilities = tx.select().from(characterAbilitiesTable).where(eq(characterAbilitiesTable.character, parseInt(id, 10))).all();
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities.map(e => ({ character: _id, ability: e.ability, value: e.value, max: e.max }))).run(); if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities.map(e => ({ character: _id, ability: e.ability, value: e.value, max: e.max }))).run();

View File

@@ -1,7 +1,7 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase'; import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema'; import { characterTable } from '~/db/schema';
import type { CharacterValues } from '~/types/character'; import { CharacterNotesValidation } from '#shared/character.util';
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id"); const id = getRouterParam(e, "id");
@@ -11,11 +11,12 @@ export default defineEventHandler(async (e) => {
return; return;
} }
const body = await readBody(e) as CharacterValues; const body = await readValidatedBody(e, CharacterNotesValidation.safeParse);
if(!body) if(!body.success)
{ {
console.error(body.error);
setResponseStatus(e, 400); setResponseStatus(e, 400);
return; throw body.error;
} }
const db = useDatabase(); const db = useDatabase();
@@ -35,8 +36,8 @@ export default defineEventHandler(async (e) => {
} }
db.update(characterTable).set({ db.update(characterTable).set({
health: body.health, public_notes: body.data.public,
mana: body.mana, private_notes: body.data.private,
}).where(eq(characterTable.id, parseInt(id, 10))).run(); }).where(eq(characterTable.id, parseInt(id, 10))).run();
setResponseStatus(e, 200); setResponseStatus(e, 200);

Some files were not shown because too many files have changed in this diff Show More