You've already forked obsidian-visualiser
Compare commits
39 Commits
master
...
1642cd513f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1642cd513f | ||
|
|
b1ac379f1a | ||
|
|
423df7bc42 | ||
| c93cc4078c | |||
|
|
17bc232602 | ||
|
|
042d4479ee | ||
|
|
da93fcd82d | ||
|
|
80a94bee86 | ||
|
|
5387dc66c3 | ||
|
|
6fe3746df4 | ||
| 893247e1eb | |||
|
|
69ee62c08e | ||
| 247b14b2c8 | |||
|
|
658499749d | ||
|
|
06276b3fbc | ||
|
|
72982a4ea9 | ||
|
|
4e5ea504ea | ||
| 920ce2e1b6 | |||
|
|
86556ec604 | ||
|
|
7d6f9162ed | ||
| 3ef98df5d2 | |||
|
|
ba8c7b05e6 | ||
|
|
a8dcc47a1b | ||
| 996b9711e4 | |||
|
|
da5c1202ed | ||
| c33bd95b81 | |||
|
|
d5851499cd | ||
|
|
e78a60f771 | ||
|
|
9a6f91a341 | ||
|
|
218b68db60 | ||
|
|
42915d699f | ||
|
|
9ca546f490 | ||
|
|
2c80cb2456 | ||
|
|
6100fd9411 | ||
| 1d41514b26 | |||
| 227d7224e5 | |||
| f49fdaac79 | |||
| 41c19b4bfb | |||
|
|
c0e625a8cb |
158
app.vue
158
app.vue
@@ -1,27 +1,56 @@
|
||||
<template>
|
||||
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
||||
<NuxtRouteAnnouncer/>
|
||||
<NuxtLoadingIndicator />
|
||||
<TooltipProvider>
|
||||
<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 relative">
|
||||
<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 />
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
<Toaster v-model="list" />
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
provideToaster();
|
||||
import { Content } from '#shared/content.util';
|
||||
import * as Floating from '#shared/floating.util';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
const { list } = useToast();
|
||||
onBeforeMount(() => {
|
||||
Content.init();
|
||||
Floating.init();
|
||||
Toaster.init();
|
||||
|
||||
const unmount = useRouter().afterEach((to, from, failure) => {
|
||||
if(failure) return;
|
||||
|
||||
document.querySelectorAll(`a[href="${from.path}"][data-active]`).forEach(e => e.classList.remove(e.getAttribute('data-active') ?? ''));
|
||||
document.querySelectorAll(`a[href="${to.path}"][data-active]`).forEach(e => e.classList.add(e.getAttribute('data-active') ?? ''));
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
unmount();
|
||||
})
|
||||
});
|
||||
</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;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
@@ -39,6 +68,123 @@ const { list } = useToast();
|
||||
@apply bg-light-50;
|
||||
@apply dark:bg-dark-50;
|
||||
}
|
||||
.callout[data-type="abstract"],
|
||||
.callout[data-type="summary"],
|
||||
.callout[data-type="tldr"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="info"]
|
||||
{
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[data-type="todo"]
|
||||
{
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[data-type="important"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="tip"],
|
||||
.callout[data-type="hint"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="success"],
|
||||
.callout[data-type="check"],
|
||||
.callout[data-type="done"]
|
||||
{
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply text-light-green;
|
||||
@apply dark:text-dark-green;
|
||||
}
|
||||
.callout[data-type="question"],
|
||||
.callout[data-type="help"],
|
||||
.callout[data-type="faq"]
|
||||
{
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[data-type="warning"],
|
||||
.callout[data-type="caution"],
|
||||
.callout[data-type="attention"]
|
||||
{
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[data-type="failure"],
|
||||
.callout[data-type="fail"],
|
||||
.callout[data-type="missing"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="danger"],
|
||||
.callout[data-type="error"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="bug"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="example"]
|
||||
{
|
||||
@apply bg-light-purple;
|
||||
@apply dark:bg-dark-purple;
|
||||
@apply text-light-purple;
|
||||
@apply dark:text-dark-purple;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
@apply bg-transparent;
|
||||
|
||||
@@ -1,931 +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 getID(length: number)
|
||||
{
|
||||
for (var id = [], i = 0; i < length; i++)
|
||||
id.push((16 * Math.random() | 0).toString(16));
|
||||
return id.join("");
|
||||
}
|
||||
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 } 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, w: width, h: height };
|
||||
});
|
||||
const updateScaleVar = useDebounceFn(() => {
|
||||
if(transformRef.value)
|
||||
{
|
||||
console.log(zoom.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;
|
||||
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)
|
||||
{
|
||||
let box = canvasRef.value?.getBoundingClientRect()!;
|
||||
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(16), 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(16), 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>
|
||||
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<Editor ref="editor" v-model="model" autofocus :gutters="false" />
|
||||
<iframe ref="iframe" class="w-full h-full border-0" sandbox="allow-same-origin allow-scripts"></iframe>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const model = defineModel<string>();
|
||||
const editor = useTemplateRef('editor'), iframe = useTemplateRef('iframe');
|
||||
|
||||
onMounted(() => {
|
||||
if(iframe.value && iframe.value.contentDocument && editor.value)
|
||||
{
|
||||
editor.value.$el.remove();
|
||||
|
||||
iframe.value.contentDocument.documentElement.setAttribute('class', document.documentElement.getAttribute('class') ?? '');
|
||||
iframe.value.contentDocument.documentElement.setAttribute('style', document.documentElement.getAttribute('style') ?? '');
|
||||
|
||||
const base = iframe.value.contentDocument.head.appendChild(iframe.value.contentDocument.createElement('base'));
|
||||
base.setAttribute('href', window.location.href);
|
||||
|
||||
for(let element of document.getElementsByTagName('link'))
|
||||
{
|
||||
if(element.getAttribute('rel') === 'stylesheet')
|
||||
iframe.value.contentDocument.head.appendChild(element.cloneNode(true));
|
||||
}
|
||||
|
||||
for(let element of document.getElementsByTagName('style'))
|
||||
{
|
||||
iframe.value.contentDocument.head.appendChild(element.cloneNode(true));
|
||||
}
|
||||
|
||||
iframe.value.contentDocument.body.setAttribute('class', document.body.getAttribute('class') ?? '');
|
||||
iframe.value.contentDocument.body.setAttribute('style', document.body.getAttribute('style') ?? '');
|
||||
|
||||
iframe.value.contentDocument.body.appendChild(editor.value.$el);
|
||||
|
||||
editor.value.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,49 +1,20 @@
|
||||
<template>
|
||||
<div v-if="content && content.length > 0">
|
||||
<ProsesRenderer #default v-if="data" :node="data" :proses="proses" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue';
|
||||
import { heading } from 'hast-util-heading';
|
||||
import { headingRank } from 'hast-util-heading-rank';
|
||||
import { parseId } from '~/shared/general.util';
|
||||
import type { Root } from 'hast';
|
||||
|
||||
const { content, proses, filter } = defineProps<{
|
||||
content: string
|
||||
proses?: Record<string, string | Component>
|
||||
filter?: string
|
||||
import render, { type MDProperties } from '#shared/markdown.util'
|
||||
const { content, filter, properties } = defineProps<{
|
||||
content?: string,
|
||||
filter?: string,
|
||||
properties?: MDProperties
|
||||
}>();
|
||||
|
||||
const parser = useMarkdown(), data = ref<Root>();
|
||||
const node = computed(() => content ? parser(content) : undefined);
|
||||
watch([node], () => {
|
||||
if(!node.value)
|
||||
data.value = undefined;
|
||||
else if(!filter)
|
||||
{
|
||||
data.value = node.value;
|
||||
}
|
||||
else
|
||||
{
|
||||
const start = node.value?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
|
||||
const container = useTemplateRef('container');
|
||||
|
||||
if(start === -1)
|
||||
data.value = node.value;
|
||||
else
|
||||
{
|
||||
let end = start;
|
||||
const rank = headingRank(node.value.children[start])!;
|
||||
while(end < node.value.children.length)
|
||||
{
|
||||
end++;
|
||||
if(heading(node.value.children[end]) && headingRank(node.value.children[end])! <= rank)
|
||||
break;
|
||||
}
|
||||
data.value = { ...node.value, children: node.value.children.slice(start, end) };
|
||||
}
|
||||
}
|
||||
}, { immediate: true, });
|
||||
</script>
|
||||
content && onMounted(() => {
|
||||
queueMicrotask(() => {
|
||||
container.value && content && container.value.replaceChildren(render(content, filter, properties));
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="container"></div>
|
||||
</template>
|
||||
@@ -1,115 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { RootContent, Root } from 'hast';
|
||||
import { Text, Comment } from 'vue';
|
||||
|
||||
import ProseP from '~/components/prose/ProseP.vue';
|
||||
import ProseA from '~/components/prose/ProseA.vue';
|
||||
import ProseBlockquote from '~/components/prose/ProseBlockquote.vue';
|
||||
import ProseCallout from './prose/ProseCallout.vue';
|
||||
import ProseCode from '~/components/prose/ProseCode.vue';
|
||||
import ProsePre from '~/components/prose/ProsePre.vue';
|
||||
import ProseEm from '~/components/prose/ProseEm.vue';
|
||||
import ProseH1 from '~/components/prose/ProseH1.vue';
|
||||
import ProseH2 from '~/components/prose/ProseH2.vue';
|
||||
import ProseH3 from '~/components/prose/ProseH3.vue';
|
||||
import ProseH4 from '~/components/prose/ProseH4.vue';
|
||||
import ProseH5 from '~/components/prose/ProseH5.vue';
|
||||
import ProseH6 from '~/components/prose/ProseH6.vue';
|
||||
import ProseHr from '~/components/prose/ProseHr.vue';
|
||||
import ProseImg from '~/components/prose/ProseImg.vue';
|
||||
import ProseUl from '~/components/prose/ProseUl.vue';
|
||||
import ProseOl from '~/components/prose/ProseOl.vue';
|
||||
import ProseLi from '~/components/prose/ProseLi.vue';
|
||||
import ProseSmall from './prose/ProseSmall.vue';
|
||||
import ProseStrong from '~/components/prose/ProseStrong.vue';
|
||||
import ProseTable from '~/components/prose/ProseTable.vue';
|
||||
import ProseTag from '~/components/prose/ProseTag.vue';
|
||||
import ProseThead from '~/components/prose/ProseThead.vue';
|
||||
import ProseTbody from '~/components/prose/ProseTbody.vue';
|
||||
import ProseTd from '~/components/prose/ProseTd.vue';
|
||||
import ProseTh from '~/components/prose/ProseTh.vue';
|
||||
import ProseTr from '~/components/prose/ProseTr.vue';
|
||||
import ProseScript from '~/components/prose/ProseScript.vue';
|
||||
|
||||
const proseList = {
|
||||
"p": ProseP,
|
||||
"a": ProseA,
|
||||
"blockquote": ProseBlockquote,
|
||||
"callout": ProseCallout,
|
||||
"code": ProseCode,
|
||||
"pre": ProsePre,
|
||||
"em": ProseEm,
|
||||
"h1": ProseH1,
|
||||
"h2": ProseH2,
|
||||
"h3": ProseH3,
|
||||
"h4": ProseH4,
|
||||
"h5": ProseH5,
|
||||
"h6": ProseH6,
|
||||
"hr": ProseHr,
|
||||
"img": ProseImg,
|
||||
"ul": ProseUl,
|
||||
"ol": ProseOl,
|
||||
"li": ProseLi,
|
||||
"small": ProseSmall,
|
||||
"strong": ProseStrong,
|
||||
"table": ProseTable,
|
||||
"tag": ProseTag,
|
||||
"thead": ProseThead,
|
||||
"tbody": ProseTbody,
|
||||
"td": ProseTd,
|
||||
"th": ProseTh,
|
||||
"tr": ProseTr,
|
||||
"script": ProseScript
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MarkdownRenderer',
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
proses: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
async setup(props) {
|
||||
if(props.proses)
|
||||
{
|
||||
for(const prose of Object.keys(props.proses))
|
||||
{
|
||||
if(typeof props.proses[prose] === 'string')
|
||||
props.proses[prose] = await resolveComponent(props.proses[prose]);
|
||||
}
|
||||
}
|
||||
return { tags: Object.assign({}, proseList, props.proses) };
|
||||
},
|
||||
render(ctx: any) {
|
||||
const { node, tags } = ctx;
|
||||
|
||||
if(!node)
|
||||
return null;
|
||||
|
||||
return h('div', null, {default: () => (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||
}
|
||||
});
|
||||
|
||||
function renderNode(node: RootContent, tags: Record<string, any>): VNode | undefined
|
||||
{
|
||||
if(node.type === 'text' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
return h(Text, node.value);
|
||||
}
|
||||
else if(node.type === 'comment' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
return h(Comment, node.value);
|
||||
}
|
||||
else if(node.type === 'element')
|
||||
{
|
||||
return h(tags[node.tagName] ?? node.tagName, { ...node.properties, class: node.properties.className }, { default: () => node.children.map(e => renderNode(e, tags)).filter(e => !!e) });
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Label class="flex justify-center items-center my-2">
|
||||
<span class="md:text-base text-sm">{{ label }}</span>
|
||||
<SwitchRoot v-model:checked="model" :disabled="disabled"
|
||||
<SwitchRoot v-model:checked="model" :disabled="disabled" :default-checked="defaultValue"
|
||||
class="group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
||||
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative">
|
||||
@@ -21,6 +21,7 @@ const { label, disabled, onIcon, offIcon } = defineProps<{
|
||||
disabled?: boolean
|
||||
onIcon?: string
|
||||
offIcon?: string
|
||||
defaultValue?: boolean
|
||||
}>();
|
||||
const model = defineModel<boolean>();
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -1,90 +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-70 dark:text-dark-70" 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-redBack dark:group-data-[type=error]:bg-dark-redBack group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red
|
||||
group-data-[type=success]:bg-light-greenBack dark:group-data-[type=success]:bg-dark-greenBack group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green" @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-redBack;
|
||||
@apply dark:bg-dark-redBack;
|
||||
}
|
||||
.ToastRoot[data-type='success'] {
|
||||
@apply border-light-green;
|
||||
@apply dark:border-dark-green;
|
||||
@apply bg-light-greenBack;
|
||||
@apply dark:bg-dark-greenBack;
|
||||
}
|
||||
.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>
|
||||
@@ -4,7 +4,7 @@
|
||||
<span tabindex="0"><slot></slot></span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent 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" :side="side" :side-offset="['left', 'right'].includes(side ?? '') ? 8 : 0">
|
||||
<TooltipContent 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" :class="$attrs.class" :side="side" :align="align" :align-offset="-16" :side-offset="['left', 'right'].includes(side ?? '') ? 8 : 0">
|
||||
{{ message }}
|
||||
<TooltipArrow class="fill-light-30 dark:fill-dark-30"></TooltipArrow>
|
||||
</TooltipContent>
|
||||
@@ -15,9 +15,10 @@
|
||||
<script setup lang="ts">
|
||||
const { message, delay = 300, side } = defineProps<{
|
||||
message: string
|
||||
delay?: number,
|
||||
delay?: number
|
||||
disabled?: boolean
|
||||
side?: 'left' | 'right' | 'top' | 'bottom'
|
||||
align?: 'start' | 'center' | 'end'
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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?.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>
|
||||
@@ -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?.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, w: number, h: 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) * w, 64);
|
||||
realh = Math.max(realh + (e.movementY / zoom) * h, 64);
|
||||
|
||||
const result = e.altKey ? undefined : snap({ ...node, x: realx, y: realy, width: realw, height: realh }, { x, y, w, h });
|
||||
|
||||
node.x = result?.x ?? realx;
|
||||
node.y = result?.y ?? realy;
|
||||
node.width = result?.w ?? realw;
|
||||
node.height = result?.h ?? 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>
|
||||
@@ -1,297 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { type Position } from '#shared/canvas.util';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
|
||||
const cancelEvent = (e: Event) => e.preventDefault();
|
||||
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);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
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 { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { clamp } from '#shared/general.util';
|
||||
import type { CanvasContent } from '~/types/content';
|
||||
|
||||
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 loading = ref(false);
|
||||
if(overview.value && !overview.value.content)
|
||||
{
|
||||
loading.value = true;
|
||||
await get(path);
|
||||
loading.value = false;
|
||||
}
|
||||
const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined);
|
||||
console.log(canvas.value);
|
||||
|
||||
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
||||
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');
|
||||
|
||||
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);
|
||||
|
||||
const reset = (_: MouseEvent) => {
|
||||
zoom.value = minZoom.value;
|
||||
|
||||
dispX.value = 0;
|
||||
dispY.value = 0;
|
||||
|
||||
updateTransform();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
let lastX = 0, lastY = 0, lastDistance = 0;
|
||||
let box = canvasRef.value?.getBoundingClientRect()!;
|
||||
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);
|
||||
});
|
||||
})
|
||||
window.addEventListener('resize', () => box = canvasRef.value?.getBoundingClientRect()!);
|
||||
canvasRef.value?.addEventListener('mousedown', (e) => {
|
||||
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 = (box.x + box.width / 2), centerY = (box.y + box.height / 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)
|
||||
|
||||
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;
|
||||
lastDistance = dist;
|
||||
|
||||
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();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none">
|
||||
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4">
|
||||
<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>
|
||||
<NuxtLink v-if="overview && isOwner || hasPermissions(user?.permissions ?? [], ['admin', 'editor'])" :to="{ name: 'explore-edit', hash: `#${encodeURIComponent(overview!.path)}` }" class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||
<Tooltip message="Modifier" side="right">
|
||||
<Icon icon="radix-icons:pencil-1" class="w-8 h-8 p-2" />
|
||||
</Tooltip>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div ref="transformRef" :style="{
|
||||
'transform-origin': 'center center',
|
||||
}" class="h-full">
|
||||
<div v-if="canvas" class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
|
||||
<div>
|
||||
<CanvasNode v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom" />
|
||||
</div>
|
||||
<div>
|
||||
<CanvasEdge v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,30 +1,22 @@
|
||||
<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>
|
||||
<span ref="container"></span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { parseURL } from 'ufo';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { iconByType } from '#shared/general.util';
|
||||
import proses, { preview } from '#shared/proses';
|
||||
import { text } from '#shared/dom.util';
|
||||
|
||||
const { href } = defineProps<{
|
||||
href: string
|
||||
class?: string
|
||||
const { href, label } = defineProps<{
|
||||
href: string,
|
||||
label: string
|
||||
}>();
|
||||
|
||||
const { hash, pathname } = parseURL(href);
|
||||
const container = useTemplateRef('container');
|
||||
|
||||
const { content } = useContent();
|
||||
const overview = computed(() => content.value.find(e => e.path === decodeURIComponent(pathname)));
|
||||
onMounted(() => {
|
||||
queueMicrotask(() => {
|
||||
container.value && container.value.appendChild(proses('a', preview, [ text(label) ], { href }) as HTMLElement);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -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="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/general.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>
|
||||
@@ -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>
|
||||
@@ -1,146 +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 { ref } from 'vue';
|
||||
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>
|
||||
|
||||
<style>
|
||||
.callout[data-type="abstract"],
|
||||
.callout[data-type="summary"],
|
||||
.callout[data-type="tldr"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="info"]
|
||||
{
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[data-type="todo"]
|
||||
{
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[data-type="important"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="tip"],
|
||||
.callout[data-type="hint"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="success"],
|
||||
.callout[data-type="check"],
|
||||
.callout[data-type="done"]
|
||||
{
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply text-light-green;
|
||||
@apply dark:text-dark-green;
|
||||
}
|
||||
.callout[data-type="question"],
|
||||
.callout[data-type="help"],
|
||||
.callout[data-type="faq"]
|
||||
{
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[data-type="warning"],
|
||||
.callout[data-type="caution"],
|
||||
.callout[data-type="attention"]
|
||||
{
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[data-type="failure"],
|
||||
.callout[data-type="fail"],
|
||||
.callout[data-type="missing"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="danger"],
|
||||
.callout[data-type="error"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="bug"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="example"]
|
||||
{
|
||||
@apply bg-light-purple;
|
||||
@apply dark:bg-dark-purple;
|
||||
@apply text-light-purple;
|
||||
@apply dark:text-dark-purple;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<em>
|
||||
<slot />
|
||||
</em>
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<ol>
|
||||
<slot />
|
||||
</ol>
|
||||
</template>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<p><slot /></p>
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<small class="text-light-60 dark:text-dark-60 text-sm italic">
|
||||
<slot />
|
||||
</small>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<strong>
|
||||
<slot />
|
||||
</strong>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<table class="mx-4 my-8 border-collapse border border-light-35 dark:border-dark-35">
|
||||
<slot />
|
||||
</table>
|
||||
</template>
|
||||
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
</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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<tbody>
|
||||
<slot />
|
||||
</tbody>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<td class="border border-light-35 dark:border-dark-35 py-1 px-2">
|
||||
<slot />
|
||||
</td>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-4 first:pt-0">
|
||||
<slot />
|
||||
</th>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<thead>
|
||||
<slot />
|
||||
</thead>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<tr>
|
||||
<slot />
|
||||
</tr>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<ul>
|
||||
<slot />
|
||||
</ul>
|
||||
</template>
|
||||
@@ -1,40 +1,45 @@
|
||||
import { Content } from '~/shared/content.util';
|
||||
import type { ExploreContent, ContentComposable, TreeItem } from '~/types/content';
|
||||
|
||||
const useContentState = () => useState<ExploreContent[]>('content', () => []);
|
||||
|
||||
export function useContent(): ContentComposable {
|
||||
const contentState = useContentState();
|
||||
return {
|
||||
content: contentState,
|
||||
tree: computed(() => {
|
||||
const arr: TreeItem[] = [];
|
||||
for(const element of contentState.value)
|
||||
{
|
||||
addChild(arr, element);
|
||||
}
|
||||
return arr;
|
||||
}),
|
||||
fetch,
|
||||
get,
|
||||
}
|
||||
const contentState = useContentState();
|
||||
|
||||
return {
|
||||
content: contentState,
|
||||
tree: computed(() => {
|
||||
const arr: TreeItem[] = [];
|
||||
for(const element of contentState.value)
|
||||
{
|
||||
addChild(arr, element);
|
||||
}
|
||||
return arr;
|
||||
}),
|
||||
fetch,
|
||||
get,
|
||||
}
|
||||
}
|
||||
|
||||
async function fetch(force: boolean) {
|
||||
async function fetch(force: boolean = false) {
|
||||
const content = useContentState();
|
||||
if(content.value.length === 0 || force)
|
||||
content.value = await useRequestFetch()('/api/file/overview');
|
||||
}
|
||||
|
||||
async function get(path: string) {
|
||||
async function get(path: string, force: boolean = false): Promise<ExploreContent | undefined> {
|
||||
const content = useContentState()
|
||||
const value = content.value;
|
||||
const item = value.find(e => e.path === path);
|
||||
if(item)
|
||||
|
||||
if(item && !item.content)
|
||||
{
|
||||
item.content = await useRequestFetch()(`/api/file/content/${encodeURIComponent(path)}`);
|
||||
}
|
||||
|
||||
content.value = value;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function addChild(arr: TreeItem[], e: ExploreContent): void {
|
||||
|
||||
@@ -7,10 +7,18 @@ import RemarkOfm from 'remark-ofm';
|
||||
import RemarkGfm from 'remark-gfm';
|
||||
import RemarkBreaks from 'remark-breaks';
|
||||
import RemarkFrontmatter from 'remark-frontmatter';
|
||||
import StripMarkdown from 'strip-markdown';
|
||||
import RemarkStringify from 'remark-stringify';
|
||||
|
||||
export default function useMarkdown(): (md: string) => Root
|
||||
interface Parser
|
||||
{
|
||||
let processor: Processor;
|
||||
parse: (md: string) => Promise<Root>;
|
||||
parseSync: (md: string) => Root;
|
||||
text: (md: string) => string;
|
||||
}
|
||||
export default function useMarkdown(): Parser
|
||||
{
|
||||
let processor: Processor, processorSync: Processor;
|
||||
|
||||
const parse = (markdown: string) => {
|
||||
if (!processor)
|
||||
@@ -19,9 +27,32 @@ export default function useMarkdown(): (md: string) => Root
|
||||
processor.use(RemarkRehype);
|
||||
}
|
||||
|
||||
const processed = processor.run(processor.parse(markdown)) as Promise<Root>;
|
||||
return processed;
|
||||
}
|
||||
|
||||
const parseSync = (markdown: string) => {
|
||||
if (!processor)
|
||||
{
|
||||
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter]);
|
||||
processor.use(RemarkRehype);
|
||||
}
|
||||
|
||||
const processed = processor.runSync(processor.parse(markdown)) as Root;
|
||||
return processed;
|
||||
}
|
||||
|
||||
return parse;
|
||||
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 };
|
||||
}
|
||||
@@ -181,7 +181,7 @@ export const _useShortcuts = () => {
|
||||
return false
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
tryOnMounted(() => {
|
||||
metaSymbol.value = macOS.value ? '⌘' : 'Ctrl'
|
||||
})
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { UserSession, UserSessionComposable } from '~/types/auth'
|
||||
import { useContent } from './useContent'
|
||||
|
||||
const useSessionState = () => useState<UserSession>('nuxt-session', () => ({}))
|
||||
const useAuthReadyState = () => useState('nuxt-auth-ready', () => false)
|
||||
const useContentFetch = (force: boolean) => useContent().fetch(force);
|
||||
|
||||
/**
|
||||
* Composable to get back the user session and utils around it.
|
||||
|
||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
73
db/schema.ts
73
db/schema.ts
@@ -1,8 +1,8 @@
|
||||
import { relations } from 'drizzle-orm';
|
||||
import { int, text, sqliteTable, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||
import { ABILITIES, MAIN_STATS } from '../types/character';
|
||||
import { int, text, sqliteTable as table, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||
import { ABILITIES, MAIN_STATS } from '~/shared/character.util';
|
||||
|
||||
export const usersTable = sqliteTable("users", {
|
||||
export const usersTable = table("users", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
username: text().notNull().unique(),
|
||||
email: text().notNull().unique(),
|
||||
@@ -10,93 +10,90 @@ export const usersTable = sqliteTable("users", {
|
||||
state: int().notNull().default(0),
|
||||
});
|
||||
|
||||
export const usersDataTable = sqliteTable("users_data", {
|
||||
export const usersDataTable = table("users_data", {
|
||||
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
logCount: int().notNull().default(0),
|
||||
});
|
||||
|
||||
export const userSessionsTable = sqliteTable("user_sessions", {
|
||||
export const userSessionsTable = table("user_sessions", {
|
||||
id: int().notNull(),
|
||||
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
}, (table) => [primaryKey({ columns: [table.id, table.user_id] })]);
|
||||
|
||||
export const userPermissionsTable = sqliteTable("user_permissions", {
|
||||
export const userPermissionsTable = table("user_permissions", {
|
||||
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
permission: text().notNull(),
|
||||
}, (table) => [primaryKey({ columns: [table.id, table.permission] })]);
|
||||
|
||||
export const explorerContentTable = sqliteTable("explorer_content", {
|
||||
path: text().primaryKey(),
|
||||
export const projectFilesTable = table("project_files", {
|
||||
id: text().primaryKey(),
|
||||
path: text().notNull().unique(),
|
||||
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
title: text().notNull(),
|
||||
type: text({ enum: ['file', 'folder', 'markdown', 'canvas', 'map'] }).notNull(),
|
||||
content: blob({ mode: 'buffer' }),
|
||||
navigable: int({ mode: 'boolean' }).notNull().default(true),
|
||||
private: int({ mode: 'boolean' }).notNull().default(false),
|
||||
order: int().notNull(),
|
||||
visit: int().notNull().default(0),
|
||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const emailValidationTable = sqliteTable("email_validation", {
|
||||
export const projectContentTable = table("project_content", {
|
||||
id: text().primaryKey(),
|
||||
content: blob({ mode: 'buffer' }),
|
||||
});
|
||||
|
||||
export const emailValidationTable = table("email_validation", {
|
||||
id: text().primaryKey(),
|
||||
timestamp: int({ mode: 'timestamp' }).notNull(),
|
||||
})
|
||||
|
||||
export const characterTable = sqliteTable("character", {
|
||||
export const characterTable = table("character", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
name: text().notNull(),
|
||||
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
people: int().notNull(),
|
||||
people: text().notNull(),
|
||||
level: int().notNull().default(1),
|
||||
variables: text({ mode: 'json' }).notNull().default('{"health": 0,"mana": 0,"spells": [],"equipment": [],"exhaustion": 0,"sickness": []}'),
|
||||
aspect: int(),
|
||||
notes: text(),
|
||||
health: int().notNull().default(0),
|
||||
mana: int().notNull().default(0),
|
||||
|
||||
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
|
||||
thumbnail: blob(),
|
||||
});
|
||||
|
||||
export const characterTrainingTable = sqliteTable("character_training", {
|
||||
export const characterTrainingTable = table("character_training", {
|
||||
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(),
|
||||
choice: int().notNull(),
|
||||
}, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]);
|
||||
|
||||
export const characterLevelingTable = sqliteTable("character_leveling", {
|
||||
export const characterLevelingTable = table("character_leveling", {
|
||||
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
level: int().notNull(),
|
||||
choice: int().notNull(),
|
||||
}, (table) => [primaryKey({ columns: [table.character, table.level] })]);
|
||||
|
||||
export const characterAbilitiesTable = sqliteTable("character_abilities", {
|
||||
export const characterAbilitiesTable = table("character_abilities", {
|
||||
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),
|
||||
max: int().notNull().default(0),
|
||||
}, (table) => [primaryKey({ columns: [table.character, table.ability] })]);
|
||||
|
||||
export const characterModifiersTable = sqliteTable("character_modifiers", {
|
||||
export const characterChoicesTable = table("character_choices", {
|
||||
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
modifier: text({ enum: MAIN_STATS }).notNull(),
|
||||
value: int().notNull().default(0),
|
||||
}, (table) => [primaryKey({ columns: [table.character, table.modifier] })]);
|
||||
|
||||
export const characterSpellsTable = sqliteTable("character_spell", {
|
||||
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
value: text().notNull(),
|
||||
}, (table) => [primaryKey({ columns: [table.character, table.value] })]);
|
||||
id: text().notNull(),
|
||||
choice: int().notNull(),
|
||||
}, (table) => [primaryKey({ columns: [table.character, table.id, table.choice] })]);
|
||||
|
||||
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
||||
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
||||
session: many(userSessionsTable),
|
||||
permission: many(userPermissionsTable),
|
||||
content: many(explorerContentTable),
|
||||
files: many(projectFilesTable),
|
||||
}));
|
||||
export const usersDataRelation = relations(usersDataTable, ({ one }) => ({
|
||||
users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }),
|
||||
@@ -107,16 +104,15 @@ export const userSessionsRelation = relations(userSessionsTable, ({ one }) => ({
|
||||
export const userPermissionsRelation = relations(userPermissionsTable, ({ one }) => ({
|
||||
users: one(usersTable, { fields: [userPermissionsTable.id], references: [usersTable.id], }),
|
||||
}));
|
||||
export const explorerContentRelation = relations(explorerContentTable, ({ one }) => ({
|
||||
users: one(usersTable, { fields: [explorerContentTable.owner], references: [usersTable.id], }),
|
||||
export const projectFilesRelation = relations(projectFilesTable, ({ one }) => ({
|
||||
users: one(usersTable, { fields: [projectFilesTable.owner], references: [usersTable.id], }),
|
||||
}));
|
||||
export const characterRelation = relations(characterTable, ({ one, many }) => ({
|
||||
user: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }),
|
||||
training: many(characterTrainingTable),
|
||||
levels: many(characterLevelingTable),
|
||||
abilities: many(characterAbilitiesTable),
|
||||
modifiers: many(characterModifiersTable),
|
||||
spells: many(characterSpellsTable)
|
||||
choices: many(characterChoicesTable)
|
||||
}));
|
||||
|
||||
export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({
|
||||
@@ -128,9 +124,6 @@ export const characterLevelingRelation = relations(characterLevelingTable, ({ on
|
||||
export const characterAbilitiesRelation = relations(characterAbilitiesTable, ({ one }) => ({
|
||||
character: one(characterTable, { fields: [characterAbilitiesTable.character], references: [characterTable.id] })
|
||||
}));
|
||||
export const characterModifierRelation = relations(characterModifiersTable, ({ one }) => ({
|
||||
character: one(characterTable, { fields: [characterModifiersTable.character], references: [characterTable.id] })
|
||||
}));
|
||||
export const characterSpellsRelation = relations(characterSpellsTable, ({ one }) => ({
|
||||
character: one(characterTable, { fields: [characterSpellsTable.character], references: [characterTable.id] })
|
||||
export const characterChoicesRelation = relations(characterChoicesTable, ({ one }) => ({
|
||||
character: one(characterTable, { fields: [characterChoicesTable.character], references: [characterTable.id] })
|
||||
}));
|
||||
21
drizzle/0006_luxuriant_blade.sql
Normal file
21
drizzle/0006_luxuriant_blade.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE `project_content` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`content` blob
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `project_files` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`path` text NOT NULL,
|
||||
`owner` integer NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`navigable` integer DEFAULT true NOT NULL,
|
||||
`private` integer DEFAULT false NOT NULL,
|
||||
`order` integer NOT NULL,
|
||||
`timestamp` integer NOT NULL,
|
||||
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_files_path_unique` ON `project_files` (`path`);--> statement-breakpoint
|
||||
DROP TABLE `explorer_content`;--> statement-breakpoint
|
||||
ALTER TABLE `users_data` DROP COLUMN `logCount`;
|
||||
27
drizzle/0013_wakeful_lake.sql
Normal file
27
drizzle/0013_wakeful_lake.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
ALTER TABLE `explorer_content` RENAME TO `project_files`;--> statement-breakpoint
|
||||
CREATE TABLE `project_content` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`content` blob
|
||||
);
|
||||
--> statement-breakpoint
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_project_files` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`path` text NOT NULL,
|
||||
`owner` integer NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`navigable` integer DEFAULT true NOT NULL,
|
||||
`private` integer DEFAULT false NOT NULL,
|
||||
`order` integer NOT NULL,
|
||||
`timestamp` integer NOT NULL,
|
||||
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `project_content`("id", "content") SELECT "path", "content" FROM `project_files`;--> statement-breakpoint
|
||||
INSERT INTO `__new_project_files`("id", "path", "owner", "title", "type", "navigable", "private", "order", "timestamp") SELECT "path", "path", "owner", "title", "type", "navigable", "private", "order", "timestamp" FROM `project_files`;--> statement-breakpoint
|
||||
DROP TABLE `project_files`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_project_files` RENAME TO `project_files`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_files_path_unique` ON `project_files` (`path`);--> statement-breakpoint
|
||||
ALTER TABLE `users_data` DROP COLUMN `logCount`;
|
||||
7
drizzle/0014_careless_nick_fury.sql
Normal file
7
drizzle/0014_careless_nick_fury.sql
Normal 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
|
||||
);
|
||||
20
drizzle/0015_typical_blade.sql
Normal file
20
drizzle/0015_typical_blade.sql
Normal 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;
|
||||
5
drizzle/0016_wild_the_anarchist.sql
Normal file
5
drizzle/0016_wild_the_anarchist.sql
Normal 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`;
|
||||
758
drizzle/meta/0013_snapshot.json
Normal file
758
drizzle/meta/0013_snapshot.json
Normal file
@@ -0,0 +1,758 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "854c13bd-59bb-40bd-a046-69632b59557e",
|
||||
"prevId": "cb7a2b9c-1392-4f23-9fc2-9ce8de2e0231",
|
||||
"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_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": {
|
||||
"\"explorer_content\"": "\"project_files\""
|
||||
},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
810
drizzle/meta/0014_snapshot.json
Normal file
810
drizzle/meta/0014_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
810
drizzle/meta/0015_snapshot.json
Normal file
810
drizzle/meta/0015_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
702
drizzle/meta/0016_snapshot.json
Normal file
702
drizzle/meta/0016_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,34 @@
|
||||
"when": 1746027790969,
|
||||
"tag": "0012_graceful_energizer",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "6",
|
||||
"when": 1753097020642,
|
||||
"tag": "0013_wakeful_lake",
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,12 +14,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NuxtError } from '#app'
|
||||
import type { NuxtError } from '#app';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
const props = defineProps({
|
||||
error: Object as () => NuxtError
|
||||
})
|
||||
});
|
||||
|
||||
const handleError = () => clearError({ redirect: '/' })
|
||||
const handleError = () => clearError({ redirect: '/' });
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<CollapsibleRoot class="flex flex-1 flex-col" v-model:open="open">
|
||||
<div class="z-50 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button icon class="!bg-transparent group md:hidden">
|
||||
@@ -18,15 +18,16 @@
|
||||
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
|
||||
<NavigationMenuItem>
|
||||
<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="text-light-70 dark:text-dark-70 border-b-[2px] border-transparent hover:border-accent-blue py-2 hover:!text-opacity-70 flex items-center" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent class="absolute top-0 left-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30">
|
||||
<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>
|
||||
<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 hover:bg-light-10 dark:hover:bg-dark-10 hover:text-light-100 dark:hover:text-dark-100 py-2 px-4" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="text-light-70 dark:text-dark-70 hover:bg-light-10 dark:hover:bg-dark-10 hover:text-light-100 dark:hover:text-dark-100 py-2 px-4" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
<div class="absolute top-full left-0 flex w-full justify-center my-4">
|
||||
<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)]" />
|
||||
<div class="absolute top-full left-0 flex w-full justify-center">
|
||||
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
|
||||
</div>
|
||||
</NavigationMenuRoot>
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
@@ -39,32 +40,20 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
|
||||
<CollapsibleContent asChild forceMount>
|
||||
<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">
|
||||
<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>
|
||||
<Tree v-if="pages" v-model="pages" :getKey="(item) => item.path" class="ps-4">
|
||||
<template #default="{ item, isExpanded }">
|
||||
<NuxtLink :href="item.value.path && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.path } } : undefined" class="flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full" :class="{ 'font-medium': item.hasChildren }" active-class="text-accent-blue" :data-private="item.value.private">
|
||||
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level / 2 - 1.5}em` }" />
|
||||
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="w-5 h-5" />
|
||||
<div class="pl-1.5 py-1.5 flex-1 truncate">
|
||||
{{ item.value.title }}
|
||||
</div>
|
||||
<Tooltip message="Privé" side="right"><Icon v-show="item.value.private" class="mx-1" icon="radix-icons:lock-closed" /></Tooltip>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</Tree>
|
||||
</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>
|
||||
<!-- </CollapsibleContent> -->
|
||||
<slot></slot>
|
||||
</div>
|
||||
</CollapsibleRoot>
|
||||
@@ -72,10 +61,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { iconByType } from '#shared/general.util';
|
||||
import type { DropdownOption } from '~/components/base/DropdownMenu.vue';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
import type { TreeItem } from '~/types/content';
|
||||
import { hasPermissions } from '#shared/auth.util';
|
||||
import { TreeDOM } from '#shared/tree';
|
||||
import { Content, iconByType } from '#shared/content.util';
|
||||
import { dom, icon, text } from '#shared/dom.util';
|
||||
import { unifySlug } from '#shared/general.util';
|
||||
import { popper, tooltip } from '#shared/floating.util';
|
||||
import { link } from '#shared/components.util';
|
||||
|
||||
const options = ref<DropdownOption[]>([{
|
||||
type: 'item',
|
||||
@@ -94,16 +87,43 @@ const { fetch } = useContent();
|
||||
await fetch(false);
|
||||
|
||||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => route.value.params.path ? Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path : undefined);
|
||||
const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined);
|
||||
|
||||
await Content.init();
|
||||
const tree = new TreeDOM((item, depth) => {
|
||||
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 / 1.5 - 1}em` } }),
|
||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
||||
])]);
|
||||
}, (item, depth) => {
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.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, [
|
||||
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
||||
])]);
|
||||
}, (item) => item.navigable);
|
||||
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
|
||||
const treeParent = useTemplateRef('treeParent');
|
||||
|
||||
const unmount = useRouter().afterEach((to, from, failure) => {
|
||||
if(failure)
|
||||
return;
|
||||
|
||||
to.name === 'explore-path' && (unifySlug(to.params.path ?? '').split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
|
||||
});
|
||||
|
||||
watch(route, () => {
|
||||
open.value = false;
|
||||
});
|
||||
|
||||
const { tree } = useContent();
|
||||
const pages = computed(() => transform(tree.value));
|
||||
function transform(list: TreeItem[] | undefined): TreeItem[] | undefined
|
||||
{
|
||||
return list?.filter(e => e.navigable)?.map(e => ({ ...e, open: path.value?.startsWith(e.path), children: transform(e.children) }));
|
||||
}
|
||||
onMounted(() => {
|
||||
if(treeParent.value)
|
||||
{
|
||||
treeParent.value.appendChild(tree.container);
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
unmount();
|
||||
})
|
||||
</script>
|
||||
@@ -5,6 +5,7 @@ import path from 'node:path'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2024-04-03',
|
||||
ssr: false,
|
||||
modules: [
|
||||
'@nuxtjs/color-mode',
|
||||
'nuxt-security',
|
||||
@@ -16,6 +17,11 @@ export default defineNuxtConfig({
|
||||
tailwindcss: {
|
||||
viewer: false,
|
||||
config: {
|
||||
content: {
|
||||
files: [
|
||||
"./shared/**/*.{vue,js,jsx,mjs,ts,tsx}"
|
||||
]
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
@@ -56,14 +62,14 @@ export default defineNuxtConfig({
|
||||
current: 'currentColor',
|
||||
light: {
|
||||
red: '#e93147',
|
||||
redBack: '#F9C7CD',
|
||||
orange: '#ec7500',
|
||||
yellow: '#e0ac00',
|
||||
green: '#08b94e',
|
||||
greenBack: '#BCECCF',
|
||||
orange: '#FF9800',
|
||||
yellow: '#FFEB3B',
|
||||
green: '#388E3C',
|
||||
indigo: '#7986CB',
|
||||
cyan: '#00bfbc',
|
||||
lime: '#8BC34A',
|
||||
blue: '#086ddd',
|
||||
purple: '#7852ee',
|
||||
purple: '#AB47BC',
|
||||
pink: '#d53984',
|
||||
0: "#ffffff",
|
||||
5: "#fcfcfc",
|
||||
@@ -125,6 +131,9 @@ export default defineNuxtConfig({
|
||||
experimental: {
|
||||
tasks: true,
|
||||
},
|
||||
watchOptions: {
|
||||
usePolling: true,
|
||||
},
|
||||
rollupConfig: {
|
||||
external: ['bun'],
|
||||
plugins: [
|
||||
@@ -134,8 +143,8 @@ export default defineNuxtConfig({
|
||||
},
|
||||
runtimeConfig: {
|
||||
session: {
|
||||
maxAge: 60*60*24*31,
|
||||
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013',
|
||||
maxAge: 60 * 60 * 24 *30,
|
||||
},
|
||||
database: 'db.sqlite',
|
||||
mail: {
|
||||
@@ -180,5 +189,10 @@ export default defineNuxtConfig({
|
||||
key: fs.readFileSync(path.resolve(__dirname, 'localhost+1-key.pem')).toString('utf-8'),
|
||||
cert: fs.readFileSync(path.resolve(__dirname, 'localhost+1.pem')).toString('utf-8'),
|
||||
}
|
||||
},
|
||||
vue: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag === 'iconify-icon',
|
||||
}
|
||||
}
|
||||
})
|
||||
52
package.json
52
package.json
@@ -4,55 +4,61 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"predev": "bun i",
|
||||
"dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 bunx --bun nuxi dev"
|
||||
"dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 bunx --bun nuxi dev --no-f",
|
||||
"premigrate": "bunx drizzle-kit generate",
|
||||
"migrate": "bun migrate.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.5.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@codemirror/lang-markdown": "^6.3.2",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.1",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||
"@codemirror/lang-markdown": "^6.3.3",
|
||||
"@floating-ui/dom": "^1.7.2",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@markdoc/markdoc": "^0.5.1",
|
||||
"@markdoc/markdoc": "^0.5.2",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/sitemap": "^7.2.5",
|
||||
"@nuxtjs/tailwindcss": "^6.13.1",
|
||||
"@nuxtjs/sitemap": "^7.4.3",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@vueuse/gesture": "^2.0.0",
|
||||
"@vueuse/math": "^12.7.0",
|
||||
"@vueuse/nuxt": "^12.7.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"@vueuse/math": "^13.5.0",
|
||||
"@vueuse/nuxt": "^13.5.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"drizzle-orm": "^0.44.3",
|
||||
"hast": "^1.0.0",
|
||||
"hast-util-heading": "^3.0.0",
|
||||
"hast-util-heading-rank": "^3.0.0",
|
||||
"iconify-icon": "^2.3.0",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"mdast-util-find-and-replace": "^3.0.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
"nuxt": "3.15.4",
|
||||
"nuxt-security": "^2.1.5",
|
||||
"radix-vue": "^1.9.15",
|
||||
"nodemailer": "^7.0.5",
|
||||
"nuxt": "^4.0.1",
|
||||
"nuxt-security": "^2.2.0",
|
||||
"radix-vue": "^1.9.17",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-ofm": "link:remark-ofm",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-vue": "^6.0.0",
|
||||
"strip-markdown": "^6.0.0",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"zod": "^3.24.2"
|
||||
"vue": "^3.5.17",
|
||||
"vue-router": "^4.5.1",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.2",
|
||||
"@types/bun": "^1.2.19",
|
||||
"@types/lodash.capitalize": "^4.2.9",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/unist": "^3.0.3",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"bun-types": "^1.2.2",
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"bun-types": "^1.2.19",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"rehype-stringify": "^10.0.1"
|
||||
}
|
||||
|
||||
@@ -31,8 +31,10 @@
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { format, iconByType } from '~/shared/general.util';
|
||||
import { format } from '#shared/general.util';
|
||||
import { iconByType } from '#shared/content.util';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
interface File
|
||||
{
|
||||
@@ -68,8 +70,6 @@ definePageMeta({
|
||||
rights: ['admin'],
|
||||
});
|
||||
|
||||
const toaster = useToast();
|
||||
|
||||
const { data: users } = useFetch('/api/admin/users', {
|
||||
transform: (users) => {
|
||||
//@ts-ignore
|
||||
@@ -124,13 +124,13 @@ async function editPermissions(user: User)
|
||||
body: permissionCopy.value,
|
||||
});
|
||||
user.permission = permissionCopy.value;
|
||||
toaster.add({
|
||||
Toaster.add({
|
||||
duration: 10000, type: 'success', content: 'Permissions mises à jour.', timer: true,
|
||||
});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
toaster.add({
|
||||
Toaster.add({
|
||||
duration: 10000, type: 'error', content: (e as any).message, timer: true,
|
||||
});
|
||||
}
|
||||
@@ -145,13 +145,13 @@ async function logout(user: User)
|
||||
|
||||
user.session.length = 0;
|
||||
|
||||
toaster.add({
|
||||
Toaster.add({
|
||||
duration: 10000, type: 'success', content: 'L\'utilisateur vient d\'être déconnecté.', timer: true,
|
||||
});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
toaster.add({
|
||||
Toaster.add({
|
||||
duration: 10000, type: 'error', content: (e as any).message, timer: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,15 +13,15 @@ const schemaList: Record<string, z.ZodObject<any> | null> = {
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin'],
|
||||
})
|
||||
const job = ref<string>('');
|
||||
|
||||
const toaster = useToast();
|
||||
const payload = reactive<Record<string, any>>({
|
||||
data: JSON.stringify({ username: "Peaceultime", id: 1, timestamp: Date.now() }),
|
||||
to: 'clem31470@gmail.com',
|
||||
@@ -51,7 +51,7 @@ async function fetch()
|
||||
error.value = null;
|
||||
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)
|
||||
{
|
||||
@@ -59,7 +59,7 @@ async function fetch()
|
||||
error.value = e as Error;
|
||||
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>
|
||||
|
||||
@@ -1,381 +1,27 @@
|
||||
<script lang="ts">
|
||||
import config from '#shared/character-config.json';
|
||||
|
||||
function raceOptionToText(option: RaceOption): string
|
||||
{
|
||||
const text = [];
|
||||
if(option.training) text.push(`+${option.training} point${option.training > 1 ? 's' : ''} de statistique${option.training > 1 ? 's' : ''}.`);
|
||||
if(option.shaping) text.push(`+${option.shaping} transformation${option.shaping > 1 ? 's' : ''} par jour.`);
|
||||
if(option.modifier) text.push(`+${option.modifier} au modifieur de votre choix.`);
|
||||
if(option.abilities) text.push(`+${option.abilities} point${option.abilities > 1 ? 's' : ''} de compétence${option.abilities > 1 ? 's' : ''}.`);
|
||||
if(option.health) text.push(`+${option.health} PV max.`);
|
||||
if(option.mana) text.push(`+${option.mana} mana max.`);
|
||||
if(option.spellslots) text.push(`+${option.spellslots} sort${option.spellslots > 1 ? 's' : ''} maitrisé${option.spellslots > 1 ? 's' : ''}.`);
|
||||
return text.join('\n');
|
||||
}
|
||||
function getFeaturesOf(stat: MainStat, progression: DoubleIndex<TrainingLevel>[]): TrainingOption[]
|
||||
{
|
||||
const characterData = config as CharacterConfig;
|
||||
return progression.map(e => characterData.training[stat][e[0]][e[1]]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function abilitySpecialFeatures(type: "points" | "max", curiosity: DoubleIndex<TrainingLevel>[], value: number): number
|
||||
{
|
||||
if(type === 'points')
|
||||
{
|
||||
if(curiosity.find(e => e[0] == 6 && e[1] === 0))
|
||||
return Math.max(6, value);
|
||||
if(curiosity.find(e => e[0] == 6 && e[1] === 2))
|
||||
return value + 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import PreviewA from '~/components/prose/PreviewA.vue';
|
||||
import { clamp } from '~/shared/general.util';
|
||||
import { defaultCharacter, elementTexts, mainStatTexts, spellTypeTexts, type Ability, type Character, type CharacterConfig, type DoubleIndex, type Level, type MainStat, type RaceOption, type SpellConfig, type SpellElement, type SpellType, type TrainingLevel, type TrainingOption } from '~/types/character';
|
||||
import { CharacterBuilder } from '#shared/character.util';
|
||||
import { unifySlug } from '~/shared/general.util';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
});
|
||||
let id = useRouter().currentRoute.value.params.id;
|
||||
const { add } = useToast();
|
||||
const characterConfig = config as CharacterConfig;
|
||||
const data = ref<Character>({ ...defaultCharacter });
|
||||
const spellFilter = ref<{
|
||||
ranks: Array<1 | 2 | 3>,
|
||||
types: Array<SpellType>,
|
||||
text: string,
|
||||
elements: Array<SpellElement>,
|
||||
tags: string[],
|
||||
}>({
|
||||
ranks: [],
|
||||
types: [],
|
||||
text: "",
|
||||
elements: [],
|
||||
tags: [],
|
||||
});
|
||||
const id = unifySlug(useRouter().currentRoute.value.params.id ?? "new");
|
||||
const container = useTemplateRef('container');
|
||||
|
||||
const peopleOpen = ref(false), trainingOpen = ref(false), abilityOpen = ref(false), spellOpen = ref(false), notesOpen = ref(false), trainingTab = ref(0);
|
||||
const raceOptions = computed(() => data.value.people !== undefined ? characterConfig.peoples[data.value.people!].options : undefined);
|
||||
const selectedRaceOptions = computed(() => raceOptions !== undefined ? data.value.leveling!.map(e => raceOptions.value![e[0]][e[1]]) : undefined);
|
||||
const trainingPoints = computed(() => raceOptions.value ? data.value.leveling?.reduce((p, v) => p + (raceOptions.value![v[0]][v[1]].training ?? 0), 0) : 0);
|
||||
const training = computed(() => Object.entries(characterConfig.training).map(e => [e[0], getFeaturesOf(e[0] as MainStat, data.value.training[e[0] as MainStat])]) as [MainStat, TrainingOption[]][]);
|
||||
const maxTraining = computed(() => Object.entries(data.value.training).reduce((p, v) => { p[v[0] as MainStat] = v[1].reduce((_p, _v) => Math.max(_p, _v[0]) , 0); return p; }, {} as Record<MainStat, number>));
|
||||
const trainingSpent = computed(() => Object.values(maxTraining.value).reduce((p, v) => p + v, 0));
|
||||
const modifiers = computed(() => Object.entries(maxTraining.value).reduce((p, v) => { p[v[0] as MainStat] = Math.floor(v[1] / 3) + (data.value.modifiers ? (data.value.modifiers[v[0] as MainStat] ?? 0) : 0); return p; }, {} as Record<MainStat, number>))
|
||||
const modifierPoints = computed(() => (selectedRaceOptions.value ? selectedRaceOptions.value.reduce((p, v) => p + (v?.modifier ?? 0), 0) : 0) + training.value.reduce((p, v) => p + v[1].reduce((_p, _v) => _p + (_v?.modifier ?? 0), 0), 0));
|
||||
const modifierSpent = computed(() => Object.values(data.value.modifiers ?? {}).reduce((p, v) => p + v, 0));
|
||||
const abilityPoints = computed(() => (selectedRaceOptions.value ? selectedRaceOptions.value.reduce((p, v) => p + (v?.abilities ?? 0), 0) : 0) + training.value.flatMap(e => e[1].filter(_e => _e.ability !== undefined)).reduce((p, v) => p + v.ability!, 0));
|
||||
const abilityMax = computed(() => Object.entries(characterConfig.abilities).reduce((p, v) => { p[v[0] as Ability] = abilitySpecialFeatures("max", data.value.training.curiosity, Math.floor(maxTraining.value[v[1].max[0]] / 3) + Math.floor(maxTraining.value[v[1].max[1]] / 3)); return p; }, {} as Record<Ability, number>));
|
||||
const abilitySpent = computed(() => Object.values(data.value.abilities ?? {}).reduce((p, v) => p + v[0], 0));
|
||||
const spellranks = computed(() => training.value.flatMap(e => e[1].filter(_e => _e.spellrank !== undefined)).reduce((p, v) => { p[v.spellrank!]++; return p; }, { instinct: 0, precision: 0, knowledge: 0 } as Record<SpellType, 0 | 1 | 2 | 3>));
|
||||
const spellsPoints = computed(() => training.value.flatMap(e => e[1].filter(_e => _e.spellslot !== undefined)).reduce((p, v) => p + (modifiers.value.hasOwnProperty(v.spellslot as MainStat) ? modifiers.value[v.spellslot as MainStat] : v.spellslot as number), 0));
|
||||
|
||||
if(id !== 'new')
|
||||
{
|
||||
const character = await useRequestFetch()(`/api/character/${id}`);
|
||||
|
||||
if(!character)
|
||||
{
|
||||
throw new Error('Donnée du personnage introuvables');
|
||||
}
|
||||
|
||||
data.value = Object.assign(defaultCharacter, data.value, character);
|
||||
}
|
||||
|
||||
function selectRaceOption(level: Level, choice: number)
|
||||
{
|
||||
const character = data.value;
|
||||
if(level > character.level)
|
||||
return;
|
||||
|
||||
if(character.leveling === undefined)
|
||||
character.leveling = [[1, 0]];
|
||||
|
||||
if(level == 1)
|
||||
return;
|
||||
|
||||
for(let i = 1; i < level; i++) //Check previous levels as a requirement
|
||||
{
|
||||
if(!character.leveling.some(e => e[0] == i))
|
||||
return;
|
||||
}
|
||||
|
||||
if(character.leveling.some(e => e[0] == level))
|
||||
{
|
||||
character.leveling.splice(character.leveling.findIndex(e => e[0] == level), 1, [level, choice]);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.leveling.push([level, choice]);
|
||||
}
|
||||
|
||||
data.value = character;
|
||||
}
|
||||
function switchTrainingOption(stat: MainStat, level: TrainingLevel, choice: number)
|
||||
{
|
||||
const character = data.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))
|
||||
onMounted(() => {
|
||||
queueMicrotask(() => {
|
||||
if(container.value)
|
||||
{
|
||||
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);
|
||||
}
|
||||
const builder = new CharacterBuilder(container.value, id === 'new' ? undefined : id);
|
||||
|
||||
useShortcuts({
|
||||
"Meta_S": () => builder.save(false),
|
||||
});
|
||||
}
|
||||
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]);
|
||||
}
|
||||
|
||||
data.value = character;
|
||||
}
|
||||
function updateLevel()
|
||||
{
|
||||
const character = data.value;
|
||||
|
||||
if(character.leveling) //Invalidate higher levels
|
||||
{
|
||||
for(let level = 20; level > character.level; level--)
|
||||
{
|
||||
const index = character.leveling.findIndex(e => e[0] == level);
|
||||
if(index !== -1)
|
||||
character.leveling.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
data.value = character;
|
||||
}
|
||||
function filterSpells(spells: SpellConfig[])
|
||||
{
|
||||
const filter = spellFilter.value
|
||||
let list = [...spells];
|
||||
list = list.filter(e => spellranks.value[e.type] >= e.rank);
|
||||
if(filter.text.length > 0) list = list.filter(e => e.name.toLowerCase().includes(filter.text.toLowerCase()));
|
||||
if(filter.types.length > 0) list = list.filter(e => filter.types.includes(e.type));
|
||||
if(filter.ranks.length > 0) list = list.filter(e => filter.ranks.includes(e.rank));
|
||||
if(filter.elements.length > 0) list = list.filter(e => filter.elements.some(f => e.elements.includes(f)));
|
||||
if(filter.tags.length > 0) list = list.filter(e => !e.tags || filter.tags.some(f => e.tags!.includes(f)));
|
||||
|
||||
return list;
|
||||
}
|
||||
async function save(leave: boolean)
|
||||
{
|
||||
if(data.value.name === '' || data.value.people === undefined || data.value.people === -1)
|
||||
{
|
||||
add({ title: 'Données manquantes', content: "Merci de saisir un nom et une race avant de pouvoir enregistrer votre personnage", type: 'error', duration: 25000, timer: true });
|
||||
return;
|
||||
}
|
||||
if(id === 'new')
|
||||
{
|
||||
id = await useRequestFetch()(`/api/character`, {
|
||||
method: 'post',
|
||||
body: data.value,
|
||||
onResponseError: (e) => {
|
||||
add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
||||
}
|
||||
});
|
||||
add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
|
||||
useRouter().replace({ name: 'character-id-edit', params: { id: id } })
|
||||
if(leave) useRouter().push({ name: 'character-id', params: { id: id } });
|
||||
}
|
||||
else
|
||||
{
|
||||
await useRequestFetch()(`/api/character/${id}`, {
|
||||
method: 'post',
|
||||
body: data.value,
|
||||
onResponseError: (e) => {
|
||||
add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
||||
}
|
||||
});
|
||||
add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
|
||||
if(leave) useRouter().push({ name: 'character-id', params: { id: id } });
|
||||
}
|
||||
}
|
||||
|
||||
useShortcuts({
|
||||
"Meta_S": () =>save(false),
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Edition de {{ data.name || 'nouveau personnage' }}</Title>
|
||||
</Head>
|
||||
<div class="flex flex-col gap-8 align-center">
|
||||
<div class="flex flex-row gap-4 align-center justify-between">
|
||||
<div></div>
|
||||
<div class="flex flex-row gap-4 align-center justify-center">
|
||||
<Tooltip side="left" message="Developpement en cours"><Avatar src="" icon="radix-icons:person" size="large" /></Tooltip>
|
||||
<Label class="flex items-start justify-between flex-col gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Nom du personnage</span>
|
||||
<input class="caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
|
||||
bg-light-20 dark:bg-dark-20 outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20"
|
||||
type="text" v-model="data.name">
|
||||
</Label>
|
||||
<Label class="flex items-start justify-between flex-col gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Niveau</span>
|
||||
<NumberFieldRoot :min="1" :max="20" v-model="data.level" @update:model-value="updateLevel" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
|
||||
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 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-start justify-between flex-col gap-2">
|
||||
<span class="pb-1 mx-6 md:p-0">Visibilité</span>
|
||||
<Select class="!my-0" v-model="data.visibility">
|
||||
<SelectItem label="Privé" value="private" />
|
||||
<SelectItem label="Public" value="public" />
|
||||
</Select>
|
||||
</Label>
|
||||
</div>
|
||||
<div class="self-center">
|
||||
<Tooltip side="right" message="Ctrl+S"><Button @click="() => save(true)">Enregistrer</Button></Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col min-w-[800px] w-[75vw] max-w-[1200px]">
|
||||
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="peopleOpen" @update:model-value="() => { trainingOpen = false; abilityOpen = false; spellOpen = false; notesOpen = false; }">
|
||||
<template #label>
|
||||
<span class="font-bold text-xl">Peuple</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="m-2 overflow-auto">
|
||||
<Combobox label="Peuple de votre personnage" v-model="data.people" :options="config.peoples.map((people, index) => [people.name, index])" @update:model-value="(index) => { data.people = index as number | undefined; data.leveling = [[1, 0]]}" />
|
||||
<template v-if="data.people !== undefined">
|
||||
<div class="w-full border-b border-light-30 dark:border-dark-30 pb-4">
|
||||
<span class="text-sm text-light-70 dark:text-dark-70">{{ characterConfig.peoples[data.people].description }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 max-h-[50vh] pe-4 relative">
|
||||
<span class="sticky top-0 py-1 bg-light-0 dark:bg-dark-0 z-10 text-xl">Niveaux restants: {{ data.level - (data.leveling?.length ?? 0) }}</span>
|
||||
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.peoples[data.people].options" :class="{ 'opacity-30': index > data.level }">
|
||||
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-64" v-for="(option, i) of level" @click="selectRaceOption(parseInt(index as unknown as string, 10) as Level, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= data.level, '!border-accent-blue bg-accent-blue bg-opacity-20': data.leveling?.some(e => e[0] == index && e[1] === i) ?? false }"><MarkdownRenderer :content="raceOptionToText(option)" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Collapsible>
|
||||
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="trainingOpen" :disabled="data.people === undefined" @update:model-value="() => { peopleOpen = false; abilityOpen = false; spellOpen = false; notesOpen = false; }">
|
||||
<template #label>
|
||||
<span class="font-bold text-xl">Entrainement</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="flex flex-col gap-4 max-h-[50vh] pe-4 relative overflow-y-auto overflow-x-hidden">
|
||||
<div class="sticky top-0 z-10 py-2 bg-light-0 dark:bg-dark-0 flex justify-between">
|
||||
<Icon icon="radix-icons:caret-left" class="w-6 h-6 border border-light-30 dark:border-dark-30 cursor-pointer" @click="() => trainingTab = clamp(trainingTab - 1, 0, 6)" />
|
||||
<span class="text-xl" :class="{ 'text-light-red dark:text-dark-red': (trainingPoints ?? 0) < trainingSpent }">Points d'entrainement restants: {{ (trainingPoints ?? 0) - trainingSpent }}</span>
|
||||
<Icon icon="radix-icons:caret-right" class="w-6 h-6 border border-light-30 dark:border-dark-30 cursor-pointer" @click="() => trainingTab = clamp(trainingTab + 1, 0, 6)" />
|
||||
</div>
|
||||
<div class="flex gap-4 relative" :style="`left: calc(calc(-100% - 1em) * ${trainingTab}); transition: left .5s ease;`">
|
||||
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative" v-for="(text, stat) of mainStatTexts">
|
||||
<div class="sticky top-1 mx-16 z-10 flex justify-between">
|
||||
<div class="py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold border border-light-30 dark:border-dark-30 flex">{{ text }}
|
||||
<div class="flex gap-2" v-if="maxTraining[stat] >= 0">: Niveau {{ maxTraining[stat] }} (+{{ modifiers[stat] }}
|
||||
<NumberFieldRoot :default-value="data.modifiers[stat] ?? 0" v-model="data.modifiers[stat]" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
|
||||
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 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-8 text-base font-normal bg-transparent px-2 outline-none caret-light-50 dark:caret-dark-50" />
|
||||
</NumberFieldRoot>
|
||||
)</div></div>
|
||||
<div class="py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 flex gap-2 justify-center items-center" :class="{ 'text-light-red dark:text-dark-red': (modifierPoints ?? 0) < modifierSpent }">Modifieur bonus: {{ modifierPoints - modifierSpent }}</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training[stat]" :class="{ 'opacity-30': index > maxTraining[stat] + 1 }">
|
||||
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption(stat, parseInt(index as unknown as string, 10) as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining[stat] + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.training[stat]?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Collapsible>
|
||||
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="abilityOpen" :disabled="data.people === undefined" @update:model-value="() => { trainingOpen = false; peopleOpen = false; spellOpen = false; notesOpen = false; }">
|
||||
<template #label>
|
||||
<span class="font-bold text-xl">Compétences</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="flex flex-col gap-2 max-h-[50vh] px-4 relative overflow-y-auto">
|
||||
<div class="sticky top-0 py-2 bg-light-0 dark:bg-dark-0 z-10 flex justify-between">
|
||||
<span class="text-xl -mx-2" :class="{ 'text-light-red dark:text-dark-red': (abilityPoints ?? 0) < abilitySpent }">Points d'entrainement restants: {{ (abilityPoints ?? 0) - abilitySpent }}</span>
|
||||
</div>
|
||||
<div class="grid gap-4 grid-cols-6">
|
||||
<div v-for="(ability, index) of characterConfig.abilities" class="flex flex-col items-center border border-light-30 dark:border-dark-30 p-2">
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<NumberFieldRoot :min="0" :default-value="data.abilities[index] ? data.abilities[index][0] : 0" @update:model-value="(value) => { data.abilities[index] = [value, data.abilities[index] ? data.abilities[index][1] : 0]; }" class="border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
|
||||
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 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-8 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
||||
</NumberFieldRoot>
|
||||
<span class="font-bold col-span-4">/{{ abilityMax[index] }}</span>
|
||||
</div>
|
||||
<span class="text-xl font-bold flex-2">{{ ability.name }}</span>
|
||||
<span class="text-sm text-light-70 dark:text-dark-70 flex-1">({{ mainStatTexts[ability.max[0]] }} + {{ mainStatTexts[ability.max[1]] }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Collapsible>
|
||||
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="spellOpen" :disabled="data.people === undefined" @update:model-value="() => { trainingOpen = false; peopleOpen = false; abilityOpen = false; notesOpen = false; }">
|
||||
<template #label>
|
||||
<span class="font-bold text-xl">Sorts</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="flex flex-col gap-2 max-h-[50vh] px-4 relative overflow-y-auto">
|
||||
<div class="sticky top-0 py-2 bg-light-0 dark:bg-dark-0 z-10 flex gap-2 items-center">
|
||||
<span class="text-xl pe-4" :class="{ 'text-light-red dark:text-dark-red': spellsPoints < (data.spells?.length ?? 0) }">Sorts: {{ data.spells?.length ?? 0 }}/{{ spellsPoints }}</span>
|
||||
<TextInput label="Nom" v-model="spellFilter.text" />
|
||||
<Combobox label="Rang" v-model="spellFilter.ranks" multiple :options="[['Rang 1', 1], ['Rang 2', 2], ['Rang 3', 3]]" />
|
||||
<Combobox label="Type" v-model="spellFilter.types" multiple :options="[['Précision', 'precision'], ['Savoir', 'knowledge'], ['Instinct', 'instinct']]" />
|
||||
<Combobox label="Element" v-model="spellFilter.elements" multiple :options="[['Feu', 'fire'], ['Glace', 'ice'], ['Foudre', 'thunder'], ['Terre', 'earth'], ['Arcane', 'arcana'], ['Air', 'air'], ['Nature', 'nature'], ['Lumière', 'light'], ['Psy', 'psyche']]" />
|
||||
</div>
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div class="py-1 px-2 border border-light-30 dark:border-dark-30 flex flex-col hover:border-light-50 dark:hover:border-dark-50 cursor-pointer" v-for="spell of filterSpells(characterConfig.spells)" :class="{ '!border-accent-blue bg-accent-blue bg-opacity-20': data.spells?.find(e => e === spell.id) }"
|
||||
@click="() => data.spells?.includes(spell.id) ? data.spells.splice(data.spells.findIndex((e: string) => e === spell.id), 1) : data.spells!.push(spell.id)">
|
||||
<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">{{ 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>
|
||||
</template>
|
||||
</Collapsible>
|
||||
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="notesOpen" @update:model-value="() => { trainingOpen = false; peopleOpen = false; abilityOpen = false; spellOpen = false; }">
|
||||
<template #label>
|
||||
<span class="font-bold text-xl">Notes libres</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<Editor class="min-h-[400px] border border-light-30 dark:border-dark-30" v-model="data.notes" />
|
||||
</template>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 max-w-full flex-col align-center" ref="container"></div>
|
||||
</template>
|
||||
@@ -1,21 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import config from '#shared/character-config.json';
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import PreviewA from '~/components/prose/PreviewA.vue';
|
||||
import type { SpellConfig } from '~/types/character';
|
||||
import { elementTexts, spellTypeTexts, type CharacterConfig } from '~/types/character';
|
||||
import { clamp, unifySlug } from '#shared/general.util';
|
||||
import type { CompiledCharacter, SpellConfig } from '~/types/character';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
import { abilityTexts, CharacterCompiler, CharacterSheet, defaultCharacter, elementTexts, spellTypeTexts } from '#shared/character.util';
|
||||
import { getText } from '#shared/i18n';
|
||||
import { preview } from '#shared/proses';
|
||||
import { div, dom, icon, text } from '#shared/dom.util';
|
||||
import markdown from '#shared/markdown.util';
|
||||
import { button, foldable } from '#shared/components.util';
|
||||
import { fullblocker, tooltip } from '~/shared/floating.util';
|
||||
/*
|
||||
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-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow
|
||||
text-light-orange dark:text-dark-orange border-light-orange dark:border-dark-orange bg-light-orange dark:bg-dark-orange
|
||||
text-light-indigo dark:text-dark-indigo border-light-indigo dark:border-dark-indigo bg-light-indigo dark:bg-dark-indigo
|
||||
text-light-lime dark:text-dark-lime border-light-lime dark:border-dark-lime bg-light-lime dark:bg-dark-lime
|
||||
text-light-green dark:text-dark-green border-light-green dark:border-dark-green bg-light-green dark:bg-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-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple
|
||||
*/
|
||||
|
||||
const characterConfig = config as CharacterConfig;
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
const id = useRouter().currentRoute.value.params.id;
|
||||
const id = useRouter().currentRoute.value.params.id ? unifySlug(useRouter().currentRoute.value.params.id!) : undefined;
|
||||
const { user } = useUserSession();
|
||||
const { add } = useToast();
|
||||
const container = useTemplateRef('container');
|
||||
|
||||
const { data: character, status, error } = await useFetch(`/api/character/${id}/compiled`);
|
||||
onMounted(() => {
|
||||
queueMicrotask(() => {
|
||||
if(container.value && id)
|
||||
{
|
||||
const character = new CharacterSheet(id, user);
|
||||
container.value.appendChild(character.container);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="status === 'pending'">
|
||||
<div ref="container"></div>
|
||||
<!-- <div v-if="status === 'pending'">
|
||||
<Head>
|
||||
<Title>d[any] - Chargement ...</Title>
|
||||
</Head>
|
||||
@@ -35,12 +63,12 @@ const { data: character, status, error } = await useFetch(`/api/character/${id}/
|
||||
</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>
|
||||
<span>{{ config.peoples[character.race]?.name ?? 'Peuple inconnu' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-6 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.hp }}/{{ character.health }}</span>
|
||||
<span class="flex flex-row items-center gap-2">Mana: {{ character.mana - character.values.mana }}/{{ character.mana }}</span>
|
||||
<div class="flex flex-row lg:border-l border-light-30 dark:border-dark-30 py-4 ps-4 gap-8">
|
||||
<span class="flex flex-row items-center gap-2 text-3xl font-light">PV: <span class="font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35">{{ character.health - character.variables.health }}</span>/ {{ character.health }}</span>
|
||||
<span class="flex flex-row items-center gap-2 text-3xl font-light">Mana: <span class="font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35">{{ character.mana - character.variables.mana }}</span>/ {{ character.mana }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="self-center">
|
||||
@@ -48,8 +76,8 @@ const { data: character, status, error } = await useFetch(`/api/character/${id}/
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col justify-center gap-4 *:py-2">
|
||||
<div class="grid 2xl:grid-cols-12 grid-cols-2 gap-4 items-center border-b border-light-30 dark:border-dark-30">
|
||||
<div class="flex relative justify-between ps-4 gap-2 2xl:col-span-6 lg:col-span-2">
|
||||
<div class="flex flex-row gap-4 items-center border-b border-light-30 dark:border-dark-30 me-4 pe-4 divide-x divide-light-30 dark:divide-dark-30">
|
||||
<div class="flex relative justify-between ps-4 gap-2">
|
||||
<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>
|
||||
@@ -58,113 +86,105 @@ const { data: character, status, error } = await useFetch(`/api/character/${id}/
|
||||
<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 relative 2xl:border-l border-light-30 dark:border-dark-30 ps-4 2xl:col-span-2">
|
||||
<div class="flex flex-1 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>
|
||||
</div>
|
||||
<!-- <div class="absolute top-0 left-0 bottom-0 right-0 bg-light-0 dark:bg-dark-0 bg-opacity-50 dark:bg-opacity-50 text-xl font-bold flex items-center justify-center">Les données secondaires arrivent bientôt.</div> -->
|
||||
<div class="flex flex-1 relative ps-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>
|
||||
</div>
|
||||
<div class="flex flex-1 relative ps-4 flex-row items-center justify-between">
|
||||
<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 class="flex relative border-l border-light-30 dark:border-dark-30 ps-4 2xl:col-span-4">
|
||||
<div class="flex flex-col px-2">
|
||||
<span class="text-xl">Défense passive: <span class="text-2xl font-bold">{{ character.defense.static }}</span>/+<span class="text-2xl font-bold">{{ character.defense.passivedodge }}</span>/+<span class="text-2xl font-bold">{{ character.defense.passiveparry }}</span></span>
|
||||
<span class="text-xl">Défense active: <span class="float-right">+<span class="text-2xl font-bold">{{ character.defense.activedodge }}</span>/+<span class="text-2xl font-bold">{{ character.defense.activeparry }}</span></span></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 flex items-center gap-4">Résistances (Attaque/Défense) <Tooltip side="right" message="Les défenses affichées incluent déjà leur modifieur de statistique."><Icon icon="radix-icons:question-mark-circled" /></Tooltip></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, resistance) of character.resistance"><span class="font-bold text-base text-light-100 dark:text-dark-100">+{{ value[0] }}/+{{ value[1] + character.modifier[characterConfig.resistances[resistance].statistic as MainStat] }}</span><span>{{ characterConfig.resistances[resistance].name }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col pe-8 gap-4 py-2 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 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 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>{{ abilityTexts[ability] }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrises</span>
|
||||
<div class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
|
||||
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes légères" label="Arme légère" />
|
||||
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes de jet" label="Arme de jet" />
|
||||
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes naturelles" label="Arme naturelle" />
|
||||
<PreviewA v-if="character.mastery.strength > 1" href="regles/annexes/equipement#Les armes" label="Arme standard" />
|
||||
<PreviewA v-if="character.mastery.strength > 1" href="regles/annexes/equipement#Les armes improvisées" label="Arme improvisée" />
|
||||
<PreviewA v-if="character.mastery.strength > 2" href="regles/annexes/equipement#Les armes lourdes" label="Arme lourde" />
|
||||
<PreviewA v-if="character.mastery.strength > 3" href="regles/annexes/equipement#Les armes à deux mains" label="Arme à deux mains" />
|
||||
<PreviewA v-if="character.mastery.dexterity > 0 && character.mastery.strength > 1" href="regles/annexes/equipement#Les armes maniables" label="Arme maniable" />
|
||||
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 1" href="regles/annexes/equipement#Les armes à projectiles" label="Arme à projectiles" />
|
||||
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 2" href="regles/annexes/equipement#Les armes longues" label="Arme longue" />
|
||||
<PreviewA v-if="character.mastery.shield > 0" href="regles/annexes/equipement#Les boucliers" label="Bouclier" />
|
||||
<PreviewA v-if="character.mastery.shield > 0 && character.mastery.strength > 3" href="regles/annexes/equipement#Les boucliers à deux mains" label="Bouclier à deux mains" />
|
||||
</div>
|
||||
<div v-if="character.mastery.armor > 0" class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
|
||||
<PreviewA v-if="character.mastery.armor > 0" href="regles/annexes/equipement#Les armures légères" label="Armure légère" />
|
||||
<PreviewA v-if="character.mastery.armor > 1" href="regles/annexes/equipement#Les armures" label="Armure standard" />
|
||||
<PreviewA v-if="character.mastery.armor > 2" href="regles/annexes/equipement#Les armures lourdes" label="Armure lourde" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
|
||||
<span>Précision: <span class="font-bold">{{ character.spellranks.precision }}</span></span>
|
||||
<span>Savoir: <span class="font-bold">{{ character.spellranks.knowledge }}</span></span>
|
||||
<span>Instinct: <span class="font-bold">{{ character.spellranks.instinct }}</span></span>
|
||||
<span>Oeuvres: <span class="font-bold">{{ character.spellranks.arts }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TabsRoot default-value="features" class="w-[60rem]">
|
||||
<TabsList class="flex flex-row gap-4 relative px-4">
|
||||
<TabsIndicator class="absolute px-8 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>
|
||||
<TabsRoot default-value="features" class="w-[60rem] max-h-full">
|
||||
<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="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue" v-if="character.spellslots > 0">Sorts</TabsTrigger>
|
||||
<TabsTrigger value="inventory" class="px-2 py-1 border-b border-transparent hover:border-accent-blue" v-if="character.capacity !== false">Inventaire</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">
|
||||
<TabsContent value="features" class="overflow-y-auto max-h-full">
|
||||
<div class="flex flex-1 flex-col ps-8 gap-4 py-4">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="flex flex-col col-span-2">
|
||||
<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')" />
|
||||
<MarkdownRenderer :content="character.lists.action?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: preview } }" />
|
||||
</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 class="flex flex-col gap-2">
|
||||
<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.lists.reaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: preview } }" />
|
||||
</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.lists.freeaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: preview } }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-lg font-semibold">Aptitudes</span>
|
||||
<MarkdownRenderer :content="character.features.misc.map(e => `> ${e}`).join('\n\n')" />
|
||||
<MarkdownRenderer :content="character.lists.passive?.map(e => getText(e))?.map(e => `> ${e}`).join('\n\n')" :properties="{ tags: { a: preview } }" />
|
||||
</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)">
|
||||
<TabsContent v-if="character.spellslots > 0" value="spells" class="overflow-y-auto max-h-full">
|
||||
<div class="flex flex-1 flex-col ps-8 gap-4 py-2">
|
||||
<div class="flex flex-1 justify-between items-baseline px-2"><div></div><div class="flex gap-4 items-baseline"><span class="italic text-light-70 dark:text-dark-70 text-sm">{{ character.variables.spells.length }} / {{ character.spellslots }} sorts maitrisés</span><Button class="!font-normal" @click="openSpellPanel">Modifier</Button></div></div>
|
||||
<div class="flex flex-col" v-if="[...(character.lists.spells ?? []), ...character.variables.spells].length > 0">
|
||||
<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.lists.spells ?? []), ...character.variables.spells].map(e => config.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">{{ elementTexts[element].text }}</span>
|
||||
<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="" v-if="spell.rank !== 4">Rang {{ spell.rank }}</span><span v-if="spell.rank !== 4">/</span>
|
||||
<span class="" v-if="spell.rank !== 4">{{ spellTypeTexts[spell.type] }}</span><span v-if="spell.rank !== 4">/</span>
|
||||
<span class="">{{ spell.cost }} mana</span><span>/</span>
|
||||
<span class="">{{ typeof spell.speed === 'string' ? spell.speed : `${spell.speed} minutes` }}</span>
|
||||
<span class="capitalize">{{ typeof spell.speed === 'string' ? spell.speed : `${spell.speed} minutes` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -173,7 +193,10 @@ const { data: character, status, error } = await useFetch(`/api/character/${id}/
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="notes">
|
||||
<TabsContent value="inventory" v-if="character.capacity !== false" class="overflow-y-auto max-h-full">
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="notes" class="overflow-y-auto max-h-full">
|
||||
<div class="flex flex-1 flex-col ps-8 gap-4 py-8">
|
||||
<MarkdownRenderer :content="character.notes" />
|
||||
</div>
|
||||
@@ -187,5 +210,5 @@ const { data: character, status, error } = await useFetch(`/api/character/${id}/
|
||||
<Title>d[any] - Erreur</Title>
|
||||
</Head>
|
||||
<div>Erreur de chargement</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</template>
|
||||
@@ -1,20 +1,20 @@
|
||||
<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({
|
||||
guestsGoesTo: '/user/login',
|
||||
})
|
||||
const { add } = useToast();
|
||||
const { user } = useUserSession();
|
||||
|
||||
const { data: characters, error, status } = await useFetch(`/api/character`);
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
async function deleteCharacter(id: number)
|
||||
{
|
||||
status.value = "pending";
|
||||
await useRequestFetch()(`/api/character/${id}`, { method: 'delete' });
|
||||
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);
|
||||
}
|
||||
async function duplicateCharacter(id: number)
|
||||
@@ -22,7 +22,7 @@ async function duplicateCharacter(id: number)
|
||||
status.value = "pending";
|
||||
const newId = await useRequestFetch()(`/api/character/${id}/duplicate`, { method: 'post' });
|
||||
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 } });
|
||||
}
|
||||
</script>
|
||||
@@ -32,59 +32,47 @@ async function duplicateCharacter(id: number)
|
||||
<Title>d[any] - Mes personnages</Title>
|
||||
</Head>
|
||||
<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">
|
||||
<Loading size="large" />
|
||||
</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">
|
||||
<div class="border border-light-30 dark:border-dark-30 p-3 flex flex-row gap-4" v-for="character of characters">
|
||||
<Avatar size="large" icon="radix-icons:person" src="" />
|
||||
<div class="flex flex-1 flex-shrink flex-col truncate">
|
||||
<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>
|
||||
<span class="text-sm truncate">Niveau {{ character.level }}</span>
|
||||
</div>
|
||||
<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>
|
||||
<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">
|
||||
<Icon icon="radix-icons:trash" class="absolute left-1.5" />
|
||||
<span>Supprimer</span>
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<DropdownMenuArrow class="fill-light-35 dark:fill-dark-35" />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuRoot>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<AlertDialogContent
|
||||
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
<AlertDialogTitle class="text-3xl font-light relative -top-2">Supprimer {{ character.name }} ?</AlertDialogTitle>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
|
||||
<AlertDialogAction asChild><Button @click="() => deleteCharacter(character.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="character of characters">
|
||||
<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">
|
||||
<div class="flex flex-row gap-8 ps-4 items-center">
|
||||
<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>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</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>
|
||||
<AlertDialogTrigger>
|
||||
<span class="text-sm font-bold text-light-red dark:text-dark-red">Supprimer</span>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<AlertDialogContent
|
||||
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
<AlertDialogTitle class="text-3xl font-light relative -top-2">Supprimer {{ character.name }} ?</AlertDialogTitle>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
|
||||
<AlertDialogAction asChild><Button @click="() => deleteCharacter(character.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
||||
@@ -15,7 +15,7 @@ const { data: characters, error, status } = await useFetch(`/api/character`, { p
|
||||
<Avatar size="large" icon="radix-icons:person" src="" />
|
||||
<div class="flex flex-1 flex-shrink flex-col truncate">
|
||||
<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>
|
||||
<span class="text-sm truncate">Niveau {{ character.progress.level }}</span>
|
||||
<span class="text-sm truncate">Niveau {{ character.level }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
26
pages/character/manage.client.vue
Normal file
26
pages/character/manage.client.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { HomebrewBuilder } from '~/shared/feature.util';
|
||||
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
});
|
||||
|
||||
const container = useTemplateRef('container');
|
||||
|
||||
onMounted(() => {
|
||||
queueMicrotask(() => {
|
||||
if(container.value)
|
||||
{
|
||||
const builder = new HomebrewBuilder(container.value);
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Edition de données</Title>
|
||||
</Head>
|
||||
<div ref="container" class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full"></div>
|
||||
</template>
|
||||
@@ -1,24 +1,23 @@
|
||||
<template>
|
||||
<div class="flex flex-1 justify-start items-start" v-if="overview">
|
||||
<div class="flex flex-1 justify-start items-start" ref="element">
|
||||
<Head>
|
||||
<Title>d[any] - {{ overview.title }}</Title>
|
||||
<Title>d[any] - {{ overview?.title ?? "Erreur" }}</Title>
|
||||
</Head>
|
||||
<Markdown v-if="overview.type === 'markdown'" :path="path" />
|
||||
<Canvas v-else-if="overview.type === 'canvas'" :path="path" />
|
||||
<ProseH2 v-else class="flex-1 text-center">Impossible d'afficher le contenu demandé</ProseH2>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Head>
|
||||
<Title>d[any] - Erreur</Title>
|
||||
</Head>
|
||||
<div><ProseH2>Impossible d'afficher le contenu demandé</ProseH2></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||
import { Content } from '#shared/content.util';
|
||||
import { unifySlug } from '#shared/general.util';
|
||||
|
||||
const { content } = useContent();
|
||||
const overview = computed(() => content.value.find(e => e.path === path.value));
|
||||
const element = useTemplateRef('element'), overview = ref();
|
||||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => unifySlug(route.value.params.path ?? ''));
|
||||
|
||||
onMounted(async () => {
|
||||
if(element.value && path.value && await Content.ready)
|
||||
{
|
||||
overview.value = Content.render(element.value, path.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -2,577 +2,97 @@
|
||||
<Head>
|
||||
<Title>d[any] - Modification</Title>
|
||||
</Head>
|
||||
<ClientOnly>
|
||||
<CollapsibleRoot asChild class="flex flex-1 flex-col xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3 overflow-hidden" v-model="open">
|
||||
<div>
|
||||
<div class="z-50 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
<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.light.svg" class="block dark:hidden" />
|
||||
<span class="text-xl max-md:hidden">d[any]</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-row relative 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">
|
||||
<div class="flex flex-row justify-between items-center pt-2 pb-4 mb-2 px-2 gap-4 border-b border-light-35 dark:border-dark-35">
|
||||
<Button @click="router.push({ name: 'explore-path', params: { path: selected ? getPath(selected) : 'index' } })">Quitter</Button>
|
||||
<Button @click="save(true);">Enregistrer</Button>
|
||||
<Tooltip side="top" message="Nouveau">
|
||||
<DropdownMenu align="end" side="bottom" :options="[{
|
||||
type: 'item',
|
||||
label: 'Markdown',
|
||||
kbd: 'Ctrl+N',
|
||||
icon: 'radix-icons:file-text',
|
||||
select: () => add('markdown'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Dossier',
|
||||
kbd: 'Ctrl+Shift+N',
|
||||
icon: 'lucide:folder',
|
||||
select: () => add('folder'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Canvas',
|
||||
icon: 'ph:graph-light',
|
||||
select: () => add('canvas'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Carte',
|
||||
icon: 'lucide:map',
|
||||
select: () => add('map'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Fichier',
|
||||
icon: 'radix-icons:file',
|
||||
select: () => add('file'),
|
||||
}]">
|
||||
<Button icon><Icon class="w-5 h-5" icon="radix-icons:plus" /></Button>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<DraggableTree class="ps-4 text-sm" :items="navigation ?? undefined" :get-key="(item: Partial<TreeItemEditable>) => item.path !== undefined ? getPath(item as TreeItemEditable) : ''" @updateTree="drop"
|
||||
v-model="selected" :defaultExpanded="defaultExpanded" :get-children="(item: Partial<TreeItemEditable>) => item.type === 'folder' ? item.children : undefined" >
|
||||
<template #default="{ handleToggle, handleSelect, isExpanded, isDragging, item }">
|
||||
<div class="flex flex-1 items-center overflow-hidden" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level / 2 - 0.5}em` }">
|
||||
<div class="flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple group-data-[selected]:text-accent-blue">
|
||||
<Icon @click="handleToggle" v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level / 2 - 1.5}em` }" />
|
||||
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="w-5 h-5" @click="handleSelect" />
|
||||
<div class="pl-1.5 py-1.5 flex-1 truncate" :title="item.value.title" @click="handleSelect" :class="{ 'font-semibold': item.hasChildren }">
|
||||
{{ item.value.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span @click="item.value.private = !item.value.private">
|
||||
<Icon v-if="item.value.private" icon="radix-icons:lock-closed" />
|
||||
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:lock-open-2" />
|
||||
</span>
|
||||
<span @click="item.value.navigable = !item.value.navigable">
|
||||
<Icon v-if="item.value.navigable" icon="radix-icons:eye-open" />
|
||||
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:eye-none" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #hint="{ instruction }">
|
||||
<div v-if="instruction" class="absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50" :style="{
|
||||
width: `calc(100% - ${instruction.currentLevel / 2 - 1.5}em)`
|
||||
}" :class="{
|
||||
'!border-b-4': instruction?.type === 'reorder-below',
|
||||
'!border-t-4': instruction?.type === 'reorder-above',
|
||||
'!border-4': instruction?.type === 'make-child',
|
||||
}"></div>
|
||||
</template>
|
||||
</DraggableTree>
|
||||
</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>
|
||||
<div class="flex flex-1 flex-row max-h-full overflow-hidden">
|
||||
<div v-if="selected" class="flex flex-1 flex-col items-start justify-start max-h-full relative">
|
||||
<Head>
|
||||
<Title>d[any] - Modification de {{ selected.title }}</Title>
|
||||
</Head>
|
||||
<CollapsibleRoot v-model:open="topOpen" class="group data-[state=open]:mt-4 w-full relative">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button class="absolute left-1/2 -translate-x-1/2 group-data-[state=open]:-bottom-3 group-data-[state=closed]:-bottom-6 z-30" icon>
|
||||
<Icon v-if="topOpen" icon="radix-icons:caret-up" class="h-4 w-4" />
|
||||
<Icon v-else icon="radix-icons:caret-down" class="h-4 w-4" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent class="xl:px-12 lg:px-8 px-6">
|
||||
<div class="pb-2 grid lg:grid-cols-2 grid-cols-1 lg:items-center justify-between gap-x-4 flex-1 border-b border-light-35 dark:border-dark-35">
|
||||
<input type="text" v-model="selected.title" @input="() => {
|
||||
if(selected && !selected.customPath)
|
||||
{
|
||||
selected.name = parsePath(selected.title);
|
||||
rebuildPath(selected.children, getPath(selected));
|
||||
}
|
||||
}" placeholder="Titre" style="line-height: normal;" class="flex-1 md:text-5xl text-4xl md:h-14 h-12 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none pb-3 font-thin bg-transparent"/>
|
||||
<div class="flex flex-row justify-between items-center gap-x-4">
|
||||
<div v-if="selected.customPath" class="flex lg:items-center truncate">
|
||||
<pre class="md:text-base text-sm truncate" style="direction: rtl">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}</pre>
|
||||
<TextInput v-model="selected.name" @input="(e: Event) => {
|
||||
if(selected && selected.customPath)
|
||||
{
|
||||
selected.name = parsePath(selected.name);
|
||||
rebuildPath(selected.children, getPath(selected));
|
||||
}
|
||||
}" class="mx-0 font-mono"/>
|
||||
</div>
|
||||
<pre v-else class="md:text-base text-sm truncate" style="direction: rtl">{{ getPath(selected) }}/</pre>
|
||||
<div class="flex gap-4">
|
||||
<Dialog :title="`Supprimer '${selected.title}'${selected.children?.length ?? 0 > 0 ? ' et ses enfants' : ''}`">
|
||||
<template #trigger><Button icon class="bg-light-red dark:bg-dark-red !bg-opacity-40 border-light-red dark:border-dark-red hover:bg-light-red dark:hover:bg-dark-red hover:!bg-opacity-70 hover:border-light-red dark:hover:border-dark-red"><Icon icon="radix-icons:trash" /></Button></template>
|
||||
<template #default>
|
||||
<div class="flex gap-4">
|
||||
<DialogClose><Button @click="navigation = tree.remove(navigation, getPath(selected)); selected = undefined;" class="bg-light-red dark:bg-dark-red !bg-opacity-40 border-light-red dark:border-dark-red hover:bg-light-red dark:hover:bg-dark-red hover:!bg-opacity-70 hover:border-light-red dark:hover:border-dark-red">Oui</Button></DialogClose>
|
||||
<DialogClose><Button>Non</Button></DialogClose>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
<Dialog title="Préférences Markdown" v-if="selected.type === 'markdown'">
|
||||
<template #trigger><Button icon><Icon icon="radix-icons:gear" /></Button></template>
|
||||
<template #default>
|
||||
<Select label="Editeur de markdown" :modelValue="preferences.markdown.editing" @update:model-value="v => preferences.markdown.editing = (v as 'reading' | 'editing' | 'split')">
|
||||
<SelectItem label="Mode lecture" value="reading" />
|
||||
<SelectItem label="Mode edition" value="editing" />
|
||||
<SelectItem label="Ecran partagé" value="split" />
|
||||
</Select>
|
||||
</template>
|
||||
</Dialog>
|
||||
<DropdownMenu align="end" :options="[{
|
||||
type: 'checkbox',
|
||||
label: 'URL custom',
|
||||
select: (state: boolean) => { selected!.customPath = state; if(!state) selected!.name = parsePath(selected!.title) },
|
||||
checked: selected.customPath
|
||||
}]">
|
||||
<Button icon><Icon icon="radix-icons:dots-vertical"/></Button>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</CollapsibleRoot>
|
||||
<div class="py-4 flex-1 w-full max-h-full flex overflow-hidden xl:px-12 lg:px-8 px-6 relative">
|
||||
<template v-if="selected.type === 'markdown'">
|
||||
<div v-if="contentStatus === 'pending'" class="flex flex-1 justify-center items-center">
|
||||
<Loading />
|
||||
</div>
|
||||
<span v-else-if="contentError">{{ contentError }}</span>
|
||||
<template v-else-if="preferences.markdown.editing === 'editing'">
|
||||
<Editor v-model="selected.content" autofocus class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto lg:mx-16 xl:mx-32 2xl:mx-64" />
|
||||
</template>
|
||||
<template v-else-if="preferences.markdown.editing === 'reading'">
|
||||
<div class="flex-1 max-h-full !overflow-y-auto px-4 xl:px-32 2xl:px-64"><MarkdownRenderer :content="(debounced as string)" :proses="{ 'a': FakeA }" /></div>
|
||||
</template>
|
||||
<template v-else-if="preferences.markdown.editing === 'split'">
|
||||
<SplitterGroup direction="horizontal" class="flex-1 w-full flex">
|
||||
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }" :defaultSize="50">
|
||||
<Editor v-model="selected.content" autofocus class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto" :class="{ 'hidden': isCollapsed }" />
|
||||
</SplitterPanel>
|
||||
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
||||
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }">
|
||||
<div class="flex-1 max-h-full !overflow-y-auto px-8" :class="{ 'hidden': isCollapsed }"><MarkdownRenderer :content="(debounced as string)" :proses="{ 'a': FakeA }" /></div>
|
||||
</SplitterPanel>
|
||||
</SplitterGroup>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="selected.type === 'canvas'">
|
||||
<CanvasEditor v-if="selected.content" :modelValue="selected.content" :path="getPath(selected)" />
|
||||
</template>
|
||||
<template v-else-if="selected.type === 'map'">
|
||||
<span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de carte en cours de développement</ProseH3></span>
|
||||
</template>
|
||||
<template v-else-if="selected.type === 'file'">
|
||||
<span>Modifier le contenu :</span><input type="file" @change="(e: Event) => console.log((e.target as HTMLInputElement).files?.length)" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3 overflow-hidden">
|
||||
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
<!-- <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.light.svg" class="block dark:hidden" />
|
||||
<span class="text-xl max-md:hidden">d[any]</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
|
||||
<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="tree"></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>
|
||||
</CollapsibleRoot>
|
||||
</ClientOnly>
|
||||
<div class="flex flex-1 flex-row max-h-full overflow-hidden" ref="container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
||||
import { iconByType, convertContentFromText, convertContentToText, DEFAULT_CONTENT,parsePath } from '#shared/general.util';
|
||||
import type { ExploreContent, FileType, TreeItem } from '~/types/content';
|
||||
import FakeA from '~/components/prose/FakeA.vue';
|
||||
import type { Preferences } from '~/types/general';
|
||||
|
||||
export type TreeItemEditable = TreeItem &
|
||||
{
|
||||
parent: string;
|
||||
name: string;
|
||||
customPath: boolean;
|
||||
children?: TreeItemEditable[];
|
||||
}
|
||||
|
||||
import { Content, Editor } from '#shared/content.util';
|
||||
import { button, loading } from '#shared/components.util';
|
||||
import { dom, icon, text } from '#shared/dom.util';
|
||||
import { modal, popper, tooltip } from '#shared/floating.util';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin', 'editor'],
|
||||
layout: 'null',
|
||||
});
|
||||
|
||||
const { user } = useUserSession();
|
||||
const tree = useTemplateRef('tree'), container = useTemplateRef('container');
|
||||
let editor: Editor;
|
||||
|
||||
const router = useRouter();
|
||||
const open = ref(true), topOpen = ref(true);
|
||||
|
||||
const toaster = useToast();
|
||||
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||
|
||||
const { content: complete, tree: project } = useContent();
|
||||
const navigation = ref<TreeItemEditable[]>(transform(JSON.parse(JSON.stringify(project.value)))!);
|
||||
const selected = ref<TreeItemEditable>(), edited = ref(false);
|
||||
const contentStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), contentError = ref<string>();
|
||||
|
||||
const preferences = useCookie<Preferences>('preferences', { default: () => ({ markdown: { editing: 'split' }, canvas: { snap: true, size: 32 } }), watch: true, maxAge: 60*60*24*31 });
|
||||
|
||||
watch(selected, async (value, old) => {
|
||||
if(selected.value)
|
||||
{
|
||||
if(!selected.value.content && selected.value.path)
|
||||
{
|
||||
contentStatus.value = 'pending';
|
||||
try
|
||||
{
|
||||
const storedEdit = sessionStorage.getItem(`editing:${encodeURIComponent(selected.value.path)}`);
|
||||
|
||||
if(storedEdit)
|
||||
{
|
||||
selected.value.content = convertContentFromText(selected.value.type, storedEdit);
|
||||
contentStatus.value = 'success';
|
||||
}
|
||||
else
|
||||
{
|
||||
selected.value.content = (await $fetch(`/api/file/content/${encodeURIComponent(selected.value.path)}`, { query: { type: 'editing'} }));
|
||||
contentStatus.value = 'success';
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
debounced.value = selected.value.content ?? '';
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
contentError.value = (e as Error).message;
|
||||
contentStatus.value = 'error';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//@ts-ignore
|
||||
debounced.value = selected.value.content ?? '';
|
||||
}
|
||||
router.replace({ hash: '#' + selected.value.path || getPath(selected.value) });
|
||||
}
|
||||
else
|
||||
{
|
||||
router.replace({ hash: '' });
|
||||
}
|
||||
})
|
||||
const content = computed(() => selected.value?.content ?? '');
|
||||
const debounced = useDebounce(content, 250, { maxWait: 500 });
|
||||
|
||||
watch(debounced, () => {
|
||||
if(selected.value && debounced.value)
|
||||
sessionStorage.setItem(`editing:${encodeURIComponent(selected.value.path)}`, typeof debounced.value === 'string' ? debounced.value : JSON.stringify(debounced.value));
|
||||
});
|
||||
useShortcuts({
|
||||
meta_s: { usingInput: true, handler: () => save(false), 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 }
|
||||
})
|
||||
|
||||
const tree = {
|
||||
remove(data: TreeItemEditable[], id: string): TreeItemEditable[] {
|
||||
return data
|
||||
.filter(item => getPath(item) !== id)
|
||||
.map((item) => {
|
||||
if (tree.hasChildren(item)) {
|
||||
return {
|
||||
...item,
|
||||
children: tree.remove(item.children ?? [], id),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
},
|
||||
insertBefore(data: TreeItemEditable[], targetId: string, newItem: TreeItemEditable): TreeItemEditable[] {
|
||||
return data.flatMap((item) => {
|
||||
if (getPath(item) === targetId)
|
||||
return [newItem, item];
|
||||
|
||||
if (tree.hasChildren(item)) {
|
||||
return {
|
||||
...item,
|
||||
children: tree.insertBefore(item.children ?? [], targetId, newItem),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
},
|
||||
insertAfter(data: TreeItemEditable[], targetId: string, newItem: TreeItemEditable): TreeItemEditable[] {
|
||||
return data.flatMap((item) => {
|
||||
if (getPath(item) === targetId)
|
||||
return [item, newItem];
|
||||
|
||||
if (tree.hasChildren(item)) {
|
||||
return {
|
||||
...item,
|
||||
children: tree.insertAfter(item.children ?? [], targetId, newItem),
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
},
|
||||
insertChild(data: TreeItemEditable[], targetId: string, newItem: TreeItemEditable): TreeItemEditable[] {
|
||||
return data.flatMap((item) => {
|
||||
if (getPath(item) === targetId) {
|
||||
// already a parent: add as first child
|
||||
return {
|
||||
...item,
|
||||
// opening item so you can see where item landed
|
||||
isOpen: true,
|
||||
children: [newItem, ...item.children ?? []],
|
||||
};
|
||||
}
|
||||
|
||||
if (!tree.hasChildren(item))
|
||||
return item;
|
||||
|
||||
return {
|
||||
...item,
|
||||
children: tree.insertChild(item.children ?? [], targetId, newItem),
|
||||
};
|
||||
});
|
||||
},
|
||||
find(data: TreeItemEditable[], itemId: string): TreeItemEditable | undefined {
|
||||
for (const item of data) {
|
||||
if (getPath(item) === itemId)
|
||||
return item;
|
||||
|
||||
if (tree.hasChildren(item)) {
|
||||
const result = tree.find(item.children ?? [], itemId);
|
||||
if (result)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
},
|
||||
search(data: TreeItemEditable[], prop: keyof TreeItemEditable, value: string): TreeItemEditable[] {
|
||||
const arr = [];
|
||||
|
||||
for (const item of data)
|
||||
{
|
||||
if (item[prop]?.toString().toLowerCase()?.startsWith(value.toLowerCase()))
|
||||
arr.push(item);
|
||||
|
||||
if (tree.hasChildren(item)) {
|
||||
arr.push(...tree.search(item.children ?? [], prop, value));
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
},
|
||||
getPathToItem({
|
||||
current,
|
||||
targetId,
|
||||
parentIds = [],
|
||||
}: {
|
||||
current: TreeItemEditable[]
|
||||
targetId: string
|
||||
parentIds?: string[]
|
||||
}): string[] | undefined {
|
||||
for (const item of current) {
|
||||
if (getPath(item) === targetId)
|
||||
return parentIds;
|
||||
|
||||
const nested = tree.getPathToItem({
|
||||
current: (item.children ?? []),
|
||||
targetId,
|
||||
parentIds: [...parentIds, getPath(item)],
|
||||
});
|
||||
if (nested)
|
||||
return nested;
|
||||
}
|
||||
},
|
||||
hasChildren(item: TreeItemEditable): boolean {
|
||||
return (item.children ?? []).length > 0;
|
||||
},
|
||||
}
|
||||
|
||||
function add(type: FileType): void
|
||||
function pull()
|
||||
{
|
||||
if(!navigation.value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const news = [...tree.search(navigation.value, '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.value = [...navigation.value, item];
|
||||
}
|
||||
else if(selected.value?.children)
|
||||
{
|
||||
item.parent = getPath(selected.value);
|
||||
navigation.value = tree.insertChild(navigation.value, item.parent, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
navigation.value = tree.insertAfter(navigation.value, getPath(selected.value), item);
|
||||
}
|
||||
}
|
||||
function updateTree(instruction: Instruction, itemId: string, targetId: string) : TreeItemEditable[] | undefined {
|
||||
if(!navigation.value)
|
||||
return;
|
||||
|
||||
const item = tree.find(navigation.value, itemId);
|
||||
const target = tree.find(navigation.value, targetId);
|
||||
|
||||
if(!item)
|
||||
return;
|
||||
|
||||
if (instruction.type === 'reparent') {
|
||||
const path = tree.getPathToItem({
|
||||
current: navigation.value,
|
||||
targetId: targetId,
|
||||
});
|
||||
if (!path) {
|
||||
console.error(`missing ${path}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const desiredId = path[instruction.desiredLevel];
|
||||
let result = tree.remove(navigation.value, 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.value;
|
||||
|
||||
if (instruction.type === 'reorder-above') {
|
||||
let result = tree.remove(navigation.value, itemId);
|
||||
result = tree.insertBefore(result, targetId, item);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (instruction.type === 'reorder-below') {
|
||||
let result = tree.remove(navigation.value, 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.value, itemId);
|
||||
result = tree.insertChild(result, targetId, item);
|
||||
rebuildPath([item], targetId);
|
||||
return result;
|
||||
}
|
||||
|
||||
return navigation.value;
|
||||
}
|
||||
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.value = updateTree(instruction, itemId, targetId) ?? navigation.value ?? [];
|
||||
}
|
||||
function rebuildPath(tree: TreeItemEditable[] | null | undefined, parentPath: string)
|
||||
{
|
||||
if(!tree)
|
||||
return;
|
||||
|
||||
tree.forEach(e => {
|
||||
e.parent = parentPath;
|
||||
rebuildPath(e.children, getPath(e));
|
||||
});
|
||||
}
|
||||
async function save(redirect: boolean): Promise<void>
|
||||
{
|
||||
//@ts-ignore
|
||||
const map = (e: TreeItemEditable[]): TreeItemEditable[] => e.map(f => ({ ...f, content: f.content ? convertContentToText(f.type, f.content) : undefined, children: f.children ? map(f.children) : undefined }));
|
||||
saveStatus.value = 'pending';
|
||||
try {
|
||||
const result = await $fetch(`/api/project`, {
|
||||
method: 'post',
|
||||
body: map(navigation.value),
|
||||
});
|
||||
saveStatus.value = 'success';
|
||||
edited.value = false;
|
||||
sessionStorage.clear();
|
||||
|
||||
toaster.clear('error');
|
||||
toaster.add({ type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000 });
|
||||
|
||||
//@ts-ignore
|
||||
complete.value = result as ExploreContent[];
|
||||
if(redirect) router.go(-1);
|
||||
} catch(e: any) {
|
||||
toaster.add({
|
||||
type: 'error', content: e.message, timer: true, duration: 10000
|
||||
})
|
||||
Content.pull().then(e => {
|
||||
Toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
|
||||
}).catch(e => {
|
||||
Toaster.add({ type: 'success', content: 'Une erreur est survenue durant la récupération des données.', timer: true, duration: 7500 });
|
||||
console.error(e);
|
||||
saveStatus.value = 'error';
|
||||
}
|
||||
});
|
||||
}
|
||||
function getPath(item: TreeItemEditable): string
|
||||
function push()
|
||||
{
|
||||
return [item.parent, parsePath(item.customPath ? item.name : item.title)].filter(e => !!e).join('/');
|
||||
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 => {
|
||||
close();
|
||||
Toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
|
||||
}).catch(e => {
|
||||
close();
|
||||
Toaster.add({ type: 'success', content: 'Une erreur est survenue durant l\'enregistrement des données.', timer: true, duration: 7500 });
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
onMounted(async () => {
|
||||
if(tree.value && container.value && await Content.ready)
|
||||
{
|
||||
const load = loading('normal');
|
||||
tree.value.appendChild(load);
|
||||
|
||||
const content = dom('div', { class: 'flex flex-row justify-start items-center gap-4 p-2' }, [
|
||||
tooltip(button(icon('ph:cloud-arrow-down', { height: 20, width: 20 }), pull, 'p-1'), 'Actualiser', 'top'),
|
||||
tooltip(button(icon('ph:cloud-arrow-up', { height: 20, width: 20 }), push, 'p-1'), 'Enregistrer', 'top'),
|
||||
])
|
||||
|
||||
tree.value.insertBefore(content, load);
|
||||
|
||||
editor = new Editor();
|
||||
|
||||
tree.value.replaceChild(editor.tree.container, load);
|
||||
container.value.appendChild(editor.container);
|
||||
}
|
||||
})
|
||||
/*watch(router.currentRoute, (value) => {
|
||||
if(value && value.hash && navigation.value)
|
||||
selected.value = tree.find(navigation.value, value.hash.substring(1));
|
||||
else
|
||||
selected.value = undefined;
|
||||
}, { immediate: true });*/
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor?.unmount();
|
||||
});
|
||||
</script>
|
||||
@@ -25,8 +25,6 @@ definePageMeta({
|
||||
usersGoesTo: '/user/profile',
|
||||
});
|
||||
|
||||
const toaster = useToast();
|
||||
|
||||
const email = ref(''), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||
|
||||
async function submit()
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
@@ -33,7 +34,6 @@ definePageMeta({
|
||||
|
||||
const query = useRouter().currentRoute.value.query;
|
||||
|
||||
const toaster = useToast();
|
||||
const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false);
|
||||
const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref('');
|
||||
|
||||
@@ -70,7 +70,7 @@ async function submit()
|
||||
{
|
||||
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' });
|
||||
}
|
||||
else
|
||||
@@ -81,7 +81,7 @@ async function submit()
|
||||
status.value = 'error';
|
||||
|
||||
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>
|
||||
@@ -26,13 +26,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
guestsGoesTo: '/user/login',
|
||||
});
|
||||
|
||||
const toaster = useToast();
|
||||
const { user } = useUserSession();
|
||||
const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false);
|
||||
const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref('');
|
||||
@@ -70,19 +70,19 @@ async function submit()
|
||||
{
|
||||
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' });
|
||||
}
|
||||
else
|
||||
{
|
||||
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) {
|
||||
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>
|
||||
@@ -18,17 +18,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ZodError } from 'zod';
|
||||
import type { ZodError } from 'zod/v4';
|
||||
import { schema, type Login } from '~/schemas/login';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
usersGoesTo: '/user/profile',
|
||||
});
|
||||
|
||||
const { add: addToast, clear: clearToasts } = useToast();
|
||||
|
||||
const state = reactive<Login>({
|
||||
usernameOrEmail: '',
|
||||
password: ''
|
||||
@@ -47,9 +46,9 @@ const toastMessage = ref('');
|
||||
async function submit()
|
||||
{
|
||||
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 === "")
|
||||
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);
|
||||
|
||||
@@ -64,8 +63,8 @@ async function submit()
|
||||
}
|
||||
else if(status.value === 'success' && login.success)
|
||||
{
|
||||
clearToasts();
|
||||
addToast({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
|
||||
Toaster.clear();
|
||||
Toaster.add({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
|
||||
await navigateTo('/user/profile');
|
||||
}
|
||||
}
|
||||
@@ -85,12 +84,12 @@ function handleErrors(error: Error | ZodError)
|
||||
{
|
||||
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
|
||||
{
|
||||
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>
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { hasPermissions } from "#shared/auth.util";
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
})
|
||||
const { user, clear } = useUserSession();
|
||||
const toaster = useToast();
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
async function revalidateUser()
|
||||
@@ -15,7 +15,7 @@ async function revalidateUser()
|
||||
method: 'post'
|
||||
});
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -27,9 +27,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ZodError } from 'zod';
|
||||
import { ZodError } from 'zod/v4';
|
||||
import { schema, type Registration } from '~/schemas/registration';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
@@ -42,7 +43,6 @@ const state = reactive<Registration>({
|
||||
password: ''
|
||||
});
|
||||
|
||||
const { add: addToast, clear: clearToasts } = useToast();
|
||||
const confirmPassword = ref("");
|
||||
|
||||
const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128);
|
||||
@@ -62,13 +62,13 @@ const { data: result, status, error, refresh } = await useFetch('/api/auth/regis
|
||||
async function submit()
|
||||
{
|
||||
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 === '')
|
||||
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 === "")
|
||||
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)
|
||||
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 });
|
||||
|
||||
const data = schema.safeParse(state);
|
||||
|
||||
@@ -83,8 +83,8 @@ async function submit()
|
||||
}
|
||||
else if(status.value === 'success' && login.success)
|
||||
{
|
||||
clearToasts();
|
||||
addToast({ duration: 10000, content: 'Vous avez été enregistré. Pensez à valider votre adresse mail.', timer: true, type: 'success' });
|
||||
Toaster.clear();
|
||||
Toaster.add({ duration: 10000, content: 'Vous avez été enregistré. Pensez à valider votre adresse mail.', timer: true, type: 'success' });
|
||||
await navigateTo('/user/profile');
|
||||
}
|
||||
}
|
||||
@@ -104,12 +104,12 @@ function handleErrors(error: Error | ZodError)
|
||||
{
|
||||
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
|
||||
{
|
||||
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>
|
||||
@@ -1,16 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const fileType = z.enum(['folder', 'file', 'markdown', 'canvas', 'map']);
|
||||
export const schema = z.object({
|
||||
path: z.string(),
|
||||
owner: z.number().finite(),
|
||||
title: z.string(),
|
||||
type: fileType,
|
||||
content: z.string(),
|
||||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
order: z.number().finite(),
|
||||
});
|
||||
|
||||
export type FileType = z.infer<typeof fileType>;
|
||||
export type File = z.infer<typeof schema>;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { fileType } from "./file";
|
||||
|
||||
export const single = z.object({
|
||||
path: z.string(),
|
||||
owner: z.number().finite(),
|
||||
title: z.string(),
|
||||
type: fileType,
|
||||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
order: z.number().finite(),
|
||||
});
|
||||
export const table = z.array(single);
|
||||
|
||||
export type Navigation = z.infer<typeof table>;
|
||||
export type NavigationItem = z.infer<typeof single>;
|
||||
@@ -1,22 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { fileType } from "./file";
|
||||
import { projectFilesTable } from "~/db/schema";
|
||||
|
||||
const baseItem = z.object({
|
||||
export const Project = z.array(z.object({
|
||||
id: z.string(),
|
||||
path: z.string(),
|
||||
parent: z.string(),
|
||||
name: z.string(),
|
||||
title: z.string(),
|
||||
type: fileType,
|
||||
type: z.enum(projectFilesTable.type.enumValues),
|
||||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
order: z.number().finite(),
|
||||
content: z.string().optional().or(z.null()),
|
||||
});
|
||||
export const item: z.ZodType<ProjectItem> = baseItem.extend({
|
||||
children: z.lazy(() => item.array().optional()),
|
||||
});
|
||||
export const project = z.array(item);
|
||||
timestamp: z.string(),
|
||||
}));
|
||||
|
||||
export type ProjectItem = z.infer<typeof baseItem> & {
|
||||
children?: ProjectItem[]
|
||||
};
|
||||
export type ProjectType = z.infer<typeof Project>;
|
||||
export type ProjectItemType = ProjectType[number];
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { SitemapUrlInput } from '#sitemap/types'
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { projectFilesTable as files } from '~/db/schema';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export default defineSitemapEventHandler(() => {
|
||||
const db = useDatabase();
|
||||
const pages = db.select({ path: explorerContentTable.path, lastMod: explorerContentTable.timestamp, navigable: explorerContentTable.navigable, private: explorerContentTable.private, type: explorerContentTable.type }).from(explorerContentTable).all();
|
||||
const pages = db.select({ path: files.path, lastMod: files.timestamp, navigable: files.navigable, private: files.private, type: files.type }).from(files).all();
|
||||
|
||||
return pages.filter(e => e.type !== 'folder' && e.navigable && !e.private && e.path.split('/').map((_, i, a) => a.slice(0, i).join('/')).every(p => !pages.find(_p => _p.path === p)?.private)).map(e => ({
|
||||
loc: `/explore/${encodeURIComponent(e.path)}`,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ne, sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
import { projectFilesTable } from '~/db/schema';
|
||||
import { hasPermissions } from '#shared/auth.util';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
@@ -16,17 +15,15 @@ export default defineEventHandler(async (e) => {
|
||||
|
||||
const db = useDatabase();
|
||||
const content = db.select({
|
||||
path: explorerContentTable.path,
|
||||
owner: explorerContentTable.owner,
|
||||
title: explorerContentTable.title,
|
||||
type: explorerContentTable.type,
|
||||
size: sql<number>`CASE WHEN ${explorerContentTable.content} IS NULL THEN 0 ELSE length(${explorerContentTable.content}) END`.as('size'),
|
||||
navigable: explorerContentTable.navigable,
|
||||
private: explorerContentTable.private,
|
||||
order: explorerContentTable.order,
|
||||
visit: explorerContentTable.visit,
|
||||
timestamp: explorerContentTable.timestamp,
|
||||
}).from(explorerContentTable).all();
|
||||
path: projectFilesTable.path,
|
||||
owner: projectFilesTable.owner,
|
||||
title: projectFilesTable.title,
|
||||
type: projectFilesTable.type,
|
||||
navigable: projectFilesTable.navigable,
|
||||
private: projectFilesTable.private,
|
||||
order: projectFilesTable.order,
|
||||
timestamp: projectFilesTable.timestamp,
|
||||
}).from(projectFilesTable).all();
|
||||
|
||||
content.sort((a, b) => {
|
||||
return a.path.split('/').length - b.path.split('/').length;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { userSessionsTable } from '~/db/schema';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { schema } from '~/schemas/login';
|
||||
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||
import { ZodError } from 'zod';
|
||||
import { ZodError } from 'zod/v4';
|
||||
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';
|
||||
|
||||
interface SuccessHandler
|
||||
@@ -50,7 +50,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||
await clearUserSession(e);
|
||||
|
||||
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);
|
||||
@@ -60,7 +60,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||
await clearUserSession(e);
|
||||
|
||||
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({
|
||||
@@ -93,8 +93,6 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||
}
|
||||
}) as UserSessionRequired);
|
||||
|
||||
db.update(usersDataTable).set({ logCount: user.data.logCount + 1 }).where(eq(usersDataTable.id, user.id)).run();
|
||||
|
||||
setResponseStatus(e, 201);
|
||||
return { success: true, session: data };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { count, eq, sql } from 'drizzle-orm';
|
||||
import { ZodError, type ZodIssue } from 'zod';
|
||||
import { ZodError } from 'zod/v4';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { usersDataTable, usersTable } from '~/db/schema';
|
||||
import { schema } from '~/schemas/registration';
|
||||
import { checkSession, logSession } from '~/server/utils/user';
|
||||
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||
import sendMail from '~/server/tasks/mail';
|
||||
import type { $ZodIssue } from 'zod/v4/core';
|
||||
|
||||
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 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)
|
||||
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)
|
||||
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)
|
||||
{
|
||||
@@ -72,7 +73,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||
|
||||
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
||||
|
||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date(), logCount: 1 } }) as UserSessionRequired);
|
||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date() } }) as UserSessionRequired);
|
||||
|
||||
const emailId = Bun.hash('register' + id.id + hash, Date.now());
|
||||
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { hash } from 'bun';
|
||||
import { eq, or } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { usersTable } from '~/db/schema';
|
||||
import sendMail from '~/server/tasks/mail';
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { count, eq, sql } from 'drizzle-orm';
|
||||
import { ZodError, type ZodIssue } from 'zod';
|
||||
import { ZodError } from 'zod/v4';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { usersDataTable, usersTable } from '~/db/schema';
|
||||
import { schema } from '~/schemas/registration';
|
||||
import { checkSession, logSession } from '~/server/utils/user';
|
||||
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||
import sendMail from '~/server/tasks/mail';
|
||||
import type { $ZodIssue } from 'zod/v4/core';
|
||||
|
||||
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 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)
|
||||
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)
|
||||
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)
|
||||
{
|
||||
@@ -72,7 +73,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||
|
||||
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
||||
|
||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date(), logCount: 1 } }) as UserSessionRequired);
|
||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date() } }) as UserSessionRequired);
|
||||
|
||||
await sendMail({
|
||||
payload: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import useDatabase from '~/composables/useDatabase';
|
||||
import { characterTable, userPermissionsTable } from '~/db/schema';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
import { group } from '~/shared/general.util';
|
||||
import type { Character, DoubleIndex, Level, MainStat, TrainingLevel } from '~/types/character';
|
||||
import type { Character, Level, MainStat, TrainingLevel } from '~/types/character';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
let { visibility } = getQuery(e) as { visibility?: "public" | "own" | "admin" };
|
||||
@@ -55,9 +55,8 @@ export default defineEventHandler(async (e) => {
|
||||
with: {
|
||||
abilities: true,
|
||||
levels: true,
|
||||
modifiers: true,
|
||||
spells: true,
|
||||
training: true,
|
||||
choices: true,
|
||||
user: {
|
||||
columns: { username: true }
|
||||
}
|
||||
@@ -75,14 +74,12 @@ export default defineEventHandler(async (e) => {
|
||||
level: character.level,
|
||||
aspect: character.aspect,
|
||||
notes: character.notes,
|
||||
health: character.health,
|
||||
mana: character.mana,
|
||||
variables: character.variables,
|
||||
|
||||
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"),
|
||||
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: group(character.levels, "level", "choice"),
|
||||
abilities: group(character.abilities, "ability", "value"),
|
||||
choices: character.choices.reduce((p, v) => { p[v.id] ??= []; p[v.id]?.push(v.choice); return p; }, {} as Record<string, number[]>),
|
||||
|
||||
owner: character.owner,
|
||||
username: character.user.username,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||
import { CharacterValidation, type Ability, type DoubleIndex, type MainStat, type TrainingLevel } from '~/types/character';
|
||||
import { characterAbilitiesTable, characterLevelingTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||
import { CharacterValidation } from '#shared/character.util';
|
||||
import { type Ability, type MainStat } from '~/types/character';
|
||||
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
@@ -31,23 +32,17 @@ export default defineEventHandler(async (e) => {
|
||||
level: body.data.level,
|
||||
aspect: body.data.aspect,
|
||||
notes: body.data.notes,
|
||||
health: body.data.health,
|
||||
mana: body.data.mana,
|
||||
variables: body.data.variables,
|
||||
visibility: body.data.visibility,
|
||||
thumbnail: body.data.thumbnail,
|
||||
}).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).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]).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();
|
||||
|
||||
const modifiers = Object.entries(body.data.modifiers).map((e) => ({ character: id, modifier: e[0] as MainStat, 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] }));
|
||||
const abilities = Object.entries(body.data.abilities).map(e => ({ character: id, ability: e[0] as Ability, value: e[1] }));
|
||||
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
|
||||
|
||||
return id;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { and, eq, sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterTable } from '~/db/schema';
|
||||
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) => {
|
||||
const id = getRouterParam(e, "id");
|
||||
@@ -26,9 +25,8 @@ export default defineEventHandler(async (e) => {
|
||||
with: {
|
||||
abilities: true,
|
||||
levels: true,
|
||||
modifiers: true,
|
||||
spells: true,
|
||||
training: true,
|
||||
choices: true,
|
||||
user: {
|
||||
columns: { username: true }
|
||||
}
|
||||
@@ -46,14 +44,12 @@ export default defineEventHandler(async (e) => {
|
||||
level: character.level,
|
||||
aspect: character.aspect,
|
||||
notes: character.notes,
|
||||
health: character.health,
|
||||
mana: character.mana,
|
||||
variables: character.variables,
|
||||
|
||||
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"),
|
||||
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: group(character.levels, "level", "choice"),
|
||||
abilities: group(character.abilities, "ability", "value"),
|
||||
choices: character.choices.reduce((p, v) => { p[v.id] ??= []; p[v.id]?.push(v.choice); return p; }, {} as Record<string, number[]>),
|
||||
|
||||
owner: character.owner,
|
||||
username: character.user.username,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||
import { CharacterValidation, type Ability, type MainStat } from '~/types/character';
|
||||
import { characterAbilitiesTable, characterChoicesTable, characterLevelingTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||
import { CharacterValidation } from '#shared/character.util';
|
||||
import { type Ability, type MainStat } from '~/types/character';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const params = getRouterParam(e, "id");
|
||||
@@ -42,34 +43,26 @@ export default defineEventHandler(async (e) => {
|
||||
level: body.data.level,
|
||||
aspect: body.data.aspect,
|
||||
notes: body.data.notes,
|
||||
health: body.data.health,
|
||||
mana: body.data.mana,
|
||||
variables: body.data.variables,
|
||||
visibility: body.data.visibility,
|
||||
thumbnail: body.data.thumbnail,
|
||||
}).where(eq(characterTable.id, id)).run();
|
||||
|
||||
tx.delete(characterLevelingTable).where(eq(characterLevelingTable.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(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();
|
||||
|
||||
const modifiers = Object.entries(body.data.modifiers).map((e) => ({ character: id, modifier: e[0] as MainStat, 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] }));
|
||||
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(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
|
||||
});
|
||||
|
||||
await useStorage('cache').removeItem(`nitro:functions:character:${id}.json`);
|
||||
|
||||
setResponseStatus(e, 200);
|
||||
return;
|
||||
});
|
||||
@@ -1,220 +0,0 @@
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { defaultCharacter, type Ability, type Character, type CharacterConfig, type CompiledCharacter, type DoubleIndex, type Feature, type Level, type MainStat, type TrainingLevel, type TrainingOption } from '~/types/character';
|
||||
import characterData from '#shared/character-config.json';
|
||||
import { group } from '~/shared/general.util';
|
||||
|
||||
export default defineEventHandler(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: [],
|
||||
misc: [],
|
||||
},
|
||||
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: {
|
||||
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
|
||||
},
|
||||
resistance: {
|
||||
stun: [0, 0],
|
||||
bleed: [0, 0],
|
||||
poison: [0, 0],
|
||||
fear: [0, 0],
|
||||
influence: [0, 0],
|
||||
charm: [0, 0],
|
||||
possesion: [0, 0],
|
||||
precision: [0, 0],
|
||||
knowledge: [0, 0],
|
||||
instinct: [0, 0]
|
||||
},
|
||||
initiative: 0,
|
||||
aspect: "",
|
||||
notes: character.notes ?? "",
|
||||
};
|
||||
|
||||
features.forEach(e => e[1].forEach((_e, i) => applyTrainingOption(e[0], _e, compiled, i === e[1].length - 1)));
|
||||
specialFeatures(compiled, character.training);
|
||||
|
||||
Object.entries(character.abilities).forEach(e => compiled.abilities[e[0] as Ability]! += e[1][0]);
|
||||
|
||||
return compiled;
|
||||
}
|
||||
function applyTrainingOption(stat: MainStat, option: TrainingOption, character: CompiledCharacter, last: boolean)
|
||||
{
|
||||
if(option.health) character.health += option.health;
|
||||
if(option.mana) character.mana += option.mana;
|
||||
if(option.mastery) character.mastery[option.mastery]++;
|
||||
if(option.speed) character.speed = option.speed;
|
||||
if(option.initiative) character.initiative += option.initiative;
|
||||
if(option.spellrank) character.spellranks[option.spellrank]++;
|
||||
if(option.defense) option.defense.forEach(e => character.defense[e]++);
|
||||
if(option.resistance) option.resistance.forEach(e => character.resistance[e[0]][e[1] === "attack" ? 0 : 1]++);
|
||||
if(option.spellslot) character.spellslots += option.spellslot in character.modifier ? character.modifier[option.spellslot as MainStat] : option.spellslot as number;
|
||||
if(option.arts) character.artslots += option.arts in character.modifier ? character.modifier[option.arts as MainStat] : option.arts as number;
|
||||
if(option.spell) character.spells.push(option.spell);
|
||||
|
||||
option.description.forEach(line => !line.disposable && (last || !line.replaced) && character.features[line.category ?? "misc"].push(line.text));
|
||||
|
||||
//if(option.features) option.features.forEach(e => applyFeature(e, character));
|
||||
}
|
||||
function specialFeatures(character: CompiledCharacter, levels: Record<MainStat, DoubleIndex<TrainingLevel>[]>)
|
||||
{
|
||||
//Cap la défense
|
||||
const strengthCap3 = levels.strength.some(e => e[0] === 0);
|
||||
const strengthCap6 = levels.strength.some(e => e[0] === 1);
|
||||
const strengthUncapped = levels.strength.some(e => e[0] === 2);
|
||||
|
||||
const dexterityCap3 = levels.dexterity.some(e => e[0] === 0);
|
||||
const dexterityCap3Stat = levels.dexterity.some(e => e[0] === 1);
|
||||
const dexterityUncapped = levels.dexterity.some(e => e[0] === 2);
|
||||
|
||||
if(!strengthUncapped || !dexterityUncapped)
|
||||
{
|
||||
if(strengthCap6)
|
||||
{
|
||||
character.defense = {
|
||||
static: 6,
|
||||
activeparry: 0,
|
||||
activedodge: 0,
|
||||
passiveparry: 0,
|
||||
passivedodge: 0,
|
||||
};
|
||||
}
|
||||
else if(strengthCap3 || dexterityCap3)
|
||||
{
|
||||
character.defense = {
|
||||
static: 3,
|
||||
activeparry: 0,
|
||||
activedodge: 0,
|
||||
passiveparry: 0,
|
||||
passivedodge: 0,
|
||||
};
|
||||
}
|
||||
else if(dexterityCap3Stat)
|
||||
{
|
||||
character.defense.static = 3;
|
||||
}
|
||||
}
|
||||
}/*
|
||||
function applyFeature(feature: Feature, character: CompiledCharacter)
|
||||
{
|
||||
|
||||
} */
|
||||
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]]);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterTable } from '~/db/schema';
|
||||
import { characterAbilitiesTable, characterLevelingTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const id = getRouterParam(e, "id");
|
||||
@@ -25,13 +25,40 @@ export default defineEventHandler(async (e) => {
|
||||
setResponseStatus(e, 401);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const _id = db.transaction((tx) => {
|
||||
const _id = tx.insert(characterTable).values({
|
||||
name: old.name,
|
||||
owner: session.user!.id,
|
||||
people: old.people!,
|
||||
level: old.level,
|
||||
aspect: old.aspect,
|
||||
notes: old.notes,
|
||||
variables: old.variables,
|
||||
visibility: old.visibility,
|
||||
thumbnail: old.thumbnail,
|
||||
}).returning({ id: characterTable.id }).get().id;
|
||||
|
||||
const leveling = tx.select().from(characterLevelingTable).where(eq(characterLevelingTable.character, parseInt(id, 10))).all();
|
||||
if(leveling.length > 0) tx.insert(characterLevelingTable).values(leveling.map(e => ({ character: _id, level: e.level, choice: e.choice }))).run();
|
||||
|
||||
const returned = await db.insert(characterTable).values({
|
||||
name: `Copie de ${old.name}`,
|
||||
progress: old.progress,
|
||||
owner: session.user.id,
|
||||
}).returning({ id: characterTable.id });
|
||||
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();
|
||||
|
||||
setResponseStatus(e, 201);
|
||||
return returned[0].id;
|
||||
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();
|
||||
|
||||
return _id;
|
||||
});
|
||||
|
||||
setResponseStatus(e, 201);
|
||||
return _id;
|
||||
}
|
||||
catch(_e)
|
||||
{
|
||||
setResponseStatus(e, 201);
|
||||
throw _e;
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user