Refactoring search, navigation, canvas and others to fit the new data model
This commit is contained in:
parent
e28d72fd1b
commit
a3d0b3b5bd
|
|
@ -262,7 +262,7 @@ html.light-mode .light-block {
|
|||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.input-form .loading {
|
||||
.loading {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 4px solid var(--color-purple);
|
||||
|
|
@ -278,3 +278,14 @@ html.light-mode .light-block {
|
|||
font-style: italic;
|
||||
font-weight: var(--font-extrabold);
|
||||
}
|
||||
|
||||
.tree-item-self[data-type]:after {
|
||||
content: attr(data-type);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-primary-alt);
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
font-size: var(--font-smaller);
|
||||
/* font-style: italic; */
|
||||
font-variant: all-petite-caps;
|
||||
}
|
||||
|
|
@ -2673,7 +2673,7 @@ body:not(.native-scrollbars) * {
|
|||
content: ': ';
|
||||
}
|
||||
|
||||
.suggestion-item.mod-complex .suggestion-highlight {
|
||||
.suggestion-item.mod-complex .highlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
|
@ -2722,7 +2722,7 @@ body:not(.native-scrollbars) * {
|
|||
margin: 0 var(--size-4-1) 0 var(--size-4-3);
|
||||
}
|
||||
|
||||
.suggestion-highlight {
|
||||
.highlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div>
|
||||
{{beforeText}}<span class="highlight">{{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);
|
||||
})
|
||||
|
||||
console.log(props, beforeText.value, afterText.value);
|
||||
</script>
|
||||
|
|
@ -1,18 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import ProseA from "~/components/content/prose/ProseA.vue";
|
||||
import ProseH1 from "~/components/content/prose/ProseH1.vue";
|
||||
import ProseH2 from "~/components/content/prose/ProseH2.vue";
|
||||
import ProseH3 from "~/components/content/prose/ProseH3.vue";
|
||||
import ProseH4 from "~/components/content/prose/ProseH4.vue";
|
||||
import ProseH5 from "~/components/content/prose/ProseH5.vue";
|
||||
import ProseH6 from "~/components/content/prose/ProseH6.vue";
|
||||
import ProseBlockquote from "~/components/content/prose/ProseBlockquote.vue";
|
||||
const props = defineProps<{
|
||||
content: string
|
||||
}>();
|
||||
const parser = useMarkdown();
|
||||
const { data: ast, status, error } = await useAsyncData(`markdown`, () => parser(props.content));
|
||||
const ast = await parser(props.content);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Suspense>
|
||||
<div v-if="status === 'pending'" class="loading-circle"></div>
|
||||
<MDCRenderer v-else-if="status === 'success'" :body="ast?.body" :data="ast?.data" />
|
||||
<template v-else>
|
||||
<div>Impossible de traiter le contenu.</div>
|
||||
<pre> {{ error }} </pre>
|
||||
<template #fallback>
|
||||
<div class="loading"></div>
|
||||
</template>
|
||||
<MDCRenderer :body="ast?.body" :data="ast?.data" :components="{
|
||||
a: ProseA,
|
||||
h1: ProseH1,
|
||||
h2: ProseH2,
|
||||
h3: ProseH3,
|
||||
h4: ProseH4,
|
||||
h5: ProseH5,
|
||||
h6: ProseH6,
|
||||
blockquote: ProseBlockquote,
|
||||
}" />
|
||||
</Suspense>
|
||||
</template>
|
||||
|
|
@ -1,47 +1,93 @@
|
|||
<script setup lang="ts">
|
||||
const { data: nav } = await useAsyncData('search', () => fetchContentNavigation());
|
||||
const input = ref('');
|
||||
const pos = ref<DOMRect>();
|
||||
import type { Search, File, Project, User } from '~/types/api';
|
||||
|
||||
function getPos(e: Event) {
|
||||
pos.value = (e.currentTarget as HTMLElement)?.getBoundingClientRect();
|
||||
}
|
||||
function flatten(val: NavItem[]): NavItem[] {
|
||||
return val.flatMap ? val?.flatMap((e: NavItem) => e.children ? flatten(e.children) : e) : val;
|
||||
}
|
||||
function clear(text: string): string
|
||||
const input = ref(''), results = ref<Search>({ projects: [], files: [], users: [] });
|
||||
const pos = ref<DOMRect>(), loading = ref(false);
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
function search(e: Event)
|
||||
{
|
||||
return text.toLowerCase().trim().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
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;
|
||||
}
|
||||
const navigation = computed(() => {
|
||||
return flatten(nav.value ?? []);
|
||||
})
|
||||
const results = computed(() => {
|
||||
return navigation.value?.filter((e) => clear(e.title).includes(clear(input.value))) ?? [];
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-view-outer">
|
||||
<div class="search-view-container">
|
||||
<span class="published-search-icon"></span>
|
||||
<input class="search-bar" type="text" placeholder="Recherche" v-model="input" @input="getPos">
|
||||
<input class="search-bar" type="text" placeholder="Recherche" v-model="input" @input="search">
|
||||
</div>
|
||||
</div>
|
||||
<Teleport to="body" v-if="input !== ''">
|
||||
<div class="search-results" :style="{top: (pos.bottom + 4) + 'px', left: pos.left + 'px', width: pos.width + 'px'}">
|
||||
<div class="suggestion-item" v-if="results.length > 0" v-for="result of results" :key="result._path" @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._path); input = ''">
|
||||
<Teleport to="body" v-if="input !== '' && !!pos">
|
||||
<div class="search-results"
|
||||
: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="suggestion-item suggestion-project" 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 = ''">
|
||||
<div class="suggestion-content">
|
||||
<div class="suggestion-title">
|
||||
<ProseA :href="result._path">
|
||||
{{ result.title.substring(0, clear(result.title).indexOf(clear(input))) }}<span class="suggestion-highlight">{{ result.title.substring(clear(result.title).indexOf(clear(input)), clear(result.title).indexOf(clear(input)) + clear(input).length + 1) }}</span>{{ result.title.substring(clear(result.title).indexOf(clear(input)) + clear(input).length + 1) }}
|
||||
<BoldContent :text="result.name" :matched="input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="suggestion-item suggestion-file" 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 = ''">
|
||||
<div class="suggestion-content">
|
||||
<div class="suggestion-title">
|
||||
<ProseA :href="result.path" :project="result.project">
|
||||
<BoldContent :text="result.title" :matched="input" />
|
||||
</ProseA>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="suggestion-empty" v-else>
|
||||
<div class="suggestion-item suggestion-user" 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(`/user/${result.id}`); input = ''">
|
||||
<div class="suggestion-content">
|
||||
<div class="suggestion-title">
|
||||
<div>
|
||||
<BoldContent :text="result.username" :matched="input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="suggestion-empty"
|
||||
v-if="results.projects.length === 0 && results.files.length === 0 && results.users.length === 0">
|
||||
Aucun résultat
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
|
@ -36,14 +36,14 @@ if(props.node.color !== undefined)
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="canvas-node" :class="classes" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-node-width': `${node.width}px`, '--canvas-node-height': `${node.height}px`, '--canvas-color': props.node?.color?.startsWith('#') ? hexToRgb(props.node.color) : undefined}">
|
||||
<div class="canvas-node" :class="classes" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-node-width': `${node.width}px`, '--canvas-node-height': `${node.height}px`, '--canvas-color': node?.color?.startsWith('#') ? hexToRgb(node.color) : undefined}">
|
||||
<div class="canvas-node-container">
|
||||
<template v-if="props.node.type === 'group' || props.zoom > Math.min(0.38, 1000 / size)">
|
||||
<template v-if="node.type === 'group' || zoom > Math.min(0.38, 1000 / size)">
|
||||
<div class="canvas-node-content markdown-embed">
|
||||
<div v-if="props.node.text?.body?.children?.length > 0" class="markdown-embed-content node-insert-event" style="">
|
||||
<div v-if="node.text?.length > 0" class="markdown-embed-content node-insert-event" style="">
|
||||
<div class="markdown-preview-view markdown-rendered node-insert-event show-indentation-guide allow-fold-headings allow-fold-lists">
|
||||
<div class="markdown-preview-sizer markdown-preview-section">
|
||||
<ContentRenderer :value="props.node.text"/>
|
||||
<Markdown :content="node.text"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -61,6 +61,6 @@ if(props.node.color !== undefined)
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="props.node.type === 'group' && props.node.label !== undefined" class="canvas-group-label" :class="{'mod-foreground-dark': darken(getColor(props?.node?.color ?? '')), 'mod-foreground-light': !darken(getColor(props?.node?.color ?? ''))}">{{ props.node.label }}</div>
|
||||
<div v-if="node.type === 'group' && node.label !== undefined" class="canvas-group-label" :class="{'mod-foreground-dark': darken(getColor(node?.color ?? '')), 'mod-foreground-light': !darken(getColor(node?.color ?? ''))}">{{ node.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import("~/assets/canvas.css")
|
||||
|
||||
import type { Canvas, CanvasNode } from '~/types/canvas';
|
||||
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
||||
|
||||
interface Props
|
||||
{
|
||||
canvas: Canvas;
|
||||
canvas: CanvasContent;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ let lastPinchLength = 0;
|
|||
let _minX = +Infinity, _minY = +Infinity, _maxX = -Infinity, _maxY = -Infinity;
|
||||
|
||||
onMounted(async () => {
|
||||
props.canvas.body.nodes.forEach((e) => {
|
||||
props.canvas.nodes.forEach((e) => {
|
||||
_minX = Math.min(_minX, e.x);
|
||||
_minY = Math.min(_minY, e.y);
|
||||
_maxX = Math.max(_maxX, e.x + e.width);
|
||||
|
|
@ -157,7 +157,7 @@ function edgePos(side: 'bottom' | 'top' | 'left' | 'right', pos: { x: number, y:
|
|||
}
|
||||
function getNode(id: string): CanvasNode | undefined
|
||||
{
|
||||
return props.canvas.body.nodes.find(e => e.id === id);
|
||||
return props.canvas.nodes.find(e => e.id === id);
|
||||
}
|
||||
function mK(e: { minX: number, minY: number, maxX: number, maxY: number }, t: 'bottom' | 'top' | 'left' | 'right'): { x: number, y: number } {
|
||||
switch (t) {
|
||||
|
|
@ -258,12 +258,12 @@ function getCenter(n: { x: number, y: number }, i: { x: number, y: number }, r:
|
|||
<div class="canvas"
|
||||
:style="{transform: `translate(${centerX}px, ${centerY}px) scale(${zoom}) translate(${dispX}px, ${dispY}px)`}">
|
||||
<svg class="canvas-edges">
|
||||
<CanvasEdge v-for="edge of props.canvas.body.edges" :key="edge.id"
|
||||
<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>
|
||||
<CanvasNode v-for="node of props.canvas.body.nodes" :key="node.id" :node="node" :zoom="zoom" />
|
||||
<template v-for="edge of props.canvas.body.edges">
|
||||
<CanvasNode v-for="node of props.canvas.nodes" :key="node.id" :node="node" :zoom="zoom" />
|
||||
<template v-for="edge of props.canvas.edges">
|
||||
<div :key="edge.id" v-if="edge.label" class="canvas-path-label-wrapper"
|
||||
:style="{ transform: labelCenter(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide) }">
|
||||
<div class="canvas-path-label">{{ edge.label }}</div>
|
||||
|
|
|
|||
|
|
@ -1,166 +1,95 @@
|
|||
<template>
|
||||
<Teleport to="body" v-if="!external && hovered && status !== 'pending'">
|
||||
<div class="popover hover-popover is-loaded" :style="pos" @mouseenter="(e) => showPreview(e, false)"
|
||||
@mouseleave="hidePreview">
|
||||
<template v-if="!!page && !!page[0]">
|
||||
<div class="markdown-embed" v-if="page[0].type === 'Markdown' && page[0].content.length > 0">
|
||||
<div class="markdown-embed-content">
|
||||
<div class="markdown-preview-view markdown-rendered node-insert-event hide-title">
|
||||
<div class="markdown-preview-sizer markdown-preview-section" style="padding-bottom: 0px;">
|
||||
<h1 v-if="page[0]?.title">{{ page[0]?.title }}</h1>
|
||||
<Markdown :content="page[0].content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="page[0].type === 'Canvas'" class="canvas-embed is-loaded">
|
||||
<CanvasRenderer :canvas="JSON.parse(page[0].content)" />
|
||||
</div>
|
||||
<div class="markdown-embed" v-else>
|
||||
<div class="not-found-container">
|
||||
<div class="not-found-title">Impossible de prévisualiser</div>
|
||||
<div class="not-found-description">Cliquez sur le lien pour accéder au contenu</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="loading"></div>
|
||||
</div>
|
||||
</Teleport>
|
||||
<NuxtLink :to="path" :class="class" noPrefetch @mouseenter="(e) => showPreview(e, true)" @mouseleave="hidePreview">
|
||||
<slot v-bind="$attrs"></slot>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MarkdownRoot, ParsedContent, TocLink } from '@nuxt/content/dist/runtime/types';
|
||||
import type { Canvas, CanvasContent } from '~/types/canvas';
|
||||
import { parseURL } from 'ufo';
|
||||
|
||||
import { stringifyParsedURL, parseURL } from 'ufo';
|
||||
|
||||
interface ParsedContentExtended extends Omit<ParsedContent, 'body'> {
|
||||
body: MarkdownRoot | CanvasContent | null;
|
||||
}
|
||||
|
||||
const tags = inject('tags/descriptions') as ParsedContent;
|
||||
const props = defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false
|
||||
}
|
||||
});
|
||||
|
||||
function sluggify(s: string): string {
|
||||
return s
|
||||
.split("/")
|
||||
.map((segment) => segment.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/^\d\. */g, '').replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q").toLowerCase()) // slugify all segments
|
||||
.filter(e => !!e)
|
||||
.join("/") // always use / as sep
|
||||
.replace(/\/$/, "")
|
||||
}
|
||||
function flatten(val: TocLink[]): TocLink[] {
|
||||
return val.flatMap ? val?.flatMap((e: TocLink) => e.children ? [e, ...flatten(e.children)] : e) : val;
|
||||
}
|
||||
function handleResult(result: ParsedContentExtended, tag: boolean = false) {
|
||||
loading.value = false;
|
||||
let body: MarkdownRoot | CanvasContent | null = JSON.parse(JSON.stringify(result.body));
|
||||
|
||||
if (anchor && result && body) {
|
||||
if (result._type == 'markdown' && (body as MarkdownRoot).children) {
|
||||
body = body as MarkdownRoot;
|
||||
const id = flatten(body?.toc?.links ?? []).find(e => "#" + sluggify(e.id) === anchor.replaceAll('/', ''));
|
||||
const tag = `h${id?.depth ?? 0}`;
|
||||
|
||||
const startIdx = body.children.findIndex(e => e.tag === tag && e?.props?.id === id?.id) ?? 0;
|
||||
const nbr = body.children.findIndex((e, i) => i > startIdx && e.tag?.match(new RegExp(`h[1-${id?.depth ?? 1}]`)));
|
||||
|
||||
body.children = [...body.children].splice(startIdx + (tag ? 1 : 0), (nbr === -1 ? body.children.length : nbr) - (startIdx + (tag ? 1 : 0)));
|
||||
}
|
||||
else if (result._type == 'canvas') {
|
||||
body = body as CanvasContent;
|
||||
const nodes = body?.groups?.find(e => "#" + sluggify(e.name) === anchor)?.nodes;
|
||||
|
||||
body.nodes = body.nodes.filter(e => nodes?.includes(e.id));
|
||||
body.edges = body.edges.filter(e => nodes?.includes(e.fromNode) && nodes?.includes(e.toNode));
|
||||
}
|
||||
}
|
||||
|
||||
content.value = JSON.parse(JSON.stringify({ ...result, body: body }));
|
||||
}
|
||||
|
||||
const link = (props.href.startsWith('/') ? '' : '/') + (props.href.includes('#') ? props.href.substring(0, props.href.indexOf('#')) : props.href).replace(/\..*$/, '').replace("/index", '');
|
||||
const anchor = props.href.includes('#') ? props.href.substring(props.href.indexOf('#'), props.href.length) : '';
|
||||
let content = ref<ParsedContentExtended | null | undefined>(null), loading = ref(false), isTag = ref(false);
|
||||
|
||||
function loadContent()
|
||||
{
|
||||
loading.value = true;
|
||||
if(props.href !== '' && !props.href.startsWith('/tags#'))
|
||||
{
|
||||
queryContent().where({ _path: new RegExp("/" + sluggify(link) + '$', 'i') }).findOne().then(handleResult).catch(() => {
|
||||
loading.value = false;
|
||||
content.value = undefined;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
isTag.value = true;
|
||||
|
||||
if(tags.value)
|
||||
handleResult(tags.value, true);
|
||||
else
|
||||
{
|
||||
loading.value = false;
|
||||
content.value = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hovered = ref(false), pos = ref<any>();
|
||||
let timeout: NodeJS.Timeout;
|
||||
function showPreview(e: Event, bbox: boolean) {
|
||||
clearTimeout(timeout);
|
||||
if(bbox)
|
||||
{
|
||||
if (bbox) {
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
const rect = target?.getBoundingClientRect();
|
||||
const r: any = {};
|
||||
if(rect.bottom + 450 < window.innerHeight)
|
||||
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)
|
||||
if (rect.right + 550 < window.innerWidth)
|
||||
r.left = rect.left + "px";
|
||||
else
|
||||
r.right = (window.innerWidth - rect.right + 4) + "px";
|
||||
pos.value = r;
|
||||
}
|
||||
hovered.value = true;
|
||||
|
||||
if(content.value === null)
|
||||
loadContent();
|
||||
}
|
||||
function hidePreview(e: Event) {
|
||||
timeout = setTimeout(() => hovered.value = false, 300);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="href !== ''">
|
||||
<a @mouseenter="(e) => showPreview(e, true)" @mouseleave="hidePreview" v-bind="$attrs"
|
||||
:href="stringifyParsedURL({ host: '/explorer', pathname: (content?._path ?? href) + (isTag && anchor ? '/' + anchor.substring(1) : ''), hash: !isTag ? anchor : '', search: '' })"
|
||||
:target="target">
|
||||
<slot></slot>
|
||||
</a>
|
||||
<Teleport to="body" v-if="hovered && (loading || !!content)">
|
||||
<div class="popover hover-popover is-loaded" :style="pos" @mouseenter="(e) => showPreview(e, false)"
|
||||
@mouseleave="hidePreview">
|
||||
<template v-if="!!content">
|
||||
<div class="markdown-embed" :class="{'tag-embed': isTag}"
|
||||
v-if="content._type == 'markdown' && ((content as ParsedContent)?.body?.children?.length ?? 0) > 0">
|
||||
<div class="markdown-embed-content">
|
||||
<div class="markdown-preview-view markdown-rendered node-insert-event hide-title">
|
||||
<div class="markdown-preview-sizer markdown-preview-section"
|
||||
style="padding-bottom: 0px;">
|
||||
<h1 v-if="content?.title && !isTag">{{ content?.title }}</h1>
|
||||
<ContentRenderer :key="content._id" :value="content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="content._type == 'canvas'" class="canvas-embed is-loaded">
|
||||
<CanvasRenderer :key="content._id" :canvas="(content as Canvas)" />
|
||||
</div>
|
||||
<div class="markdown-embed" v-else>
|
||||
<div class="not-found-container">
|
||||
<div class="not-found-image"></div>
|
||||
<div class="not-found-title">Impossible de prévisualiser</div>
|
||||
<div v-if="!isTag" class="not-found-description">Cliquez sur le lien pour accéder au contenu
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="preload" style="text-align:center">
|
||||
<svg style="width:50px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<path style="transform-origin:50px 50px;animation:1s linear infinite rotate" fill="currentColor"
|
||||
d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
<NuxtLink v-bind="$attrs" :to="href" v-else>
|
||||
<slot></slot>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
interface Prop
|
||||
{
|
||||
href?: string;
|
||||
class?: string;
|
||||
project?: number;
|
||||
}
|
||||
const props = defineProps<Prop>();
|
||||
const path = ref(props.href), external = ref(false), hovered = ref(false), pos = ref<any>();
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
if (parseURL(props.href).protocol !== undefined)
|
||||
{
|
||||
external.value = true;
|
||||
}
|
||||
|
||||
let id = ref(props.project);
|
||||
if(id.value === undefined)
|
||||
{
|
||||
id = useProject().id;
|
||||
}
|
||||
|
||||
const { data: page, status, error, execute } = await useLazyFetch(`/api/project/${id.value}/file`, {
|
||||
query: {
|
||||
search: "%" + parseURL(props.href).pathname,
|
||||
}
|
||||
});
|
||||
|
||||
if(external.value)
|
||||
{
|
||||
execute();
|
||||
}
|
||||
|
||||
if (page.value && page.value[0])
|
||||
{
|
||||
path.value = `/explorer/${id.value}${page.value[0].path}`;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -35,7 +35,9 @@ function hideLeftPanel(_: Event)
|
|||
<template>
|
||||
<div class="tree-item" v-if="project && !isNaN(project)">
|
||||
<template v-if="hasChildren">
|
||||
<div class="tree-item-self" :class="{ 'is-collapsed': collapsed, 'mod-collapsible is-clickable': hasChildren }" data-path="{{ props.link.title }}" @click="collapsed = hasChildren && !collapsed">
|
||||
<div class="tree-item-self"
|
||||
:class="{ 'is-collapsed': collapsed, 'mod-collapsible is-clickable': hasChildren }"
|
||||
data-path="{{ props.link.title }}" @click="collapsed = hasChildren && !collapsed">
|
||||
<div v-if="hasChildren" class="tree-item-icon collapse-icon">
|
||||
<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"
|
||||
|
|
@ -46,10 +48,11 @@ function hideLeftPanel(_: Event)
|
|||
<div class="tree-item-inner">{{ link.title }}</div>
|
||||
</div>
|
||||
<div v-if="!collapsed" class="tree-item-children">
|
||||
<NavigationLink v-if="hasChildren" v-for="l of link.children" :link="l" :project="project"/>
|
||||
<NavigationLink v-if="hasChildren" v-for="l of link.children" :link="l" :project="project" />
|
||||
</div>
|
||||
</template>
|
||||
<NuxtLink @click="hideLeftPanel" v-else class="tree-item-self" :to="`/explorer/${project}${link.path}`" :active-class="'mod-active'" >
|
||||
<NuxtLink @click="hideLeftPanel" v-else class="tree-item-self" :to="`/explorer/${project}${link.path}`"
|
||||
:active-class="'mod-active'" :data-type="link.type === 'Canvas' ? 'graph' : undefined">
|
||||
<div class="tree-item-inner">{{ link.title }}</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
interface Props
|
||||
{
|
||||
id: number;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { data: comments } = await useFetch(`/api/project/1/file/${props.id}/comment`);
|
||||
const { data: comments } = await useFetch(`/api/project/1/comment`, {
|
||||
query: {
|
||||
path: unifySlug(useRoute().params.slug)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import type { Project } from "~/server/api/project.get";
|
||||
|
||||
export default function useProject()
|
||||
{
|
||||
const id = useState<number>("projectId", () => 1);
|
||||
const name = useState<string>("projectName", undefined);
|
||||
const owner = useState<number>("projectOwner", undefined);
|
||||
const home = useState<string | null>("projectHomepage", () => null);
|
||||
|
||||
return {
|
||||
id, name, owner, home, get, set
|
||||
}
|
||||
}
|
||||
|
||||
async function get(): Promise<void> {
|
||||
const id = useState<number>("projectId");
|
||||
|
||||
if (!id.value)
|
||||
return;
|
||||
|
||||
try {
|
||||
const result = await $fetch(`/api/project/${id}`) as Project;
|
||||
|
||||
const name = useState<string>("projectName");
|
||||
const owner = useState<number>("projectOwner");
|
||||
const home = useState<string | null>("projectHomepage");
|
||||
|
||||
name.value = result.name;
|
||||
owner.value = result.owner;
|
||||
home.value = result.home;
|
||||
} catch(e) {}
|
||||
}
|
||||
function set(id: number): void {
|
||||
const _id = useState<number>("projectId");
|
||||
|
||||
_id.value = id;
|
||||
}
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
const path = unifySlug(route.params.slug);
|
||||
|
||||
const { data: content } = await useFetch(`/api/project/${route.params.projectId}/file`, {
|
||||
query: {
|
||||
path: path
|
||||
path: unifySlug(route.params.slug)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title v-if="content && content[0]">{{ content[0].title }}</Title>
|
||||
</Head>
|
||||
<div class="site-body-center-column">
|
||||
<div class="render-container">
|
||||
<template v-if="!!content && content[0] && content[0].type == 'Markdown' && !!content[0].content">
|
||||
|
|
@ -23,7 +25,7 @@ const { data: content } = await useFetch(`/api/project/${route.params.projectId}
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RightComponent :id="content[0].id" />
|
||||
<RightComponent/>
|
||||
</template>
|
||||
<CanvasRenderer v-else-if="!!content && content[0] && content[0].type == 'Canvas' && !!content[0].content" :canvas="JSON.parse(content[0].content)" />
|
||||
<div v-else-if="!!content && content[0]">
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Le logo a été créé grâce aux icones de [Game Icons](https://game-icons.net)
|
|||
<div class="publish-renderer">
|
||||
<div class="markdown-preview-view markdown-rendered node-insert-event hide-title">
|
||||
<div class="markdown-preview-sizer markdown-preview-section" style="padding-bottom: 0px;">
|
||||
<MDC :value="data" />
|
||||
<Markdown :content="data" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export interface Project
|
||||
{
|
||||
id: number;
|
||||
title: string;
|
||||
owner: number;
|
||||
homepage: number;
|
||||
}
|
||||
export default defineEventHandler(async (e) => {
|
||||
const query = getQuery(e);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const project = getRouterParam(e, "projectId");
|
||||
|
||||
const where = ["project = $project"];
|
||||
const criteria: Record<string, any> = { $project: project };
|
||||
|
||||
if (!!project) {
|
||||
const db = useDatabase();
|
||||
|
||||
const content = db.query(`SELECT * FROM explorer_projects WHERE ${where.join(" and ")}`).get(criteria) as Project;
|
||||
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const project = getRouterParam(e, "projectId");
|
||||
|
||||
const where = ["project = $project"];
|
||||
const criteria: Record<string, any> = { $project: project };
|
||||
|
||||
if (!!project) {
|
||||
const db = useDatabase();
|
||||
|
||||
const content = db.query(`SELECT * FROM explorer_projects WHERE ${where.join(" and ")}`).all(criteria) as Project[];
|
||||
|
||||
if (content.length > 0) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const project = getRouterParam(e, "projectId");
|
||||
|
||||
const where = ["project = $project"];
|
||||
const criteria: Record<string, any> = { $project: project };
|
||||
|
||||
if (!!project) {
|
||||
const db = useDatabase();
|
||||
|
||||
const content = db.query(`SELECT * FROM explorer_projects WHERE ${where.join(" and ")}`).all(criteria) as Project[];
|
||||
|
||||
if (content.length > 0) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
});
|
||||
|
|
@ -1,27 +1,17 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export interface Comment
|
||||
{
|
||||
file: number;
|
||||
user_id: number;
|
||||
sequence: number;
|
||||
position: number;
|
||||
length: number;
|
||||
content: string;
|
||||
}
|
||||
export default defineEventHandler(async (e) => {
|
||||
const project = getRouterParam(e, "projectId");
|
||||
const file = getRouterParam(e, "fileId");
|
||||
const query = getQuery(e);
|
||||
|
||||
if(!project)
|
||||
if(!project || !query.path)
|
||||
{
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
|
||||
const where = ["project = $project", "file = $file"];
|
||||
const criteria: Record<string, any> = { $project: project, $file: file };
|
||||
const where = ["project = $project", "path = $path"];
|
||||
const criteria: Record<string, any> = { $project: project, $path: query.path };
|
||||
|
||||
if(where.length > 1)
|
||||
{
|
||||
|
|
@ -1,14 +1,5 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export interface File
|
||||
{
|
||||
id: number;
|
||||
path: string;
|
||||
title: string;
|
||||
type: 'Markdown' | 'Canvas' | 'File';
|
||||
content: string;
|
||||
owner: number;
|
||||
}
|
||||
export default defineEventHandler(async (e) => {
|
||||
const project = getRouterParam(e, "projectId");
|
||||
const query = getQuery(e);
|
||||
|
|
@ -37,6 +28,11 @@ export default defineEventHandler(async (e) => {
|
|||
where.push("type = $type");
|
||||
criteria["$type"] = query.type;
|
||||
}
|
||||
if (query && query.search !== undefined)
|
||||
{
|
||||
where.push("path LIKE $search");
|
||||
criteria["$search"] = query.search;
|
||||
}
|
||||
|
||||
if(where.length > 1)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export interface Navigation
|
||||
{
|
||||
title: string;
|
||||
path: string;
|
||||
type: string;
|
||||
order: number
|
||||
children?: Navigation[];
|
||||
}
|
||||
export default defineEventHandler(async (e) => {
|
||||
const project = getRouterParam(e, "projectId");
|
||||
|
||||
|
|
@ -19,7 +11,7 @@ export default defineEventHandler(async (e) => {
|
|||
|
||||
const db = useDatabase();
|
||||
|
||||
const content = db.query(`SELECT "path", "title", "type", "order" FROM explorer_files WHERE project = ?1 and navigable = 1`).all(project!).sort((a: any, b: any) => a.path.length - b.path.length) as any[];
|
||||
const content = db.query(`SELECT "path", "title", "type", "order" FROM explorer_files WHERE project = ?1 and navigable = 1 and private = 0`).all(project!).sort((a: any, b: any) => a.path.length - b.path.length) as any[];
|
||||
|
||||
if(content.length > 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const query = getQuery(e);
|
||||
|
||||
if (query.search) {
|
||||
const db = useDatabase();
|
||||
|
||||
const projects = db.query(`SELECT * FROM explorer_projects WHERE name LIKE ?1`).all(query.search) as Project[];
|
||||
const files = db.query(`SELECT * FROM explorer_files WHERE title LIKE ?1 `).all(query.search) as File[];
|
||||
const users = db.query(`SELECT id, username FROM users WHERE username LIKE ?1 `).all(query.search) as User[];
|
||||
|
||||
return {
|
||||
projects,
|
||||
files,
|
||||
users
|
||||
} as Search;
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
export interface Project {
|
||||
id: number;
|
||||
name: string;
|
||||
owner: number;
|
||||
home: string;
|
||||
}
|
||||
export interface Navigation {
|
||||
title: string;
|
||||
path: string;
|
||||
type: string;
|
||||
order: number
|
||||
children?: Navigation[];
|
||||
}
|
||||
export interface File {
|
||||
id: number;
|
||||
project: number;
|
||||
path: string;
|
||||
title: string;
|
||||
type: 'Markdown' | 'Canvas' | 'File' | 'Folder';
|
||||
content: string;
|
||||
owner: number;
|
||||
}
|
||||
export interface Comment {
|
||||
file: number;
|
||||
user_id: number;
|
||||
sequence: number;
|
||||
position: number;
|
||||
length: number;
|
||||
content: string;
|
||||
}
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
export interface Search {
|
||||
projects: Project[];
|
||||
files: File[];
|
||||
users: User[];
|
||||
}
|
||||
|
|
@ -1,8 +1,3 @@
|
|||
export interface Canvas {
|
||||
_id: string;
|
||||
_type: string;
|
||||
body: CanvasContent
|
||||
}
|
||||
export interface CanvasContent {
|
||||
nodes: CanvasNode[];
|
||||
edges: CanvasEdge[];
|
||||
|
|
|
|||
Loading…
Reference in New Issue