First reworks
This commit is contained in:
parent
fef8c092a9
commit
1b2472bc1a
|
|
@ -5,7 +5,6 @@
|
||||||
.nitro
|
.nitro
|
||||||
.cache
|
.cache
|
||||||
dist
|
dist
|
||||||
content
|
|
||||||
|
|
||||||
# Node dependencies
|
# Node dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
@ -23,5 +22,3 @@ logs
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
db.sqlite-*
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "client: chrome",
|
|
||||||
"url": "http://localhost:3000",
|
|
||||||
"webRoot": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "server: nuxt",
|
|
||||||
"outputCapture": "std",
|
|
||||||
"program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs",
|
|
||||||
"args": [
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"compounds": [
|
|
||||||
{
|
|
||||||
"name": "fullstack: nuxt",
|
|
||||||
"configurations": [
|
|
||||||
"server: nuxt",
|
|
||||||
"client: chrome"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
55
app.vue
55
app.vue
|
|
@ -1,49 +1,12 @@
|
||||||
<style>
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
@apply bg-light-40;
|
|
||||||
@apply dark:bg-dark-40;
|
|
||||||
@apply rounded-md;
|
|
||||||
@apply border-2;
|
|
||||||
@apply border-solid;
|
|
||||||
@apply border-transparent;
|
|
||||||
@apply bg-clip-padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
@apply bg-light-50;
|
|
||||||
@apply dark:bg-dark-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
@apply w-full;
|
|
||||||
@apply h-full;
|
|
||||||
@apply flex;
|
|
||||||
@apply items-center;
|
|
||||||
@apply justify-center;
|
|
||||||
@apply before:border-4;
|
|
||||||
@apply before:border-accent-purple;
|
|
||||||
@apply before:border-t-transparent;
|
|
||||||
@apply before:rounded-full;
|
|
||||||
@apply before:w-8;
|
|
||||||
@apply before:h-8;
|
|
||||||
@apply before:animate-spin;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const page = ref();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
||||||
<NuxtRouteAnnouncer></NuxtRouteAnnouncer>
|
<NuxtRouteAnnouncer/>
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage ref="page"></NuxtPage>
|
<TooltipProvider>
|
||||||
</NuxtLayout>
|
<ToastProvider>
|
||||||
</div>
|
<NuxtPage></NuxtPage>
|
||||||
|
</ToastProvider>
|
||||||
|
</TooltipProvider>
|
||||||
|
</NuxtLayout>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
### Librairies, framework et outils libres utilisés:
|
|
||||||
- [Vuejs](https://vuejs.org/) - MIT License - Copyright (c) 2018-present, Yuxi (Evan) You and Vue contributors
|
|
||||||
- [vue-router](https://router.vuejs.org/) - MIT License - Copyright (c) 2019-present Eduardo San Martin Morote
|
|
||||||
- [Nuxt](https://nuxt.com/) - MIT License - Copyright (c) 2016-present - Nuxt Team
|
|
||||||
- [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils) - MIT License - Copyright (c) 2023 Sébastien Chopin
|
|
||||||
|
|
||||||
Le logo a été créé grace aux icones de [Game Icons](https://game-icons.net).
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
<style>
|
|
||||||
.editor
|
|
||||||
{
|
|
||||||
white-space: pre-line;
|
|
||||||
overflow: auto;
|
|
||||||
outline: none;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="editor">
|
|
||||||
<template
|
|
||||||
v-if="model && model.length > 0">
|
|
||||||
<MarkdownRenderer
|
|
||||||
v-if="node"
|
|
||||||
:key="key"
|
|
||||||
:node="node"
|
|
||||||
:proses="{
|
|
||||||
'a': LiveA,
|
|
||||||
'h1': LiveH1,
|
|
||||||
'h2': LiveH2,
|
|
||||||
'h3': LiveH3,
|
|
||||||
'h4': LiveH4,
|
|
||||||
'h5': LiveH5,
|
|
||||||
'h6': LiveH6,
|
|
||||||
'blockquote': LiveBlockquote,
|
|
||||||
}"
|
|
||||||
></MarkdownRenderer>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import LiveA from "~/components/prose/live/LiveA.vue";
|
|
||||||
import LiveH1 from "~/components/prose/live/LiveH1.vue";
|
|
||||||
import LiveH2 from "~/components/prose/live/LiveH2.vue";
|
|
||||||
import LiveH3 from "~/components/prose/live/LiveH3.vue";
|
|
||||||
import LiveH4 from "~/components/prose/live/LiveH4.vue";
|
|
||||||
import LiveH5 from "~/components/prose/live/LiveH5.vue";
|
|
||||||
import LiveH6 from "~/components/prose/live/LiveH6.vue";
|
|
||||||
import LiveBlockquote from "~/components/prose/ProseBlockquote.vue";
|
|
||||||
|
|
||||||
import { hash } from 'ohash'
|
|
||||||
import { watch, computed } from 'vue'
|
|
||||||
import type { Root, Node } from 'hast';
|
|
||||||
/* import { diffLines as diff } from 'diff'; */
|
|
||||||
|
|
||||||
const model = defineModel<string>();
|
|
||||||
|
|
||||||
const parser = useMarkdown();
|
|
||||||
const key = computed(() => hash(model.value));
|
|
||||||
|
|
||||||
const node = ref<Root>(), changes = ref();
|
|
||||||
|
|
||||||
watch(model, update);
|
|
||||||
update(model.value, "");
|
|
||||||
|
|
||||||
async function update(value: string | undefined, old: string | undefined) {
|
|
||||||
if(value && old)
|
|
||||||
{
|
|
||||||
if(node.value)
|
|
||||||
{
|
|
||||||
/* const differences = diff(old, value, {
|
|
||||||
newlineIsToken: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let removeStart = 0, removeEnd = 0; //Character count
|
|
||||||
let addStart = 0, addEnd = 0; //Character count
|
|
||||||
|
|
||||||
const needAdd = differences.find(e => e.added) !== undefined;
|
|
||||||
const needRemove = differences.find(e => e.removed) !== undefined;
|
|
||||||
|
|
||||||
for(const difference of differences)
|
|
||||||
{
|
|
||||||
if(!difference.added && !difference.removed)
|
|
||||||
{
|
|
||||||
removeStart += difference.value.length;
|
|
||||||
addStart += difference.value.length;
|
|
||||||
}
|
|
||||||
else if(difference.added)
|
|
||||||
{
|
|
||||||
addEnd = addStart + difference.value.length;
|
|
||||||
}
|
|
||||||
else if(difference.removed)
|
|
||||||
{
|
|
||||||
removeEnd = removeStart + difference.value.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if((!needAdd || addEnd !== 0) && (!needRemove || removeEnd !== 0))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldNodes = getNodes(node.value.children, removeStart - 1, removeEnd + 1);
|
|
||||||
let newNodes;
|
|
||||||
|
|
||||||
if(oldNodes.length === 0)
|
|
||||||
{
|
|
||||||
node.value = parser(value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const newStart = oldNodes[0].position?.start.offset;
|
|
||||||
const newEnd = oldNodes[oldNodes.length - 1].position?.end.offset;
|
|
||||||
|
|
||||||
const lengthDiff = value.length - old.length;
|
|
||||||
|
|
||||||
newNodes = parser(value.substring(newStart ?? 0, (newEnd ?? 0) + lengthDiff));
|
|
||||||
|
|
||||||
const root = node.value;
|
|
||||||
|
|
||||||
node.value = parser(value);
|
|
||||||
} */
|
|
||||||
node.value = parser(value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
node.value = parser(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(value)
|
|
||||||
{
|
|
||||||
node.value = parser(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getNodes(nodes: Node[], start: number, end: number)
|
|
||||||
{
|
|
||||||
return nodes.filter(e => (e.position?.start.offset ?? 0) <= end && (e.position?.end.offset ?? 0) >= start);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<ToastRoot class="ToastRoot bg-light-10 dark:bg-dark-10 p-3 border border-light-30 dark:border-dark-30" v-model:open="model">
|
||||||
|
<ToastTitle asChild><slot name="title"></slot></ToastTitle>
|
||||||
|
<ToastDescription asChild><slot></slot></ToastDescription>
|
||||||
|
<ToastClose v-if="closeable"><button>×</button></ToastClose>
|
||||||
|
</ToastRoot>
|
||||||
|
|
||||||
|
<ToastViewport class="fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-96 z-50 outline-none" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const model = defineModel<boolean>();
|
||||||
|
|
||||||
|
const { closeable = true } = defineProps<{
|
||||||
|
closeable?: boolean
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ToastRoot[data-swipe='move'] {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='cancel'] {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 200ms ease-out;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='end'] {
|
||||||
|
animation: swipeRight 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swipeRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<TooltipRoot :delay-duration="delay">
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<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" :side="side" :side-offset="['left', 'right'].includes(side ?? '') ? 8 : 0">
|
||||||
|
{{ message }}
|
||||||
|
<TooltipArrow class="fill-light-30 dark:fill-dark-30"></TooltipArrow>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
|
</TooltipRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { message, delay = 300, side } = defineProps<{
|
||||||
|
message: string
|
||||||
|
delay?: number
|
||||||
|
side?: 'left' | 'right' | 'top' | 'bottom'
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.TooltipContent {
|
||||||
|
animation-duration: .3s;
|
||||||
|
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.TooltipContent[data-side="top"] {
|
||||||
|
animation-name: slideUp;
|
||||||
|
}
|
||||||
|
.TooltipContent[data-side="bottom"] {
|
||||||
|
animation-name: slideDown;
|
||||||
|
}
|
||||||
|
.TooltipContent[data-side="left"] {
|
||||||
|
animation-name: slideLeft;
|
||||||
|
}
|
||||||
|
.TooltipContent[data-side="right"] {
|
||||||
|
animation-name: slideRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes slideRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideLeft {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
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">
|
|
||||||
<div class="">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
||||||
class="svg-icon lucide-align-left">
|
|
||||||
<line x1="21" y1="6" x2="3" y2="6"></line>
|
|
||||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="17" y1="18" x2="3" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</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,244 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useDrag, usePinch, useWheel } from '@vueuse/gesture';
|
|
||||||
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
|
||||||
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 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, }
|
|
||||||
})
|
|
||||||
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, }
|
|
||||||
})
|
|
||||||
</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">
|
|
||||||
<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"
|
|
||||||
aria-label="Zoom in" data-tooltip-position="left">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round">
|
|
||||||
<path d="M5 12h14"></path>
|
|
||||||
<path d="M12 5v14"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<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" aria-label="Reset zoom"
|
|
||||||
data-tooltip-position="left">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round">
|
|
||||||
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
|
|
||||||
<path d="M21 3v5h-5"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<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" aria-label="Zoom to fit"
|
|
||||||
data-tooltip-position="left">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round">
|
|
||||||
<path d="M8 3H5a2 2 0 0 0-2 2v3"></path>
|
|
||||||
<path d="M21 8V5a2 2 0 0 0-2-2h-3"></path>
|
|
||||||
<path d="M3 16v3a2 2 0 0 0 2 2h3"></path>
|
|
||||||
<path d="M16 21h3a2 2 0 0 0 2-2v-3"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<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"
|
|
||||||
aria-label="Zoom out" data-tooltip-position="left">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round">
|
|
||||||
<path d="M5 12h14"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
comments: {
|
|
||||||
type: [Object],
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="comments !== undefined && comments !== null" class="site-body-right-column">
|
|
||||||
<div class="site-body-right-column-inner">
|
|
||||||
<div>
|
|
||||||
{{ comments.length }} commentaire{{ comments.length === 1 ? '' : 's' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const route = useRoute();
|
|
||||||
const project = computed(() => parseInt(route.params.projectId as string));
|
|
||||||
|
|
||||||
const { data: navigation, refresh } = await useFetch(() => `/api/project/${project.value}/navigation`, { immediate: false });
|
|
||||||
if(!isNaN(project.value))
|
|
||||||
{
|
|
||||||
await refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits(['navigate']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="relative flex-auto">
|
|
||||||
<div class="absolute top-0 bottom-0 left-0 right-0 overflow-auto xl:px-4 px-2">
|
|
||||||
<NavigationLink @navigate="e => emit('navigate')" class="ps-2" v-if="!!navigation" v-for="link of navigation" :project="project" :link="link" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Navigation } from '~/types/api';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
link: Navigation;
|
|
||||||
project?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
const hasChildren = computed(() => {
|
|
||||||
return props.link && props.link.children && props.link.children.length > 0 || false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const collapsed = ref(!unifySlug(useRoute().params.slug).startsWith(props.link.path));
|
|
||||||
|
|
||||||
const emit = defineEmits(['navigate']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="py-1" v-if="project && !isNaN(project)">
|
|
||||||
<Accordion v-if="hasChildren" v-model="collapsed" class="data-[type]:after:content-[attr(data-type)] data-[type]:after:border data-[type]:after:border-light-35 data-[type]:dark:after:border-dark-35
|
|
||||||
data-[type]:after:text-sm data-[type]:after:px-1 data-[type]:after:bg-light-20 data-[type]:dark:after:bg-dark-20 data-[type]:after:[font-variant:all-petite-caps]">
|
|
||||||
<template #header>{{ link.title }}</template>
|
|
||||||
<template #content >
|
|
||||||
<NavigationLink @navigate="e => emit('navigate')" class="border-light-40 dark:border-dark-40 ms-2 ps-4 border-l" v-if="hasChildren" v-for="l of link.children" :link="l" :project="project" />
|
|
||||||
</template>
|
|
||||||
</Accordion>
|
|
||||||
<NuxtLink v-else @click="e => emit('navigate')" class="text-light-100 dark:text-dark-100 cursor-pointer hover:text-opacity-75 xl:text-base text-sm flex justify-between items-center
|
|
||||||
data-[type]:after:content-[attr(data-type)] data-[type]:after:border data-[type]:after:border-light-35 data-[type]:dark:after:border-dark-35
|
|
||||||
data-[type]:after:text-sm data-[type]:after:px-1 data-[type]:after:bg-light-20 data-[type]:dark:after:bg-dark-20 data-[type]:after:[font-variant:all-petite-caps]" :to="{ path: `/explorer/${project}/${link.path}`, force: true }"
|
|
||||||
active-class="!text-accent-blue relative before:border-l-2 before:border-accent-blue before:absolute before:-top-1 before:-bottom-1 before:-left-4 before:-mx-px" :data-type="(link.type === 'Canvas' ? 'graph' : (link.private ? 'privé' : undefined))">
|
|
||||||
<div>{{ link.title }}</div>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
<template>
|
|
||||||
<HoverPopup @before-show="fetch">
|
|
||||||
<template #content>
|
|
||||||
<Suspense suspensible>
|
|
||||||
<div :class="[{'is-loaded': fetched}, file?.type === 'Markdown' ? 'overflow-auto' : 'overflow-hidden']">
|
|
||||||
<div v-if="pending" class="loading w-[550px] h-[450px]"></div>
|
|
||||||
<template v-else-if="!!file">
|
|
||||||
<div v-if="file.type === 'Markdown'" class="p-6 ms-6">
|
|
||||||
<ProseH1>{{ file.title }}</ProseH1>
|
|
||||||
<Markdown :content="file.content"></Markdown>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="file.type === 'Canvas'" class="w-[550px] h-[450px] overflow-hidden">
|
|
||||||
<CanvasRenderer :canvas="JSON.parse(file.content) " />
|
|
||||||
</div>
|
|
||||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
|
||||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Fichier vide</div>
|
|
||||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est vide</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
|
||||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Impossible d'afficher</div>
|
|
||||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est impossible à traiter</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #fallback><div class="loading w-[550px] h-[450px]"></div></template>
|
|
||||||
</Suspense>
|
|
||||||
</template>
|
|
||||||
<template #default><slot name="default"></slot></template>
|
|
||||||
</HoverPopup>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { CommentedFile } from '~/types/api';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
project: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
anchor: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const file = ref<CommentedFile | null>();
|
|
||||||
const pending = ref(false), fetched = ref(false);
|
|
||||||
|
|
||||||
async function fetch()
|
|
||||||
{
|
|
||||||
if(fetched.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
fetched.value = true;
|
|
||||||
pending.value = true;
|
|
||||||
|
|
||||||
const data = await $fetch(`/api/project/${props.project}/file/${encodeURIComponent(props.path)}`);
|
|
||||||
|
|
||||||
pending.value = false;
|
|
||||||
file.value = data;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<template>
|
|
||||||
<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()}`" />
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { parseURL } from 'ufo';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
href: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
class: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
if(!!pathname && !protocol)
|
|
||||||
{
|
|
||||||
data.value = await $fetch(`/api/project/${project.value}/file`, {
|
|
||||||
query: {
|
|
||||||
search: `%${pathname}`
|
|
||||||
},
|
|
||||||
ignoreResponseError: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
<template>
|
|
||||||
<blockquote ref="el">
|
|
||||||
<slot />
|
|
||||||
</blockquote>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const attrs = useAttrs(), el = ref<HTMLQuoteElement>(), title = ref<Element | null>(null);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if(el && el.value && attrs.hasOwnProperty("dataCalloutFold"))
|
|
||||||
{
|
|
||||||
title.value = el.value.querySelector('.callout-title');
|
|
||||||
title.value?.addEventListener('click', toggle);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
title.value?.removeEventListener('click', toggle);
|
|
||||||
})
|
|
||||||
function toggle() {
|
|
||||||
el.value?.classList?.toggle('is-collapsed');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
blockquote:not(.callout)
|
|
||||||
{
|
|
||||||
@apply ps-4;
|
|
||||||
@apply my-4;
|
|
||||||
@apply relative;
|
|
||||||
@apply before:absolute;
|
|
||||||
@apply before:-top-1;
|
|
||||||
@apply before:-bottom-1;
|
|
||||||
@apply before:left-0;
|
|
||||||
@apply before:w-1;
|
|
||||||
@apply before:bg-light-30;
|
|
||||||
@apply dark:before:bg-dark-30;
|
|
||||||
}
|
|
||||||
blockquote:empty
|
|
||||||
{
|
|
||||||
@apply before:hidden;
|
|
||||||
}
|
|
||||||
.callout {
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
}
|
|
||||||
.callout.is-collapsible .callout-title
|
|
||||||
{
|
|
||||||
@apply cursor-pointer;
|
|
||||||
}
|
|
||||||
.callout .fold
|
|
||||||
{
|
|
||||||
@apply transition-transform;
|
|
||||||
}
|
|
||||||
.callout.is-collapsed .fold
|
|
||||||
{
|
|
||||||
@apply -rotate-90;
|
|
||||||
}
|
|
||||||
.callout.is-collapsed > p
|
|
||||||
{
|
|
||||||
@apply hidden;
|
|
||||||
}
|
|
||||||
.callout[datacallout="abstract"],
|
|
||||||
.callout[datacallout="summary"],
|
|
||||||
.callout[datacallout="tldr"] {
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[datacallout="info"] {
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
@apply text-light-blue;
|
|
||||||
@apply dark:text-dark-blue;
|
|
||||||
}
|
|
||||||
.callout[datacallout="todo"] {
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
@apply text-light-blue;
|
|
||||||
@apply dark:text-dark-blue;
|
|
||||||
}
|
|
||||||
.callout[datacallout="important"] {
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[datacallout="tip"],
|
|
||||||
.callout[datacallout="hint"] {
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[datacallout="success"],
|
|
||||||
.callout[datacallout="check"],
|
|
||||||
.callout[datacallout="done"] {
|
|
||||||
@apply bg-light-green;
|
|
||||||
@apply dark:bg-dark-green;
|
|
||||||
@apply text-light-green;
|
|
||||||
@apply dark:text-dark-green;
|
|
||||||
}
|
|
||||||
.callout[datacallout="question"],
|
|
||||||
.callout[datacallout="help"],
|
|
||||||
.callout[datacallout="faq"] {
|
|
||||||
@apply bg-light-orange;
|
|
||||||
@apply dark:bg-dark-orange;
|
|
||||||
@apply text-light-orange;
|
|
||||||
@apply dark:text-dark-orange;
|
|
||||||
}
|
|
||||||
.callout[datacallout="warning"],
|
|
||||||
.callout[datacallout="caution"],
|
|
||||||
.callout[datacallout="attention"] {
|
|
||||||
@apply bg-light-orange;
|
|
||||||
@apply dark:bg-dark-orange;
|
|
||||||
@apply text-light-orange;
|
|
||||||
@apply dark:text-dark-orange;
|
|
||||||
}
|
|
||||||
.callout[datacallout="failure"],
|
|
||||||
.callout[datacallout="fail"],
|
|
||||||
.callout[datacallout="missing"] {
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[datacallout="danger"],
|
|
||||||
.callout[datacallout="error"] {
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[datacallout="bug"] {
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[datacallout="example"] {
|
|
||||||
@apply bg-light-purple;
|
|
||||||
@apply dark:bg-dark-purple;
|
|
||||||
@apply text-light-purple;
|
|
||||||
@apply dark:text-dark-purple;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callout
|
|
||||||
{
|
|
||||||
@apply overflow-hidden;
|
|
||||||
@apply my-4;
|
|
||||||
@apply p-3;
|
|
||||||
@apply ps-6;
|
|
||||||
@apply bg-blend-lighten;
|
|
||||||
@apply !bg-opacity-25;
|
|
||||||
@apply border-l-4;
|
|
||||||
@apply inline-block;
|
|
||||||
@apply pe-8;
|
|
||||||
}
|
|
||||||
.callout-icon
|
|
||||||
{
|
|
||||||
@apply w-6;
|
|
||||||
@apply h-6;
|
|
||||||
@apply stroke-2;
|
|
||||||
}
|
|
||||||
.callout-title
|
|
||||||
{
|
|
||||||
@apply flex;
|
|
||||||
@apply items-center;
|
|
||||||
@apply gap-2;
|
|
||||||
}
|
|
||||||
.callout-title-inner
|
|
||||||
{
|
|
||||||
@apply inline-block;
|
|
||||||
@apply font-bold;
|
|
||||||
}
|
|
||||||
.callout > p
|
|
||||||
{
|
|
||||||
@apply mt-2;
|
|
||||||
@apply font-semibold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
<template>
|
|
||||||
<code><slot /></code>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<em>
|
|
||||||
<slot />
|
|
||||||
</em>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<template>
|
|
||||||
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2 relative sm:right-8 right-4">
|
|
||||||
<slot />
|
|
||||||
</h1>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<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">
|
|
||||||
<slot />
|
|
||||||
</h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<h3 :id="parseId(id)" class="text-2xl font-bold mt-2 mb-4">
|
|
||||||
<slot />
|
|
||||||
</h3>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,9 +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">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<h5 :id="parseId(id)" class="text-lg font-semibold my-1">
|
|
||||||
<slot />
|
|
||||||
</h5>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<h6 :id="parseId(id)">
|
|
||||||
<slot />
|
|
||||||
</h6>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
<template>
|
|
||||||
<hr class="border-light-35 dark:border-dark-35 m-4">
|
|
||||||
</template>
|
|
||||||
|
|
@ -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,3 +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>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<ol>
|
|
||||||
<slot />
|
|
||||||
</ol>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<template>
|
|
||||||
<p><slot /></p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.text-comment
|
|
||||||
{
|
|
||||||
@apply text-light-50;
|
|
||||||
@apply dark:text-dark-50;
|
|
||||||
@apply text-sm;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -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>
|
|
||||||
<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,54 +0,0 @@
|
||||||
<template>
|
|
||||||
<HoverPopup @before-show="fetch">
|
|
||||||
<template #content>
|
|
||||||
<Suspense suspensible>
|
|
||||||
<div class="mw-[400px]">
|
|
||||||
<div v-if="fetched === false" class="loading w-[400px] h-[150px]"></div>
|
|
||||||
<template v-else-if="!!data">
|
|
||||||
<div v-if="data.description" class="pb-4 pt-3 px-8">
|
|
||||||
<span class="text-2xl font-semibold">#{{ data.tag }}</span>
|
|
||||||
<Markdown :content="data.description"></Markdown>
|
|
||||||
</div>
|
|
||||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
|
||||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Fichier vide</div>
|
|
||||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est vide</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
|
||||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Impossible d'afficher</div>
|
|
||||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est impossible à traiter</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #fallback><div class="loading w-[400px] h-[150px]"></div></template>
|
|
||||||
</Suspense>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<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>
|
|
||||||
</HoverPopup>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Tag } from '~/types/api';
|
|
||||||
|
|
||||||
const { tag } = defineProps({
|
|
||||||
tag: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = ref<Tag>(), fetched = ref(false);
|
|
||||||
const route = useRoute();
|
|
||||||
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
|
|
||||||
async function fetch()
|
|
||||||
{
|
|
||||||
if(fetched.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
data.value = await $fetch(`/api/project/${project.value}/tags/${encodeURIComponent(tag)}`);
|
|
||||||
fetched.value = true;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -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,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<span class="text-accent-blue inline-flex items-center cursor-pointer">
|
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
<template>
|
|
||||||
<blockquote ref="el">
|
|
||||||
<slot />
|
|
||||||
</blockquote>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const attrs = useAttrs(), el = ref<HTMLQuoteElement>(), title = ref<Element | null>(null);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if(el && el.value && attrs.hasOwnProperty("dataCalloutFold"))
|
|
||||||
{
|
|
||||||
title.value = el.value.querySelector('.callout-title');
|
|
||||||
title.value?.addEventListener('click', toggle);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
title.value?.removeEventListener('click', toggle);
|
|
||||||
})
|
|
||||||
function toggle() {
|
|
||||||
el.value?.classList?.toggle('is-collapsed');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
blockquote:not(.callout)
|
|
||||||
{
|
|
||||||
@apply ps-4;
|
|
||||||
@apply my-4;
|
|
||||||
@apply relative;
|
|
||||||
@apply before:absolute;
|
|
||||||
@apply before:-top-1;
|
|
||||||
@apply before:-bottom-1;
|
|
||||||
@apply before:left-0;
|
|
||||||
@apply before:w-1;
|
|
||||||
@apply before:bg-light-30;
|
|
||||||
@apply dark:before:bg-dark-30;
|
|
||||||
}
|
|
||||||
blockquote:empty
|
|
||||||
{
|
|
||||||
@apply before:hidden;
|
|
||||||
}
|
|
||||||
.callout {
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
}
|
|
||||||
.callout.is-collapsible .callout-title
|
|
||||||
{
|
|
||||||
@apply cursor-pointer;
|
|
||||||
}
|
|
||||||
.callout .fold
|
|
||||||
{
|
|
||||||
@apply transition-transform;
|
|
||||||
}
|
|
||||||
.callout.is-collapsed .fold
|
|
||||||
{
|
|
||||||
@apply -rotate-90;
|
|
||||||
}
|
|
||||||
.callout.is-collapsed > p
|
|
||||||
{
|
|
||||||
@apply hidden;
|
|
||||||
}
|
|
||||||
.callout[datacallout="abstract"],
|
|
||||||
.callout[datacallout="summary"],
|
|
||||||
.callout[datacallout="tldr"] {
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[datacallout="info"] {
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
@apply text-light-blue;
|
|
||||||
@apply dark:text-dark-blue;
|
|
||||||
}
|
|
||||||
.callout[datacallout="todo"] {
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
@apply text-light-blue;
|
|
||||||
@apply dark:text-dark-blue;
|
|
||||||
}
|
|
||||||
.callout[datacallout="important"] {
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[datacallout="tip"],
|
|
||||||
.callout[datacallout="hint"] {
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[datacallout="success"],
|
|
||||||
.callout[datacallout="check"],
|
|
||||||
.callout[datacallout="done"] {
|
|
||||||
@apply bg-light-green;
|
|
||||||
@apply dark:bg-dark-green;
|
|
||||||
@apply text-light-green;
|
|
||||||
@apply dark:text-dark-green;
|
|
||||||
}
|
|
||||||
.callout[datacallout="question"],
|
|
||||||
.callout[datacallout="help"],
|
|
||||||
.callout[datacallout="faq"] {
|
|
||||||
@apply bg-light-orange;
|
|
||||||
@apply dark:bg-dark-orange;
|
|
||||||
@apply text-light-orange;
|
|
||||||
@apply dark:text-dark-orange;
|
|
||||||
}
|
|
||||||
.callout[datacallout="warning"],
|
|
||||||
.callout[datacallout="caution"],
|
|
||||||
.callout[datacallout="attention"] {
|
|
||||||
@apply bg-light-orange;
|
|
||||||
@apply dark:bg-dark-orange;
|
|
||||||
@apply text-light-orange;
|
|
||||||
@apply dark:text-dark-orange;
|
|
||||||
}
|
|
||||||
.callout[datacallout="failure"],
|
|
||||||
.callout[datacallout="fail"],
|
|
||||||
.callout[datacallout="missing"] {
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[datacallout="danger"],
|
|
||||||
.callout[datacallout="error"] {
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[datacallout="bug"] {
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[datacallout="example"] {
|
|
||||||
@apply bg-light-purple;
|
|
||||||
@apply dark:bg-dark-purple;
|
|
||||||
@apply text-light-purple;
|
|
||||||
@apply dark:text-dark-purple;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callout
|
|
||||||
{
|
|
||||||
@apply overflow-hidden;
|
|
||||||
@apply my-4;
|
|
||||||
@apply p-3;
|
|
||||||
@apply ps-6;
|
|
||||||
@apply bg-blend-lighten;
|
|
||||||
@apply !bg-opacity-25;
|
|
||||||
@apply border-l-4;
|
|
||||||
@apply inline-block;
|
|
||||||
@apply pe-8;
|
|
||||||
}
|
|
||||||
.callout-icon
|
|
||||||
{
|
|
||||||
@apply w-6;
|
|
||||||
@apply h-6;
|
|
||||||
@apply stroke-2;
|
|
||||||
}
|
|
||||||
.callout-title
|
|
||||||
{
|
|
||||||
@apply flex;
|
|
||||||
@apply items-center;
|
|
||||||
@apply gap-2;
|
|
||||||
}
|
|
||||||
.callout-title-inner
|
|
||||||
{
|
|
||||||
@apply inline-block;
|
|
||||||
@apply font-bold;
|
|
||||||
}
|
|
||||||
.callout > p
|
|
||||||
{
|
|
||||||
@apply mt-2;
|
|
||||||
@apply font-semibold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,9 +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">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +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">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<h3 :id="parseId(id)" class="text-2xl font-bold mt-2 mb-4">
|
|
||||||
<slot />
|
|
||||||
</h3>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,9 +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">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<h5 :id="parseId(id)" class="text-lg font-semibold my-1">
|
|
||||||
<slot />
|
|
||||||
</h5>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<h6 :id="parseId(id)">
|
|
||||||
<slot />
|
|
||||||
</h6>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,5 +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>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const collapsed = defineModel<boolean>({
|
|
||||||
default: true,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div @click="collapsed = !collapsed" class="flex gap-2 items-center cursor-pointer hover:text-opacity-75 text-light-100 dark:text-dark-100" :class="$attrs.class">
|
|
||||||
<div class="w-4 h-4 transition-transform" :class="{'-rotate-90': collapsed}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M3 8L12 17L21 8"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="font-semibold xl:text-base text-sm flex-1"><slot name="header"></slot></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!collapsed">
|
|
||||||
<slot name="content"></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
<template>
|
|
||||||
<Teleport to="#teleports" v-if="visible">
|
|
||||||
<div @click.self="() => !focused && hide()" class="z-[100] absolute top-0 bottom-0 left-0 right-0 bg-light-0 dark:bg-dark-0 !bg-opacity-80 flex justify-center items-center">
|
|
||||||
<div class="relative border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 min-h-40 min-w-72">
|
|
||||||
<span v-if="closeIcon" class="cursor-pointer absolute top-1 right-1 flex hover:opacity-75" @click="() => hide()"><Icon :width="20" :height="20" icon="icons/close" /></span>
|
|
||||||
<div class="p-6" :class="$attrs.class">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
focused: {
|
|
||||||
type: Boolean,
|
|
||||||
defualt: false,
|
|
||||||
},
|
|
||||||
closeIcon: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['show', 'hide']);
|
|
||||||
const visible = ref(false);
|
|
||||||
|
|
||||||
function show()
|
|
||||||
{
|
|
||||||
visible.value = true;
|
|
||||||
emit('show');
|
|
||||||
}
|
|
||||||
function hide()
|
|
||||||
{
|
|
||||||
visible.value = false;
|
|
||||||
emit('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({hide, show});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
{{beforeText}}<span class="font-bold">{{matchedText}}</span>{{afterText}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
interface Prop
|
|
||||||
{
|
|
||||||
text: string;
|
|
||||||
matched: string;
|
|
||||||
}
|
|
||||||
const props = defineProps<Prop>();
|
|
||||||
|
|
||||||
const beforeText = computed(() => {
|
|
||||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase());
|
|
||||||
return props.text.substring(0, pos);
|
|
||||||
})
|
|
||||||
const matchedText = computed(() => {
|
|
||||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase());
|
|
||||||
return props.text.substring(pos, pos + props.matched.length);
|
|
||||||
})
|
|
||||||
const afterText = computed(() => {
|
|
||||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase()) + props.matched.length;
|
|
||||||
return props.text.substring(pos);
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<template>
|
|
||||||
<Teleport to="#teleports" v-if="display">
|
|
||||||
<div class="absolute border-2 border-light-35 dark:border-dark-35 max-w-[550px] max-h-[450px] bg-light-0 dark:bg-dark-0 text-light-100 dark:text-dark-100 overflow-hidden" :style="pos"
|
|
||||||
@mouseenter="debounce(show, 250)" @mouseleave="debounce(() => { emit('beforeHide'); display = false }, 250)">
|
|
||||||
<slot name="content"></slot>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
<span ref="el" :class="$attrs.class" @mouseenter="debounce(show, 250)" @mouseleave="debounce(() => { emit('beforeHide'); display = false }, 250)">
|
|
||||||
<slot name="default"></slot>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useTemplateRef } from 'vue';
|
|
||||||
|
|
||||||
const display = ref(false), fetched = ref(false);
|
|
||||||
const el = useTemplateRef('el'), pos = ref<Record<string, string>>();
|
|
||||||
const emit = defineEmits(['beforeShow', 'beforeHide']);
|
|
||||||
|
|
||||||
async function show()
|
|
||||||
{
|
|
||||||
if(display.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
emit('beforeShow');
|
|
||||||
|
|
||||||
const rect = (el.value as HTMLDivElement)?.getBoundingClientRect();
|
|
||||||
|
|
||||||
if(!rect)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const r: Record<string, string> = {};
|
|
||||||
if (rect.bottom + 450 < window.innerHeight)
|
|
||||||
r.top = (rect.bottom + 4) + "px";
|
|
||||||
else
|
|
||||||
r.bottom = (window.innerHeight - rect.top + 4) + "px";
|
|
||||||
if (rect.right + 550 < window.innerWidth)
|
|
||||||
r.left = rect.left + "px";
|
|
||||||
else
|
|
||||||
r.right = (window.innerWidth - rect.right + 4) + "px";
|
|
||||||
|
|
||||||
pos.value = r;
|
|
||||||
display.value = true;
|
|
||||||
}
|
|
||||||
let debounceFn: () => void, timeout: NodeJS.Timeout;
|
|
||||||
function debounce(fn: () => void, ms: number)
|
|
||||||
{
|
|
||||||
debounceFn = fn;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(debounceFn, ms);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
interface Prop
|
|
||||||
{
|
|
||||||
icon: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
defineProps<Prop>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span :class="$attrs.class" class="inline-block bg-light-100 dark:bg-dark-100 [mask-size:100%] [mask-repeat:no-repeat] [mask-position:center]" :style="`width: ${width}px; height: ${height}px; mask-image: url(/${icon}.svg)`"></span>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const emit = defineEmits(['input', 'change', 'focus', 'blur']);
|
|
||||||
const model = defineModel();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<input v-model="model" v-bind="$attrs" @input="e => emit('input', e)" @change="e => emit('change', e)" @focus="e => emit('focus', e)" @blur="e => emit('blur', e)"
|
|
||||||
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 appearance-none 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"/>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
interface Prop
|
|
||||||
{
|
|
||||||
error?: string | boolean;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
const props = defineProps<Prop>();
|
|
||||||
const model = defineModel<string>();
|
|
||||||
|
|
||||||
const err = ref<string | boolean | undefined>(props.error);
|
|
||||||
|
|
||||||
watchEffect(() => err.value = props.error);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="m-1 flex justify-between items-center gap-8">
|
|
||||||
<label v-if="title" class="pe-4">{{ title }}</label>
|
|
||||||
<Input class="flex-1" @input="err = false" :class="{ 'input-has-error': !!err }" v-model="model" v-bind="$attrs" />
|
|
||||||
</div>
|
|
||||||
<span v-if="err && typeof err === 'string'" class="text-light-red dark:text-dark-red block pb-2">{{ err }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<template>
|
|
||||||
<template
|
|
||||||
v-if="content && content.length > 0">
|
|
||||||
<Suspense :timeout="0">
|
|
||||||
<MarkdownRenderer #default :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
|
||||||
<template #fallback><div class="loading"></div></template>
|
|
||||||
</Suspense>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { hash } from 'ohash'
|
|
||||||
|
|
||||||
const { content } = defineProps({
|
|
||||||
content: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const parser = useMarkdown();
|
|
||||||
const key = computed(() => hash(content));
|
|
||||||
const node = computed(() => content ? parser(content) : undefined);
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,111 +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 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 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,
|
|
||||||
"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,
|
|
||||||
"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,76 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useTemplateRef } from 'vue';
|
|
||||||
|
|
||||||
const containerRef = useTemplateRef("container");
|
|
||||||
const { loggedIn, user } = useUserSession();
|
|
||||||
const { direction } = useSwipe(window, { threshold: 150 });
|
|
||||||
|
|
||||||
watch(direction, () => {
|
|
||||||
if(direction.value === 'right')
|
|
||||||
toggleNavigation(true);
|
|
||||||
if(direction.value === 'left')
|
|
||||||
toggleNavigation(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleNavigation(bool?: boolean)
|
|
||||||
{
|
|
||||||
containerRef.value?.setAttribute('aria-expanded', bool === undefined ? (containerRef.value?.getAttribute('aria-expanded') !== 'true').toString() : bool.toString());
|
|
||||||
}
|
|
||||||
const hideNavigation = () => toggleNavigation(false);
|
|
||||||
onMounted(() => {
|
|
||||||
const links = containerRef.value?.getElementsByTagName('a');
|
|
||||||
if(links)
|
|
||||||
{
|
|
||||||
for(const link of links)
|
|
||||||
{
|
|
||||||
link.addEventListener("click", hideNavigation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
const links = containerRef.value?.getElementsByTagName('a');
|
|
||||||
if(links)
|
|
||||||
{
|
|
||||||
for(const link of links)
|
|
||||||
{
|
|
||||||
link.addEventListener("click", hideNavigation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div ref="container" aria-expanded="false" class="group flex h-screen overflow-hidden">
|
|
||||||
<div class="z-50 sm:hidden block absolute top-0 left-0 p-2 border-e border-b border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25" @click="e => toggleNavigation()">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
||||||
class="w-4 h-4 fill-light-100 dark:fill-dark-100">
|
|
||||||
<line x1="21" y1="6" x2="3" y2="6"></line>
|
|
||||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="17" y1="18" x2="3" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="bg-light-0 sm:my-8 sm:py-3 dark:bg-dark-0 top-0 z-40 xl:w-96 sm:w-[15em] w-full border-r border-light-30 dark:border-dark-30 flex flex-col justify-between max-sm:absolute max-sm:-top-0 max-sm:-bottom-0 sm:left-0 max-sm:-left-full max-sm:group-aria-expanded:left-0 max-sm:transition-[left] py-8 max-sm:z-40">
|
|
||||||
<div class="relative bottom-6 flex flex-1 flex-col gap-4 xl:px-6 px-3">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<NuxtLink @click="hideNavigation" class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-sm:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }"><ThemeIcon class="inline" icon="logo" :width=56 :height=56 /></NuxtLink>
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<ThemeSwitch />
|
|
||||||
<NuxtLink @click="hideNavigation" class="" :to="{ path: '/user/profile', force: true }"><div class=" hover:border-opacity-70 flex border p-px border-light-70 dark:border-dark-70">
|
|
||||||
<Icon v-if="!loggedIn" icon="icons/user-login" :width=28 :height=28 />
|
|
||||||
<Picture v-else :src="`/users/${user?.id}/small.jpg`" :width=28 :height=28 class="flex" >
|
|
||||||
<Icon :icon="`icons/unknown`" :width=28 :height=28 ></Icon>
|
|
||||||
</Picture>
|
|
||||||
</div></NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex"><SearchView @navigate="hideNavigation" /></div>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
<div class="text-center xl:text-sm text-xs text-light-70 dark:text-dark-70">
|
|
||||||
<NuxtLink class="hover:underline italic" :to="{ path: '/third-party', force: true }">Mentions légales</NuxtLink>
|
|
||||||
<p>Copyright Peaceultime - 2024</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<template>
|
|
||||||
<span>
|
|
||||||
<img v-show="src && !fallback" @load="hideFallback" @error="showFallback" :src="src" :width="width" :height="height" />
|
|
||||||
<span v-show="!src || fallback"><slot></slot></span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
src: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const fallback = ref(false);
|
|
||||||
|
|
||||||
const showFallback = () => toggleFallback(true);
|
|
||||||
const hideFallback = () => toggleFallback(false);
|
|
||||||
|
|
||||||
function toggleFallback(toggle: boolean): void
|
|
||||||
{
|
|
||||||
console.log("Something happened")
|
|
||||||
fallback.value = toggle;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Search, File, Project, User } from '~/types/api';
|
|
||||||
|
|
||||||
const input = ref(''), results = ref<Search>({ projects: [], files: [], users: [] });
|
|
||||||
const pos = ref<DOMRect>(), loading = ref(false);
|
|
||||||
|
|
||||||
let timeout: NodeJS.Timeout;
|
|
||||||
|
|
||||||
const emit = defineEmits(['navigate']);
|
|
||||||
|
|
||||||
function search(e: Event)
|
|
||||||
{
|
|
||||||
pos.value = (e.currentTarget as HTMLElement)?.getBoundingClientRect();
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(debounced, 250);
|
|
||||||
}
|
|
||||||
async function debounced()
|
|
||||||
{
|
|
||||||
if(!input.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const fetch = await useFetch(`/api/search`, {
|
|
||||||
query: {
|
|
||||||
"search": `%${input.value}%`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
results.value = fetch.data.value;
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
results.value = { projects: [], files: [], users: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-full border border-light-30 dark:border-dark-30">
|
|
||||||
<Input class="w-full" type="text" placeholder="Recherche" v-model="input" @input="search" />
|
|
||||||
</div>
|
|
||||||
<Teleport to="#teleports" v-if="input !== '' && !!pos">
|
|
||||||
<div class="absolute z-50 border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 text-light-100 dark:text-dark-100 divide-y divide-light-35 dark:divide-dark-35 max-h-96 overflow-auto
|
|
||||||
scroll"
|
|
||||||
:style="{top: (pos.bottom + 4) + 'px', left: pos.left + 'px', width: pos.width + 'px'}">
|
|
||||||
<div class="loading" v-if="loading"></div>
|
|
||||||
<template v-else>
|
|
||||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.projects" :key="result.id"
|
|
||||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
|
||||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
|
||||||
@mousedown.prevent="navigateTo(`/explorer/${result.id}/${result.home}`); input = ''; emit('navigate');">
|
|
||||||
<div class="">
|
|
||||||
<Highlight class="text-lg" :text="result.name" :matched="input" />
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<div class="">{{ result.username }}</div>
|
|
||||||
<div class="">{{ result.pages }} pages</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.files" :key="result.id"
|
|
||||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
|
||||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
|
||||||
@mousedown.prevent="navigateTo(`/explorer/${result.project}/${result.path}`); input = ''; emit('navigate');">
|
|
||||||
<div class="">
|
|
||||||
<Highlight class="text-lg" :text="result.title" :matched="input" />
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<div class="">{{ result.username }}</div>
|
|
||||||
<div class="">{{ result.comments }} commentaires</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.users" :key="result.id"
|
|
||||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
|
||||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
|
||||||
@mousedown.prevent="navigateTo(`/users/${result.id}`); input = ''; emit('navigate');">
|
|
||||||
<div class="">
|
|
||||||
<Highlight class="text-lg" :text="result.username" :matched="input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=""
|
|
||||||
v-if="results.projects.length === 0 && results.files.length === 0 && results.users.length === 0">
|
|
||||||
Aucun résultat
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
interface Prop
|
|
||||||
{
|
|
||||||
icon: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
defineProps<Prop>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span>
|
|
||||||
<img :src="`/icons/${icon}.light.svg`" class="block dark:hidden" :width="width" :height="height" />
|
|
||||||
<img :src="`/icons/${icon}.dark.svg`" class="dark:block hidden" :width="width" :height="height" />
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<script setup>
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
|
|
||||||
const isDark = computed({
|
|
||||||
get() {
|
|
||||||
return colorMode.value === 'dark'
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex relative">
|
|
||||||
<span class="hidden dark:block absolute top-[3px] left-[2px] z-[1] px-[5px]">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 stroke-light-100 dark:stroke-dark-100">
|
|
||||||
<path d="M12 3a6.364 6.364 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<div class="
|
|
||||||
before:absolute before:top-0 before:left-0 before:right-0 before:bottom-0 before:opacity-0 before:block before:z-10
|
|
||||||
after:transition-all after:w-4 after:h-4 after:border after:border-light-30 dark:after:border-dark-30 after:block after:m-[3px] after:top-[-1px] after:pointer-events-none after:absolute after:left-0 after:bg-light-0 after:translate-x-[1px] dark:after:translate-x-[26px]
|
|
||||||
inline-block relative cursor-pointer w-[50px] h-[22px] select-none border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-30 dark:hover:border-dark-35" @click="isDark = !isDark"></div>
|
|
||||||
<span class="block dark:hidden absolute top-[3px] left-[22px] z-[1] px-[5px]">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 stroke-light-100 dark:stroke-dark-100 ">
|
|
||||||
<circle cx="12" cy="12" r="4"></circle>
|
|
||||||
<path d="M12 2v2"></path>
|
|
||||||
<path d="M12 20v2"></path>
|
|
||||||
<path d="m4.93 4.93 1.41 1.41"></path>
|
|
||||||
<path d="m17.66 17.66 1.41 1.41"></path>
|
|
||||||
<path d="M2 12h2"></path>
|
|
||||||
<path d="M20 12h2"></path>
|
|
||||||
<path d="m6.34 17.66-1.41 1.41"></path>
|
|
||||||
<path d="m19.07 4.93-1.41 1.41"></path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<template>
|
|
||||||
<label>
|
|
||||||
<slot></slot>
|
|
||||||
<input ref="input" :accept="accept" :multiple="multiple" type="file" class="hidden" @change.self="(e) => files = [...(e.target as HTMLInputElement)?.files ?? []]"/>
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
accept: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
multiple: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const input = useTemplateRef('input');
|
|
||||||
const files = ref<File[]>([]);
|
|
||||||
|
|
||||||
watch([files], () => emit('change', files.value));
|
|
||||||
|
|
||||||
const emit = defineEmits(['change']);
|
|
||||||
|
|
||||||
defineExpose({ files });
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,21 +1,13 @@
|
||||||
|
import ".dotenv/config";
|
||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
|
|
||||||
let instance: Database | undefined;
|
export default function useDatabase()
|
||||||
|
|
||||||
export default function useDatabase(): Database {
|
|
||||||
if(instance === undefined)
|
|
||||||
instance = getDatabase();
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDatabase(): Database
|
|
||||||
{
|
{
|
||||||
const { dbFile } = useRuntimeConfig();
|
const sqlite = new Database(process.env.DB_FILE);
|
||||||
|
const db = drizzle({ client: sqlite });
|
||||||
|
|
||||||
const db = new Database(dbFile);
|
db.run("PRAGMA journal_mode = WAL;");
|
||||||
|
|
||||||
db.exec("PRAGMA journal_mode = WAL;");
|
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { unified, type Processor } from "unified";
|
|
||||||
import type { Root } from 'hast';
|
|
||||||
import RemarkParse from "remark-parse";
|
|
||||||
|
|
||||||
import RemarkRehype from 'remark-rehype';
|
|
||||||
import RemarkOfm from 'remark-ofm';
|
|
||||||
import RemarkBreaks from 'remark-breaks'
|
|
||||||
import RemarkGfm from 'remark-gfm';
|
|
||||||
import RemarkFrontmatter from 'remark-frontmatter';
|
|
||||||
import RehypeRaw from 'rehype-raw';
|
|
||||||
|
|
||||||
export default function useMarkdown(): (md: string) => Root
|
|
||||||
{
|
|
||||||
let processor: Processor;
|
|
||||||
|
|
||||||
const parse = (markdown: string) => {
|
|
||||||
if (!processor)
|
|
||||||
{
|
|
||||||
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter]);
|
|
||||||
processor.use(RemarkRehype, { allowDangerousHtml: true });
|
|
||||||
processor.use(RehypeRaw);
|
|
||||||
}
|
|
||||||
|
|
||||||
const processed = processor.runSync(processor.parse(markdown)) as Root;
|
|
||||||
return processed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parse;
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import type { Project } from "~/types/api";
|
|
||||||
|
|
||||||
export default function useProject()
|
|
||||||
{
|
|
||||||
const project = useCookie('project');
|
|
||||||
|
|
||||||
const id = useState<number>("projectId", () => parseInt(project.value ?? '0'));
|
|
||||||
const name = useState<string>("projectName", undefined);
|
|
||||||
const owner = useState<number>("projectOwner", undefined);
|
|
||||||
const home = useState<string | null>("projectHomepage", () => null);
|
|
||||||
const summary = useState<string | null>("projectSummary", () => null);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id, name, owner, home, summary, get, set
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function get(): Promise<boolean> {
|
|
||||||
const id = useState<number>("projectId");
|
|
||||||
|
|
||||||
if (!id.value)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await $fetch(`/api/project/${id.value}`) as Project;
|
|
||||||
|
|
||||||
const name = useState<string>("projectName");
|
|
||||||
const owner = useState<number>("projectOwner");
|
|
||||||
const home = useState<string | null>("projectHomepage");
|
|
||||||
const summary = useState<string | null>("projectSummary");
|
|
||||||
|
|
||||||
name.value = result.name;
|
|
||||||
owner.value = result.owner;
|
|
||||||
home.value = result.home;
|
|
||||||
summary.value = result.summary;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch(e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function set(id: number): Promise<boolean> {
|
|
||||||
const _id = useState<number>("projectId");
|
|
||||||
|
|
||||||
_id.value = id;
|
|
||||||
const project = useCookie('project');
|
|
||||||
project.value = id.toString();
|
|
||||||
|
|
||||||
return await get();
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import type { UserSession, UserSessionComposable } from '~/types/auth'
|
|
||||||
|
|
||||||
const useSessionState = () => useState<UserSession>('nuxt-session', () => ({}))
|
|
||||||
const useAuthReadyState = () => useState('nuxt-auth-ready', () => false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to get back the user session and utils around it.
|
|
||||||
* @see https://github.com/atinux/nuxt-auth-utils
|
|
||||||
*/
|
|
||||||
export function useUserSession(): UserSessionComposable {
|
|
||||||
const sessionState = useSessionState()
|
|
||||||
const authReadyState = useAuthReadyState()
|
|
||||||
return {
|
|
||||||
ready: computed(() => authReadyState.value),
|
|
||||||
loggedIn: computed(() => Boolean(sessionState.value.user)),
|
|
||||||
user: computed(() => sessionState.value.user || null),
|
|
||||||
session: sessionState,
|
|
||||||
fetch,
|
|
||||||
clear,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetch() {
|
|
||||||
const authReadyState = useAuthReadyState()
|
|
||||||
useSessionState().value = await useRequestFetch()('/api/auth/session', {
|
|
||||||
headers: {
|
|
||||||
Accept: 'text/json',
|
|
||||||
},
|
|
||||||
retry: false,
|
|
||||||
}).catch(() => ({}))
|
|
||||||
if (!authReadyState.value) {
|
|
||||||
authReadyState.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clear() {
|
|
||||||
await $fetch('/api/auth/session', { method: 'DELETE' })
|
|
||||||
useSessionState().value = {}
|
|
||||||
useRouter().go(0);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { int, text, sqliteTable, type SQLiteTableExtraConfig, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||||
|
|
||||||
|
export const usersTable = sqliteTable("users", {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
username: text().notNull().unique(),
|
||||||
|
email: text().notNull().unique(),
|
||||||
|
hash: text().notNull().unique(),
|
||||||
|
state: int().default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usersDataTable = sqliteTable("users_data", {
|
||||||
|
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSessionsTable = sqliteTable("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): SQLiteTableExtraConfig => {
|
||||||
|
return {
|
||||||
|
pk: primaryKey({ columns: [ table.id, table.user_id ] }),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const explorerContentTable = sqliteTable("explorer_content", {
|
||||||
|
path: text().primaryKey(),
|
||||||
|
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
title: text().notNull(),
|
||||||
|
type: text({ enum: ['file', 'folder', 'markdown', 'canvas'] }).notNull(),
|
||||||
|
content: blob({ mode: 'buffer' }),
|
||||||
|
navigable: int({ mode: 'boolean' }).default(true),
|
||||||
|
private: int({ mode: 'boolean' }).default(false),
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: './drizzle',
|
||||||
|
schema: './db/schema.ts',
|
||||||
|
dialect: 'sqlite',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DB_FILE!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
CREATE TABLE `explorer_content` (
|
||||||
|
`path` text PRIMARY KEY NOT NULL,
|
||||||
|
`owner` integer NOT NULL,
|
||||||
|
`title` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`content` blob,
|
||||||
|
`navigable` integer DEFAULT true,
|
||||||
|
`private` integer DEFAULT false,
|
||||||
|
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `user_sessions` (
|
||||||
|
`id` integer NOT NULL,
|
||||||
|
`user_id` integer NOT NULL,
|
||||||
|
`timestamp` integer NOT NULL,
|
||||||
|
PRIMARY KEY(`id`, `user_id`),
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `users_data` (
|
||||||
|
`id` integer PRIMARY KEY NOT NULL,
|
||||||
|
FOREIGN KEY (`id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`username` text NOT NULL,
|
||||||
|
`email` text NOT NULL,
|
||||||
|
`hash` text NOT NULL,
|
||||||
|
`state` integer DEFAULT 0
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_hash_unique` ON `users` (`hash`);
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "ddf5d5b3-bf1e-4d8d-89cb-230f8e90137a",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"explorer_content": {
|
||||||
|
"name": "explorer_content",
|
||||||
|
"columns": {
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"navigable": {
|
||||||
|
"name": "navigable",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"explorer_content_owner_users_id_fk": {
|
||||||
|
"name": "explorer_content_owner_users_id_fk",
|
||||||
|
"tableFrom": "explorer_content",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": false,
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1730124775172,
|
||||||
|
"tag": "0000_youthful_ma_gnuci",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
<template>
|
<template>
|
||||||
<NavBar>
|
<slot></slot>
|
||||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Projets" :to="{ path: `/explorer`, force: true }">Projets</NuxtLink>
|
|
||||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Editeur" :to="{ path: '/editing', force: true }">Editeur</NuxtLink>
|
|
||||||
</NavBar>
|
|
||||||
<div class="flex-1 flex items-baseline overflow-auto py-8 sm:px-8 px-4 relative">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<NavBar>
|
|
||||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Projets" :to="{ path: `/explorer`, force: true }">Projets</NuxtLink>
|
|
||||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Editeur" :to="{ path: '/editing', force: true }">Editeur</NuxtLink>
|
|
||||||
<hr class="border-light-35 dark:border-dark-35"/>
|
|
||||||
<ExplorerNavigation></ExplorerNavigation>
|
|
||||||
</NavBar>
|
|
||||||
<div class="flex-1 flex items-baseline overflow-auto py-8 sm:px-8 px-4 relative">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
|
||||||
const { loggedIn, ready, fetch } = useUserSession();
|
|
||||||
const meta = to.meta;
|
|
||||||
|
|
||||||
if(!ready)
|
|
||||||
await fetch();
|
|
||||||
|
|
||||||
if(!!meta.guestsGoesTo && !loggedIn.value)
|
|
||||||
{
|
|
||||||
return navigateTo(meta.guestsGoesTo);
|
|
||||||
}
|
|
||||||
else if(meta.requireAuth && !loggedIn.value)
|
|
||||||
{
|
|
||||||
return abortNavigation();
|
|
||||||
}
|
|
||||||
else if(!!meta.usersGoesTo && loggedIn.value)
|
|
||||||
{
|
|
||||||
return navigateTo(meta.usersGoesTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import { defineNuxtModule } from '@nuxt/kit'
|
|
||||||
import { Database } from "bun:sqlite";
|
|
||||||
|
|
||||||
interface Schema
|
|
||||||
{
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
tbl_name: string;
|
|
||||||
sql: string;
|
|
||||||
}
|
|
||||||
interface Structure
|
|
||||||
{
|
|
||||||
cid: number;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
[key: string]: number | string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineNuxtModule({
|
|
||||||
setup (options, nuxt) {
|
|
||||||
nuxt.hook('build:done', () => {
|
|
||||||
console.log("Building database");
|
|
||||||
|
|
||||||
const template = new Database(nuxt.options.runtimeConfig.templateFile);
|
|
||||||
const schema = template.query(`SELECT * FROM sqlite_schema WHERE type = 'table'`).all() as Schema[];
|
|
||||||
const structure = template.query(`SELECT * FROM pragma_table_info(?1)`);
|
|
||||||
|
|
||||||
const db = new Database(nuxt.options.runtimeConfig.dbFile);
|
|
||||||
db.exec(`PRAGMA foreign_keys = OFF; PRAGMA defer_foreign_keys = OFF;`);
|
|
||||||
const oldSchema = db.query(`SELECT * FROM sqlite_schema WHERE type = 'table'`).all() as Schema[];
|
|
||||||
const oldStructure = db.query(`SELECT * FROM pragma_table_info(?1)`);
|
|
||||||
|
|
||||||
(db.transaction((tables: Schema[], oldTables: Schema[]) => {
|
|
||||||
for(const table of tables)
|
|
||||||
{
|
|
||||||
const oldIdx = oldTables.findIndex(e => e.name === table.name);
|
|
||||||
|
|
||||||
if(table.name === 'sqlite_sequence')
|
|
||||||
{
|
|
||||||
oldTables.splice(oldIdx, 1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = structure.all(table.name) as Structure[];
|
|
||||||
const oldColumns = oldStructure.all(table.name) as Structure[];
|
|
||||||
|
|
||||||
if(oldIdx !== -1)
|
|
||||||
{
|
|
||||||
oldTables.splice(oldIdx, 1);
|
|
||||||
|
|
||||||
const filteredColumns = oldColumns.filter(e => columns.find(f => e.name === f.name)).map(e => `"${e.name}"`).join(', ')
|
|
||||||
|
|
||||||
db.exec(table.sql.replace(`CREATE TABLE "${table.name}"`, `CREATE TABLE "${table.name}_new"`));
|
|
||||||
db.exec(`INSERT INTO ${table.name}_new (${filteredColumns}) SELECT ${filteredColumns} FROM ${table.name}`);
|
|
||||||
db.exec(`DROP TABLE ${table.name}`);
|
|
||||||
db.exec(`ALTER TABLE ${table.name}_new RENAME TO ${table.name}`);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
db.exec(table.sql);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(const table of oldTables)
|
|
||||||
{
|
|
||||||
db.exec(`DROP TABLE ${table.name}`);
|
|
||||||
}
|
|
||||||
})).immediate(schema, oldSchema);
|
|
||||||
|
|
||||||
db.exec(`PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;`);
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
template.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -1,21 +1,13 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
import DatabaseSync from './modules/database.sync/module';
|
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2024-04-03',
|
||||||
modules: [
|
modules: [
|
||||||
"@nuxtjs/color-mode",
|
'@nuxtjs/color-mode',
|
||||||
"nuxt-security",
|
'nuxt-security',
|
||||||
"@nuxtjs/tailwindcss",
|
'@nuxtjs/tailwindcss',
|
||||||
"@vueuse/nuxt",
|
'@vueuse/nuxt',
|
||||||
DatabaseSync
|
'radix-vue/nuxt',
|
||||||
],
|
],
|
||||||
runtimeConfig: {
|
|
||||||
dbFile: 'db.sqlite',
|
|
||||||
templateFile: 'template.sqlite',
|
|
||||||
session: {
|
|
||||||
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
viewer: false,
|
viewer: false,
|
||||||
config: {
|
config: {
|
||||||
|
|
@ -23,7 +15,7 @@ export default defineNuxtConfig({
|
||||||
extend: {
|
extend: {
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
raw: '0 0 0 4px var(--tw-shadow-color)'
|
raw: '0 0 0 4px var(--tw-shadow-color)'
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
transparent: 'transparent',
|
transparent: 'transparent',
|
||||||
|
|
@ -83,21 +75,13 @@ export default defineNuxtConfig({
|
||||||
app: {
|
app: {
|
||||||
pageTransition: false,
|
pageTransition: false,
|
||||||
layoutTransition: false
|
layoutTransition: false
|
||||||
},
|
}/*,
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
path: '~/components',
|
path: '~/components',
|
||||||
pathPrefix: false,
|
pathPrefix: false,
|
||||||
},
|
},
|
||||||
],
|
]*/,
|
||||||
nitro: {
|
|
||||||
experimental: {
|
|
||||||
tasks: true,
|
|
||||||
},
|
|
||||||
scheduledTasks: {
|
|
||||||
'0 */1 * * *': ['sync']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
security: {
|
security: {
|
||||||
rateLimiter: false,
|
rateLimiter: false,
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -106,5 +90,4 @@ export default defineNuxtConfig({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
compatibilityDate: '2024-07-25',
|
|
||||||
})
|
})
|
||||||
42
package.json
42
package.json
|
|
@ -1,28 +1,22 @@
|
||||||
{
|
{
|
||||||
"devDependencies": {
|
"name": "d-any",
|
||||||
"@nuxtjs/color-mode": "^3.4.4",
|
"private": true,
|
||||||
"@nuxtjs/tailwindcss": "^6.12.1",
|
"type": "module",
|
||||||
"@types/bun": "^1.1.8",
|
|
||||||
"@types/diff": "^5.2.2",
|
|
||||||
"@vueuse/gesture": "^2.0.0",
|
|
||||||
"@vueuse/nuxt": "^11.0.3",
|
|
||||||
"hast-util-to-html": "^9.0.2",
|
|
||||||
"nuxt": "^3.13.1",
|
|
||||||
"nuxt-security": "^2.0.0-rc.9",
|
|
||||||
"remark-breaks": "^4.0.0",
|
|
||||||
"remark-ofm": "link:remark-ofm",
|
|
||||||
"vue": "^3.5.3",
|
|
||||||
"vue-router": "^4.4.3",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff": "^5.2.0",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||||
"rehype-raw": "^7.0.0",
|
"@vueuse/nuxt": "^11.1.0",
|
||||||
"remark-frontmatter": "^5.0.0",
|
"drizzle-orm": "^0.35.3",
|
||||||
"remark-gfm": "^4.0.0",
|
"nuxt": "^3.13.2",
|
||||||
"remark-parse": "^11.0.0",
|
"nuxt-security": "^2.0.0",
|
||||||
"remark-rehype": "^11.1.0",
|
"radix-vue": "^1.9.8",
|
||||||
"unified": "^11.0.5"
|
"vue": "latest",
|
||||||
|
"vue-router": "latest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.1.12",
|
||||||
|
"better-sqlite3": "^11.5.0",
|
||||||
|
"bun-types": "^1.1.33",
|
||||||
|
"drizzle-kit": "^0.26.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
<template>
|
<template>
|
||||||
<Head>
|
Page inconnue.
|
||||||
<Title>Inconnu</Title>
|
|
||||||
</Head>
|
|
||||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center">
|
|
||||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Introuvable</div>
|
|
||||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page n'existe pas</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue