Add user deletion, ProseA hover cards, Canvas
This commit is contained in:
parent
057efb848c
commit
42658558c5
2
app.vue
2
app.vue
|
|
@ -3,7 +3,7 @@
|
||||||
<NuxtRouteAnnouncer/>
|
<NuxtRouteAnnouncer/>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<div class="xl:ps-12 xl:pe-12 ps-6 pe-4 flex flex-1 justify-center overflow-auto max-h-full">
|
<div class="xl:ps-12 xl:pe-12 ps-6 pe-4 flex flex-1 justify-center overflow-auto max-h-full relative">
|
||||||
<NuxtPage></NuxtPage>
|
<NuxtPage></NuxtPage>
|
||||||
</div>
|
</div>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<template v-if="content && content.length > 0">
|
<template v-if="content && content.length > 0">
|
||||||
<Suspense :timeout="0">
|
<Suspense :timeout="0">
|
||||||
<MarkdownRenderer class="px-8" #default :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
<MarkdownRenderer #default :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
||||||
<template #fallback><Loading /></template>
|
<template #fallback><Loading /></template>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
<button :disabled="disabled" class="text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
||||||
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40"
|
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
|
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50"
|
||||||
:class="{'p-1': loading || icon, 'h-[35px] px-[15px]': !loading && !icon}" @click="!loading && emit('click')">
|
:class="{'p-1': loading || icon, 'h-[35px] px-[15px]': !loading && !icon}" @click="!loading && emit('click')">
|
||||||
<Loading v-if="loading" />
|
<Loading v-if="loading" />
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
|
|
@ -8,9 +9,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { icon = false, loading = false } = defineProps<{
|
const { icon = false, loading = false, disabled = false } = defineProps<{
|
||||||
icon?: boolean
|
icon?: boolean
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
|
disabled?: boolean
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits(['click']);
|
const emit = defineEmits(['click']);
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardPortal v-if="!disabled">
|
<HoverCardPortal v-if="!disabled">
|
||||||
<HoverCardContent :side="side" class="data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 p-5 data-[state=open]:transition-all text-light-100 dark:text-dark-100" >
|
<HoverCardContent :class="$attrs.class" :side="side" class="data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 p-5 data-[state=open]:transition-all text-light-100 dark:text-dark-100" >
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
<HoverCardArrow class="fill-light-35 dark:fill-dark-35" />
|
<HoverCardArrow class="fill-light-35 dark:fill-dark-35" />
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 font-medium xl:text-base text-sm" :items="model" :get-key="(item) => item.link ?? item.label">
|
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm" :items="model" :get-key="(item) => item.link ?? item.label">
|
||||||
<TreeItem v-for="item in flattenItems" v-slot="{ isExpanded }" :key="item._id" :style="{ 'padding-left': `${item.level - 0.5}em` }" v-bind="item.bind" class="flex items-center px-2 outline-none relative cursor-pointer">
|
<TreeItem v-for="item in flattenItems" v-slot="{ isExpanded }" :key="item._id" :style="{ 'padding-left': `${item.level - 0.5}em` }" v-bind="item.bind" class="flex items-center px-2 outline-none relative cursor-pointer">
|
||||||
<NuxtLink :href="item.value.link && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.link } } : undefined" no-prefetch class="flex flex-1 items-center" active-class="text-accent-blue">
|
<NuxtLink :href="item.value.link && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.link } } : undefined" no-prefetch class="flex flex-1 items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" :class="{ 'border-s': !item.hasChildren, 'font-medium': item.hasChildren }" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
||||||
<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 - 1}em` }" />
|
<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 - 1}em` }" />
|
||||||
<div class="pl-3 py-1 flex-1 truncate border-light-35 dark:border-dark-35 hover:border-accent-blue" :class="{ 'border-s': !item.hasChildren }" :data-tag="item.value.tag">
|
<div class="pl-3 py-1 flex-1 truncate" :data-tag="item.value.tag">
|
||||||
{{ item.value.label }}
|
{{ item.value.label }}
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
@ -23,3 +23,28 @@ interface TreeItem
|
||||||
}
|
}
|
||||||
const model = defineModel<TreeItem[]>();
|
const model = defineModel<TreeItem[]>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
[data-tag="canvas"]:after,
|
||||||
|
[data-tag="private"]:after
|
||||||
|
{
|
||||||
|
@apply text-sm;
|
||||||
|
@apply font-normal;
|
||||||
|
@apply float-end;
|
||||||
|
@apply border ;
|
||||||
|
@apply border-light-35 ;
|
||||||
|
@apply dark:border-dark-35;
|
||||||
|
@apply px-1;
|
||||||
|
@apply bg-light-20;
|
||||||
|
@apply dark:bg-dark-20;
|
||||||
|
font-variant: small-caps;
|
||||||
|
}
|
||||||
|
[data-tag="canvas"]:after
|
||||||
|
{
|
||||||
|
content: 'Canvas'
|
||||||
|
}
|
||||||
|
[data-tag="private"]:after
|
||||||
|
{
|
||||||
|
content: 'Privé'
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDrag, usePinch, useWheel } from '@vueuse/gesture';
|
||||||
|
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import { clamp } from '#imports';
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
canvas: CanvasContent;
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
||||||
|
const canvas = useTemplateRef('canvas');
|
||||||
|
|
||||||
|
const reset = (_: MouseEvent) => {
|
||||||
|
zoom.value = minZoom.value;
|
||||||
|
|
||||||
|
dispX.value = 0;
|
||||||
|
dispY.value = 0;
|
||||||
|
}
|
||||||
|
function edgePos(side: 'bottom' | 'top' | 'left' | 'right', pos: { x: number, y: number }, offset: number): { x: number, y: number } {
|
||||||
|
switch (side) {
|
||||||
|
case "left":
|
||||||
|
return {
|
||||||
|
x: pos.x - offset,
|
||||||
|
y: pos.y
|
||||||
|
};
|
||||||
|
case "right":
|
||||||
|
return {
|
||||||
|
x: pos.x + offset,
|
||||||
|
y: pos.y
|
||||||
|
};
|
||||||
|
case "top":
|
||||||
|
return {
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y - offset
|
||||||
|
};
|
||||||
|
case "bottom":
|
||||||
|
return {
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y + offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getNode(id: string): CanvasNode | undefined
|
||||||
|
{
|
||||||
|
return props.canvas.nodes.find(e => e.id === id);
|
||||||
|
}
|
||||||
|
function posFromDir(e: { minX: number, minY: number, maxX: number, maxY: number }, t: 'bottom' | 'top' | 'left' | 'right'): { x: number, y: number } {
|
||||||
|
switch (t) {
|
||||||
|
case "top":
|
||||||
|
return { x: (e.minX + e.maxX) / 2, y: e.minY };
|
||||||
|
case "right":
|
||||||
|
return { x: e.maxX, y: (e.minY + e.maxY) / 2 };
|
||||||
|
case "bottom":
|
||||||
|
return { x: (e.minX + e.maxX) / 2, y: e.maxY };
|
||||||
|
case "left":
|
||||||
|
return { x: e.minX, y: (e.minY + e.maxY) / 2 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getBbox(node: CanvasNode): { minX: number, minY: number, maxX: number, maxY: number } {
|
||||||
|
return { minX: node.x, minY: node.y, maxX: node.x + node.width, maxY: node.y + node.height };
|
||||||
|
}
|
||||||
|
function path(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
|
||||||
|
if(from === undefined || to === undefined)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
path: '',
|
||||||
|
from: {},
|
||||||
|
to: {},
|
||||||
|
toSide: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const start = posFromDir(getBbox(from), fromSide), end = posFromDir(getBbox(to), toSide);
|
||||||
|
return bezier(start, fromSide, end, toSide);
|
||||||
|
}
|
||||||
|
function bezier(from: { x: number, y: number }, fromSide: 'bottom' | 'top' | 'left' | 'right', to: { x: number, y: number }, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
|
||||||
|
const r = Math.hypot(from.x - to.x, from.y - to.y), o = clamp(r / 2, 70, 150), a = edgePos(fromSide, from, o), s = edgePos(toSide, to, o);
|
||||||
|
return {
|
||||||
|
path: `M${from.x},${from.y} C${a.x},${a.y} ${s.x},${s.y} ${to.x},${to.y}`,
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
side: toSide,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function labelCenter(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): string {
|
||||||
|
const start = posFromDir(getBbox(from), fromSide), end = posFromDir(getBbox(to), toSide);
|
||||||
|
const len = Math.hypot(start.x - end.x, start.y - end.y), offset = clamp(len / 2, 70, 150), b = edgePos(fromSide, start, offset), s = edgePos(toSide, end, offset);
|
||||||
|
const center = getCenter(start, end, b, s, 0.5);
|
||||||
|
return `translate(${center.x}px, ${center.y}px)`;
|
||||||
|
}
|
||||||
|
function getCenter(n: { x: number, y: number }, i: { x: number, y: number }, r: { x: number, y: number }, o: { x: number, y: number }, e: number): { x: number, y: number } {
|
||||||
|
const a = 1 - e, s = a * a * a, l = 3 * e * a * a, c = 3 * e * e * a, u = e * e * e;
|
||||||
|
return {
|
||||||
|
x: s * n.x + l * r.x + c * o.x + u * i.x,
|
||||||
|
y: s * n.y + l * r.y + c * o.y + u * i.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const dragHandler = useDrag(({ event: Event, delta: [x, y] }: { event: Event, delta: number[] }) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
dispX.value += x / zoom.value;
|
||||||
|
dispY.value += y / zoom.value;
|
||||||
|
}, {
|
||||||
|
domTarget: canvas,
|
||||||
|
eventOptions: { passive: false, }
|
||||||
|
})
|
||||||
|
const pinchHandler = usePinch(({ event: Event, offset: [z] }: { event: Event, offset: number[] }) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
console.log(z);
|
||||||
|
zoom.value = clamp(z / 2048, minZoom.value, 3);
|
||||||
|
}, {
|
||||||
|
domTarget: canvas,
|
||||||
|
eventOptions: { passive: false, }
|
||||||
|
})
|
||||||
|
const wheelHandler = useWheel(({ event: Event, delta: [x, y] }: { event: Event, delta: number[] }) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
zoom.value = clamp(zoom.value + y * -0.001, minZoom.value, 3);
|
||||||
|
}, {
|
||||||
|
domTarget: canvas,
|
||||||
|
eventOptions: { passive: false, }
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Suspense>
|
||||||
|
<template #default>
|
||||||
|
<div id="canvas" ref="canvas" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none"
|
||||||
|
:style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
||||||
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 absolute sm:top-2 top-10 left-2 z-30 overflow-hidden">
|
||||||
|
<Tooltip message="Zoom avant" side="right">
|
||||||
|
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" 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" 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 * 0.9, minZoom, 3)" 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="absolute top-0 left-0 w-full h-full origin-center pointer-events-none *:pointer-events-auto *:select-none"
|
||||||
|
:style="{transform: `scale(${zoom}) translate(${dispX}px, ${dispY}px)`}">
|
||||||
|
<div>
|
||||||
|
<CanvasNode v-for="node of props.canvas.nodes" :key="node.id" :node="node" :zoom="zoom" />
|
||||||
|
</div>
|
||||||
|
<template v-for="edge of props.canvas.edges">
|
||||||
|
<div :key="edge.id" v-if="edge.label" class="absolute z-10"
|
||||||
|
:style="{ transform: labelCenter(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide) }">
|
||||||
|
<div class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 -translate-x-[50%] -translate-y-[50%]">{{ edge.label }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<svg class="absolute top-0 left-0 overflow-visible w-full h-full origin-top pointer-events-none">
|
||||||
|
<CanvasEdge v-for="edge of props.canvas.edges" :key="edge.id"
|
||||||
|
:path="path(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide)"
|
||||||
|
:color="edge.color" :label="edge.label" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #fallback>
|
||||||
|
<div class="loading"></div>
|
||||||
|
</template>
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CanvasColor } from "~/types/canvas";
|
||||||
|
|
||||||
|
type Direction = 'bottom' | 'top' | 'left' | 'right';
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
path: {
|
||||||
|
path: string;
|
||||||
|
from: { x: number; y: number };
|
||||||
|
to: { x: number; y: number };
|
||||||
|
side: Direction;
|
||||||
|
};
|
||||||
|
color?: CanvasColor;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const rotation: Record<Direction, string> = {
|
||||||
|
top: "180",
|
||||||
|
bottom: "0",
|
||||||
|
left: "90",
|
||||||
|
right: "270"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<g :style="{'--canvas-color': color?.hex}" class="z-0">
|
||||||
|
<path :style="`stroke-linecap: butt; stroke-width: calc(3px * var(--zoom-multiplier));`" :class="color?.class ? `stroke-light-${color.class} dark:stroke-dark-${color.class}` : ((color && color?.hex !== undefined) ? 'stroke-[color:var(--canvas-color)]' : 'stroke-light-40 dark:stroke-dark-40')" class="fill-none stroke-[4px]" :d="path.path"></path>
|
||||||
|
<g :style="`transform: translate(${path.to.x}px, ${path.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[path.side]}deg);`">
|
||||||
|
<polygon :class="color?.class ? `fill-light-${color.class} dark:fill-dark-${color.class}` : ((color && color?.hex !== undefined) ? 'fill-[color:var(--canvas-color)]' : 'fill-light-40 dark:fill-dark-40')" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import type { CanvasNode } from '~/types/canvas';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
node: CanvasNode;
|
||||||
|
zoom: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const size = Math.max(props.node.width, props.node.height);
|
||||||
|
const colors = computed(() => {
|
||||||
|
if(props.node.color)
|
||||||
|
{
|
||||||
|
const color = props.node.color;
|
||||||
|
return color?.class ? { bg: `bg-light-${color?.class} dark:bg-dark-${color?.class}`, border: `border-light-${color?.class} dark:border-dark-${color?.class}`} : { bg: `bg-colored`, border: `border-[color:var(--canvas-color)]` };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return { border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-colored
|
||||||
|
{
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgba(from var(--canvas-color) r g b / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<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="[colors.border]" class="border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex">
|
||||||
|
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07]" :class="colors.bg">
|
||||||
|
<template v-if="node.type === 'group' || zoom > Math.min(0.4, 1000 / size)">
|
||||||
|
<div v-if="node.text?.length > 0" class="flex items-center">
|
||||||
|
<Markdown :content="node.text" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex flex-1 justify-center items-center bg-light-30 dark:bg-dark-30">
|
||||||
|
<Icon icon="radix-icons:text-align-left" class="w-8 h-8"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="node.type === 'group' && node.label !== undefined" :class="[colors.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>
|
||||||
|
|
@ -1,50 +1,60 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="text-accent-blue inline-flex items-center cursor-pointer hover:text-opacity-85"><slot v-bind="$attrs"></slot></span>
|
|
||||||
<!-- <Suspense suspensible>
|
|
||||||
<NuxtLink no-prefetch class="text-accent-blue inline-flex items-center" v-if="data && data[0]"
|
<NuxtLink no-prefetch class="text-accent-blue inline-flex items-center" v-if="data && data[0]"
|
||||||
:to="{ path: `/explorer/${project}/${data[0].path}`, hash: hash }" :class="class">
|
:to="{ name: 'explore-path', params: { path: data[0].path }, hash: hash }" :class="class">
|
||||||
<PreviewContent :project="project" :path="data[0].path" :anchor="hash">
|
<HoverCard class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': data[0].type === 'canvas'}">
|
||||||
|
<template #content>
|
||||||
|
<template v-if="data[0].type === 'markdown'">
|
||||||
|
<div class="px-10">
|
||||||
|
<Markdown :content="data[0].content" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="data[0].type === 'canvas'">
|
||||||
|
<div class="w-[600px] h-[600px] relative">
|
||||||
|
<Canvas :canvas="JSON.parse(data[0].content)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<slot v-bind="$attrs"></slot>
|
<slot v-bind="$attrs"></slot>
|
||||||
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'Markdown'" :height="20" :width="20"
|
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'markdown'" :icon="iconByType[data[0].type]" />
|
||||||
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
|
||||||
</template>
|
</template>
|
||||||
</PreviewContent>
|
</HoverCard>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink no-prefetch v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
<NuxtLink no-prefetch v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
||||||
<slot v-bind="$attrs"></slot>
|
<slot v-bind="$attrs"></slot>
|
||||||
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'Markdown'" :height="20" :width="20"
|
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'markdown'" :height="20" :width="20"
|
||||||
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<slot :class="class" v-else v-bind="$attrs"></slot>
|
<slot :class="class" v-else v-bind="$attrs"></slot>
|
||||||
</Suspense> -->
|
|
||||||
</template>
|
</template>
|
||||||
<!--<script setup lang="ts">
|
|
||||||
|
<script setup lang="ts">
|
||||||
import { parseURL } from 'ufo';
|
import { parseURL } from 'ufo';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const iconByType: Record<string, string> = {
|
||||||
href: {
|
'folder': 'circum:folder-on',
|
||||||
type: String,
|
'canvas': 'ph:graph-light',
|
||||||
required: false,
|
'file': 'radix-icons:file',
|
||||||
},
|
|
||||||
class: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
}
|
}
|
||||||
});
|
const { href } = defineProps<{
|
||||||
|
href: string
|
||||||
|
class?: string
|
||||||
|
}>();
|
||||||
|
|
||||||
const route = useRoute();
|
const { hash, pathname, protocol } = parseURL(href);
|
||||||
const { hash, pathname, protocol } = parseURL(props.href);
|
const data = ref(), loading = ref(false);
|
||||||
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
|
|
||||||
const data = ref();
|
|
||||||
|
|
||||||
if(!!pathname && !protocol)
|
if(!!pathname && !protocol)
|
||||||
{
|
{
|
||||||
data.value = await $fetch(`/api/project/${project.value}/file`, {
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
data.value = await $fetch(`/api/file`, {
|
||||||
query: {
|
query: {
|
||||||
search: `%${pathname}`
|
search: `%${pathname}`
|
||||||
},
|
},
|
||||||
ignoreResponseError: true,
|
|
||||||
});
|
});
|
||||||
|
} catch(e) { }
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
</script>-->
|
</script>
|
||||||
|
|
@ -162,17 +162,14 @@ blockquote:empty
|
||||||
@apply w-6;
|
@apply w-6;
|
||||||
@apply h-6;
|
@apply h-6;
|
||||||
@apply stroke-2;
|
@apply stroke-2;
|
||||||
}
|
@apply float-start;
|
||||||
.callout-title
|
@apply me-2;
|
||||||
{
|
|
||||||
@apply flex;
|
|
||||||
@apply items-center;
|
|
||||||
@apply gap-2;
|
|
||||||
}
|
}
|
||||||
.callout-title-inner
|
.callout-title-inner
|
||||||
{
|
{
|
||||||
@apply inline-block;
|
@apply block;
|
||||||
@apply font-bold;
|
@apply font-bold;
|
||||||
|
@apply ps-8;
|
||||||
}
|
}
|
||||||
.callout > p
|
.callout > p
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2 relative sm:right-8 right-4">
|
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2 relative sm:right-4 right-2">
|
||||||
<slot />
|
<slot />
|
||||||
</h2>
|
</h2>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export default function useDatabase()
|
||||||
instance = drizzle({ client: sqlite, schema });
|
instance = drizzle({ client: sqlite, schema });
|
||||||
|
|
||||||
instance.run("PRAGMA journal_mode = WAL;");
|
instance.run("PRAGMA journal_mode = WAL;");
|
||||||
|
instance.run("PRAGMA foreign_keys = true;");
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
|
|
||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<CollapsibleRoot class="flex flex-1 flex-col" :v-model="open">
|
<CollapsibleRoot class="flex flex-1 flex-col" v-model:open="open">
|
||||||
<div class="z-50 md:hidden flex w-full items-center justify-between h-12 border-b border-light-35 dark:border-dark-35">
|
<div class="z-50 md:hidden flex w-full items-center justify-between h-12 border-b border-light-35 dark:border-dark-35">
|
||||||
<div class="flex items-center px-2">
|
<div class="flex items-center px-2">
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
|
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
|
||||||
<CollapsibleContent asChild forceMount>
|
<CollapsibleContent asChild forceMount>
|
||||||
<div class="bg-light-0 md:my-8 md:py-3 dark:bg-dark-0 z-40 xl:w-96 md:w-[15em] w-full border-r border-light-30 dark:border-dark-30 flex flex-col justify-between max-md:absolute max-md:-top-0 max-md:-bottom-0 md:left-0 max-md:data-[state=closed]:-left-full max-md:transition-[left] py-8 max-md:z-40 max-md:data-[state=open]:left-0">
|
<div class="bg-light-0 md:py-11 dark:bg-dark-0 z-40 xl:w-96 md:w-[15em] w-full border-r border-light-30 dark:border-dark-30 flex flex-col justify-between max-md:absolute max-md:-top-0 max-md:-bottom-0 md:left-0 max-md:data-[state=closed]:-left-full max-md:transition-[left] py-8 max-md:z-40 max-md:data-[state=open]:left-0">
|
||||||
<div class="relative bottom-6 flex flex-col gap-4 xl:px-6 px-3">
|
<div class="relative bottom-6 flex flex-col gap-4 xl:px-6 px-3">
|
||||||
<div class="flex justify-between items-center max-md:hidden">
|
<div class="flex justify-between items-center max-md:hidden">
|
||||||
<NuxtLink class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-md:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }">
|
<NuxtLink class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-md:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }">
|
||||||
|
|
@ -65,13 +65,11 @@ const { data: pages } = await useLazyFetch('/api/navigation', {
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(useRouter().currentRoute, () => {
|
watch(useRouter().currentRoute, () => {
|
||||||
console.log(open.value);
|
|
||||||
console.log('Changing');
|
|
||||||
open.value = false;
|
open.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
function transform(list: any[]): any[]
|
function transform(list: any[]): any[]
|
||||||
{
|
{
|
||||||
return list?.map(e => ({ label: e.title, children: transform(e.children), link: e.path, tag: e.private ? 'Privé' : e.type }))
|
return list?.map(e => ({ label: e.title, children: transform(e.children), link: e.path, tag: e.private ? 'private' : e.type }))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
rights: ['admin']
|
rights: ['admin', 'editor'],
|
||||||
})
|
})
|
||||||
const model = defineModel<string>({
|
const model = defineModel<string>({
|
||||||
default: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam quis orci et est malesuada vulputate. Aenean sagittis congue eros, non feugiat metus bibendum consectetur. Duis volutpat leo nisi, in maximus nulla rhoncus ac. Sed scelerisque ipsum et volutpat dignissim. Integer massa nibh, imperdiet quis condimentum vitae, imperdiet quis quam. Cras pretium ex eget hendrerit porttitor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque rutrum scelerisque quam, sit amet malesuada mi convallis aliquam. Curabitur eget dolor in diam scelerisque tincidunt at et sapien. Nulla vel nisl finibus odio porttitor sagittis ac ut sem. Aenean orci enim, fringilla eu porta eget, egestas vel libero. Aenean ac efficitur nunc, id finibus nibh. Suspendisse potenti. Quisque vel vestibulum ante. Morbi mi nulla, gravida ac malesuada at, hendrerit nec nibh.
|
default: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam quis orci et est malesuada vulputate. Aenean sagittis congue eros, non feugiat metus bibendum consectetur. Duis volutpat leo nisi, in maximus nulla rhoncus ac. Sed scelerisque ipsum et volutpat dignissim. Integer massa nibh, imperdiet quis condimentum vitae, imperdiet quis quam. Cras pretium ex eget hendrerit porttitor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque rutrum scelerisque quam, sit amet malesuada mi convallis aliquam. Curabitur eget dolor in diam scelerisque tincidunt at et sapien. Nulla vel nisl finibus odio porttitor sagittis ac ut sem. Aenean orci enim, fringilla eu porta eget, egestas vel libero. Aenean ac efficitur nunc, id finibus nibh. Suspendisse potenti. Quisque vel vestibulum ante. Morbi mi nulla, gravida ac malesuada at, hendrerit nec nibh.
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@
|
||||||
<Markdown :content="page.content" />
|
<Markdown :content="page.content" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="page.type === 'canvas'">
|
||||||
|
<Canvas :canvas="JSON.parse(page.content)" />
|
||||||
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span>En cours de développement</span>
|
<ProseH2 class="flex-1 text-center">Impossible d'afficher le contenu demandé</ProseH2>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="status === 'error'">
|
<div v-else-if="status === 'error'">
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
||||||
<SplitterPanel asChild>
|
<SplitterPanel asChild>
|
||||||
<div class="flex-1 max-h-full !overflow-y-auto"><Markdown :content="page.content" /></div>
|
<div class="flex-1 max-h-full !overflow-y-auto px-8"><Markdown :content="page.content" /></div>
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
</SplitterGroup>
|
</SplitterGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@ definePageMeta({
|
||||||
})
|
})
|
||||||
let { user, clear } = useUserSession();
|
let { user, clear } = useUserSession();
|
||||||
|
|
||||||
const deleting = ref(false);
|
async function deleteUser()
|
||||||
|
{
|
||||||
|
await $fetch(`/api/users/${user.value?.id}`, {
|
||||||
|
method: 'delete'
|
||||||
|
});
|
||||||
|
clear();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -28,13 +34,13 @@ const deleting = ref(false);
|
||||||
<template v-slot:content><span>Tant que votre adresse mail n'as pas été validée, vous n'avez que
|
<template v-slot:content><span>Tant que votre adresse mail n'as pas été validée, vous n'avez que
|
||||||
des droits de lecture.</span></template>
|
des droits de lecture.</span></template>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
<Button class="ms-4">Renvoyez un mail</Button>
|
<Tooltip message="En cours de développement"><Button class="ms-4" disabled>Renvoyez un mail</Button></Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col self-center flex-1 gap-4">
|
<div class="flex flex-col self-center flex-1 gap-4">
|
||||||
<Button @click="async () => await clear()">Se deconnecter</Button>
|
<Button @click="async () => await clear()">Se deconnecter</Button>
|
||||||
<Button>Modifier mon profil</Button>
|
<Button disabled><Tooltip message="En cours de développement">Modifier mon profil</Tooltip></Button>
|
||||||
<AlertDialogRoot v-model="deleting">
|
<AlertDialogRoot>
|
||||||
<AlertDialogTrigger asChild><Button
|
<AlertDialogTrigger asChild><Button
|
||||||
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">Supprimer
|
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">Supprimer
|
||||||
mon compte</Button></AlertDialogTrigger>
|
mon compte</Button></AlertDialogTrigger>
|
||||||
|
|
@ -50,14 +56,13 @@ const deleting = ref(false);
|
||||||
Êtes vous sûr de vouloir supprimer votre compte ?
|
Êtes vous sûr de vouloir supprimer votre compte ?
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
<div class="flex flex-1 justify-end gap-4">
|
<div class="flex flex-1 justify-end gap-4">
|
||||||
<Button @click="() => { deleting = false; }" class="">Annuler</Button>
|
<AlertDialogCancel asChild><Button>Annuler</Button></AlertDialogCancel>
|
||||||
<Button @click="() => { deleting = false; }"
|
<AlertDialogAction asChild><Button @click="() => deleteUser()" 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">Supprimer</Button></AlertDialogAction>
|
||||||
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">Supprimer</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialogPortal>
|
</AlertDialogPortal>
|
||||||
</AlertDialogRoot>
|
</AlertDialogRoot>
|
||||||
<NuxtLink v-if="hasPermissions(user.permissions, ['admin'])" :href="{ name: 'admin' }"><Button>Administration</Button></NuxtLink>
|
<NuxtLink v-if="hasPermissions(user.permissions, ['admin'])" :href="{ name: 'admin' }" class="flex" no-prefetch><Button class="flex-1">Administration</Button></NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex" v-if="user.permissions">
|
<div class="flex" v-if="user.permissions">
|
||||||
<ProseTable class="!m-0">
|
<ProseTable class="!m-0">
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,41 @@
|
||||||
|
import { and, eq, like, sql } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import type { File } from '~/types/api';
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const project = getRouterParam(e, "projectId");
|
|
||||||
const query = getQuery(e);
|
const query = getQuery(e);
|
||||||
|
|
||||||
if(!project)
|
|
||||||
{
|
|
||||||
setResponseStatus(e, 404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const where = [];
|
const where = [];
|
||||||
const criteria: Record<string, any> = { };
|
|
||||||
|
|
||||||
if(query && query.path !== undefined)
|
if(query && query.path !== undefined)
|
||||||
{
|
{
|
||||||
where.push("path = $path");
|
where.push(eq(explorerContentTable.path, sql.placeholder('path')));
|
||||||
criteria["$path"] = query.path;
|
|
||||||
}
|
}
|
||||||
if(query && query.title !== undefined)
|
if(query && query.title !== undefined)
|
||||||
{
|
{
|
||||||
where.push("title = $title");
|
where.push(eq(explorerContentTable.title, sql.placeholder('title')));
|
||||||
criteria["$title"] = query.title;
|
|
||||||
}
|
}
|
||||||
if(query && query.type !== undefined)
|
if(query && query.type !== undefined)
|
||||||
{
|
{
|
||||||
where.push("type = $type");
|
where.push(eq(explorerContentTable.type, sql.placeholder('type')));
|
||||||
criteria["$type"] = query.type;
|
|
||||||
}
|
}
|
||||||
if (query && query.search !== undefined)
|
if (query && query.search !== undefined)
|
||||||
{
|
{
|
||||||
where.push("path LIKE $search");
|
where.push(like(explorerContentTable.path, sql.placeholder('search')));
|
||||||
criteria["$search"] = query.search;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(where.length > 1)
|
if(where.length > 0)
|
||||||
{
|
{
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
|
|
||||||
const content = db.query(`SELECT * FROM explorer_files WHERE ${where.join(" and ")}`).all(criteria) as File[];
|
const content = db.select({
|
||||||
|
'path': explorerContentTable.path,
|
||||||
|
'owner': explorerContentTable.owner,
|
||||||
|
'title': explorerContentTable.title,
|
||||||
|
'type': explorerContentTable.type,
|
||||||
|
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
||||||
|
'navigable': explorerContentTable.navigable,
|
||||||
|
'private': explorerContentTable.private,
|
||||||
|
}).from(explorerContentTable).where(and(...where)).prepare().all(query);
|
||||||
|
|
||||||
if(content.length > 0)
|
if(content.length > 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import useDatabase from "~/composables/useDatabase";
|
||||||
|
import { usersTable } from "~/db/schema";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!session.user)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = getRouterParam(e, 'id');
|
||||||
|
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(session.user.id.toString() !== id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
|
||||||
|
clearUserSession(e);
|
||||||
|
db.delete(usersTable).where(eq(usersTable.id, session.user.id)).run();
|
||||||
|
});
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import useDatabase from "~/composables/useDatabase";
|
import useDatabase from "~/composables/useDatabase";
|
||||||
import { userSessionsTable } from "~/db/schema";
|
import { userSessionsTable } from "~/db/schema";
|
||||||
import { eq, and, sql } from "drizzle-orm";
|
import { eq, and, sql, lte } from "drizzle-orm";
|
||||||
import { refreshSessionFromDB } from "../utils/user";
|
import { refreshSessionFromDB } from "../utils/user";
|
||||||
|
|
||||||
const monthAsMs = 60 * 60 * 24 * 30 * 1000;
|
const monthAsMs = 60 * 60 * 24 * 30 * 1000;
|
||||||
|
|
@ -9,6 +9,7 @@ export default defineNitroPlugin(() => {
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
|
|
||||||
sessionHooks.hook('fetch', async (session, event) => {
|
sessionHooks.hook('fetch', async (session, event) => {
|
||||||
|
db.delete(userSessionsTable).where(and(eq(userSessionsTable.user_id, sql.placeholder('id')), lte(userSessionsTable.timestamp, sql.placeholder('timestamp')))).prepare().run({ id: session.user.id, timestamp: Math.round((Date.now() - monthAsMs) / 1000) });
|
||||||
const result = db.select({ timestamp: userSessionsTable.timestamp }).from(userSessionsTable).where(and(eq(userSessionsTable.id, sql.placeholder('id')), eq(userSessionsTable.user_id, sql.placeholder('user_id')))).prepare().get({ id: session.id, user_id: session.user.id });
|
const result = db.select({ timestamp: userSessionsTable.timestamp }).from(userSessionsTable).where(and(eq(userSessionsTable.id, sql.placeholder('id')), eq(userSessionsTable.user_id, sql.placeholder('user_id')))).prepare().get({ id: session.id, user_id: session.user.id });
|
||||||
|
|
||||||
if(!result)
|
if(!result)
|
||||||
|
|
@ -16,11 +17,6 @@ export default defineNitroPlugin(() => {
|
||||||
await clearUserSession(event);
|
await clearUserSession(event);
|
||||||
throw createError({ statusCode: 401, message: 'Unauthorized' });
|
throw createError({ statusCode: 401, message: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
else if(result && result.timestamp && result.timestamp.getTime() < Date.now() - monthAsMs)
|
|
||||||
{
|
|
||||||
await clearUserSession(event);
|
|
||||||
throw createError({ statusCode: 401, message: 'Session has expired' });
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await db.update(userSessionsTable).set({
|
await db.update(userSessionsTable).set({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue