Compare commits
12 Commits
1c239f161b
...
f22e63bd4d
| Author | SHA1 | Date |
|---|---|---|
|
|
f22e63bd4d | |
|
|
e83d8e802f | |
|
|
3e463ea286 | |
|
|
4125cbb3a2 | |
|
|
4df9297d47 | |
|
|
d71e8b7910 | |
|
|
20ab51a66c | |
|
|
2855d4ba2e | |
|
|
4f2fc31695 | |
|
|
6e7243982b | |
|
|
9c52494f8e | |
|
|
d0de943df2 |
|
|
@ -1,13 +1,16 @@
|
|||
<template>
|
||||
<template v-for="(item, idx) of options">
|
||||
<template v-if="item.type === 'item'">
|
||||
<DropdownMenuItem :disabled="item.disabled" :textValue="item.label" @select="item.select" :class="{'pe-1': item.kbd}" class="group cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
|
||||
<DropdownMenuItem :disabled="item.disabled" :textValue="item.label" @select="item.select" :class="{'!pe-1': item.kbd}" class="group cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
|
||||
<Icon v-if="item.icon" :icon="item.icon" class="absolute left-1.5" />
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.kbd" class="mx-2 text-xs font-mono text-light-70 dark:text-dark-70 relative top-0.5"> {{ item.kbd }} </span>
|
||||
<div class="flex flex-1 justify-between">
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.kbd" class="mx-2 text-xs font-mono text-light-70 dark:text-dark-70 relative top-0.5"> {{ item.kbd }} </span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
|
||||
<!-- TODO -->
|
||||
<template v-else-if="item.type === 'checkbox'">
|
||||
<DropdownMenuCheckboxItem :disabled="item.disabled" :textValue="item.label" @update:checked="item.select">
|
||||
<DropdownMenuItemIndicator>
|
||||
|
|
@ -18,6 +21,7 @@
|
|||
</DropdownMenuCheckboxItem>
|
||||
</template>
|
||||
|
||||
<!-- TODO -->
|
||||
<template v-if="item.type === 'radio'">
|
||||
<DropdownMenuLabel>{{ item.label }}</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup @update:model-value="item.change">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<Label class="py-4 flex flex-row justify-center items-center">
|
||||
<span>{{ label }}</span>
|
||||
<Label class="my-2 flex flex-1 items-center justify-between flex-col md:flex-row">
|
||||
<span class="pb-1 md:p-0">{{ label }}</span>
|
||||
<SelectRoot v-model="model">
|
||||
<SelectTrigger :disabled="disabled" class="mx-4 inline-flex min-w-[160px] items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1
|
||||
bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none data-[placeholder]:font-normal
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
class="mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
|
||||
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20"
|
||||
:type="type" v-model="model" :data-disabled="disabled || undefined" v-bind="$attrs">
|
||||
:type="type" v-model="model" :data-disabled="disabled || undefined" v-bind="$attrs" @change="(e) => emits('change', e)" @input="(e) => emits('input', e)">
|
||||
</Label>
|
||||
</template>
|
||||
|
||||
|
|
@ -16,5 +16,10 @@ const { type = 'text', label, disabled = false, placeholder } = defineProps<{
|
|||
disabled?: boolean
|
||||
placeholder?: string
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
change: [Event]
|
||||
input: [Event]
|
||||
}>();
|
||||
const model = defineModel<string>();
|
||||
</script>
|
||||
|
|
@ -1,56 +1,20 @@
|
|||
<template>
|
||||
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm" :items="model" :get-key="(item) => item.link ?? item.label" :defaultExpanded="flatten(model)">
|
||||
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm" :items="model" :get-key="getKey" :defaultExpanded="flatten(model)">
|
||||
<TreeItem v-for="item in flattenItems" v-slot="{ isExpanded }" :key="item._id" :style="{ 'padding-left': `${item.level - 0.5}em` }" v-bind="item.bind" class="flex items-center px-2 outline-none relative cursor-pointer">
|
||||
<NuxtLink :href="item.value.link && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.link } } : undefined" no-prefetch class="flex flex-1 items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" :class="{ 'border-s': !item.hasChildren, 'font-medium': item.hasChildren }" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
||||
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level - 1}em` }" />
|
||||
<div class="pl-3 py-1 flex-1 truncate" :data-tag="item.value.tag">
|
||||
{{ item.value.label }}
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<slot :isExpanded="isExpanded" :item="item" />
|
||||
</TreeItem>
|
||||
</TreeRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
<script setup lang="ts" generic="T extends Record<string, any>">
|
||||
const { getKey } = defineProps<{
|
||||
getKey: (val: T) => string
|
||||
}>();
|
||||
|
||||
interface TreeItem
|
||||
{
|
||||
label: string
|
||||
link?: string
|
||||
tag?: string
|
||||
open?: boolean
|
||||
children?: TreeItem[]
|
||||
}
|
||||
const model = defineModel<TreeItem[]>();
|
||||
const model = defineModel<T[]>();
|
||||
|
||||
function flatten(arr: TreeItem[]): string[]
|
||||
function flatten(arr: T[]): string[]
|
||||
{
|
||||
return arr.filter(e => e.open).flatMap(e => [e?.link ?? e.label, ...flatten(e.children ?? [])]);
|
||||
return arr.filter(e => e.open).flatMap(e => [getKey(e), ...flatten(e.children ?? [])]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
[data-tag="canvas"]:after,
|
||||
[data-tag="private"]:after
|
||||
{
|
||||
@apply text-sm;
|
||||
@apply font-normal;
|
||||
@apply float-end;
|
||||
@apply border ;
|
||||
@apply border-light-35 ;
|
||||
@apply dark:border-dark-35;
|
||||
@apply px-1;
|
||||
@apply bg-light-20;
|
||||
@apply dark:bg-dark-20;
|
||||
font-variant: small-caps;
|
||||
}
|
||||
[data-tag="canvas"]:after
|
||||
{
|
||||
content: 'Canvas'
|
||||
}
|
||||
[data-tag="private"]:after
|
||||
{
|
||||
content: 'Privé'
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
|
|
@ -31,12 +31,8 @@
|
|||
<script setup lang="ts">
|
||||
import { parseURL } from 'ufo';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { iconByType } from '#shared/general.utils';
|
||||
|
||||
const iconByType: Record<string, string> = {
|
||||
'folder': 'circum:folder-on',
|
||||
'canvas': 'ph:graph-light',
|
||||
'file': 'radix-icons:file',
|
||||
}
|
||||
const { href } = defineProps<{
|
||||
href: string
|
||||
class?: string
|
||||
|
|
|
|||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -8,7 +8,7 @@
|
|||
<Icon icon="si:error-line" class="w-12 h-12 text-light-60 dark:text-dark-60"/>
|
||||
<div class="text-3xl">Une erreur est survenue.</div>
|
||||
</div>
|
||||
<pre class="">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
|
||||
<pre class="text-center text-wrap">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
|
||||
<Button @click="handleError">Revenir en lieu sûr</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -76,11 +76,21 @@
|
|||
<NuxtLink :href="{ name: 'explore' }" no-prefetch class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
||||
<div class="pl-3 py-1 flex-1 truncate">Projet</div>
|
||||
</NuxtLink>
|
||||
<Tree v-if="pages" v-model="pages"/>
|
||||
<Tree v-if="pages" v-model="pages" :getKey="(item) => item.path">
|
||||
<template #default="{ item, isExpanded }">
|
||||
<NuxtLink :href="item.value.path && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.path } } : undefined" no-prefetch class="flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple" :class="{ 'font-medium': item.hasChildren }" active-class="text-accent-blue" :data-private="item.value.private">
|
||||
<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` }" />
|
||||
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="w-5 h-5" />
|
||||
<div class="pl-3 py-1 flex-1 truncate">
|
||||
{{ item.value.title }}
|
||||
</div>
|
||||
<Tooltip message="Privé" side="right"><Icon v-show="item.value.private" icon="radix-icons:lock-closed" /></Tooltip>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</Tree>
|
||||
</div>
|
||||
<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="{ name: 'roadmap' }">Roadmap</NuxtLink> -
|
||||
<NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||
<p>Copyright Peaceultime - 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -93,6 +103,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import type { NavigationTreeItem } from '~/server/api/navigation.get';
|
||||
import { iconByType } from '#shared/general.utils';
|
||||
|
||||
const open = ref(false);
|
||||
const { loggedIn, clear } = useUserSession();
|
||||
|
|
@ -109,8 +120,8 @@ const { data: pages } = await useLazyFetch('/api/navigation', {
|
|||
watch: [useRouter().currentRoute]
|
||||
});
|
||||
|
||||
function transform(list: NavigationTreeItem[] | undefined): any[] | undefined
|
||||
function transform(list: NavigationTreeItem[] | undefined): NavigationTreeItem[] | undefined
|
||||
{
|
||||
return list?.map(e => ({ label: e.title, children: transform(e?.children ?? undefined), link: e.path, tag: e.private ? 'private' : e.type, open: path.value?.startsWith(e.path)}))
|
||||
return list?.map(e => ({ ...e, open: path.value?.startsWith(e.path), children: transform(e.children) }));
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
import vuePlugin from 'rollup-plugin-vue'
|
||||
import postcssPlugin from 'rollup-plugin-postcss'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2024-04-03',
|
||||
modules: [
|
||||
|
|
@ -114,17 +117,42 @@ export default defineNuxtConfig({
|
|||
path: '~/components',
|
||||
pathPrefix: false,
|
||||
},
|
||||
{
|
||||
path: '~/server/components',
|
||||
pathPrefix: true,
|
||||
global: true,
|
||||
},
|
||||
],
|
||||
nitro: {
|
||||
alias: {
|
||||
'public': '//public',
|
||||
},
|
||||
publicAssets: [{
|
||||
baseURL: 'public',
|
||||
dir: 'public',
|
||||
}],
|
||||
preset: 'bun',
|
||||
experimental: {
|
||||
tasks: true,
|
||||
},
|
||||
rollupConfig: {
|
||||
plugins: [
|
||||
vuePlugin({ include: /\.vue$/, target: 'node' })
|
||||
]
|
||||
},
|
||||
},
|
||||
runtimeConfig: {
|
||||
session: {
|
||||
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013'
|
||||
},
|
||||
database: 'db.sqlite'
|
||||
database: 'db.sqlite',
|
||||
mail: {
|
||||
host: '',
|
||||
port: '',
|
||||
user: '',
|
||||
passwd: '',
|
||||
dkim: '',
|
||||
}
|
||||
},
|
||||
security: {
|
||||
rateLimiter: false,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"hast": "^1.0.0",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"mdast-util-find-and-replace": "^3.0.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"nuxt": "^3.14.159",
|
||||
"nuxt-security": "^2.0.0",
|
||||
"radix-vue": "^1.9.8",
|
||||
|
|
@ -29,6 +30,8 @@
|
|||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-vue": "^6.0.0",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vue": "latest",
|
||||
|
|
@ -38,6 +41,7 @@
|
|||
"devDependencies": {
|
||||
"@types/bun": "^1.1.12",
|
||||
"@types/lodash.capitalize": "^4.2.9",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@types/unist": "^3.0.3",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
"bun-types": "^1.1.34",
|
||||
|
|
|
|||
|
|
@ -1,27 +1,54 @@
|
|||
<script lang="ts">
|
||||
const mailSchema = z.object({
|
||||
to: z.string().email(),
|
||||
template: z.string(),
|
||||
data: z.string(),
|
||||
});
|
||||
|
||||
const schemaList: Record<string, z.ZodObject<any> | null> = {
|
||||
'pull': null,
|
||||
'push': null,
|
||||
'mail': mailSchema,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod';
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin'],
|
||||
})
|
||||
const job = ref<string>('');
|
||||
|
||||
const toaster = useToast();
|
||||
const data = ref(), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), success = ref(false), err = ref(false), error = ref();
|
||||
const payload = reactive<Record<string, any>>({
|
||||
data: JSON.stringify({ username: "Peaceultime", id: 1, timestamp: Date.now() }),
|
||||
to: 'clem31470@gmail.com',
|
||||
});
|
||||
const data = ref(), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), success = ref(false), error = ref<Error | null>();
|
||||
async function fetch()
|
||||
{
|
||||
status.value = 'pending';
|
||||
data.value = null;
|
||||
error.value = null;
|
||||
err.value = false;
|
||||
success.value = false;
|
||||
|
||||
try
|
||||
{
|
||||
const schema = schemaList[job.value];
|
||||
|
||||
if(schema)
|
||||
{
|
||||
console.log(payload);
|
||||
const parsedPayload = schema.parse(payload);
|
||||
}
|
||||
|
||||
data.value = await $fetch(`/api/admin/jobs/${job.value}`, {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
});
|
||||
status.value = 'success';
|
||||
error.value = null;
|
||||
err.value = false;
|
||||
success.value = true;
|
||||
|
||||
toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, });
|
||||
|
|
@ -29,11 +56,10 @@ async function fetch()
|
|||
catch(e)
|
||||
{
|
||||
status.value = 'error';
|
||||
error.value = e;
|
||||
err.value = true;
|
||||
error.value = e as Error;
|
||||
success.value = false;
|
||||
|
||||
toaster.add({ duration: 10000, content: error.value, type: 'error', timer: true, });
|
||||
toaster.add({ duration: 10000, content: error.value.message, type: 'error', timer: true, });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -42,12 +68,20 @@ async function fetch()
|
|||
<Head>
|
||||
<Title>d[any] - Administration</Title>
|
||||
</Head>
|
||||
<div class="flex flex-col justify-start">
|
||||
<div class="flex flex-col justify-start items-center">
|
||||
<ProseH2>Administration</ProseH2>
|
||||
<Select label="Job" v-model="job">
|
||||
<SelectItem label="Récupérer les données d'Obsidian" value="pull" />
|
||||
<SelectItem label="Envoyer les données dans Obsidian" value="push" disabled />
|
||||
</Select>
|
||||
<div class="flex flex-row w-full gap-8">
|
||||
<Select label="Job" v-model="job">
|
||||
<SelectItem label="Récupérer les données d'Obsidian" value="pull" />
|
||||
<SelectItem label="Envoyer les données dans Obsidian" value="push" disabled />
|
||||
<SelectItem label="Envoyer un mail de test" value="mail" />
|
||||
</Select>
|
||||
<Select v-if="job === 'mail'" v-model="payload.template" label="Modèle" class="w-full" ><SelectItem label="Inscription" value="registration" /></Select>
|
||||
</div>
|
||||
<div v-if="job === 'mail'" class="flex justify-center items-center flex-col">
|
||||
<TextInput label="Destinataire" class="w-full" v-model="payload.to" />
|
||||
<textarea v-model="payload.data" class="w-[640px] bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none m-2 px-2"></textarea>
|
||||
</div>
|
||||
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
|
||||
<span>Executer</span>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -3,40 +3,103 @@
|
|||
<Title>d[any] - Configuration du projet</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-row gap-4 p-6 items-start" v-if="navigation">
|
||||
<DraggableTree class="list-none select-none border border-light-35 dark:border-dark-35 text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm overflow-auto w-[450px] max-h-full"
|
||||
:items="navigation ?? undefined" :get-key="(item: Partial<ProjectItem>) => item.path ? getPath(item as ProjectItem) : ''" @updateTree="drop">
|
||||
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, isDraggedOver, item }">
|
||||
<div class="flex flex-1 px-2" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }">
|
||||
<span class="py-2 px-2" @click="handleToggle" v-if="item.hasChildren" >
|
||||
<Icon :icon="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"/>
|
||||
</span>
|
||||
<div class="ps-2 py-1 flex-1 truncate" :class="{'!ps-4 border-s border-light-35 dark:border-dark-35': !item.hasChildren}" :title="item.value.title" @click="() => { handleSelect(); selected = isSelected ? undefined : item.value; }">
|
||||
{{ item.value.title }}
|
||||
<div class="flex flex-1 flex-col w-[450px] max-w-[450px] max-h-full">
|
||||
<DraggableTree class="list-none select-none border border-light-35 dark:border-dark-35 text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm overflow-auto"
|
||||
:items="navigation ?? undefined" :get-key="(item: Partial<ProjectExtendedItem>) => item.path !== undefined ? getPath(item as ProjectExtendedItem) : ''" @updateTree="drop">
|
||||
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, item }">
|
||||
<div class="flex flex-1 items-center px-2" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }">
|
||||
<span class="py-2 px-2" @click="handleToggle" v-if="item.hasChildren" >
|
||||
<Icon :icon="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"/>
|
||||
</span>
|
||||
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="group-[:hover]:text-accent-purple mx-2" @click="() => { handleSelect(); selected = isSelected ? undefined : item.value; }" />
|
||||
<div class="pl-3 py-1 flex-1 truncate" :title="item.value.title" @click="() => { handleSelect(); selected = isSelected ? undefined : item.value; }">
|
||||
{{ item.value.title }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span @click="item.value.private = !item.value.private">
|
||||
<Icon v-if="item.value.private" icon="radix-icons:lock-closed" />
|
||||
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:lock-open-2" />
|
||||
</span>
|
||||
<span @click="item.value.navigable = !item.value.navigable">
|
||||
<Icon v-if="item.value.navigable" icon="radix-icons:eye-open" />
|
||||
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:eye-none" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #hint="{ instruction }">
|
||||
<div v-if="instruction" class="absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50" :style="{
|
||||
width: `calc(100% - ${instruction.currentLevel - 1}em)`
|
||||
}" :class="{
|
||||
'!border-b-4': instruction?.type === 'reorder-below',
|
||||
'!border-t-4': instruction?.type === 'reorder-above',
|
||||
'!border-4': instruction?.type === 'make-child',
|
||||
}"></div>
|
||||
</template>
|
||||
</DraggableTree>
|
||||
</template>
|
||||
<template #hint="{ instruction }">
|
||||
<div v-if="instruction" class="absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50" :style="{
|
||||
width: `calc(100% - ${instruction.currentLevel - 1}em)`
|
||||
}" :class="{
|
||||
'!border-b-4': instruction?.type === 'reorder-below',
|
||||
'!border-t-4': instruction?.type === 'reorder-above',
|
||||
'!border-4': instruction?.type === 'make-child',
|
||||
}"></div>
|
||||
</template>
|
||||
</DraggableTree>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex self-end gap-4 px-4">
|
||||
<DropdownMenu align="center" side="bottom" :options="[{
|
||||
type: 'item',
|
||||
label: 'Markdown',
|
||||
kbd: 'Ctrl+N',
|
||||
icon: 'radix-icons:file',
|
||||
select: () => add('markdown'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Dossier',
|
||||
kbd: 'Ctrl+Shift+N',
|
||||
icon: 'lucide:folder',
|
||||
select: () => add('folder'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Canvas',
|
||||
icon: 'ph:graph-light',
|
||||
select: () => add('canvas'),
|
||||
}, {
|
||||
type: 'item',
|
||||
label: 'Fichier',
|
||||
icon: 'radix-icons:file-text',
|
||||
select: () => add('file'),
|
||||
}]">
|
||||
<Button>Nouveau</Button>
|
||||
</DropdownMenu>
|
||||
<Button @click="navigation = tree.remove(navigation, getPath(selected))" v-if="selected" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer</Button>
|
||||
<Tooltip message="Ctrl+S" side="bottom"><Button @click="() => save(true)" :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></Tooltip>
|
||||
<Tooltip message="Ctrl+Shift+Z" side="bottom"><NuxtLink :href="{ name: 'explore' }"><Button>Annuler</Button></NuxtLink></Tooltip>
|
||||
</div>
|
||||
<div v-if="selected" class="flex-1 flex justify-start items-start">
|
||||
<div class="flex flex-col flex-1 justify-start items-start">
|
||||
<input type="text" v-model="selected.title" placeholder="Titre" class="flex-1 mx-4 h-16 w-full 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" />
|
||||
<span><pre class="ps-2 inline">{{ selected.parent }}/</pre><input v-model="selected.name" placeholder="Titre" class="font-mono border-b border-light-35 dark:border-dark-35 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 bg-transparent" /></span>
|
||||
<div class="flex ms-6 flex-col justify-start items-start">
|
||||
<Tooltip message="Consultable uniquement par le propriétaire" side="right"><Switch label="Privé" v-model="selected.private" /></Tooltip>
|
||||
<Tooltip message="Afficher dans le menu de navigation" side="right"><Switch label="Navigable" v-model="selected.navigable" /></Tooltip>
|
||||
<input type="text" v-model="selected.title" @change="(e) => {
|
||||
if(selected && !selected.customPath)
|
||||
{
|
||||
selected.name = parsePath(selected.title);
|
||||
rebuildPath(selected.children, getPath(selected));
|
||||
}
|
||||
}" placeholder="Titre" class="flex-1 mx-4 h-16 w-full 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 class="flex ms-6 flex-col justify-start items-start gap-2">
|
||||
<div class="flex flex-col justify-start items-start">
|
||||
<Switch label="Chemin personnalisé" v-model="selected.customPath" />
|
||||
<span>
|
||||
<pre v-if="selected.customPath" class="flex items-center">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}<TextInput v-model="selected.name" @input="(e) => {
|
||||
if(selected && selected.customPath)
|
||||
{
|
||||
selected.name = parsePath(selected.name);
|
||||
rebuildPath(selected.children, getPath(selected));
|
||||
}
|
||||
}" class="mx-0"/></pre>
|
||||
<pre v-else>/{{ getPath(selected) }}</pre>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<HoverCard class="!py-2 !px-4"><Icon icon="radix-icons:question-mark-circled" /><template #content><span class="text-sm italic text-light-60 dark:text-dark-60">Un fichier privé n'est consultable que par le propriétaire du projet. Rendre un dossier privé cache automatiquement son contenu sans avoir à chaque fichier un par un.</span></template></HoverCard>
|
||||
<Switch label="Privé" v-model="selected.private" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<HoverCard class="!py-2 !px-4"><Icon icon="radix-icons:question-mark-circled" /><template #content><span class="text-sm italic text-light-60 dark:text-dark-60">Un fichier navigable est disponible dans le menu de navigation à gauche. Les fichiers non navigable peuvent toujours être utilisés dans des liens.</span></template></HoverCard>
|
||||
<Switch label="Navigable" v-model="selected.navigable" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -44,26 +107,42 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
||||
import { parsePath } from '#shared/general.utils';
|
||||
import type { ProjectItem } from '~/schemas/project';
|
||||
import type { FileType } from '~/schemas/file';
|
||||
import { iconByType } from '#shared/general.utils';
|
||||
|
||||
interface ProjectExtendedItem extends ProjectItem
|
||||
{
|
||||
customPath: boolean
|
||||
children?: ProjectExtendedItem[]
|
||||
}
|
||||
interface ProjectExtended
|
||||
{
|
||||
items: ProjectExtendedItem[]
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin', 'editor'],
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const route = router.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: project } = await useFetch(`/api/project`);
|
||||
const navigation = computed({
|
||||
const { data: project } = await useFetch(`/api/project`, {
|
||||
transform: (project) =>{
|
||||
if(project)
|
||||
(project as ProjectExtended).items = transform(project.items)!;
|
||||
|
||||
return project as ProjectExtended;
|
||||
}
|
||||
});
|
||||
const navigation = computed<ProjectExtendedItem[] | undefined>({
|
||||
get: () => project.value?.items,
|
||||
set: (value) => {
|
||||
const proj = project.value;
|
||||
|
|
@ -74,15 +153,17 @@ const navigation = computed({
|
|||
project.value = proj;
|
||||
}
|
||||
});
|
||||
const selected = ref<ProjectItem>();
|
||||
const selected = ref<ProjectExtendedItem>();
|
||||
|
||||
useShortcuts({
|
||||
meta_s: { usingInput: true, handler: () => save(false) },
|
||||
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore-path', params: { path: path.value }}) }
|
||||
meta_n: { usingInput: true, handler: () => add('markdown') },
|
||||
meta_shift_n: { usingInput: true, handler: () => add('folder') },
|
||||
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore' }) }
|
||||
})
|
||||
|
||||
const tree = {
|
||||
remove(data: ProjectItem[], id: string): ProjectItem[] {
|
||||
remove(data: ProjectExtendedItem[], id: string): ProjectExtendedItem[] {
|
||||
return data
|
||||
.filter(item => getPath(item) !== id)
|
||||
.map((item) => {
|
||||
|
|
@ -95,7 +176,7 @@ const tree = {
|
|||
return item;
|
||||
});
|
||||
},
|
||||
insertBefore(data: ProjectItem[], targetId: string, newItem: ProjectItem): ProjectItem[] {
|
||||
insertBefore(data: ProjectExtendedItem[], targetId: string, newItem: ProjectExtendedItem): ProjectExtendedItem[] {
|
||||
return data.flatMap((item) => {
|
||||
if (getPath(item) === targetId)
|
||||
return [newItem, item];
|
||||
|
|
@ -109,7 +190,7 @@ const tree = {
|
|||
return item;
|
||||
});
|
||||
},
|
||||
insertAfter(data: ProjectItem[], targetId: string, newItem: ProjectItem): ProjectItem[] {
|
||||
insertAfter(data: ProjectExtendedItem[], targetId: string, newItem: ProjectExtendedItem): ProjectExtendedItem[] {
|
||||
return data.flatMap((item) => {
|
||||
if (getPath(item) === targetId)
|
||||
return [item, newItem];
|
||||
|
|
@ -124,7 +205,7 @@ const tree = {
|
|||
return item;
|
||||
});
|
||||
},
|
||||
insertChild(data: ProjectItem[], targetId: string, newItem: ProjectItem): ProjectItem[] {
|
||||
insertChild(data: ProjectExtendedItem[], targetId: string, newItem: ProjectExtendedItem): ProjectExtendedItem[] {
|
||||
return data.flatMap((item) => {
|
||||
if (getPath(item) === targetId) {
|
||||
// already a parent: add as first child
|
||||
|
|
@ -145,7 +226,7 @@ const tree = {
|
|||
};
|
||||
});
|
||||
},
|
||||
find(data: ProjectItem[], itemId: string): ProjectItem | undefined {
|
||||
find(data: ProjectExtendedItem[], itemId: string): ProjectExtendedItem | undefined {
|
||||
for (const item of data) {
|
||||
if (getPath(item) === itemId)
|
||||
return item;
|
||||
|
|
@ -157,12 +238,27 @@ const tree = {
|
|||
}
|
||||
}
|
||||
},
|
||||
search(data: ProjectExtendedItem[], prop: keyof ProjectExtendedItem, value: string): ProjectExtendedItem[] {
|
||||
const arr = [];
|
||||
|
||||
for (const item of data)
|
||||
{
|
||||
if (item[prop]?.toString().toLowerCase()?.startsWith(value.toLowerCase()))
|
||||
arr.push(item);
|
||||
|
||||
if (tree.hasChildren(item)) {
|
||||
arr.push(...tree.search(item.children ?? [], prop, value));
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
},
|
||||
getPathToItem({
|
||||
current,
|
||||
targetId,
|
||||
parentIds = [],
|
||||
}: {
|
||||
current: ProjectItem[]
|
||||
current: ProjectExtendedItem[]
|
||||
targetId: string
|
||||
parentIds?: string[]
|
||||
}): string[] | undefined {
|
||||
|
|
@ -179,12 +275,37 @@ const tree = {
|
|||
return nested;
|
||||
}
|
||||
},
|
||||
hasChildren(item: ProjectItem): boolean {
|
||||
hasChildren(item: ProjectExtendedItem): boolean {
|
||||
return (item.children ?? []).length > 0;
|
||||
},
|
||||
}
|
||||
|
||||
function updateTree(instruction: Instruction, itemId: string, targetId: string) : ProjectItem[] | undefined {
|
||||
function add(type: FileType): void
|
||||
{
|
||||
if(!navigation.value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const news = [...tree.search(navigation.value, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i);
|
||||
const title = `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`;
|
||||
const item: ProjectExtendedItem = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: type === 'folder' ? [] : undefined, customPath: false };
|
||||
|
||||
if(!selected.value)
|
||||
{
|
||||
navigation.value = [...navigation.value, item];
|
||||
}
|
||||
else if(selected.value?.children)
|
||||
{
|
||||
item.parent = getPath(selected.value);
|
||||
navigation.value = tree.insertChild(navigation.value, item.parent, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
navigation.value = tree.insertAfter(navigation.value, getPath(selected.value), item);
|
||||
}
|
||||
}
|
||||
function updateTree(instruction: Instruction, itemId: string, targetId: string) : ProjectExtendedItem[] | undefined {
|
||||
if(!navigation.value)
|
||||
return;
|
||||
|
||||
|
|
@ -238,14 +359,16 @@ function updateTree(instruction: Instruction, itemId: string, targetId: string)
|
|||
|
||||
return navigation.value;
|
||||
}
|
||||
|
||||
function transform(items: ProjectItem[] | undefined): ProjectExtendedItem[] | undefined
|
||||
{
|
||||
return items?.map(e => ({...e, customPath: e.name !== parsePath(e.title), children: transform(e.children)}));
|
||||
}
|
||||
function drop(instruction: Instruction, itemId: string, targetId: string)
|
||||
{
|
||||
navigation.value = updateTree(instruction, itemId, targetId) ?? navigation.value ?? [];
|
||||
}
|
||||
function rebuildPath(tree: ProjectItem[] | null | undefined, parentPath: string)
|
||||
function rebuildPath(tree: ProjectExtendedItem[] | null | undefined, parentPath: string)
|
||||
{
|
||||
debugger;
|
||||
if(!tree)
|
||||
return;
|
||||
|
||||
|
|
@ -269,7 +392,7 @@ async function save(redirect: boolean): Promise<void>
|
|||
type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000
|
||||
});
|
||||
|
||||
if(redirect) router.push({ name: 'explore-path', params: { path: path.value } });
|
||||
if(redirect) router.push({ name: 'explore' });
|
||||
} catch(e: any) {
|
||||
toaster.add({
|
||||
type: 'error', content: e.message, timer: true, duration: 10000
|
||||
|
|
@ -277,8 +400,8 @@ async function save(redirect: boolean): Promise<void>
|
|||
saveStatus.value = 'error';
|
||||
}
|
||||
}
|
||||
function getPath(item: ProjectItem): string
|
||||
function getPath(item: ProjectExtendedItem): string
|
||||
{
|
||||
return [item.parent, parsePath(item?.name ?? item.title)].filter(e => !!e).join('/');
|
||||
return [item.parent, parsePath(item.customPath ? item.name : item.title)].filter(e => !!e).join('/');
|
||||
}
|
||||
</script>
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
</div>
|
||||
<div class="flex flex-col gap-2 justify-start">
|
||||
<ProseH3>Utilisateur</ProseH3>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Validation du compte par mail<ProseTag>prioritaire</ProseTag></span></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Validation du compte par mail<ProseTag>prioritaire</ProseTag></span></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Modification de profil</span></Label>
|
||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Image de profil</span></Label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Validation de votre adresse mail</Title>
|
||||
</Head>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<ProseH2>Votre compte a été validé ! 🎉</ProseH2>
|
||||
<div class="flex flex-row gap-8">
|
||||
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'user-login', replace: true }">Se connecter</NuxtLink></Button>
|
||||
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'index', replace: true }">Retourner à l'accueil</NuxtLink></Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'login'
|
||||
})
|
||||
</script>
|
||||
|
|
@ -4,13 +4,26 @@ import { hasPermissions } from "#shared/auth.util";
|
|||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
})
|
||||
let { user, clear } = useUserSession();
|
||||
const { user, clear } = useUserSession();
|
||||
const toaster = useToast();
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
async function revalidateUser()
|
||||
{
|
||||
loading.value = true;
|
||||
await $fetch(`/api/users/${user.value?.id}/revalidate`, {
|
||||
method: 'get'
|
||||
});
|
||||
loading.value = false;
|
||||
toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' });
|
||||
}
|
||||
async function deleteUser()
|
||||
{
|
||||
loading.value = true;
|
||||
await $fetch(`/api/users/${user.value?.id}`, {
|
||||
method: 'delete'
|
||||
});
|
||||
loading.value = false;
|
||||
clear();
|
||||
}
|
||||
</script>
|
||||
|
|
@ -36,14 +49,14 @@ async function deleteUser()
|
|||
<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>
|
||||
<Tooltip message="En cours de développement"><Button class="ms-4" disabled>Renvoyez un mail</Button></Tooltip>
|
||||
<Button class="ms-4" @click="revalidateUser" :loading="loading">Renvoyez un mail</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col self-center flex-1 gap-4">
|
||||
<Button @click="async () => await clear()">Se deconnecter</Button>
|
||||
<Button @click="clear">Se deconnecter</Button>
|
||||
<Button disabled><Tooltip message="En cours de développement">Modifier mon profil</Tooltip></Button>
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger asChild><Button
|
||||
<AlertDialogTrigger asChild><Button :loading="loading"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -11,19 +11,13 @@
|
|||
<TextInput type="text" label="Nom d'utilisateur" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/>
|
||||
<TextInput type="email" label="Email" autocomplete="email" v-model="state.email" class="w-full md:w-auto"/>
|
||||
<TextInput type="password" label="Mot de passe" autocomplete="new-password" v-model="state.password" class="w-full md:w-auto"/>
|
||||
<div class="flex flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none">
|
||||
<span class="">Votre mot de passe doit respecter les critères de sécurité suivants
|
||||
:</span>
|
||||
<span class="ps-4" :class="{'text-light-red dark:text-dark-red': !checkedLength}">Entre 8 et 128
|
||||
caractères</span>
|
||||
<span class="ps-4" :class="{'text-light-red dark:text-dark-red': !checkedLowerUpper}">Au moins
|
||||
une minuscule et une majuscule</span>
|
||||
<span class="ps-4" :class="{'text-light-red dark:text-dark-red': !checkedDigit}">Au moins un
|
||||
chiffre</span>
|
||||
<span class="ps-4" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}">Au moins un
|
||||
caractère spécial parmi la liste suivante:
|
||||
<pre class="text-wrap">! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~</pre>
|
||||
</span>
|
||||
<div class="grid grid-cols-2 flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none">
|
||||
<span class="col-span-2">Prérequis de sécurité</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLength}"><Icon v-show="!checkedLength" icon="radix-icons:cross-2" />8 à 128 caractères</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLower}"><Icon v-show="!checkedLower" icon="radix-icons:cross-2" />Une minuscule</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedUpper}"><Icon v-show="!checkedUpper" icon="radix-icons:cross-2" />Une majuscule</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedDigit}"><Icon v-show="!checkedDigit" icon="radix-icons:cross-2" />Un chiffre</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
|
||||
</div>
|
||||
<TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/>
|
||||
<Button class="border border-light-35 dark:border-dark-35 max-w-48 w-full order-9 col-span-2 md:col-span-1 m-auto" :loading="status === 'pending'">S'inscrire</Button>
|
||||
|
|
@ -52,7 +46,8 @@ const { add: addToast, clear: clearToasts } = useToast();
|
|||
const confirmPassword = ref("");
|
||||
|
||||
const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128);
|
||||
const checkedLowerUpper = computed(() => state.password.toLowerCase() !== state.password && state.password.toUpperCase() !== state.password);
|
||||
const checkedLower = computed(() => state.password.toUpperCase() !== state.password);
|
||||
const checkedUpper = computed(() => state.password.toLowerCase() !== state.password);
|
||||
const checkedDigit = computed(() => /[0-9]/.test(state.password));
|
||||
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => state.password.includes(e)));
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
|
|
@ -1,14 +1,16 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const fileType = z.enum(['folder', 'file', 'markdown', 'canvas']);
|
||||
export const schema = z.object({
|
||||
path: z.string(),
|
||||
owner: z.number().finite(),
|
||||
title: z.string(),
|
||||
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
|
||||
type: fileType,
|
||||
content: z.string(),
|
||||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
order: z.number().finite(),
|
||||
});
|
||||
|
||||
export type FileType = z.infer<typeof fileType>;
|
||||
export type File = z.infer<typeof schema>;
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { z } from "zod";
|
||||
import { fileType } from "./file";
|
||||
|
||||
export const single = z.object({
|
||||
path: z.string(),
|
||||
owner: z.number().finite(),
|
||||
title: z.string(),
|
||||
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
|
||||
type: fileType,
|
||||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
order: z.number().finite(),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { z } from "zod";
|
||||
import { fileType } from "./file";
|
||||
|
||||
const baseItem = z.object({
|
||||
path: z.string(),
|
||||
parent: z.string(),
|
||||
name: z.string().optional(),
|
||||
name: z.string(),
|
||||
title: z.string(),
|
||||
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
|
||||
type: fileType,
|
||||
navigable: z.boolean(),
|
||||
private: z.boolean(),
|
||||
order: z.number().finite(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { hasPermissions } from "#shared/auth.util";
|
||||
|
||||
declare module 'nitropack'
|
||||
{
|
||||
interface TaskPayload
|
||||
{
|
||||
type: string
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
|
||||
|
|
@ -9,19 +17,31 @@ export default defineEventHandler(async (e) => {
|
|||
return;
|
||||
}
|
||||
const id = getRouterParam(e, 'id');
|
||||
const payload: Record<string, any> = await readBody(e);
|
||||
|
||||
if(!id)
|
||||
{
|
||||
setResponseStatus(e, 400);
|
||||
return;
|
||||
}
|
||||
|
||||
payload.type = id;
|
||||
payload.data = JSON.parse(payload.data);
|
||||
|
||||
const result = await runTask(id);
|
||||
const result = await runTask(id, {
|
||||
payload: payload
|
||||
});
|
||||
|
||||
if(!result.result)
|
||||
{
|
||||
setResponseStatus(e, 500);
|
||||
throw result.error ?? new Error('Erreur inconnue');
|
||||
|
||||
if(result.error && (result.error as Error).message)
|
||||
throw result.error;
|
||||
else if(result.error)
|
||||
throw new Error(result.error);
|
||||
else
|
||||
throw new Error('Erreur inconnue');
|
||||
}
|
||||
return
|
||||
return;
|
||||
});
|
||||
|
|
@ -73,6 +73,19 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
|||
|
||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [] } }) as UserSessionRequired);
|
||||
|
||||
runTask('mail', {
|
||||
payload: {
|
||||
type: 'mail',
|
||||
to: [body.data.email],
|
||||
template: 'registration',
|
||||
data: {
|
||||
username: body.data.username,
|
||||
timestamp: Date.now(),
|
||||
id: id.id,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setResponseStatus(e, 201);
|
||||
return { success: true, session };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ export default defineEventHandler(async (e) => {
|
|||
navigable: explorerContentTable.navigable,
|
||||
private: explorerContentTable.private,
|
||||
order: explorerContentTable.order,
|
||||
}).from(explorerContentTable).prepare().all();
|
||||
}).from(explorerContentTable).all();
|
||||
|
||||
content.sort((a, b) => {
|
||||
return a.path.split('/').length - b.path.split('/').length;
|
||||
});
|
||||
|
||||
if(content.length > 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ export default defineEventHandler(async (e) => {
|
|||
order: explorerContentTable.order,
|
||||
}).from(explorerContentTable).prepare().all();
|
||||
|
||||
content.sort((a, b) => {
|
||||
return a.path.split('/').length - b.path.split('/').length;
|
||||
});
|
||||
|
||||
if(content.length > 0)
|
||||
{
|
||||
const project: Project = {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import useDatabase from '~/composables/useDatabase';
|
|||
import { explorerContentTable } from '~/db/schema';
|
||||
import { project, type ProjectItem } from '~/schemas/project';
|
||||
import { parsePath } from "#shared/general.utils";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const { user } = await getUserSession(e);
|
||||
|
|
@ -22,6 +23,15 @@ export default defineEventHandler(async (e) => {
|
|||
const items = buildOrder(body.data.items);
|
||||
|
||||
const db = useDatabase();
|
||||
const { content, ...cols } = getTableColumns(explorerContentTable);
|
||||
const full = db.select(cols).from(explorerContentTable).all();
|
||||
|
||||
for(let i = full.length - 1; i >= 0; i--)
|
||||
{
|
||||
if(items.find(e => (e.path === '' ? [e.parent, parsePath(e.name === '' ? e.title : e.name)].filter(e => !!e).join('/') : e.path) === full[i].path))
|
||||
full.splice(i, 1);
|
||||
}
|
||||
|
||||
db.transaction((tx) => {
|
||||
for(let i = 0; i < items.length; i++)
|
||||
{
|
||||
|
|
@ -35,10 +45,10 @@ export default defineEventHandler(async (e) => {
|
|||
navigable: item.navigable,
|
||||
private: item.private,
|
||||
order: item.order,
|
||||
content: Buffer.from('', 'utf-8'),
|
||||
content: null,
|
||||
}).onConflictDoUpdate({
|
||||
set: {
|
||||
path: [item.parent, parsePath(item?.name ?? item.title)].filter(e => !!e).join('/'),
|
||||
path: [item.parent, parsePath(item.name === '' ? item.title : item.name)].filter(e => !!e).join('/'),
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
navigable: item.navigable,
|
||||
|
|
@ -48,6 +58,10 @@ export default defineEventHandler(async (e) => {
|
|||
target: explorerContentTable.path,
|
||||
}).run();
|
||||
}
|
||||
for(let i = 0; i < full.length; i++)
|
||||
{
|
||||
tx.delete(explorerContentTable).where(eq(explorerContentTable.path, full[i].path)).run();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import useDatabase from "~/composables/useDatabase";
|
||||
import { usersTable } from "~/db/schema";
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
|
||||
if(!session || !session.user || !session.user.id)
|
||||
{
|
||||
return createError({
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
const id = getRouterParam(e, 'id');
|
||||
|
||||
if(!id)
|
||||
{
|
||||
return createError({
|
||||
statusCode: 403,
|
||||
message: 'Forbidden',
|
||||
});
|
||||
}
|
||||
if(session.user.id.toString() !== id)
|
||||
{
|
||||
return createError({
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
||||
const db = useDatabase();
|
||||
const data = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, session.user.id)).get();
|
||||
|
||||
if(!data)
|
||||
{
|
||||
return createError({
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
if(data.state === 1)
|
||||
{
|
||||
setResponseStatus(e, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
runTask('mail', {
|
||||
payload: {
|
||||
type: 'mail',
|
||||
to: [session.user.email],
|
||||
template: 'registration', //@TODO
|
||||
data: {
|
||||
username: session.user.username,
|
||||
timestamp: Date.now(),
|
||||
id: session.user.id,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setResponseStatus(e, 200);
|
||||
return;
|
||||
})
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<div style='margin-left: auto; margin-right: auto; width: 75%; font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; line-height: 1.5rem; color: #171717;'>
|
||||
<div style="margin-left: auto; margin-right: auto; text-align: center;">
|
||||
<a href="https://obsidian.peaceultime.com">
|
||||
<img src="https://obsidian.peaceultime.com/logo.light.png" alt="Logo" title="d[any] logo" width="64" height="64" style="display: block; height: 4rem; width: 4rem; margin-left: auto; margin-right: auto;" />
|
||||
<span style="margin-inline-end: 1rem; font-size: 1.5rem; line-height: 2rem; font-weight: 700; font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;">d[any]</span>
|
||||
</a>
|
||||
</div>
|
||||
<div style="padding: 1rem;">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background-color: #171717;">
|
||||
<p style="padding-top: 1rem; padding-bottom: 1rem; text-align: center; font-size: 0.75rem; line-height: 1rem; color: #fff;">Copyright Peaceultime - d[any] - 2024</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<div style="max-width: 800px; margin-left: auto; margin-right: auto;">
|
||||
<p style="font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;">Bienvenue sur d[any], <span>{{ username }}</span>.</p>
|
||||
<p>Nous vous invitons à valider votre compte afin de profiter de toutes les fonctionnalités de d[any].</p>
|
||||
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
|
||||
<a :href="`https://obsidian.peaceultime.com/user/mailvalidation?u=${id}&t=${timestamp}&h=${hash}`" target="_blank"><span style="display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;">Je valide mon compte</span></a>
|
||||
<span style="display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;">Ce lien est valable 1 heure.</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Vous pouvez egalement copier le lien suivant pour valider votre compte: </span>
|
||||
<pre style="display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;">{{ `https://obsidian.peaceultime.com/user/mailvalidation?u=${id}&t=${timestamp}&h=${hash}` }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import Bun from 'bun';
|
||||
|
||||
const { id, username, timestamp } = defineProps<{
|
||||
id: number
|
||||
username: string
|
||||
timestamp: number
|
||||
}>();
|
||||
const hash = computed(() => Bun.hash(id.toString(), timestamp));
|
||||
</script>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="">
|
||||
<img />
|
||||
<p>Bienvenue sur d[any], <span>{{ username }}</span>.</p>
|
||||
</div>
|
||||
<p class="">Nous vous invitons à valider votre compte en cliquant <a :href="`https://obsidian.peaceultime.com/user/mail-validation?u=${id}&t=${timestamp}&h=${hash(id.toString(), timestamp)}`"><Button>ici</Button></a> afin de profiter de toutes les fonctionnalités de d[any]</p>
|
||||
<p class="">Vous pouvez egalement copier le lien suivant pour valider votre compte: {{ `https://obsidian.peaceultime.com/user/mail-validation?u=${id}&t=${timestamp}&h=${hash(id.toString(), timestamp)}` }}</p>
|
||||
<span>Ce lien est valable 1 heure.</span>
|
||||
<footer></footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { hash } from 'bun';
|
||||
|
||||
const { id, username, timestamp } = defineProps<{
|
||||
id: number
|
||||
username: string
|
||||
timestamp: number
|
||||
}>();
|
||||
</script>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import useDatabase from "~/composables/useDatabase";
|
||||
import { usersTable } from "~/db/schema";
|
||||
|
||||
const schema = z.object({
|
||||
h: z.coerce.string(),
|
||||
u: z.coerce.number(),
|
||||
t: z.coerce.number(),
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const query = await getValidatedQuery(e, schema.safeParse);
|
||||
|
||||
if(!query.success)
|
||||
throw query.error;
|
||||
|
||||
if(Bun.hash(query.data.u.toString(), query.data.t).toString() !== query.data.h)
|
||||
{
|
||||
return createError({
|
||||
statusCode: 400,
|
||||
message: 'Lien incorrect',
|
||||
})
|
||||
}
|
||||
if(Date.now() > query.data.t + (60 * 60 * 1000))
|
||||
{
|
||||
return createError({
|
||||
statusCode: 400,
|
||||
message: 'Le lien a expiré',
|
||||
})
|
||||
}
|
||||
|
||||
const db = useDatabase();
|
||||
const result = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, query.data.u)).get();
|
||||
|
||||
if(result === undefined)
|
||||
{
|
||||
return createError({
|
||||
statusCode: 400,
|
||||
message: 'Aucune donnée utilisateur trouvée',
|
||||
})
|
||||
}
|
||||
if(result?.state === 1)
|
||||
{
|
||||
return createError({
|
||||
statusCode: 400,
|
||||
message: 'Votre compte a déjà été validé',
|
||||
})
|
||||
}
|
||||
|
||||
db.update(usersTable).set({ state: 1 }).where(eq(usersTable.id, query.data.u)).run();
|
||||
|
||||
sendRedirect(e, '/user/mailvalidated');
|
||||
})
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import nodemailer from 'nodemailer';
|
||||
import { createSSRApp, h } from 'vue';
|
||||
import { renderToString } from 'vue/server-renderer';
|
||||
import postcss from 'postcss';
|
||||
import tailwindcss from 'tailwindcss';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import base from '../components/mail/base.vue';
|
||||
import registration from '../components/mail/registration.vue';
|
||||
import revalidation from '../components/mail/revalidation.vue';
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const [domain, selector, dkim] = config.mail.dkim.split(":");
|
||||
|
||||
export const templates: Record<string, { component: any, subject: string }> = {
|
||||
"registration": { component: registration, subject: 'Bienvenue sur d[any] 😎' },
|
||||
"revalidate-mail": { component: revalidation, subject: 'd[any]: Valider votre email' },
|
||||
};
|
||||
|
||||
import 'nitropack/types';
|
||||
import type Mail from 'nodemailer/lib/mailer';
|
||||
declare module 'nitropack/types'
|
||||
{
|
||||
interface TaskPayload
|
||||
{
|
||||
type: 'mail'
|
||||
to: string[]
|
||||
template: string
|
||||
data: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
const transport = nodemailer.createTransport({
|
||||
//@ts-ignore
|
||||
pool: true,
|
||||
host: config.mail.host,
|
||||
port: config.mail.port,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: config.mail.user,
|
||||
pass: config.mail.passwd,
|
||||
},
|
||||
requireTLS: true,
|
||||
dkim: {
|
||||
domainName: domain,
|
||||
keySelector: selector,
|
||||
privateKey: dkim,
|
||||
},
|
||||
});
|
||||
|
||||
export default defineTask({
|
||||
meta: {
|
||||
name: 'mail',
|
||||
description: 'Send email',
|
||||
},
|
||||
async run(e) {
|
||||
try {
|
||||
if(e.payload.type !== 'mail')
|
||||
{
|
||||
throw new Error(`Données inconnues`);
|
||||
}
|
||||
|
||||
const payload = e.payload;
|
||||
const template = templates[payload.template];
|
||||
|
||||
if(!template)
|
||||
{
|
||||
throw new Error(`Modèle de mail ${payload.template} inconnu`);
|
||||
}
|
||||
|
||||
console.time('Generating HTML');
|
||||
const mail: Mail.Options = {
|
||||
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
|
||||
to: payload.to,
|
||||
html: await render(template.component, payload.data),
|
||||
subject: template.subject,
|
||||
textEncoding: 'quoted-printable',
|
||||
};
|
||||
console.timeEnd('Generating HTML');
|
||||
|
||||
if(mail.html === '')
|
||||
return { result: false, error: new Error("Invalid content") };
|
||||
|
||||
console.time('Sending Mail');
|
||||
const status = await transport.sendMail(mail);
|
||||
console.timeEnd('Sending Mail');
|
||||
|
||||
if(status.rejected.length > 0)
|
||||
{
|
||||
return { result: false, error: status.response, details: status.rejectedErrors };
|
||||
}
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
return { result: false, error: e };
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
async function render(component: any, data: Record<string, any>): Promise<string>
|
||||
{
|
||||
const app = createSSRApp({
|
||||
render(){
|
||||
return h(base, null, { default: () => h(component, data, { default: () => null }) });
|
||||
}
|
||||
});
|
||||
|
||||
const html = await renderToString(app);
|
||||
|
||||
return (`<html><body><div>${html}</div></body></html>`);
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import useDatabase from "~/composables/useDatabase";
|
||||
import type { FileType } from '~/types/api';
|
||||
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";
|
||||
import { eq, ne } from "drizzle-orm";
|
||||
|
||||
const typeMapping: Record<string, FileType> = {
|
||||
".md": "markdown",
|
||||
|
|
@ -26,14 +27,88 @@ export default defineTask({
|
|||
}
|
||||
}) as 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);
|
||||
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
|
||||
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] : 0,
|
||||
title: order && order[2] ? order[2] : title,
|
||||
type: 'folder',
|
||||
content: null,
|
||||
owner: '1',
|
||||
navigable: true,
|
||||
private: e.path.startsWith('98.Privé'),
|
||||
}
|
||||
}
|
||||
|
||||
const extension = extname(e.path);
|
||||
const title = basename(e.path, extension);
|
||||
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
|
||||
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
|
||||
const content = (await $fetch(`https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/raw/${encodeURIComponent(e.path)}`));
|
||||
|
||||
return {
|
||||
path: (extension === '.md' ? path.replace(extension, '') : path).toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
|
||||
order: order && order[1] ? order[1] : 0,
|
||||
title: order && order[2] ? order[2] : title,
|
||||
type: (typeMapping[extension] ?? 'file'),
|
||||
content: reshapeContent(content as string, typeMapping[extension] ?? 'File'),
|
||||
owner: '1',
|
||||
navigable: true,
|
||||
private: e.path.startsWith('98.Privé')
|
||||
}
|
||||
}));
|
||||
|
||||
const db = useDatabase();
|
||||
const files = db.select().from(explorerContentTable).where(ne(explorerContentTable.type, 'folder')).all();
|
||||
db.delete(explorerContentTable).run();
|
||||
db.insert(explorerContentTable).values(files).run();
|
||||
|
||||
useStorage('cache').clear();
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.error(e);
|
||||
|
||||
return { result: false, error: e };
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
function reshapeContent(content: string, type: FileType): string | null
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case "markdown":
|
||||
case "file":
|
||||
return content;
|
||||
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':
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getColor(color: string): CanvasColor
|
||||
{
|
||||
const colors: Record<string, string> = {
|
||||
'1': 'red',
|
||||
'2': 'orange',
|
||||
'3': 'yellow',
|
||||
'4': 'green',
|
||||
'5': 'cyan',
|
||||
'6': 'purple',
|
||||
};
|
||||
if(colors.hasOwnProperty(color))
|
||||
return { class: colors[color] };
|
||||
else
|
||||
return { hex: color };
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
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 type { FileType } from '~/types/api';
|
||||
import { explorerContentTable } from "~/db/schema";
|
||||
import { eq, ne } from "drizzle-orm";
|
||||
|
||||
const typeMapping: Record<string, FileType> = {
|
||||
".md": "markdown",
|
||||
|
|
@ -27,47 +26,8 @@ export default defineTask({
|
|||
}
|
||||
}) as 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);
|
||||
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
|
||||
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,
|
||||
title: order && order[2] ? order[2] : title,
|
||||
type: 'folder',
|
||||
content: null,
|
||||
owner: '1',
|
||||
navigable: true,
|
||||
private: e.path.startsWith('98.Privé'),
|
||||
}
|
||||
}
|
||||
|
||||
const extension = extname(e.path);
|
||||
const title = basename(e.path, extension);
|
||||
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
|
||||
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
|
||||
const content = (await $fetch(`https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/raw/${encodeURIComponent(e.path)}`));
|
||||
|
||||
return {
|
||||
path: (extension === '.md' ? path.replace(extension, '') : path).toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
|
||||
//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'),
|
||||
owner: '1',
|
||||
navigable: true,
|
||||
private: e.path.startsWith('98.Privé')
|
||||
}
|
||||
}));
|
||||
|
||||
const db = useDatabase();
|
||||
db.delete(explorerContentTable).run();
|
||||
db.insert(explorerContentTable).values(files).run();
|
||||
|
||||
useStorage('cache').clear();
|
||||
const files = db.select().from(explorerContentTable).where(ne(explorerContentTable.type, 'folder')).all();
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
|
|
@ -76,37 +36,4 @@ export default defineTask({
|
|||
return { result: false, error: e };
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function reshapeContent(content: string, type: FileType): string | null
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case "markdown":
|
||||
case "file":
|
||||
return content;
|
||||
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':
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getColor(color: string): CanvasColor
|
||||
{
|
||||
const colors: Record<string, string> = {
|
||||
'1': 'red',
|
||||
'2': 'orange',
|
||||
'3': 'yellow',
|
||||
'4': 'green',
|
||||
'5': 'cyan',
|
||||
'6': 'purple',
|
||||
};
|
||||
if(colors.hasOwnProperty(color))
|
||||
return { class: colors[color] };
|
||||
else
|
||||
return { hex: color };
|
||||
}
|
||||
})
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import type { FileType } from "~/schemas/file";
|
||||
|
||||
export function unifySlug(slug: string | string[]): string
|
||||
{
|
||||
return (Array.isArray(slug) ? slug.join('/') : slug);
|
||||
}
|
||||
export function parsePath(path: string): string
|
||||
{
|
||||
return path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
||||
return path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replaceAll(/[\u0300-\u036f]/g, "").replaceAll('(', '').replaceAll(')', '');
|
||||
}
|
||||
export function parseId(id: string | undefined): string |undefined
|
||||
{
|
||||
|
|
@ -43,4 +45,11 @@ export function clamp(x: number, min: number, max: number): number {
|
|||
if (x < min)
|
||||
return min;
|
||||
return x;
|
||||
}
|
||||
|
||||
export const iconByType: Record<FileType, string> = {
|
||||
'folder': 'lucide:folder',
|
||||
'canvas': 'ph:graph-light',
|
||||
'file': 'radix-icons:file',
|
||||
'markdown': 'radix-icons:file',
|
||||
}
|
||||
Loading…
Reference in New Issue