Add sync, Tree, Markdown, content editor.
This commit is contained in:
parent
41951d7603
commit
721e7ff3db
26
app.vue
26
app.vue
|
|
@ -3,7 +3,7 @@
|
|||
<NuxtRouteAnnouncer/>
|
||||
<TooltipProvider>
|
||||
<NuxtLayout>
|
||||
<div class="px-12 flex flex-1 justify-center">
|
||||
<div class="px-12 flex flex-1 justify-center overflow-auto max-h-full">
|
||||
<NuxtPage></NuxtPage>
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
|
|
@ -16,4 +16,26 @@
|
|||
provideToaster();
|
||||
|
||||
const { list } = useToast();
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<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;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<script setup lang="ts">
|
||||
import { dropCursor, crosshairCursor, keymap, EditorView } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language';
|
||||
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||
import { searchKeymap } from '@codemirror/search';
|
||||
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
|
||||
const editor = useTemplateRef('editor');
|
||||
const view = ref<EditorView>();
|
||||
const state = ref<EditorState>();
|
||||
|
||||
const model = defineModel<string>();
|
||||
|
||||
onMounted(() => {
|
||||
if(editor.value)
|
||||
{
|
||||
state.value = EditorState.create({
|
||||
doc: model.value,
|
||||
extensions: [
|
||||
history(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
crosshairCursor(),
|
||||
EditorView.lineWrapping,
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap
|
||||
])
|
||||
]
|
||||
});
|
||||
view.value = new EditorView({
|
||||
state: state.value,
|
||||
parent: editor.value,
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (view.value) {
|
||||
view.value?.destroy()
|
||||
view.value = undefined
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (model.value === void 0) {
|
||||
return;
|
||||
}
|
||||
const currentValue = view.value ? view.value.state.doc.toString() : "";
|
||||
if (view.value && model.value !== currentValue) {
|
||||
view.value.dispatch({
|
||||
changes: { from: 0, to: currentValue.length, insert: model.value || "" }
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-1 justify-center items-start p-12">
|
||||
<div ref="editor" class="flex flex-1 justify-center items-stretch border border-light-35 dark:border-dark-35 caret-light-100 dark:caret-dark-100" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.cm-editor
|
||||
{
|
||||
@apply bg-transparent;
|
||||
}
|
||||
.cm-editor .cm-content
|
||||
{
|
||||
@apply caret-light-100;
|
||||
@apply dark:caret-dark-100;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<template v-if="content && content.length > 0">
|
||||
<Suspense :timeout="0">
|
||||
<MarkdownRenderer class="px-8" #default :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
||||
<template #fallback><Loading /></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>
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<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,6 +1,6 @@
|
|||
<template>
|
||||
<DialogRoot v-if="!priority">
|
||||
<DialogTrigger asChild><Button>{{ label }}</Button></DialogTrigger>
|
||||
<DialogRoot v-if="!priority" v-model="model">
|
||||
<DialogTrigger asChild><Button v-if="!!label">{{ label }}</Button><slot name="trigger" /></DialogTrigger>
|
||||
<DialogPortal v-if="!disabled">
|
||||
<DialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<DialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
|
|
@ -13,8 +13,8 @@
|
|||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
<AlertDialogRoot v-else>
|
||||
<AlertDialogTrigger asChild><Button>{{ label }}</Button></AlertDialogTrigger>
|
||||
<AlertDialogRoot v-else v-model="model">
|
||||
<AlertDialogTrigger asChild><Button v-if="!!label">{{ label }}</Button><slot name="trigger" /></AlertDialogTrigger>
|
||||
<AlertDialogPortal v-if="!disabled">
|
||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<AlertDialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
|
|
@ -35,4 +35,5 @@ const { label, title, description, priority = false, disabled = false, iconClose
|
|||
disabled?: boolean
|
||||
iconClose?: boolean
|
||||
}>();
|
||||
const model = defineModel();
|
||||
</script>
|
||||
|
|
@ -6,8 +6,8 @@
|
|||
<ToastClose v-if="toast.closeable" aria-label="Close" class="text-xl -translate-y-2 translate-x-4 cursor-pointer"><span aria-hidden>×</span></ToastClose>
|
||||
<ToastDescription v-if="toast.content" class="text-sm col-span-8 text-light-70 dark:text-dark-70" asChild><span>{{ toast.content }}</span></ToastDescription>
|
||||
</div>
|
||||
<TimerProgress v-if="toast.timer" shape="thin" :delay="toast.duration" class="mb-0 mt-0 w-full group-data-[type=error]:bg-light-red dark:group-data-[type=error]:bg-dark-red group-data-[type=error]:bg-opacity-50 dark:group-data-[type=error]:bg-opacity-50 group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red
|
||||
group-data-[type=success]:bg-light-green dark:group-data-[type=success]:bg-dark-green group-data-[type=success]:bg-opacity-50 dark:group-data-[type=success]:bg-opacity-50 group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green" @finish="() => tryClose(toast, false)" />
|
||||
<TimerProgress v-if="toast.timer" shape="thin" :delay="toast.duration" class="mb-0 mt-0 w-full group-data-[type=error]:bg-light-redBack dark:group-data-[type=error]:bg-dark-redBack group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red
|
||||
group-data-[type=success]:bg-light-greenBack dark:group-data-[type=success]:bg-dark-greenBack group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green" @finish="() => tryClose(toast, false)" />
|
||||
</ToastRoot>
|
||||
|
||||
<ToastViewport class="fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72" />
|
||||
|
|
@ -37,16 +37,14 @@ function tryClose(config: ExtraToastConfig, state: boolean)
|
|||
.ToastRoot[data-type='error'] {
|
||||
@apply border-light-red;
|
||||
@apply dark:border-dark-red;
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply bg-opacity-15;
|
||||
@apply bg-light-redBack;
|
||||
@apply dark:bg-dark-redBack;
|
||||
}
|
||||
.ToastRoot[data-type='success'] {
|
||||
@apply border-light-green;
|
||||
@apply dark:border-dark-green;
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply bg-opacity-15;
|
||||
@apply bg-light-greenBack;
|
||||
@apply dark:bg-dark-greenBack;
|
||||
}
|
||||
.ToastRoot[data-state='open'] {
|
||||
animation: slideIn .15s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,25 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 font-medium xl:text-base text-sm cursor-pointer" :items="model" :get-key="(item) => item.link ?? item.label">
|
||||
<TreeItem v-for="item in flattenItems" v-slot="{ isExpanded }" :key="item._id" :style="{ 'padding-left': `${item.level - 0.5}em` }" v-bind="item.bind" class="flex items-center px-2 outline-none relative">
|
||||
<NuxtLink :href="item.value.link && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.link } } : undefined" no-prefetch class="flex flex-1 items-center" active-class="text-accent-blue">
|
||||
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level - 1}em` }" />
|
||||
<div class="pl-3 py-1 flex-1 truncate border-light-35 dark:border-dark-35 hover:border-accent-blue" :class="{ 'border-s': !item.hasChildren }" :data-tag="item.value.tag">
|
||||
{{ item.value.label }}
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</TreeItem>
|
||||
</TreeRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
interface TreeItem
|
||||
{
|
||||
label: string
|
||||
link?: string
|
||||
tag?: string
|
||||
children?: TreeItem[]
|
||||
}
|
||||
const model = defineModel<TreeItem[]>();
|
||||
</script>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
</blockquote>
|
||||
</template>
|
||||
|
||||
<!-- <script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
const attrs = useAttrs(), el = ref<HTMLQuoteElement>(), title = ref<Element | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -179,4 +179,4 @@ blockquote:empty
|
|||
@apply mt-2;
|
||||
@apply font-semibold;
|
||||
}
|
||||
</style> -->
|
||||
</style>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
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;
|
||||
}
|
||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<CollapsibleRoot class="flex flex-1 flex-col" v-model="open">
|
||||
<div class="z-50 sm:hidden flex w-full items-center justify-between h-12 border-b border-light-35 dark:border-dark-35">
|
||||
<div class="z-50 md:hidden flex w-full items-center justify-between h-12 border-b border-light-35 dark:border-dark-35">
|
||||
<div class="flex items-center px-2">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button icon class="ms-2 !bg-transparent group">
|
||||
|
|
@ -8,25 +8,25 @@
|
|||
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<NuxtLink class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-sm:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }">Accueil</NuxtLink>
|
||||
<NuxtLink class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-md:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }">Accueil</NuxtLink>
|
||||
</div>
|
||||
<div class="flex items-center px-2">
|
||||
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
||||
<Tooltip :message="loggedIn ? 'Mon profil' : 'Se connecter'" side="right">
|
||||
<NuxtLink class="" :to="{ path: '/user/profile', force: true }">
|
||||
<div class="hover:border-opacity-70 flex">
|
||||
<Icon icon="radix-icons:person" class="w-7 h-7 p-1" />
|
||||
<Icon :icon="loggedIn ? 'radix-icons:avatar' : 'radix-icons:person'" class="w-7 h-7 p-1" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-row relative">
|
||||
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
|
||||
<CollapsibleContent asChild forceMount>
|
||||
<div class="bg-light-0 sm:my-8 sm:py-3 dark:bg-dark-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:data-[state=closed]:-left-full max-sm:transition-[left] py-8 max-sm:z-40 max-sm:data-[state=open]:left-0">
|
||||
<div class="relative bottom-6 flex flex-1 flex-col gap-4 xl:px-6 px-3">
|
||||
<div class="flex justify-between items-center max-sm:hidden">
|
||||
<NuxtLink class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-sm:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }">
|
||||
<div class="bg-light-0 md:my-8 md:py-3 dark:bg-dark-0 z-40 xl:w-96 md:w-[15em] w-full border-r border-light-30 dark:border-dark-30 flex flex-col justify-between max-md:absolute max-md:-top-0 max-md:-bottom-0 md:left-0 max-md:data-[state=closed]:-left-full max-md:transition-[left] py-8 max-md:z-40 max-md:data-[state=open]:left-0">
|
||||
<div class="relative bottom-6 flex flex-col gap-4 xl:px-6 px-3">
|
||||
<div class="flex justify-between items-center max-md:hidden">
|
||||
<NuxtLink class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-md:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }">
|
||||
<Avatar src="/logo.svg" />
|
||||
</NuxtLink>
|
||||
<div class="flex gap-4 items-center">
|
||||
|
|
@ -34,15 +34,16 @@
|
|||
<Tooltip :message="loggedIn ? 'Mon profil' : 'Se connecter'" side="right">
|
||||
<NuxtLink class="" :to="{ path: '/user/profile', force: true }">
|
||||
<div class="bg-light-20 dark:bg-dark-20 hover:border-opacity-70 flex border p-px border-light-50 dark:border-dark-50">
|
||||
<Icon icon="radix-icons:person" class="w-7 h-7 p-1" />
|
||||
<Icon :icon="loggedIn ? 'radix-icons:avatar' : 'radix-icons:person'" class="w-7 h-7 p-1" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tree v-if="pages" v-model="pages" class="flex-1 xl:px-6 px-3 max-w-full max-h-full overflow-y-auto overflow-x-hidden"/>
|
||||
<div class="xl:px-12 px-6 text-start text-xs text-light-60 dark:text-dark-60 relative top-4">
|
||||
<NuxtLink class="hover:underline italic" :to="{ path: '/third-party', force: true }">Mentions légales</NuxtLink>
|
||||
<NuxtLink class="hover:underline italic" :to="{ path: '/legal', force: true }">Mentions légales</NuxtLink>
|
||||
<p>Copyright Peaceultime - 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -57,4 +58,13 @@ import { Icon } from '@iconify/vue/dist/iconify.js';
|
|||
|
||||
const open = ref(true);
|
||||
const { loggedIn } = useUserSession();
|
||||
|
||||
const { data: pages } = await useLazyFetch('/api/navigation', {
|
||||
transform: transform,
|
||||
});
|
||||
|
||||
function transform(list: any[]): any[]
|
||||
{
|
||||
return list?.map(e => ({ label: e.title, children: transform(e.children), link: e.path, tag: e.private ? 'Privé' : e.type }))
|
||||
}
|
||||
</script>
|
||||
|
|
@ -26,21 +26,8 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
|||
{
|
||||
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
||||
}
|
||||
else
|
||||
else if(!hasPermissions(user.value.permissions, meta.rights))
|
||||
{
|
||||
let valid = false;
|
||||
for(let i = 0; i < meta.rights.length; i++)
|
||||
{
|
||||
const list = meta.rights[i].split(' ');
|
||||
|
||||
if(list.every(e => user.value.permissions.includes(e)))
|
||||
{
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!valid)
|
||||
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default defineNuxtConfig({
|
|||
'nuxt-security',
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@vueuse/nuxt',
|
||||
'radix-vue/nuxt'
|
||||
'radix-vue/nuxt',
|
||||
],
|
||||
tailwindcss: {
|
||||
viewer: false,
|
||||
|
|
@ -51,9 +51,11 @@ export default defineNuxtConfig({
|
|||
current: 'currentColor',
|
||||
light: {
|
||||
red: '#e93147',
|
||||
redBack: '#F9C7CD',
|
||||
orange: '#ec7500',
|
||||
yellow: '#e0ac00',
|
||||
green: '#08b94e',
|
||||
greenBack: '#BCECCF',
|
||||
cyan: '#00bfbc',
|
||||
blue: '#086ddd',
|
||||
purple: '#7852ee',
|
||||
|
|
@ -73,9 +75,11 @@ export default defineNuxtConfig({
|
|||
},
|
||||
dark: {
|
||||
red: '#fb464c',
|
||||
redBack: '#5A292B',
|
||||
orange: '#e9973f',
|
||||
yellow: '#e0de71',
|
||||
green: '#44cf6e',
|
||||
greenBack: '#284E34',
|
||||
cyan: '#53dfdd',
|
||||
blue: '#027aff',
|
||||
purple: '#a882ff',
|
||||
|
|
@ -128,6 +132,7 @@ export default defineNuxtConfig({
|
|||
contentSecurityPolicy: {
|
||||
"img-src": "'self' data: blob:"
|
||||
}
|
||||
}
|
||||
},
|
||||
xssValidator: false,
|
||||
},
|
||||
})
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@vueuse/nuxt": "^11.1.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"drizzle-orm": "^0.35.3",
|
||||
"nuxt": "^3.14.159",
|
||||
"nuxt-security": "^2.0.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<Head>
|
||||
<Title>Editeur</Title>
|
||||
</Head>
|
||||
<Editor v-model="model" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
rights: ['admin']
|
||||
})
|
||||
const model = defineModel<string>({
|
||||
default: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam quis orci et est malesuada vulputate. Aenean sagittis congue eros, non feugiat metus bibendum consectetur. Duis volutpat leo nisi, in maximus nulla rhoncus ac. Sed scelerisque ipsum et volutpat dignissim. Integer massa nibh, imperdiet quis condimentum vitae, imperdiet quis quam. Cras pretium ex eget hendrerit porttitor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque rutrum scelerisque quam, sit amet malesuada mi convallis aliquam. Curabitur eget dolor in diam scelerisque tincidunt at et sapien. Nulla vel nisl finibus odio porttitor sagittis ac ut sem. Aenean orci enim, fringilla eu porta eget, egestas vel libero. Aenean ac efficitur nunc, id finibus nibh. Suspendisse potenti. Quisque vel vestibulum ante. Morbi mi nulla, gravida ac malesuada at, hendrerit nec nibh.
|
||||
|
||||
Fusce sodales convallis velit, ac tempor sem auctor sed.Aenean commodo sodales lorem eu mollis.Suspendisse lectus diam, bibendum quis maximus id, euismod placerat velit.Vestibulum hendrerit justo vel ultricies molestie.Donec rhoncus, ante at facilisis fermentum, diam diam hendrerit nunc, et dapibus lacus leo in massa.Duis iaculis sem sed molestie posuere.Morbi a erat hendrerit, volutpat libero non, elementum dui.
|
||||
|
||||
Cras imperdiet velit cursus, fringilla tellus eu, lacinia neque.Sed id est suscipit quam gravida vestibulum ut sed tortor.Aliquam erat volutpat.Praesent non orci ac quam consequat tempor.Nulla facilisi.Proin at vulputate lectus.Nunc at tellus at diam faucibus eleifend et et diam.Duis pellentesque lobortis lectus id egestas.Sed quis lacinia sapien.Quisque porta tincidunt pulvinar.Aliquam hendrerit hendrerit quam, sed pulvinar turpis dictum nec.
|
||||
|
||||
Donec bibendum, orci nec tempus fermentum, diam tellus pretium elit, vel porttitor ligula lectus a augue.Aliquam tristique, mi eu mollis sodales, enim lorem hendrerit est, id semper dui tellus id felis.Duis finibus lacus nunc, vitae tincidunt metus sagittis at.Curabitur euismod neque sed malesuada consectetur.Aliquam eget efficitur urna.Sed neque sem, interdum in turpis vitae, efficitur aliquam neque.Integer consectetur consequat diam, sed suscipit arcu maximus ac.Nunc imperdiet leo condimentum tellus luctus porta.Aenean et lorem sit amet eros rutrum fermentum.
|
||||
|
||||
Nam placerat leo sed nulla imperdiet dapibus.Etiam vitae tortor efficitur, interdum ipsum non, tincidunt ante.Quisque et placerat nisi, eu bibendum neque.Nulla facilisi.Pellentesque accumsan lacus arcu, vitae iaculis elit sollicitudin quis.Sed et iaculis neque.In quis nunc laoreet turpis fermentum sodales.Etiam eget sodales lorem.Nunc id risus ac purus mollis auctor.Integer imperdiet placerat massa eu efficitur.` });
|
||||
</script>
|
||||
|
|
@ -1,3 +1,29 @@
|
|||
<template>
|
||||
Current path: {{ $route.params.path }}
|
||||
</template>
|
||||
<div v-if="status === 'pending'" class="flex"><Loading /></div>
|
||||
<div class="flex flex-1 justify-start items-start" v-if="page">
|
||||
<template v-if="page.type === 'markdown'">
|
||||
<div class="flex flex-1 justify-start items-start flex-col px-24 py-6">
|
||||
<div class="flex flex-1 flex-row justify-between items-center">
|
||||
<ProseH1>{{ page.title }}</ProseH1>
|
||||
<Button v-if="isOwner"><NuxtLink :href="{ name: 'explore-edit-path', params: { path: path } }">Modifier</NuxtLink></Button>
|
||||
</div>
|
||||
<Markdown :content="page.content" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>Contenu non traitable</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="status === 'pending'"><Loading /></div>
|
||||
<div v-else-if="status === 'error'">{{ error?.message }}</div>
|
||||
<div v-else>Impossible de retrouver le contenu demandé</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||
const { loggedIn, user } = useUserSession();
|
||||
|
||||
const { data: page, status, error } = await useFetch(`/api/file/${encodeURIComponent(path.value)}`, { watch: [route, path], });
|
||||
const isOwner = computed(() => user.value?.id === page.value?.owner);
|
||||
</script>
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<div v-if="page" class="p-12 flex flex-1 flex-col items-start justify-start">
|
||||
<Head>
|
||||
<Title>Modification de {{ page.title }}</Title>
|
||||
</Head>
|
||||
<div class="flex justify-between items-center w-full px-4 max-h-full">
|
||||
<div class="flex gap-16 items-center justify-start">
|
||||
<input type="text" v-model="page.title" placeholder="Titre" class="mx-4 h-16 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none px-3 py-1 text-5xl font-thin bg-transparent" />
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<Tooltip message="Consultable uniquement par le propriétaire" side="bottom"><Switch label="Privé" v-model="page.private" /></Tooltip>
|
||||
<Tooltip message="Afficher dans le menu de navigation" side="bottom"><Switch label="Navigable" v-model="page.navigable" /></Tooltip>
|
||||
<Button @click="() => save()" :loading="saveStatus === 'pending'" class="border-light-blue dark:border-dark-blue hover:border-light-blue dark:hover:border-dark-blue focus:shadow-light-blue dark:focus:shadow-dark-blue">Enregistrer</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-light-35 dark:border-dark-35 mt-4 flex-1 w-full max-h-full flex pt-4">
|
||||
<template v-if="page.type === 'markdown'">
|
||||
<textarea v-model="page.content" class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none"></textarea>
|
||||
<div class="flex-1 border-s border-light-35 dark:border-dark-35 ms-4 pt-4 ps-4 max-h-full">
|
||||
<Markdown class="max-h-full overflow-auto" :content="page.content" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page.type === 'canvas'">
|
||||
<span class="flex-1 items-center"><ProseH1>Editeur de graphe en cours de développement</ProseH1></span>
|
||||
</template>
|
||||
<template v-else-if="page.type === 'file'">
|
||||
<span>Modifier le contenu :</span><input type="file" @change="(e) => console.log(e)" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="status === 'pending'" class="flex">
|
||||
<Head>
|
||||
<Title>Chargement</Title>
|
||||
</Head>
|
||||
<Loading />
|
||||
</div>
|
||||
<div v-else-if="status === 'error'">{{ error?.message }}</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||
|
||||
const toaster = useToast();
|
||||
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||
|
||||
const { data: page, status, error } = await useLazyFetch(`/api/file/${encodeURIComponent(path.value)}`, { watch: [ route, path ]});
|
||||
|
||||
async function save(): Promise<void>
|
||||
{
|
||||
saveStatus.value = 'pending';
|
||||
try {
|
||||
await $fetch(`/api/file`, {
|
||||
method: 'post',
|
||||
body: page.value,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
saveStatus.value = 'success';
|
||||
|
||||
toaster.clear('error');
|
||||
toaster.add({
|
||||
type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000
|
||||
});
|
||||
|
||||
useRouter().push({ name: 'explore-path', params: { path: path.value } });
|
||||
} catch(e: any) {
|
||||
toaster.add({
|
||||
type: 'error', content: e.message, timer: true, duration: 10000
|
||||
})
|
||||
saveStatus.value = 'error';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,3 +1,33 @@
|
|||
<template>
|
||||
|
||||
<div class="flex flex-col max-w-[1200px] p-16">
|
||||
<ProseH3>Mentions Légales</ProseH3>
|
||||
<ProseH4>Collecte et Traitement des Données Personnelles</ProseH4>
|
||||
Ce site collecte des données personnelles durant l'inscription et des données anonymes durant la navigation sur
|
||||
le site dans un but de collecte statistiques.<br />
|
||||
|
||||
Conformément à la réglementation en vigueur, vous disposez d'un droit d'accès, de rectification et de
|
||||
suppression de vos données personnelles. <br />
|
||||
|
||||
Pour exercer ces droits, vous pouvez vous rendre dans votre profil et selectionner l'option "Supprimer mon
|
||||
compte" qui garanti une suppression de l'intégralité de vos données personnelles.
|
||||
|
||||
<ProseH4>Utilisation des Cookies</ProseH4>
|
||||
Ce site utilise des cookies uniquement pour maintenir la connexion des utilisateurs et faciliter leur navigation
|
||||
lors de chaque visite. Aucune information de suivi ou de profilage n'est réalisée. Ces cookies sont essentiels
|
||||
au fonctionnement du site et ne nécessitent pas de consentement préalable. <br />
|
||||
|
||||
Vous pouvez gérer les cookies en configurant les paramètres de votre navigateur, mais la désactivation de ces
|
||||
cookies pourrait affecter votre expérience de navigation.
|
||||
|
||||
<ProseH4>Limitation de Responsabilité</ProseH4>
|
||||
Les informations publiées sur ce site sont fournies à titre indicatif et peuvent contenir des erreurs. <br />
|
||||
L'éditeur décline toute responsabilité quant à l'usage qui pourrait être fait de ces informations.
|
||||
|
||||
<ProseH4>Propriété Intellectuelle</ProseH4>
|
||||
Tous les contenus présents sur ce site (textes, images, logos, etc.) sont protégés par les lois en vigueur
|
||||
sur la propriété intellectuelle. Toute reproduction ou utilisation de ces contenus sans autorisation préalable
|
||||
est interdite. <br /><br />
|
||||
|
||||
<span class="text-light-60 dark:text-dark-60 text-lg">© Copyright Peaceultime - 2024</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -3,28 +3,63 @@ definePageMeta({
|
|||
guestsGoesTo: '/user/login',
|
||||
})
|
||||
let { user, clear } = useUserSession();
|
||||
|
||||
const deleting = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head>
|
||||
<Title>Mon profil</Title>
|
||||
</Head>
|
||||
<div class="grid grid-cols-4 w-full items-start py-8 gap-6 content-start" v-if="user">
|
||||
<div class="flex gap-4 col-span-3 border border-light-35 dark:border-dark-35 p-4">
|
||||
<Avatar icon="radix-icons:person" :src="`/users/${user?.id}.medium.jpg`" class="w-32 h-32" />
|
||||
<div class="flex flex-col items-start">
|
||||
<ProseH5>{{ user.username }}</ProseH5>
|
||||
<ProseH5>{{ user.email }}</ProseH5>
|
||||
<div class="grid lg:grid-cols-4 grid-col-2 w-full items-start py-8 gap-6 content-start" v-if="user">
|
||||
<div class="flex flex-col gap-4 col-span-4 lg:col-span-3 border border-light-35 dark:border-dark-35 p-4">
|
||||
<div class="flex gap-4">
|
||||
<Avatar icon="radix-icons:person" :src="`/users/${user?.id}.medium.jpg`" class="w-32 h-32" />
|
||||
<div class="flex flex-col items-start">
|
||||
<ProseH5>{{ user.username }}</ProseH5>
|
||||
<ProseH5>{{ user.email }}</ProseH5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-light-red dark:border-dark-red bg-light-redBack dark:bg-dark-redBack text-light-red dark:text-dark-red py-1 px-3 flex items-center justify-between flex-col md:flex-row"
|
||||
v-if="user.state === 0">
|
||||
<HoverCard>
|
||||
<template v-slot:default><div class="border-light-red dark:border-dark-red bg-light-red bg-opacity-25 dark:bg-dark-red dark:bg-opacity-25 text-light-red dark:text-dark-red py-1 px-3" v-if="user.state === 0">Votre adresse mail n'as pas encore été validée. <Button class="ms-4">Renvoyez un mail</Button></div></template>
|
||||
<template v-slot:content><span>Tant que votre adresse mail n'as pas été validée, vous n'avez que des droits de lecture.</span></template>
|
||||
<template v-slot:default>Votre adresse mail n'as pas encore été validée. </template>
|
||||
<template v-slot:content><span>Tant que votre adresse mail n'as pas été validée, vous n'avez que
|
||||
des droits de lecture.</span></template>
|
||||
</HoverCard>
|
||||
<Button class="ms-4">Renvoyez un mail</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-col self-center flex-1 gap-4">
|
||||
<Button @click="async () => await clear()">Se deconnecter</Button>
|
||||
<Button>Modifier mon profil</Button>
|
||||
<AlertDialogRoot v-model="deleting">
|
||||
<AlertDialogTrigger asChild><Button
|
||||
class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer
|
||||
mon compte</Button></AlertDialogTrigger>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<AlertDialogContent
|
||||
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
<AlertDialogTitle class="text-3xl font-light relative -top-2">Suppression de compte
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription class="text-base pb-2">Supprimer votre compte va supprimer toutes vos
|
||||
données personnelles de notre base de données, incluant vos commentaires et vos
|
||||
contributions. <br />
|
||||
Êtes vous sûr de vouloir supprimer votre compte ?
|
||||
</AlertDialogDescription>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<Button @click="() => { deleting = false; }" class="">Annuler</Button>
|
||||
<Button @click="() => { deleting = false; }"
|
||||
class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer</Button>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
<Button v-if="hasPermissions(user.permissions, ['admin'])"><NuxtLink :href="{ name: 'admin' }">Administration</NuxtLink></Button>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="flex" v-if="user.permissions">
|
||||
<ProseTable class="!m-0">
|
||||
<ProseThead>
|
||||
<ProseTr>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const schema = z.object({
|
||||
path: z.string(),
|
||||
owner: z.number(),
|
||||
title: z.string(),
|
||||
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
|
||||
content: z.string(),
|
||||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
});
|
||||
|
||||
export type File = z.infer<typeof schema>;
|
||||
|
|
@ -7,5 +7,12 @@ export default defineEventHandler(async (e) => {
|
|||
return;
|
||||
}
|
||||
|
||||
return await runTask(id);
|
||||
const result = await runTask(id);
|
||||
|
||||
if(!result.result)
|
||||
{
|
||||
setResponseStatus(e, 500);
|
||||
throw result.error ?? new Error('Erreur inconnue');
|
||||
}
|
||||
return
|
||||
});
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import { schema } from '~/schemas/login';
|
||||
import type { User, UserExtendedData, UserRawData, UserSession, UserSessionRequired } from '~/types/auth';
|
||||
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||
import { ZodError } from 'zod';
|
||||
import { checkSession, logSession } from '~/server/utils/user';
|
||||
import { usersTable } from '~/db/schema';
|
||||
import { eq, or, sql } from 'drizzle-orm';
|
||||
import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite';
|
||||
|
||||
interface SuccessHandler
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import type { File } from '~/types/api';
|
||||
|
||||
export default defineCachedEventHandler(async (e) => {
|
||||
export default defineEventHandler(async (e) => {
|
||||
const project = getRouterParam(e, "projectId");
|
||||
const query = getQuery(e);
|
||||
|
||||
|
|
@ -48,7 +48,4 @@ export default defineCachedEventHandler(async (e) => {
|
|||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
}, {
|
||||
maxAge: 60*60*24,
|
||||
getKey: (e) => `${getRouterParam(e, "projectId")}-${JSON.stringify(getQuery(e))}`
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import { schema } from '~/schemas/file';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const body = await readValidatedBody(e, schema.safeParse);
|
||||
if(!body.success)
|
||||
{
|
||||
setResponseStatus(e, 403);
|
||||
throw body.error;
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(body.data.content, 'utf-8');
|
||||
|
||||
const db = useDatabase();
|
||||
const content = db.insert(explorerContentTable).values({ ...body.data, content: buffer }).onConflictDoUpdate({ target: explorerContentTable.path, set: { ...body.data, content: buffer } });
|
||||
|
||||
if(content !== undefined)
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
});
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { eq, sql } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import type { CommentedFile, CommentSearch, File } from '~/types/api';
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
|
||||
export default defineCachedEventHandler(async (e) => {
|
||||
export default defineEventHandler(async (e) => {
|
||||
const path = decodeURIComponent(getRouterParam(e, "path") ?? '');
|
||||
|
||||
if(!path)
|
||||
|
|
@ -10,26 +11,23 @@ export default defineCachedEventHandler(async (e) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const where = ["path = $path"];
|
||||
const criteria: Record<string, any> = { $path: path };
|
||||
const db = useDatabase();
|
||||
|
||||
if(where.length > 1)
|
||||
const content = db.select({
|
||||
'path': explorerContentTable.path,
|
||||
'owner': explorerContentTable.owner,
|
||||
'title': explorerContentTable.title,
|
||||
'type': explorerContentTable.type,
|
||||
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
||||
'navigable': explorerContentTable.navigable,
|
||||
'private': explorerContentTable.private,
|
||||
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
|
||||
|
||||
if(content !== undefined)
|
||||
{
|
||||
const db = useDatabase();
|
||||
|
||||
const content = db.query(`SELECT * FROM explorer_files WHERE ${where.join(" and ")}`).get(criteria) as File;
|
||||
|
||||
if(content !== undefined)
|
||||
{
|
||||
const comments = db.query(`SELECT comment.*, user.username FROM explorer_comments comment LEFT JOIN users user ON comment.user_id = user.id WHERE ${where.join(" and ")}`).all(criteria) as CommentSearch[];
|
||||
|
||||
return { ...content, comments } as CommentedFile;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}, {
|
||||
maxAge: 60*60*24,
|
||||
getKey: (e) => `file-${getRouterParam(e, "path")}`
|
||||
});
|
||||
});
|
||||
|
|
@ -1,21 +1,17 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import { Navigation } from '~/types/api';
|
||||
|
||||
type NavigatioNExtension = Navigation & { owner: number, navigable: boolean };
|
||||
import { explorerContentTable } from '~/db/schema';
|
||||
import type { Navigation } from '~/types/api';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const project = getRouterParam(e, "projectId");
|
||||
const { user } = await getUserSession(e);
|
||||
|
||||
if(!project)
|
||||
/*if(!user)
|
||||
{
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
throw createError({ statusCode: 401, statusText: 'Unauthorized' });
|
||||
}*/
|
||||
|
||||
const db = useDatabase();
|
||||
|
||||
const content = db.query(`SELECT "path", "title", "type", "order", "private", "navigable", "owner" FROM explorer_files`).sort((a: any, b: any) => a.path.length - b.path.length) as NavigatioNExtension[];
|
||||
const content = db.select({ path: explorerContentTable.path, title: explorerContentTable.title, type: explorerContentTable.type, private: explorerContentTable.private, navigable: explorerContentTable.navigable, owner: explorerContentTable.owner }).from(explorerContentTable).prepare().all() as (Navigation & { owner: number, navigable: boolean })[];
|
||||
|
||||
if(content.length > 0)
|
||||
{
|
||||
|
|
@ -62,11 +58,6 @@ function addChild(arr: Navigation[], e: Navigation): void
|
|||
}
|
||||
else
|
||||
{
|
||||
arr.push({ title: e.title, path: e.path, type: e.type, order: e.order, private: e.private });
|
||||
arr.sort((a, b) => {
|
||||
if(a.order && b.order)
|
||||
return a.order - b.order;
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
arr.push({ title: e.title, path: e.path, type: e.type, private: e.private });
|
||||
}
|
||||
}
|
||||
|
|
@ -2,20 +2,20 @@ import useDatabase from "~/composables/useDatabase";
|
|||
import { extname, basename } from 'node:path';
|
||||
import type { File, FileType, Tag } from '~/types/api';
|
||||
import type { CanvasColor, CanvasContent } from "~/types/canvas";
|
||||
import { explorerContentTable } from "~/db/schema";
|
||||
|
||||
const typeMapping: Record<string, FileType> = {
|
||||
".md": "Markdown",
|
||||
".canvas": "Canvas"
|
||||
".md": "markdown",
|
||||
".canvas": "canvas"
|
||||
};
|
||||
|
||||
export default defineTask({
|
||||
meta: {
|
||||
name: 'sync',
|
||||
description: 'Synchronise the project 1 with Obsidian',
|
||||
description: 'Synchronise the project with Obsidian',
|
||||
},
|
||||
async run(event) {
|
||||
console.log('Task "sync" executed');
|
||||
/* try {
|
||||
try {
|
||||
const tree = await $fetch('https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/git/trees/master', {
|
||||
method: 'get',
|
||||
headers: {
|
||||
|
|
@ -27,7 +27,8 @@ export default defineTask({
|
|||
}
|
||||
}) as any;
|
||||
|
||||
const files: File[] = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e: any) => {
|
||||
|
||||
const files: typeof explorerContentTable.$inferInsert = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e: any) => {
|
||||
if(e.type === 'tree')
|
||||
{
|
||||
const title = basename(e.path);
|
||||
|
|
@ -35,10 +36,13 @@ export default defineTask({
|
|||
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
|
||||
return {
|
||||
path: path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
|
||||
order: order && order[1] ? order[1] : 50,
|
||||
//order: order && order[1] ? order[1] : 50,
|
||||
title: order && order[2] ? order[2] : title,
|
||||
type: 'Folder',
|
||||
content: null
|
||||
type: 'folder',
|
||||
content: null,
|
||||
owner: '1',
|
||||
navigable: true,
|
||||
private: e.path.startsWith('98.Privé')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,14 +54,17 @@ export default defineTask({
|
|||
|
||||
return {
|
||||
path: (extension === '.md' ? path.replace(extension, '') : path).toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
|
||||
order: order && order[1] ? order[1] : 50,
|
||||
//order: order && order[1] ? order[1] : 50,
|
||||
title: order && order[2] ? order[2] : title,
|
||||
type: (typeMapping[extension] ?? 'File'),
|
||||
content: reshapeContent(content as string, typeMapping[extension] ?? 'File')
|
||||
type: (typeMapping[extension] ?? 'file'),
|
||||
content: reshapeContent(content as string, typeMapping[extension] ?? 'File'),
|
||||
owner: '1',
|
||||
navigable: true,
|
||||
private: e.path.startsWith('98.Privé')
|
||||
}
|
||||
}));
|
||||
|
||||
let tags: Tag[] = [];
|
||||
/*let tags: Tag[] = [];
|
||||
const tagFile = files.find(e => e.path === "tags");
|
||||
|
||||
if(tagFile)
|
||||
|
|
@ -70,11 +77,13 @@ export default defineTask({
|
|||
const end = titles.length === i + 1 ? tagFile.content.length : titles[i + 1].position.start.offset - 1;
|
||||
tags.push({ tag: titles[i].properties.id, description: tagFile.content.substring(titles[i].position?.end.offset + 1, end) });
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
const db = useDatabase();
|
||||
db.delete(explorerContentTable).run();
|
||||
db.insert(explorerContentTable).values(files).run();
|
||||
|
||||
const oldFiles = db.prepare(`SELECT * FROM explorer_files WHERE project = ?1`).all('1') as File[];
|
||||
/*const oldFiles = db.prepare(`SELECT * FROM explorer_files WHERE project = ?1`).all('1') as File[];
|
||||
const removeFiles = db.prepare(`DELETE FROM explorer_files WHERE project = ?1 AND path = ?2`);
|
||||
db.transaction((data: File[]) => data.forEach(e => removeFiles.run('1', e.path)))(oldFiles.filter(e => !files.find(f => f.path = e.path)));
|
||||
removeFiles.finalize();
|
||||
|
|
@ -118,7 +127,7 @@ export default defineTask({
|
|||
})(tags);
|
||||
|
||||
insertTags.finalize();
|
||||
updateTags.finalize();
|
||||
updateTags.finalize();*/
|
||||
|
||||
useStorage('cache').clear();
|
||||
|
||||
|
|
@ -126,8 +135,8 @@ export default defineTask({
|
|||
}
|
||||
catch(e)
|
||||
{
|
||||
return { result: false };
|
||||
} */
|
||||
return { result: false, error: e };
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -135,16 +144,16 @@ function reshapeContent(content: string, type: FileType): string | null
|
|||
{
|
||||
switch(type)
|
||||
{
|
||||
case "Markdown":
|
||||
case "File":
|
||||
case "markdown":
|
||||
case "file":
|
||||
return content;
|
||||
case "Canvas":
|
||||
case "canvas":
|
||||
const data = JSON.parse(content) as CanvasContent;
|
||||
data.edges.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
|
||||
data.nodes.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
|
||||
return JSON.stringify(data);
|
||||
default:
|
||||
case 'Folder':
|
||||
case 'folder':
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,11 @@ export interface Navigation {
|
|||
title: string;
|
||||
path: string;
|
||||
type: string;
|
||||
order: number;
|
||||
private: boolean;
|
||||
children?: Navigation[];
|
||||
}
|
||||
export type FileMetadata = Record<string, boolean | string | number>;
|
||||
export type FileType = 'Markdown' | 'Canvas' | 'File' | 'Folder';
|
||||
export type FileType = 'markdown' | 'canvas' | 'file' | 'folder';
|
||||
export interface File {
|
||||
project: number;
|
||||
path: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
export function hasPermissions(userPermissions: string[], neededPermissions: string[]): boolean
|
||||
{
|
||||
for(let i = 0; i < neededPermissions.length; i++)
|
||||
{
|
||||
const list = neededPermissions[i].split(' ');
|
||||
|
||||
if(list.every(e => userPermissions.includes(e)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Loading…
Reference in New Issue