Add user deletion, ProseA hover cards, Canvas

This commit is contained in:
Peaceultime 2024-11-10 22:29:59 +01:00
parent 057efb848c
commit 42658558c5
23 changed files with 476 additions and 100 deletions

View File

@ -3,7 +3,7 @@
<NuxtRouteAnnouncer/>
<TooltipProvider>
<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>
</div>
</NuxtLayout>

View File

@ -1,7 +1,7 @@
<template>
<template v-if="content && content.length > 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>
</Suspense>
</template>

View File

@ -1,6 +1,7 @@
<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
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"
<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
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')">
<Loading v-if="loading" />
<slot v-else />
@ -8,9 +9,10 @@
</template>
<script setup lang="ts">
const { icon = false, loading = false } = defineProps<{
const { icon = false, loading = false, disabled = false } = defineProps<{
icon?: boolean
loading?: boolean
disabled?: boolean
}>();
const emit = defineEmits(['click']);
</script>

View File

@ -4,7 +4,7 @@
<slot></slot>
</HoverCardTrigger>
<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>
<HoverCardArrow class="fill-light-35 dark:fill-dark-35" />
</HoverCardContent>

View File

@ -1,9 +1,9 @@
<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">
<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` }" />
<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 }}
</div>
</NuxtLink>
@ -23,3 +23,28 @@ interface TreeItem
}
const model = defineModel<TreeItem[]>();
</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,50 +1,60 @@
<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]"
:to="{ path: `/explorer/${project}/${data[0].path}`, hash: hash }" :class="class">
<PreviewContent :project="project" :path="data[0].path" :anchor="hash">
<template #default>
<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="`icons/link-${data[0].type.toLowerCase()}`" />
<NuxtLink no-prefetch class="text-accent-blue inline-flex items-center" v-if="data && data[0]"
:to="{ name: 'explore-path', params: { path: data[0].path }, hash: hash }" :class="class">
<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>
</PreviewContent>
</NuxtLink>
<NuxtLink no-prefetch v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
<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="`icons/link-${data[0].type.toLowerCase()}`" />
</NuxtLink>
<slot :class="class" v-else v-bind="$attrs"></slot>
</Suspense> -->
<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>
<slot v-bind="$attrs"></slot>
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'markdown'" :icon="iconByType[data[0].type]" />
</template>
</HoverCard>
</NuxtLink>
<NuxtLink no-prefetch v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
<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="`icons/link-${data[0].type.toLowerCase()}`" />
</NuxtLink>
<slot :class="class" v-else v-bind="$attrs"></slot>
</template>
<!--<script setup lang="ts">
<script setup lang="ts">
import { parseURL } from 'ufo';
import { Icon } from '@iconify/vue/dist/iconify.js';
const props = defineProps({
href: {
type: String,
required: false,
},
class: {
type: String,
required: false,
}
});
const iconByType: Record<string, string> = {
'folder': 'circum:folder-on',
'canvas': 'ph:graph-light',
'file': 'radix-icons:file',
}
const { href } = defineProps<{
href: string
class?: string
}>();
const route = useRoute();
const { hash, pathname, protocol } = parseURL(props.href);
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
const data = ref();
const { hash, pathname, protocol } = parseURL(href);
const data = ref(), loading = ref(false);
if(!!pathname && !protocol)
{
data.value = await $fetch(`/api/project/${project.value}/file`, {
query: {
search: `%${pathname}`
},
ignoreResponseError: true,
});
loading.value = true;
try {
data.value = await $fetch(`/api/file`, {
query: {
search: `%${pathname}`
},
});
} catch(e) { }
loading.value = false;
}
</script>-->
</script>

View File

@ -162,17 +162,14 @@ blockquote:empty
@apply w-6;
@apply h-6;
@apply stroke-2;
}
.callout-title
{
@apply flex;
@apply items-center;
@apply gap-2;
@apply float-start;
@apply me-2;
}
.callout-title-inner
{
@apply inline-block;
@apply block;
@apply font-bold;
@apply ps-8;
}
.callout > p
{

View File

@ -1,5 +1,5 @@
<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 />
</h2>
</template>

View File

@ -12,6 +12,7 @@ export default function useDatabase()
instance = drizzle({ client: sqlite, schema });
instance.run("PRAGMA journal_mode = WAL;");
instance.run("PRAGMA foreign_keys = true;");
}
return instance;

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
<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="flex items-center px-2">
<CollapsibleTrigger asChild>
@ -23,7 +23,7 @@
</div>
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
<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="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 }">
@ -65,13 +65,11 @@ const { data: pages } = await useLazyFetch('/api/navigation', {
});
watch(useRouter().currentRoute, () => {
console.log(open.value);
console.log('Changing');
open.value = false;
});
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>

View File

@ -7,7 +7,7 @@
<script setup lang="ts">
definePageMeta({
rights: ['admin']
rights: ['admin', 'editor'],
})
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.

View File

@ -18,8 +18,11 @@
<Markdown :content="page.content" />
</div>
</template>
<template v-else-if="page.type === 'canvas'">
<Canvas :canvas="JSON.parse(page.content)" />
</template>
<template v-else>
<span>En cours de développement</span>
<ProseH2 class="flex-1 text-center">Impossible d'afficher le contenu demandé</ProseH2>
</template>
</div>
<div v-else-if="status === 'error'">

View File

@ -19,7 +19,7 @@
</SplitterPanel>
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
<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>
</SplitterGroup>
</template>

View File

@ -4,7 +4,13 @@ definePageMeta({
})
let { user, clear } = useUserSession();
const deleting = ref(false);
async function deleteUser()
{
await $fetch(`/api/users/${user.value?.id}`, {
method: 'delete'
});
clear();
}
</script>
<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
des droits de lecture.</span></template>
</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 class="flex flex-col self-center flex-1 gap-4">
<Button @click="async () => await clear()">Se deconnecter</Button>
<Button>Modifier mon profil</Button>
<AlertDialogRoot v-model="deleting">
<Button disabled><Tooltip message="En cours de développement">Modifier mon profil</Tooltip></Button>
<AlertDialogRoot>
<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
mon compte</Button></AlertDialogTrigger>
@ -50,14 +56,13 @@ const deleting = ref(false);
Êtes vous sûr de vouloir supprimer votre compte ?
</AlertDialogDescription>
<div class="flex flex-1 justify-end gap-4">
<Button @click="() => { deleting = false; }" class="">Annuler</Button>
<Button @click="() => { deleting = false; }"
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>
<AlertDialogCancel asChild><Button>Annuler</Button></AlertDialogCancel>
<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>
</div>
</AlertDialogContent>
</AlertDialogPortal>
</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 class="flex" v-if="user.permissions">
<ProseTable class="!m-0">

View File

@ -1,45 +1,41 @@
import { and, eq, like, sql } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import type { File } from '~/types/api';
import { explorerContentTable } from '~/db/schema';
export default defineEventHandler(async (e) => {
const project = getRouterParam(e, "projectId");
const query = getQuery(e);
if(!project)
{
setResponseStatus(e, 404);
return;
}
const where = [];
const criteria: Record<string, any> = { };
if(query && query.path !== undefined)
{
where.push("path = $path");
criteria["$path"] = query.path;
where.push(eq(explorerContentTable.path, sql.placeholder('path')));
}
if(query && query.title !== undefined)
{
where.push("title = $title");
criteria["$title"] = query.title;
where.push(eq(explorerContentTable.title, sql.placeholder('title')));
}
if(query && query.type !== undefined)
{
where.push("type = $type");
criteria["$type"] = query.type;
where.push(eq(explorerContentTable.type, sql.placeholder('type')));
}
if (query && query.search !== undefined)
{
where.push("path LIKE $search");
criteria["$search"] = query.search;
where.push(like(explorerContentTable.path, sql.placeholder('search')));
}
if(where.length > 1)
if(where.length > 0)
{
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)
{

View File

@ -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();
});

View File

@ -1,6 +1,6 @@
import useDatabase from "~/composables/useDatabase";
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";
const monthAsMs = 60 * 60 * 24 * 30 * 1000;
@ -9,6 +9,7 @@ export default defineNitroPlugin(() => {
const db = useDatabase();
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 });
if(!result)
@ -16,11 +17,6 @@ export default defineNitroPlugin(() => {
await clearUserSession(event);
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
{
await db.update(userSessionsTable).set({