Add order in file schema and main projecct edition

This commit is contained in:
Peaceultime 2024-11-13 00:25:18 +01:00
parent b54402fc19
commit adb37b255a
32 changed files with 549 additions and 34 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -2,7 +2,7 @@
import { useDrag, usePinch, useWheel } from '@vueuse/gesture';
import type { CanvasContent, CanvasNode } from '~/types/canvas';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { clamp } from '#imports';
import { clamp } from '#shared/general.utils';
interface Props
{
@ -151,16 +151,12 @@ dark:border-dark-purple
*/
const pinchHandler = usePinch(({ event, offset: [z] }: { event: Event, offset: number[] }) => {
event.stopPropagation();
event.preventDefault();
zoom.value = clamp(z / 2048, minZoom.value, 3);
}, {
domTarget: canvas,
eventOptions: { passive: false, }
})
const dragHandler = useDrag(({ event, delta: [x, y] }: { event: Event, delta: number[] }) => {
event.stopPropagation();
event.preventDefault();
dispX.value += x / zoom.value;
dispY.value += y / zoom.value;
}, {
@ -168,8 +164,6 @@ const dragHandler = useDrag(({ event, delta: [x, y] }: { event: Event, delta: nu
eventOptions: { passive: false, }
})
const wheelHandler = useWheel(({ event, delta: [x, y] }: { event: Event, delta: number[] }) => {
event.stopPropagation();
event.preventDefault();
zoom.value = clamp(zoom.value + y * -0.001, minZoom.value, 3);
}, {
domTarget: canvas,

View File

@ -5,5 +5,6 @@
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.utils';
const props = defineProps<{ id?: string }>()
</script>

View File

@ -5,6 +5,7 @@
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.utils';
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)

View File

@ -5,6 +5,7 @@
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.utils';
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)

View File

@ -5,5 +5,6 @@
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.utils';
const props = defineProps<{ id?: string }>()
</script>

View File

@ -5,6 +5,7 @@
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.utils';
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)

View File

@ -5,6 +5,7 @@
</template>
<script setup lang="ts">
import { parseId } from '#shared/general.utils';
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -39,8 +39,9 @@ export const explorerContentTable = sqliteTable("explorer_content", {
title: text().notNull(),
type: text({ enum: ['file', 'folder', 'markdown', 'canvas'] }).notNull(),
content: blob({ mode: 'buffer' }),
navigable: int({ mode: 'boolean' }).default(true),
private: int({ mode: 'boolean' }).default(false),
navigable: int({ mode: 'boolean' }).notNull().default(true),
private: int({ mode: 'boolean' }).notNull().default(false),
order: int().unique('order').notNull(),
});
export const usersRelation = relations(usersTable, ({ one, many }) => ({

View File

@ -0,0 +1,2 @@
ALTER TABLE `explorer_content` ADD `order` integer;--> statement-breakpoint
CREATE UNIQUE INDEX `order` ON `explorer_content` (`order`);

View File

@ -0,0 +1,313 @@
{
"version": "6",
"dialect": "sqlite",
"id": "a1a7b478-d0c3-4fc6-b74a-1a010c1d8ca1",
"prevId": "6da7ff20-0db8-4055-a353-bb0ea2fa5e0b",
"tables": {
"explorer_content": {
"name": "explorer_content",
"columns": {
"path": {
"name": "path",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"navigable": {
"name": "navigable",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": true
},
"private": {
"name": "private",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"order": {
"name": "order",
"columns": [
"order"
],
"isUnique": true
}
},
"foreignKeys": {
"explorer_content_owner_users_id_fk": {
"name": "explorer_content_owner_users_id_fk",
"tableFrom": "explorer_content",
"tableTo": "users",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_permissions": {
"name": "user_permissions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_permissions_id_users_id_fk": {
"name": "user_permissions_id_users_id_fk",
"tableFrom": "user_permissions",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_permissions_id_permission_pk": {
"columns": [
"id",
"permission"
],
"name": "user_permissions_id_permission_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user_sessions": {
"name": "user_sessions",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_sessions_user_id_users_id_fk": {
"name": "user_sessions_user_id_users_id_fk",
"tableFrom": "user_sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {
"user_sessions_id_user_id_pk": {
"columns": [
"id",
"user_id"
],
"name": "user_sessions_id_user_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users_data": {
"name": "users_data",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"signin": {
"name": "signin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_data_id_users_id_fk": {
"name": "users_data_id_users_id_fk",
"tableFrom": "users_data",
"tableTo": "users",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hash": {
"name": "hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
],
"isUnique": true
},
"users_email_unique": {
"name": "users_email_unique",
"columns": [
"email"
],
"isUnique": true
},
"users_hash_unique": {
"name": "users_hash_unique",
"columns": [
"hash"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -22,6 +22,13 @@
"when": 1730985155814,
"tag": "0002_messy_solo",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1731344368953,
"tag": "0003_cultured_skaar",
"breakpoints": true
}
]
}

View File

@ -6,7 +6,7 @@
<div class="text-3xl">Une erreur est survenue.</div>
</div>
<pre class="">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
<NuxtLink :href="{ name: 'index' }"><Button>Revenir en lieu sûr</Button></NuxtLink>
<Button @click="handleError">Revenir en lieu sûr</Button>
</div>
</template>

View File

@ -42,7 +42,12 @@
</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="flex-1 xl:px-6 px-3 max-w-full max-h-full overflow-y-auto overflow-x-hidden">
<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"/>
</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: 'legal' }">Mentions légales</NuxtLink>
<p>Copyright Peaceultime - 2024</p>
@ -62,6 +67,7 @@ const { loggedIn } = useUserSession();
const { data: pages } = await useLazyFetch('/api/navigation', {
transform: transform,
watch: [useRouter().currentRoute]
});
watch(useRouter().currentRoute, () => {

View File

@ -1,3 +1,5 @@
import { hasPermissions } from "#shared/auth.util";
export default defineNuxtRouteMiddleware(async (to, from) => {
const { loggedIn, fetch, user } = useUserSession();
const meta = to.meta;

View File

@ -10,20 +10,36 @@
"@iconify/vue": "^4.1.2",
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/tailwindcss": "^6.12.2",
"@vueuse/gesture": "^2.0.0",
"@vueuse/nuxt": "^11.1.0",
"codemirror": "^6.0.1",
"drizzle-orm": "^0.35.3",
"hast": "^1.0.0",
"lodash.capitalize": "^4.2.1",
"mdast-util-find-and-replace": "^3.0.1",
"nuxt": "^3.14.159",
"nuxt-security": "^2.0.0",
"radix-vue": "^1.9.8",
"rehype-raw": "^7.0.0",
"remark-breaks": "^4.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"vue": "latest",
"vue-router": "latest",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/bun": "^1.1.12",
"@types/lodash.capitalize": "^4.2.9",
"@types/unist": "^3.0.3",
"better-sqlite3": "^11.5.0",
"bun-types": "^1.1.34",
"drizzle-kit": "^0.26.2"
"drizzle-kit": "^0.26.2",
"mdast-util-to-string": "^4.0.0",
"rehype-stringify": "^10.0.1"
}
}

View File

@ -42,6 +42,14 @@
<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);
watch(path, () => {
if(path.value === 'index')
{
useRouter().push({ name: 'explore' });
}
}, { immediate: true });
const { loggedIn, user } = useUserSession();
const { data: page, status, error } = await useFetch(`/api/file/${encodeURIComponent(path.value)}`, { watch: [route, path], });

View File

@ -6,9 +6,14 @@
<div class="flex flex-col xl:flex-row xl:justify-between justify-center items-center w-full px-4 pb-4 border-b border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0">
<input type="text" v-model="page.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" />
<div class="flex gap-4 self-end xl:self-auto flex-wrap">
<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 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>
</div>
<div class="flex gap-4">
<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>
<NuxtLink :href="{ name: 'explore-path', params: { path: path } }"><Button>Annuler</Button></NuxtLink>
</div>
</div>
</div>
<div class="my-4 flex-1 w-full max-h-full flex">

View File

@ -0,0 +1,45 @@
<template>
</template>
<script setup lang="ts">
const { user, loggedIn } = useUserSession();
const toaster = useToast();
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
const { data: page, status, error } = await useLazyFetch(`/api/navigation`);
if(!loggedIn || (page.value && page.value.owner !== user.value?.id))
{
useRouter().replace({ name: 'explore-path', params: { path: path.value } });
}
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>

View File

@ -1,3 +1,42 @@
<template>
Index
</template>
<div v-if="status === 'pending'" class="flex">
<Head>
<Title>d[any] - Chargement</Title>
</Head>
<Loading />
</div>
<div class="flex flex-1 justify-start items-start" v-else-if="page">
<Head>
<Title>d[any] - Accueil</Title>
</Head>
<div class="flex flex-1 justify-start items-start flex-col xl:px-24 md:px-8 px-4 py-6">
<div class="flex flex-1 flex-row justify-between items-center">
<ProseH1>{{ page.title }}</ProseH1>
<div class="flex gap-4">
<NuxtLink :href="{ name: 'explore-edit-path', params: { path: 'index' } }"><Button v-if="isOwner">Modifier la page</Button></NuxtLink>
<NuxtLink :href="{ name: 'explore-edit' }"><Button v-if="isOwner">Configurer le projet</Button></NuxtLink>
</div>
</div>
<Markdown :content="page.content" />
</div>
</div>
<div v-else-if="status === 'error'">
<Head>
<Title>d[any] - Erreur</Title>
</Head>
<span>{{ error?.message }}</span>
</div>
<div v-else>
<Head>
<Title>d[any] - Erreur</Title>
</Head>
<div><ProseH2>Impossible d'afficher le contenu demandé</ProseH2></div>
</div>
</template>
<script setup lang="ts">
const { loggedIn, user } = useUserSession();
const { data: page, status, error } = await useFetch(`/api/file/index`);
const isOwner = computed(() => user.value?.id === page.value?.owner);
</script>

View File

@ -1,14 +1,3 @@
<script setup lang="ts">
const open = ref(false), username = ref(""), price = ref(750), disabled = ref(false), loading = ref(false);
watch(loading, (value) => {
if(value)
{
setTimeout(() => { open.value = true; loading.value = false }, 1500);
}
})
</script>
<template>
<Head>
<Title>d[any] - Accueil</Title>

View File

@ -1,4 +1,6 @@
<script setup lang="ts">
import { hasPermissions } from "#shared/auth.util";
definePageMeta({
guestsGoesTo: '/user/login',
})

View File

@ -2,12 +2,13 @@ import { z } from "zod";
export const schema = z.object({
path: z.string(),
owner: z.number(),
owner: z.number().finite(),
title: z.string(),
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
content: z.string(),
navigable: z.boolean(),
private: z.boolean(),
order: z.number().finite(),
});
export type File = z.infer<typeof schema>;

15
schemas/navigation.ts Normal file
View File

@ -0,0 +1,15 @@
import { z } from "zod";
export const single = z.object({
path: z.string(),
owner: z.number().finite(),
title: z.string(),
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
navigable: z.boolean(),
private: z.boolean(),
order: z.number().finite(),
});
export const table = z.array(single);
export type Navigation = z.infer<typeof table>;
export type NavigationItem = z.infer<typeof single>;

View File

@ -1,3 +1,5 @@
import { hasPermissions } from "#shared/auth.util";
export default defineEventHandler(async (e) => {
const session = await getUserSession(e);

View File

@ -1,6 +1,6 @@
import useDatabase from '~/composables/useDatabase';
import { explorerContentTable } from '~/db/schema';
import type { Navigation } from '~/types/api';
import type { Navigation, NavigationItem } from '~/schemas/navigation';
export default defineEventHandler(async (e) => {
const { user } = await getUserSession(e);
@ -11,11 +11,19 @@ export default defineEventHandler(async (e) => {
}*/
const db = useDatabase();
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 })[];
const content = db.select({
path: explorerContentTable.path,
type: explorerContentTable.type,
owner: explorerContentTable.owner,
title: explorerContentTable.title,
navigable: explorerContentTable.navigable,
private: explorerContentTable.private,
order: explorerContentTable.order,
}).from(explorerContentTable).prepare().all();
if(content.length > 0)
{
const navigation: Navigation[] = [];
const navigation: Navigation = [];
for(const idx in content)
{
@ -45,7 +53,7 @@ export default defineEventHandler(async (e) => {
setResponseStatus(e, 404);
});
function addChild(arr: Navigation[], e: Navigation): void
function addChild(arr: Navigation, e: NavigationItem): void
{
const parent = arr.find(f => e.path.startsWith(f.path));
@ -58,6 +66,11 @@ function addChild(arr: Navigation[], e: Navigation): void
}
else
{
arr.push({ title: e.title, path: e.path, type: e.type, private: e.private });
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);
});
}
}

View File

@ -0,0 +1,49 @@
import { hasPermissions } from "#shared/auth.util";
import useDatabase from '~/composables/useDatabase';
import { explorerContentTable } from '~/db/schema';
import { schema } from '~/schemas/navigation';
export default defineEventHandler(async (e) => {
const { user } = await getUserSession(e);
if(!user || !hasPermissions(user.permissions, ['admin', 'editor']))
{
throw createError({ statusCode: 401, statusText: 'Unauthorized' });
}
const body = await readValidatedBody(e, schema.safeParse);
if(!body.success)
{
throw body.error;
}
const db = useDatabase();
db.transaction((tx) => {
for(let i = 0; i < body.data.length; i++)
{
tx.insert(explorerContentTable).values({
path: body.data[i].path,
owner: body.data[i].owner,
title: body.data[i].title,
type: body.data[i].type,
navigable: body.data[i].navigable,
private: body.data[i].private,
order: body.data[i].order,
content: Buffer.from('', 'utf-8'),
}).onConflictDoUpdate({
set: {
owner: body.data[i].owner,
title: body.data[i].title,
type: body.data[i].type,
navigable: body.data[i].navigable,
private: body.data[i].private,
order: body.data[i].order,
},
target: explorerContentTable.path,
})
}
})
setResponseStatus(e, 404);
});