Layouts, ProseA rework and PreviewContent creation. Trying to fix hydration by making better SSR.
This commit is contained in:
parent
2e92c389a2
commit
04785ecf27
73
app.vue
73
app.vue
|
|
@ -1,73 +1,8 @@
|
|||
<script lang="ts">
|
||||
let icon: HTMLDivElement | null;
|
||||
function toggleLeftPanel(_: Event): void {
|
||||
document.querySelector('.published-container')?.classList.toggle('is-left-column-open');
|
||||
}
|
||||
function hideLeftPanel(_: Event): void {
|
||||
document.querySelector('.published-container')?.classList.remove('is-left-column-open');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { id: project, home, get } = useProject();
|
||||
|
||||
if(project.value !== 0 && home.value === null)
|
||||
{
|
||||
await useAsyncData(`project:get:${project}`, get);
|
||||
}
|
||||
|
||||
const toggled = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
icon = document.querySelector('.site-nav-bar .clickable-icon');
|
||||
icon?.removeEventListener('click', toggleLeftPanel);
|
||||
icon?.addEventListener('click', toggleLeftPanel);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="published-container print has-navigation has-outline">
|
||||
<div class="site-nav-bar">
|
||||
<div>
|
||||
<div class="clickable-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"
|
||||
class="svg-icon lucide-menu">
|
||||
<line x1="4" y1="12" x2="20" y2="12"></line>
|
||||
<line x1="4" y1="6" x2="20" y2="6"></line>
|
||||
<line x1="4" y1="18" x2="20" y2="18"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="gapx-3 flex align-stretch">
|
||||
<NuxtLink @click="hideLeftPanel" class="site-nav-bar-text" aria-label="Accueil" :to="{ path: '/', force: true }">
|
||||
<ThemeIcon icon="logo" :width=40 :height=40 />
|
||||
</NuxtLink>
|
||||
<div class="site-nav-bar-text mobile-hidden">
|
||||
<NuxtLink aria-label="Projet" :to="{ path: project === 0 ? `/explorer` : `/explorer/${project}${home}`, force: true }"
|
||||
:class="{'mod-active': $route.path.startsWith('/explorer')}">Projet</NuxtLink>
|
||||
<div class="arrow-down" :class="{active: toggled}" @click="toggled = !toggled"></div>
|
||||
<div class="arrow-group" @click="toggled = false">
|
||||
<NuxtLink class="arrow-group-item" aria-label="Projets" :to="{ path: '/explorer', force: true }">Liste des projets</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink class="site-nav-bar-text mobile-hidden" aria-label="Editeur" :to="{ path: '/editing', force: true }"
|
||||
:class="{'mod-active': $route.path.startsWith('/editing')}">Editeur</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-bigger">
|
||||
<SearchView />
|
||||
</div>
|
||||
<div class="ps-1 gapx-1 flex align-center">
|
||||
<ThemeSwitch class="mobile-hidden" />
|
||||
<NuxtLink class="site-login" :to="{ path: '/user/profile', force: true }">
|
||||
<ThemeIcon icon="user" :width=32 :height=32 />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-body">
|
||||
<LeftComponent />
|
||||
<NuxtPage />
|
||||
</div>
|
||||
<div class="published-container">
|
||||
<NuxtLayout>
|
||||
<NuxtPage></NuxtPage>
|
||||
</NuxtLayout>
|
||||
<div class="site-footer">
|
||||
<p>Copyright Peaceultime - 2024</p>
|
||||
<NuxtLink :to="{ path: '/third-party', force: true }">Applications tierces et crédits</NuxtLink>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
- [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
|
||||
- [NuxtAuth](https://auth.sidebase.io/) - MIT License - Copyright (c) 2022 SIDESTREAM GmbH
|
||||
- [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).
|
||||
|
|
@ -43,7 +43,7 @@ import LiveBlockquote from "~/components/prose/ProseBlockquote.vue";
|
|||
|
||||
import { hash } from 'ohash'
|
||||
import { watch, computed } from 'vue'
|
||||
import type { Root } from 'hast';
|
||||
import type { Root, Node } from 'hast';
|
||||
import { diffLines as diff } from 'diff';
|
||||
|
||||
const model = defineModel<string>();
|
||||
|
|
@ -51,55 +51,81 @@ const model = defineModel<string>();
|
|||
const parser = useMarkdown();
|
||||
const key = computed(() => hash(model.value));
|
||||
|
||||
const node = ref<Root>();
|
||||
const node = ref<Root>(), changes = ref();
|
||||
|
||||
watch(model, async (value, old) => {
|
||||
watch(model, update);
|
||||
update(model.value, "");
|
||||
|
||||
async function update(value: string | undefined, old: string | undefined) {
|
||||
if(value && old)
|
||||
{
|
||||
if(node.value)
|
||||
{
|
||||
let content = "", line = 0, pos = -1, len = 0, child;
|
||||
const d = diff(old, value);
|
||||
const children = node.value?.children.filter(e => e.hasOwnProperty('position'));
|
||||
const differences = diff(old, value, {
|
||||
newlineIsToken: true,
|
||||
});
|
||||
|
||||
for(let i = 0; i < d.length; i++)
|
||||
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(d[i].added) //Nouvelle ligne
|
||||
if(!difference.added && !difference.removed)
|
||||
{
|
||||
const next = d.length > i ? d[i + 1] : undefined;
|
||||
if(pos === -1 && (!next || !next.removed)) //Nouvelle ligne
|
||||
{
|
||||
child = children.filter(e => e.position?.start.line <= line && e.position?.end.line >= line); //Je cherche tout les blocs qui était inclus dans les lignes éditées.
|
||||
if(child.length > 0)
|
||||
{
|
||||
pos = child[0].position?.start.offset ?? 0; //Je pars du premier caractère du bloc
|
||||
len += (child[child.length - 1].position?.end.offset ?? 0) + 1; //Je m'arrete au dernier caractère du bloc + le \n
|
||||
removeStart += difference.value.length;
|
||||
addStart += difference.value.length;
|
||||
}
|
||||
}
|
||||
len += d[i].value.length; // J'ajoute le nouveau nombre de caractère
|
||||
}
|
||||
else if(d[i].removed) //Ancienne ligne
|
||||
else if(difference.added)
|
||||
{
|
||||
child = children.filter(e => e.position?.start.line <= line + 1 && e.position?.end.line >= line + (d[i].count ?? 1)); //Je cherche tout les blocs qui était inclus dans les lignes éditées.
|
||||
if(child.length > 0)
|
||||
{
|
||||
pos = child[0].position?.start.offset ?? 0; //Je pars du premier caractère du bloc
|
||||
len += child[child.length - 1].position?.end.offset ?? 0 + 1; //Je m'arrete au dernier caractère du bloc
|
||||
len -= d[i].value.length; //Je supprime l'ancien nombre de caractère
|
||||
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
|
||||
{
|
||||
line += d[i].count ?? 0;
|
||||
}
|
||||
}
|
||||
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;
|
||||
//root.position?.end.offset
|
||||
|
||||
node.value = parser(value);
|
||||
}
|
||||
console.log(node.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
node.value = parser(value);
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
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>
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<template
|
||||
v-if="model && model.length > 0">
|
||||
<MarkdownRenderer :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
||||
<Suspense>
|
||||
<MarkdownRenderer #default :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
||||
<template #fallback><div class="loading"></div></template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Node, Text, Element, Comment, Root } from 'hast';
|
||||
import { Text as HText, Comment as HComment } from 'vue';
|
||||
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';
|
||||
|
|
@ -90,30 +90,30 @@ export default defineComponent({
|
|||
render(ctx: any) {
|
||||
const { node, tags } = ctx;
|
||||
|
||||
const div = h('div', null, (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e));
|
||||
return div;
|
||||
if(!node)
|
||||
return null;
|
||||
|
||||
return h('div', null, {default: () => (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||
}
|
||||
});
|
||||
|
||||
function renderNode(node: Node, tags: Record<string, any>): VNode | undefined
|
||||
function renderNode(node: RootContent, tags: Record<string, any>): VNode | undefined
|
||||
{
|
||||
if(node.type === 'text')
|
||||
if(node.type === 'text' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
const text = node as Text;
|
||||
if(text.value.length > 0 && text.value !== '\n')
|
||||
return h(HText, (node as Text).value);
|
||||
return h(Text, node.value);
|
||||
}
|
||||
else if(node.type === 'comment')
|
||||
else if(node.type === 'comment' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
const comment = node as Comment;
|
||||
if(comment.value.length > 0 && comment.value !== '\n')
|
||||
return h(HComment, (node as Comment).value);
|
||||
return h(Comment, node.value);
|
||||
}
|
||||
else if(node.type === 'element')
|
||||
{
|
||||
const element = node as Element;
|
||||
|
||||
return h(tags[element.tagName], { ...element.properties, class: element.properties.className }, element.children.map(e => renderNode(e, tags)).filter(e => !!e));
|
||||
return h(tags[node.tagName] ?? node.tagName, { ...node.properties, class: node.properties.className }, {default: () => node.children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||
}
|
||||
else if(node.type === 'raw')
|
||||
{
|
||||
console.warn("???");
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ async function debounced()
|
|||
<input class="search-bar" type="text" placeholder="Recherche" v-model="input" @input="search">
|
||||
</div>
|
||||
</div>
|
||||
<Teleport to="body" v-if="input !== '' && !!pos">
|
||||
<Teleport to="#teleports" 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>
|
||||
|
|
@ -1,33 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
function hideLeftPanel(_: Event)
|
||||
{
|
||||
document?.querySelector('.published-container')?.classList.remove('is-left-column-open');
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const showing = ref(false);
|
||||
|
||||
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
|
||||
|
||||
const { data: navigation, execute, status, error } = await useFetch(() => `/api/project/${project.value}/navigation`, {
|
||||
immediate: false,
|
||||
});
|
||||
|
||||
watch(route, () => {
|
||||
if(route.params.projectId && project.value !== 0)
|
||||
{
|
||||
showing.value = true;
|
||||
execute();
|
||||
}
|
||||
else
|
||||
{
|
||||
showing.value = false;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{'desktop-hidden': !showing}" class="site-body-left-column">
|
||||
<div class="site-body-left-column">
|
||||
<div class="site-body-left-column-inner">
|
||||
<div class="nav-view-outer">
|
||||
<div class="nav-view">
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<Teleport to="#teleports">
|
||||
<div class="popover hover-popover" :class="{'is-loaded': pending === false && content !== ''}" :style="{'top': (pos?.y ?? 0) - 4, 'left': (pos?.y ?? 0) + 4}">
|
||||
<template v-if="display">
|
||||
<div v-if="pending" class="loading"></div>
|
||||
<div class="markdown-embed" v-else-if="content !== ''">
|
||||
<div class="markdown-preview-view markdown-rendered">
|
||||
<div class="markdown-embed-content">
|
||||
<h1>{{ title }}</h1>
|
||||
<Markdown v-model="content"></Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="not-found-container" v-else>
|
||||
<div class="not-found-image"></div>
|
||||
<div class="not-found-title">Impossible d'afficher (ou vide)</div>
|
||||
<div class="not-found-description">Cette page est actuellement vide ou impossible à traiter</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Teleport>
|
||||
<span ref="el" @mouseenter="show" @mouseleave="hide">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn as debounce } from '@vueuse/core';
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
anchor: {
|
||||
type: String,
|
||||
required: false,
|
||||
}
|
||||
})
|
||||
const content = ref(''), title = ref(''), display = ref(false), pending = ref(false);
|
||||
const el = ref(), pos = ref<DOMRect>();
|
||||
watch(display, () => display.value && !pending.value && content.value === '' && fetch());
|
||||
|
||||
onMounted(() => {
|
||||
if(el && el.value)
|
||||
{
|
||||
pos.value = (el.value as HTMLDivElement).getBoundingClientRect();
|
||||
debugger;
|
||||
}
|
||||
})
|
||||
|
||||
async function fetch()
|
||||
{
|
||||
pending.value = true;
|
||||
|
||||
const data = await $fetch(`/api/project/${props.project}/file`, {
|
||||
method: 'get',
|
||||
query: {
|
||||
path: props.path,
|
||||
},
|
||||
});
|
||||
|
||||
pending.value = false;
|
||||
|
||||
if(data && data[0])
|
||||
{
|
||||
content.value = data[0].content;
|
||||
title.value = data[0].title;
|
||||
}
|
||||
}
|
||||
const show = debounce(() => display.value = true, 250), hide = debounce(() => display.value = false, 250);
|
||||
</script>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
interface TocItem {
|
||||
text: string
|
||||
id: string
|
||||
children?: TocItem[]
|
||||
|
||||
[key: string]: any
|
||||
}
|
||||
interface Props {
|
||||
link: TocItem;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const hasChildren = computed(() => {
|
||||
return props.link && props.link.children && props.link.children.length > 0 || false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tree-item">
|
||||
<div class="tree-item-self" :class="{'is-clickable': hasChildren}" data-path="{{ props.link.title }}">
|
||||
<NuxtLink no-prefetch class="tree-item-inner" :to="{ hash: '#' + props.link.id, force: true }">{{ props.link.text }}</NuxtLink>
|
||||
</div>
|
||||
<div class="tree-item-children">
|
||||
<TocLink v-if="hasChildren" v-for="link of props.link.children" :link="link" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,103 +1,39 @@
|
|||
<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 v-model="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, force: true }" :class="class" noPrefetch @mouseenter="(e) => showPreview(e, true)" @mouseleave="hidePreview">
|
||||
<NuxtLink v-if="data && data[0] && status !== 'pending'" :to="{ path: `/explorer/${project}${data[0].path}`, hash: hash }" :class="class">
|
||||
<PreviewContent :project="project" :path="data[0].path" :anchor="hash">
|
||||
<slot v-bind="$attrs"></slot>
|
||||
</PreviewContent>
|
||||
</NuxtLink>
|
||||
<NuxtLink v-else-if="href" :to="{ path: href }" :class="class">
|
||||
<slot v-bind="$attrs"></slot>
|
||||
</NuxtLink>
|
||||
<slot :class="class" v-else v-bind="$attrs"></slot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { parseURL } from 'ufo';
|
||||
|
||||
function showPreview(e: Event, bbox: boolean) {
|
||||
clearTimeout(timeout);
|
||||
if (bbox) {
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
const rect = target?.getBoundingClientRect();
|
||||
const r: any = {};
|
||||
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;
|
||||
}
|
||||
hovered.value = true;
|
||||
}
|
||||
function hidePreview(e: Event) {
|
||||
timeout = setTimeout(() => hovered.value = false, 300);
|
||||
}
|
||||
|
||||
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.value = useProject().id.value;
|
||||
}
|
||||
|
||||
const { data: page, status, error, execute } = await useLazyFetch(`/api/project/${id.value}/file`, {
|
||||
query: {
|
||||
search: "%" + parseURL(props.href).pathname,
|
||||
const props = defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
immediate: false,
|
||||
dedupe: 'defer',
|
||||
class: {
|
||||
type: String,
|
||||
required: false,
|
||||
}
|
||||
});
|
||||
|
||||
if(!external.value)
|
||||
{
|
||||
await execute();
|
||||
}
|
||||
|
||||
if(status.value === 'error')
|
||||
{
|
||||
console.error(error.value);
|
||||
}
|
||||
|
||||
if (page.value && page.value[0])
|
||||
{
|
||||
path.value = `/explorer/${id.value}${page.value[0].path}`;
|
||||
console.log(path.value);
|
||||
}
|
||||
const route = useRoute();
|
||||
const { hash, pathname } = parseURL(props.href);
|
||||
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
|
||||
const { data, status } = await useFetch(`/api/project/${project.value}/file`, {
|
||||
method: 'GET',
|
||||
query: {
|
||||
search: `%${pathname}`
|
||||
},
|
||||
transform: (data) => data?.map(e => ({ path: e.path })),
|
||||
key: `file:${project.value}:%${pathname}`,
|
||||
dedupe: 'defer'
|
||||
});
|
||||
</script>
|
||||
|
|
@ -4,6 +4,7 @@ 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';
|
||||
|
||||
export default function useMarkdown(): (md: string) => Root
|
||||
|
|
@ -13,7 +14,7 @@ export default function useMarkdown(): (md: string) => Root
|
|||
const parse = (markdown: string) => {
|
||||
if (!processor)
|
||||
{
|
||||
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkRehype]);
|
||||
processor = unified().use([RemarkParse, RemarkBreaks, RemarkGfm, RemarkOfm, RemarkRehype]);
|
||||
}
|
||||
|
||||
const processed = processor.runSync(processor.parse(markdown)) as Root;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
<script lang="ts">
|
||||
let icon: HTMLDivElement | null;
|
||||
function toggleLeftPanel(_: Event): void {
|
||||
document.querySelector('.published-container')?.classList.toggle('is-left-column-open');
|
||||
}
|
||||
function hideLeftPanel(_: Event): void {
|
||||
document.querySelector('.published-container')?.classList.remove('is-left-column-open');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { id: project, home, get } = useProject();
|
||||
|
||||
if(project.value !== 0 && home.value === null)
|
||||
{
|
||||
await useAsyncData(`project:get:${project}`, get);
|
||||
}
|
||||
|
||||
const toggled = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
icon = document.querySelector('.site-nav-bar .clickable-icon');
|
||||
icon?.removeEventListener('click', toggleLeftPanel);
|
||||
icon?.addEventListener('click', toggleLeftPanel);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="site-nav-bar">
|
||||
<div>
|
||||
<div class="clickable-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"
|
||||
class="svg-icon lucide-menu">
|
||||
<line x1="4" y1="12" x2="20" y2="12"></line>
|
||||
<line x1="4" y1="6" x2="20" y2="6"></line>
|
||||
<line x1="4" y1="18" x2="20" y2="18"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="gapx-3 flex align-stretch">
|
||||
<NuxtLink @click="hideLeftPanel" class="site-nav-bar-text" aria-label="Accueil" :to="{ path: '/', force: true }">
|
||||
<ThemeIcon icon="logo" :width=40 :height=40 />
|
||||
</NuxtLink>
|
||||
<div class="site-nav-bar-text mobile-hidden">
|
||||
<NuxtLink aria-label="Projet" :to="{ path: project === 0 ? `/explorer` : `/explorer/${project}${home}`, force: true }"
|
||||
:class="{'mod-active': $route.path.startsWith('/explorer')}">Projet</NuxtLink>
|
||||
<div class="arrow-down" :class="{active: toggled}" @click="toggled = !toggled"></div>
|
||||
<div class="arrow-group" @click="toggled = false">
|
||||
<NuxtLink class="arrow-group-item" aria-label="Projets" :to="{ path: '/explorer', force: true }">Liste des projets</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink class="site-nav-bar-text mobile-hidden" aria-label="Editeur" :to="{ path: '/editing', force: true }"
|
||||
:class="{'mod-active': $route.path.startsWith('/editing')}">Editeur</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-bigger">
|
||||
<SearchView />
|
||||
</div>
|
||||
<div class="ps-1 gapx-1 flex align-center">
|
||||
<ThemeSwitch class="mobile-hidden" />
|
||||
<NuxtLink class="site-login" :to="{ path: '/user/profile', force: true }">
|
||||
<ThemeIcon icon="user" :width=32 :height=32 />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-body">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<script lang="ts">
|
||||
let icon: HTMLDivElement | null;
|
||||
function toggleLeftPanel(_: Event): void {
|
||||
document.querySelector('.published-container')?.classList.toggle('is-left-column-open');
|
||||
}
|
||||
function hideLeftPanel(_: Event): void {
|
||||
document.querySelector('.published-container')?.classList.remove('is-left-column-open');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { id: project, home, get } = useProject();
|
||||
|
||||
if(project.value !== 0 && home.value === null)
|
||||
{
|
||||
await useAsyncData(`project:get:${project}`, get);
|
||||
}
|
||||
|
||||
const toggled = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
icon = document.querySelector('.site-nav-bar .clickable-icon');
|
||||
icon?.removeEventListener('click', toggleLeftPanel);
|
||||
icon?.addEventListener('click', toggleLeftPanel);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="site-nav-bar">
|
||||
<div>
|
||||
<div class="clickable-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"
|
||||
class="svg-icon lucide-menu">
|
||||
<line x1="4" y1="12" x2="20" y2="12"></line>
|
||||
<line x1="4" y1="6" x2="20" y2="6"></line>
|
||||
<line x1="4" y1="18" x2="20" y2="18"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="gapx-3 flex align-stretch">
|
||||
<NuxtLink @click="hideLeftPanel" class="site-nav-bar-text" aria-label="Accueil" :to="{ path: '/', force: true }">
|
||||
<ThemeIcon icon="logo" :width=40 :height=40 />
|
||||
</NuxtLink>
|
||||
<div class="site-nav-bar-text mobile-hidden">
|
||||
<NuxtLink aria-label="Projet" :to="{ path: project === 0 ? `/explorer` : `/explorer/${project}${home}`, force: true }"
|
||||
:class="{'mod-active': $route.path.startsWith('/explorer')}">Projet</NuxtLink>
|
||||
<div class="arrow-down" :class="{active: toggled}" @click="toggled = !toggled"></div>
|
||||
<div class="arrow-group" @click="toggled = false">
|
||||
<NuxtLink class="arrow-group-item" aria-label="Projets" :to="{ path: '/explorer', force: true }">Liste des projets</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink class="site-nav-bar-text mobile-hidden" aria-label="Editeur" :to="{ path: '/editing', force: true }"
|
||||
:class="{'mod-active': $route.path.startsWith('/editing')}">Editeur</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-bigger">
|
||||
<SearchView />
|
||||
</div>
|
||||
<div class="ps-1 gapx-1 flex align-center">
|
||||
<ThemeSwitch class="mobile-hidden" />
|
||||
<NuxtLink class="site-login" :to="{ path: '/user/profile', force: true }">
|
||||
<ThemeIcon icon="user" :width=32 :height=32 />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-body">
|
||||
<ExplorerNavigation></ExplorerNavigation>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'explorer'
|
||||
})
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { data: content } = await useFetch(`/api/project/${route.params.projectId}/file`, {
|
||||
|
|
@ -6,10 +10,6 @@ const { data: content } = await useFetch(`/api/project/${route.params.projectId}
|
|||
path: unifySlug(route.params.slug)
|
||||
}
|
||||
});
|
||||
|
||||
const { set } = useProject();
|
||||
|
||||
await set(parseInt(route.params.projectId as string));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -24,14 +24,14 @@ await set(parseInt(route.params.projectId as string));
|
|||
<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>{{ content[0].title }}</h1>
|
||||
<Markdown v-model="content[0].content" />
|
||||
<Markdown v-model="content[0].content"></Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RightComponent/>
|
||||
<CommentSide></CommentSide>
|
||||
</template>
|
||||
<CanvasRenderer v-else-if="!!content && content[0] && content[0].type == 'Canvas' && !!content[0].content" :canvas="JSON.parse(content[0].content)" />
|
||||
<CanvasRenderer v-else-if="!!content && content[0] && content[0].type == 'Canvas' && !!content[0].content" :canvas="JSON.parse(content[0].content)"></CanvasRenderer>
|
||||
<div v-else-if="!!content && content[0]">
|
||||
<div class="not-found-container">
|
||||
<div class="not-found-image"></div>
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
<!--<script lang="ts">
|
||||
let icon: HTMLDivElement | null;
|
||||
function toggleLeftPanel(_: Event) {
|
||||
icon!.parentElement!.parentElement!.parentElement!.parentElement!.classList.toggle('is-left-column-open');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
const tag = computed(() => {
|
||||
return Array.isArray(route.params.slug) ? route.params.slug.join('/') : route.params.slug;
|
||||
});
|
||||
const descriptions = inject('tags/descriptions') as ParsedContent;
|
||||
|
||||
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 getContent(content: ParsedContent)
|
||||
{
|
||||
const body = JSON.parse(JSON.stringify(content.body)) as MarkdownRoot;
|
||||
const id = flatten(body?.toc?.links ?? []).find(e => sluggify(e.id) === tag.value.replaceAll('/', ''));
|
||||
const htag = `h${id?.depth ?? 0}`;
|
||||
|
||||
const startIdx = body.children.findIndex(e => e.tag === htag && 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 + 1, (nbr === -1 ? body.children.length : nbr) - (startIdx + 1));
|
||||
|
||||
return { ... content, body };
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.querySelectorAll('.callout.is-collapsible .callout-title').forEach(e => {
|
||||
e.addEventListener('click', (_) => {
|
||||
e.parentElement!.classList.toggle('is-collapsed');
|
||||
})
|
||||
})
|
||||
|
||||
icon = document.querySelector('.site-header .clickable-icon');
|
||||
icon?.removeEventListener('click', toggleLeftPanel);
|
||||
icon?.addEventListener('click', toggleLeftPanel);
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<LeftComponent />
|
||||
<ContentList :query="{ where: { frontmatter: { tags: { $contains: tag }}}}">
|
||||
<template #default="{ list }">
|
||||
<div class="render-container-inner">
|
||||
<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;">
|
||||
<h1>#{{ tag }}</h1>
|
||||
<ContentRenderer :value="getContent(descriptions)" />
|
||||
<h2>Pages contenant le tag</h2>
|
||||
<div class="card-container">
|
||||
<NuxtLink :key="item._id" :to="{ path: '/explorer' + item._path, force: true }" v-for="item of list" class="tag"> {{ item.title }} </NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #not-found>
|
||||
<div class="not-found-container">
|
||||
<div class="not-found-image"></div>
|
||||
<div class="not-found-title">Introuvable</div>
|
||||
<div class="not-found-description">Cette page n'existe pas</div>
|
||||
</div>
|
||||
</template>
|
||||
</ContentList>
|
||||
</template>-->
|
||||
|
||||
<template></template>
|
||||
|
|
@ -3,9 +3,9 @@ const data = `### 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
|
||||
- [NuxtAuth](https://auth.sidebase.io/) - MIT License - Copyright (c) 2022 SIDESTREAM GmbH
|
||||
- [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils) - MIT License - Copyright (c) 2023 Sébastien Chopin
|
||||
|
||||
Le logo a été créé grâce aux icones de [Game Icons](https://game-icons.net).`;
|
||||
Le logo a été créé grace aux icones de [Game Icons](https://game-icons.net).`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import type { File } from '~/types/api';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const project = getRouterParam(e, "projectId");
|
||||
|
|
|
|||
|
|
@ -29,16 +29,19 @@ export interface Navigation {
|
|||
children?: Navigation[];
|
||||
}
|
||||
export interface File {
|
||||
id: number;
|
||||
project: number;
|
||||
path: string;
|
||||
owner: number;
|
||||
title: string;
|
||||
order: number;
|
||||
type: 'Markdown' | 'Canvas' | 'File' | 'Folder';
|
||||
content: string;
|
||||
owner: number;
|
||||
navigable: boolean;
|
||||
private: boolean;
|
||||
}
|
||||
export interface Comment {
|
||||
file: number;
|
||||
project: number;
|
||||
path: number;
|
||||
user_id: number;
|
||||
sequence: number;
|
||||
position: number;
|
||||
|
|
|
|||
Loading…
Reference in New Issue