Compare commits
23 Commits
fa2d8e5035
...
42658558c5
| Author | SHA1 | Date |
|---|---|---|
|
|
42658558c5 | |
|
|
057efb848c | |
|
|
721e7ff3db | |
|
|
41951d7603 | |
|
|
a392841012 | |
|
|
b3fae0b5db | |
|
|
1af78e5ab7 | |
|
|
83ddaf19d4 | |
|
|
e8b521f122 | |
|
|
0105a6aaea | |
|
|
633231f821 | |
|
|
5ce2d3e236 | |
|
|
8a19448a38 | |
|
|
bd32d176b1 | |
|
|
cbce979aa9 | |
|
|
f80c6d5326 | |
|
|
a5a9086eb7 | |
|
|
f37c3e4cc9 | |
|
|
4c8fb0ff77 | |
|
|
f4f4be6b27 | |
|
|
97f8ca499a | |
|
|
1b2472bc1a | |
|
|
fef8c092a9 |
|
|
@ -5,7 +5,6 @@
|
||||||
.nitro
|
.nitro
|
||||||
.cache
|
.cache
|
||||||
dist
|
dist
|
||||||
content
|
|
||||||
|
|
||||||
# Node dependencies
|
# Node dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
@ -23,5 +22,3 @@ logs
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
db.sqlite-*
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "client: chrome",
|
|
||||||
"url": "http://localhost:3000",
|
|
||||||
"webRoot": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "server: nuxt",
|
|
||||||
"outputCapture": "std",
|
|
||||||
"program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs",
|
|
||||||
"args": [
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"compounds": [
|
|
||||||
{
|
|
||||||
"name": "fullstack: nuxt",
|
|
||||||
"configurations": [
|
|
||||||
"server: nuxt",
|
|
||||||
"client: chrome"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
48
app.vue
48
app.vue
|
|
@ -1,3 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
||||||
|
<NuxtRouteAnnouncer/>
|
||||||
|
<TooltipProvider>
|
||||||
|
<NuxtLayout>
|
||||||
|
<div class="xl:ps-12 xl:pe-12 ps-6 pe-4 flex flex-1 justify-center overflow-auto max-h-full relative">
|
||||||
|
<NuxtPage></NuxtPage>
|
||||||
|
</div>
|
||||||
|
</NuxtLayout>
|
||||||
|
<Toaster v-model="list" />
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
provideToaster();
|
||||||
|
|
||||||
|
const { list } = useToast();
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
|
|
@ -18,32 +38,4 @@
|
||||||
@apply bg-light-50;
|
@apply bg-light-50;
|
||||||
@apply dark:bg-dark-50;
|
@apply dark:bg-dark-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
|
||||||
@apply w-full;
|
|
||||||
@apply h-full;
|
|
||||||
@apply flex;
|
|
||||||
@apply items-center;
|
|
||||||
@apply justify-center;
|
|
||||||
@apply before:border-4;
|
|
||||||
@apply before:border-accent-purple;
|
|
||||||
@apply before:border-t-transparent;
|
|
||||||
@apply before:rounded-full;
|
|
||||||
@apply before:w-8;
|
|
||||||
@apply before:h-8;
|
|
||||||
@apply before:animate-spin;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const page = ref();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
|
||||||
<NuxtRouteAnnouncer></NuxtRouteAnnouncer>
|
|
||||||
<NuxtLayout>
|
|
||||||
<NuxtPage ref="page"></NuxtPage>
|
|
||||||
</NuxtLayout>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
### Librairies, framework et outils libres utilisés:
|
|
||||||
- [Vuejs](https://vuejs.org/) - MIT License - Copyright (c) 2018-present, Yuxi (Evan) You and Vue contributors
|
|
||||||
- [vue-router](https://router.vuejs.org/) - MIT License - Copyright (c) 2019-present Eduardo San Martin Morote
|
|
||||||
- [Nuxt](https://nuxt.com/) - MIT License - Copyright (c) 2016-present - Nuxt Team
|
|
||||||
- [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils) - MIT License - Copyright (c) 2023 Sébastien Chopin
|
|
||||||
|
|
||||||
Le logo a été créé grace aux icones de [Game Icons](https://game-icons.net).
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
<style>
|
|
||||||
.editor
|
|
||||||
{
|
|
||||||
white-space: pre-line;
|
|
||||||
overflow: auto;
|
|
||||||
outline: none;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="editor">
|
|
||||||
<template
|
|
||||||
v-if="model && model.length > 0">
|
|
||||||
<MarkdownRenderer
|
|
||||||
v-if="node"
|
|
||||||
:key="key"
|
|
||||||
:node="node"
|
|
||||||
:proses="{
|
|
||||||
'a': LiveA,
|
|
||||||
'h1': LiveH1,
|
|
||||||
'h2': LiveH2,
|
|
||||||
'h3': LiveH3,
|
|
||||||
'h4': LiveH4,
|
|
||||||
'h5': LiveH5,
|
|
||||||
'h6': LiveH6,
|
|
||||||
'blockquote': LiveBlockquote,
|
|
||||||
}"
|
|
||||||
></MarkdownRenderer>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import LiveA from "~/components/prose/live/LiveA.vue";
|
|
||||||
import LiveH1 from "~/components/prose/live/LiveH1.vue";
|
|
||||||
import LiveH2 from "~/components/prose/live/LiveH2.vue";
|
|
||||||
import LiveH3 from "~/components/prose/live/LiveH3.vue";
|
|
||||||
import LiveH4 from "~/components/prose/live/LiveH4.vue";
|
|
||||||
import LiveH5 from "~/components/prose/live/LiveH5.vue";
|
|
||||||
import LiveH6 from "~/components/prose/live/LiveH6.vue";
|
|
||||||
import LiveBlockquote from "~/components/prose/ProseBlockquote.vue";
|
|
||||||
|
|
||||||
import { hash } from 'ohash'
|
|
||||||
import { watch, computed } from 'vue'
|
|
||||||
import type { Root, Node } from 'hast';
|
|
||||||
/* import { diffLines as diff } from 'diff'; */
|
|
||||||
|
|
||||||
const model = defineModel<string>();
|
|
||||||
|
|
||||||
const parser = useMarkdown();
|
|
||||||
const key = computed(() => hash(model.value));
|
|
||||||
|
|
||||||
const node = ref<Root>(), changes = ref();
|
|
||||||
|
|
||||||
watch(model, update);
|
|
||||||
update(model.value, "");
|
|
||||||
|
|
||||||
async function update(value: string | undefined, old: string | undefined) {
|
|
||||||
if(value && old)
|
|
||||||
{
|
|
||||||
if(node.value)
|
|
||||||
{
|
|
||||||
/* const differences = diff(old, value, {
|
|
||||||
newlineIsToken: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let removeStart = 0, removeEnd = 0; //Character count
|
|
||||||
let addStart = 0, addEnd = 0; //Character count
|
|
||||||
|
|
||||||
const needAdd = differences.find(e => e.added) !== undefined;
|
|
||||||
const needRemove = differences.find(e => e.removed) !== undefined;
|
|
||||||
|
|
||||||
for(const difference of differences)
|
|
||||||
{
|
|
||||||
if(!difference.added && !difference.removed)
|
|
||||||
{
|
|
||||||
removeStart += difference.value.length;
|
|
||||||
addStart += difference.value.length;
|
|
||||||
}
|
|
||||||
else if(difference.added)
|
|
||||||
{
|
|
||||||
addEnd = addStart + difference.value.length;
|
|
||||||
}
|
|
||||||
else if(difference.removed)
|
|
||||||
{
|
|
||||||
removeEnd = removeStart + difference.value.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if((!needAdd || addEnd !== 0) && (!needRemove || removeEnd !== 0))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldNodes = getNodes(node.value.children, removeStart - 1, removeEnd + 1);
|
|
||||||
let newNodes;
|
|
||||||
|
|
||||||
if(oldNodes.length === 0)
|
|
||||||
{
|
|
||||||
node.value = parser(value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const newStart = oldNodes[0].position?.start.offset;
|
|
||||||
const newEnd = oldNodes[oldNodes.length - 1].position?.end.offset;
|
|
||||||
|
|
||||||
const lengthDiff = value.length - old.length;
|
|
||||||
|
|
||||||
newNodes = parser(value.substring(newStart ?? 0, (newEnd ?? 0) + lengthDiff));
|
|
||||||
|
|
||||||
const root = node.value;
|
|
||||||
|
|
||||||
node.value = parser(value);
|
|
||||||
} */
|
|
||||||
node.value = parser(value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
node.value = parser(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(value)
|
|
||||||
{
|
|
||||||
node.value = parser(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getNodes(nodes: Node[], start: number, end: number)
|
|
||||||
{
|
|
||||||
return nodes.filter(e => (e.position?.start.offset ?? 0) <= end && (e.position?.end.offset ?? 0) >= start);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -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 #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,16 @@
|
||||||
|
<script setup>
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
const isDark = computed({
|
||||||
|
get() {
|
||||||
|
return colorMode.value === 'dark'
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Switch v-model="isDark" onIcon="radix-icons:moon" offIcon="radix-icons:sun" />
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<AvatarRoot class="inline-flex h-12 w-12 select-none items-center justify-center overflow-hidden align-middle">
|
||||||
|
<AvatarImage class="h-full w-full object-cover" :src="src" asChild @loading-status-change="(status) => loading = status === 'loading'">
|
||||||
|
<img :src="src" />
|
||||||
|
</AvatarImage>
|
||||||
|
<AvatarFallback :delay-ms="0" class="text-light-100 dark:text-dark-100 leading-1 flex h-full w-full p-4 items-center justify-center bg-light-25 dark:bg-dark-25 font-medium">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<Icon v-else-if="!!icon" :icon="icon" class="w-full h-full" />
|
||||||
|
<span v-else-if="!!text">{{ text }}</span>
|
||||||
|
</AvatarFallback>
|
||||||
|
</AvatarRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
const { src, icon, text } = defineProps<{
|
||||||
|
src: string
|
||||||
|
icon?: string
|
||||||
|
text?: string
|
||||||
|
}>();
|
||||||
|
const loading = ref(true);
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<button :disabled="disabled" class="text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
||||||
|
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
|
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50"
|
||||||
|
:class="{'p-1': loading || icon, 'h-[35px] px-[15px]': !loading && !icon}" @click="!loading && emit('click')">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<slot v-else />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { icon = false, loading = false, disabled = false } = defineProps<{
|
||||||
|
icon?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits(['click']);
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<CollapsibleRoot v-model:open="model" :disabled="disabled">
|
||||||
|
<div class="flex flex-row justify-center items-center">
|
||||||
|
<span v-if="!!label">{{ label }}</span>
|
||||||
|
<CollapsibleTrigger class="ms-4" asChild>
|
||||||
|
<Button icon :disabled="disabled">
|
||||||
|
<Icon v-if="model" icon="radix-icons:cross-2" class="h-4 w-4" />
|
||||||
|
<Icon v-else icon="radix-icons:row-spacing" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</div>
|
||||||
|
<slot name="alwaysVisible"></slot>
|
||||||
|
<CollapsibleContent class="overflow-hidden data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out]">
|
||||||
|
<slot></slot>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</CollapsibleRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
const { label, disabled = false } = defineProps<{
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}>();
|
||||||
|
const model = defineModel<boolean>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes collapseOpen {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: var(--radix-collapsible-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes collapseClose {
|
||||||
|
from {
|
||||||
|
height: var(--radix-collapsible-content-height);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<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">
|
||||||
|
<DialogTitle v-if="!!title" class="text-3xl font-light relative -top-2">{{ title }}</DialogTitle>
|
||||||
|
<DialogDescription v-if="!!description" class="text-base pb-2">{{ description }}</DialogDescription>
|
||||||
|
<slot />
|
||||||
|
<DialogClose v-if="iconClose" class="text-light-100 dark:text-dark-100 absolute top-2 right-2 inline-flex h-6 w-6 appearance-none items-center justify-center outline-none text-xl" aria-label="Close">
|
||||||
|
<span aria-hidden>×</span>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</DialogRoot>
|
||||||
|
<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">
|
||||||
|
<AlertDialogTitle v-if="!!title" class="text-3xl font-light relative -top-2">{{ title }}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription v-if="!!description" class="text-base pb-2">{{ description }}</AlertDialogDescription>
|
||||||
|
<slot />
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
</AlertDialogRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { label, title, description, priority = false, disabled = false, iconClose = true } = defineProps<{
|
||||||
|
label?: string
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
priority?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
iconClose?: boolean
|
||||||
|
}>();
|
||||||
|
const model = defineModel();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<HoverCardRoot :open-delay="delay">
|
||||||
|
<HoverCardTrigger class="inline-block cursor-help outline-none">
|
||||||
|
<slot></slot>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardPortal v-if="!disabled">
|
||||||
|
<HoverCardContent :class="$attrs.class" :side="side" class="data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 p-5 data-[state=open]:transition-all text-light-100 dark:text-dark-100" >
|
||||||
|
<slot name="content"></slot>
|
||||||
|
<HoverCardArrow class="fill-light-35 dark:fill-dark-35" />
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCardPortal>
|
||||||
|
</HoverCardRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { delay = 500, disabled = false, side = 'bottom' } = defineProps<{
|
||||||
|
delay?: number
|
||||||
|
disabled?: boolean
|
||||||
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<span :class="{'w-6 h-6 border-4 after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 border-2 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 border-[6px] after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}" class="rounded-full border-light-35 dark:border-dark-35 after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin"></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { size = 'normal' } = defineProps<{
|
||||||
|
size?: 'small' | 'normal' | 'large'
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<Label class="my-2 flex">{{ label }}
|
||||||
|
<NumberFieldRoot :min="min" :max="max" v-model="model" :disabled="disabled" :step="step" class="ms-4 flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
|
||||||
|
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
|
||||||
|
<NumberFieldDecrement class="data-[disabled]:opacity-50 px-1"><Icon icon="radix-icons:minus" :inline="true" class="w-6 text-light-100 dark:text-dark-100 opacity-100" /></NumberFieldDecrement>
|
||||||
|
<NumberFieldInput class="text-sm tabular-nums w-20 appearance-none bg-transparent px-2 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
||||||
|
<NumberFieldIncrement class="data-[disabled]:opacity-50 px-1"><Icon icon="radix-icons:plus" :inline="true" class="w-6 text-light-100 dark:text-dark-100 opacity-100" /></NumberFieldIncrement>
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
const { min = 0, max = 100, disabled = false, step = 1, label } = defineProps<{
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
disabled?: boolean
|
||||||
|
step?: number
|
||||||
|
label?: string
|
||||||
|
}>();
|
||||||
|
const model = defineModel<number>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<Label class="my-2">{{ label }}
|
||||||
|
<PinInputRoot :disabled="disabled" :default-value="model?.split('')" @update:model-value="(v) => model = v.join('')" @complete="() => emit('complete')" class="flex gap-2 items-center mt-1">
|
||||||
|
<PinInputInput :type="hidden ? 'password' : undefined" v-for="(id, index) in amount" :key="id" :index="index" class="border border-light-35 dark:border-dark-35 w-10 h-10 outline-none
|
||||||
|
bg-light-20 dark:bg-dark-20 text-center text-light-100 dark:text-dark-100 placeholder:text-light-60 dark:placeholder:text-dark-60 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
|
||||||
|
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 caret-light-50 dark:caret-dark-50" />
|
||||||
|
</PinInputRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { hidden = false, amount, label, disabled = false } = defineProps<{
|
||||||
|
hidden?: boolean
|
||||||
|
label?: string
|
||||||
|
amount: number
|
||||||
|
disabled?: boolean
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string>();
|
||||||
|
const emit = defineEmits(['complete']);
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
<ProgressRoot :max="max" v-model="model" class="my-2 relative overflow-hidden bg-light-25 dark:bg-dark-25 w-48 h-3 data-[shape=thin]:h-1 data-[shape=large]:h-6" :data-shape="shape" style="transform: translateZ(0)" >
|
||||||
|
<ProgressIndicator class="bg-light-50 dark:bg-dark-50 h-full transition-[width] duration-[660ms] ease-[cubic-bezier(0.65, 0, 0.35, 1)]" :style="`width: ${((model ?? 0) / max) * 100}%`" />
|
||||||
|
</ProgressRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { max = 100, shape = 'normal' } = defineProps<{
|
||||||
|
max?: number
|
||||||
|
shape?: 'thin' | 'normal' | 'large'
|
||||||
|
}>();
|
||||||
|
const model = defineModel<number>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<RadioGroupRoot :disabled="disabled" v-model="model" class="flex flex-col gap-2 p-2">
|
||||||
|
<Label v-for="option in options" class="flex items-center gap-2">
|
||||||
|
<RadioGroupItem :disabled="(option as RadioOption).disabled ?? false"
|
||||||
|
:value="(option as RadioOption).value ?? option"
|
||||||
|
class="border border-light-60 dark:border-dark-35 bg-light-20 dark:bg-dark-25 w-5 h-5 outline-none cursor-default hover:border-light-70 dark:hover:border-dark-40
|
||||||
|
focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 data-[disabled]:bg-light-10 dark:data-[disabled]:bg-dark-10 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20">
|
||||||
|
<RadioGroupIndicator>
|
||||||
|
<Icon icon="radix-icons:check" class="relative w-5 h-5 -top-px -left-px" />
|
||||||
|
</RadioGroupIndicator>
|
||||||
|
</RadioGroupItem>
|
||||||
|
{{ (option as RadioOption).label ?? option }}
|
||||||
|
</Label>
|
||||||
|
</RadioGroupRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
export interface RadioOption {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const { disabled = false, options } = defineProps<{
|
||||||
|
disabled?: boolean
|
||||||
|
options: string[] | RadioOption[]
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<Label class="py-4 flex flex-row justify-center items-center">
|
||||||
|
<span>{{ 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
|
||||||
|
data-[placeholder]:text-light-50 dark:data-[placeholder]:text-dark-50 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
|
hover:border-light-50 dark:hover:border-dark-50">
|
||||||
|
<SelectValue :placeholder="placeholder" />
|
||||||
|
<Icon icon="radix-icons:caret-down" class="h-4 w-4" />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectPortal :disabled="disabled">
|
||||||
|
<SelectContent :position="position"
|
||||||
|
class="min-w-[160px] bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] z-40">
|
||||||
|
<SelectScrollUpButton>
|
||||||
|
<Icon icon="radix-icons:chevron-up" />
|
||||||
|
</SelectScrollUpButton>
|
||||||
|
<SelectViewport>
|
||||||
|
<slot />
|
||||||
|
</SelectViewport>
|
||||||
|
<SelectScrollDownButton>
|
||||||
|
<Icon icon="radix-icons:chevron-down" />
|
||||||
|
</SelectScrollDownButton>
|
||||||
|
</SelectContent>
|
||||||
|
</SelectPortal>
|
||||||
|
</SelectRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SelectContent, SelectPortal, SelectRoot, SelectScrollDownButton, SelectScrollUpButton, SelectTrigger, SelectValue, SelectViewport } from 'radix-vue'
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
const { placeholder, disabled = false, position = 'popper', label } = defineProps<{
|
||||||
|
placeholder?: string
|
||||||
|
disabled?: boolean
|
||||||
|
position?: 'item-aligned' | 'popper'
|
||||||
|
label?: string
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<SelectGroup :disabled="disabled" class="">
|
||||||
|
<SelectLabel class="">{{ label }}</SelectLabel>
|
||||||
|
<slot />
|
||||||
|
</SelectGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SelectGroup } from 'radix-vue';
|
||||||
|
const { label, disabled = false } = defineProps<{
|
||||||
|
label: string
|
||||||
|
disabled?: boolean
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<SelectItem :value="value" :disabled="disabled" class="text-base py-2 leading-none text-light-60 dark:text-dark-60 flex items-center px-6 relative select-none data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-light-30 dark:data-[highlighted]:bg-dark-30 data-[highlighted]:text-light-100 dark:data-[highlighted]:text-dark-100">
|
||||||
|
<SelectItemText class="">{{ label }}</SelectItemText>
|
||||||
|
<SelectItemIndicator class="absolute left-1 w-4 inline-flex items-center justify-center">
|
||||||
|
<Icon icon="radix-icons:check" />
|
||||||
|
</SelectItemIndicator>
|
||||||
|
</SelectItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import { SelectItem, SelectItemIndicator, SelectItemText } from 'radix-vue'
|
||||||
|
const { disabled = false, value } = defineProps<{
|
||||||
|
disabled?: boolean
|
||||||
|
value: NonNullable<any>
|
||||||
|
label: string
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<SelectSeparator class="" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SelectSeparator } from 'radix-vue';
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<Label class="flex justify-center items-center my-2">{{ label }}
|
||||||
|
<SliderRoot class="mx-4 relative flex items-center select-none touch-none w-[160px] h-5"
|
||||||
|
:default-value="model ? [model] : undefined" :v-model="[model]" :disabled="disabled"
|
||||||
|
@update:model-value="(value) => model = value ? value[0] : min" :min="min" :max="max" :step="step">
|
||||||
|
<SliderTrack class="bg-light-30 dark:bg-dark-30 relative h-1 w-full data-[disabled]:bg-light-10 dark:data-[disabled]:bg-dark-10">
|
||||||
|
<SliderRange class="absolute bg-light-40 dark:bg-dark-40 h-full data-[disabled]:bg-light-30 dark:data-[disabled]:bg-dark-30" />
|
||||||
|
</SliderTrack>
|
||||||
|
<SliderThumb
|
||||||
|
class="block w-5 h-5 bg-light-50 dark:bg-dark-50 outline-none focus:shadow-raw transition-[box-shadow] focus:shadow-light-60 dark:focus:shadow-dark-60 border border-light-50 dark:border-dark-50
|
||||||
|
hover:border-light-60 dark:hover:border-dark-60 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20" />
|
||||||
|
</SliderRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { min = 0, max = 100, step = 1, label, disabled = false } = defineProps<{
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
step?: number
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}>();
|
||||||
|
const model = defineModel<number>()
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<Label class="flex justify-center items-center my-2">{{ label }}
|
||||||
|
<SwitchRoot v-model:checked="model" :disabled="disabled"
|
||||||
|
class="group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
||||||
|
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
|
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative">
|
||||||
|
<SwitchThumb
|
||||||
|
class="block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 data-[state=checked]:translate-x-[26px]
|
||||||
|
data-[disabled]:bg-light-30 dark:data-[disabled]:bg-dark-30 data-[disabled]:border-light-30 dark:data-[disabled]:border-dark-30" />
|
||||||
|
<Icon v-if="onIcon && offIcon" :icon="onIcon" class="group-data-[state=checked]:opacity-100 group-data-[state=unchecked]:opacity-0 absolute top-1 left-1 transition-opacity" />
|
||||||
|
<Icon v-if="onIcon && offIcon" :icon="offIcon" class="group-data-[state=checked]:opacity-0 group-data-[state=unchecked]:opacity-100 absolute top-1 right-1 transition-opacity" />
|
||||||
|
</SwitchRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
const { label, disabled, onIcon, offIcon } = defineProps<{
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
onIcon?: string
|
||||||
|
offIcon?: string
|
||||||
|
}>();
|
||||||
|
const model = defineModel<boolean>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<input :placeholder="placeholder" :disabled="disabled"
|
||||||
|
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">
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { type = 'text', label, disabled = false, placeholder } = defineProps<{
|
||||||
|
type?: 'text' | 'password' | 'email' | 'tel' | 'url'
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<ProgressRoot class="my-2 relative overflow-hidden bg-light-25 dark:bg-dark-25 w-48 h-3 data-[shape=thin]:h-1 data-[shape=large]:h-6" :data-shape="shape" style="transform: translateZ(0)" >
|
||||||
|
<ProgressIndicator class="bg-light-50 dark:bg-dark-50 h-full w-0 transition-[width] ease-linear" :style="`transition-duration: ${delay}ms; width: ${progress ? 100 : 0}%`" />
|
||||||
|
</ProgressRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { delay = 1500, decreasing = false, shape = 'normal' } = defineProps<{
|
||||||
|
delay?: number
|
||||||
|
decreasing?: boolean
|
||||||
|
shape?: 'thin' | 'normal' | 'large'
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['finish']);
|
||||||
|
|
||||||
|
const progress = ref(false);
|
||||||
|
nextTick(() => {
|
||||||
|
progress.value = true;
|
||||||
|
setTimeout(emit, delay, 'finish');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<ToastProvider>
|
||||||
|
<ToastRoot v-for="toast in model" :key="toast.id" :duration="toast.duration" class="ToastRoot bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 group" :open="toast.state ?? true" @update:open="(state: boolean) => tryClose(toast, state)" :data-type="toast.type ?? 'info'">
|
||||||
|
<div class="grid grid-cols-8 px-3 pt-2 pb-2">
|
||||||
|
<ToastTitle v-if="toast.title" class="font-semibold text-xl col-span-7 text-light-70 dark:text-dark-70" asChild><h4>{{ toast.title }}</h4></ToastTitle>
|
||||||
|
<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-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" />
|
||||||
|
</ToastProvider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const model = defineModel<ExtraToastConfig[]>();
|
||||||
|
|
||||||
|
function tryClose(config: ExtraToastConfig, state: boolean)
|
||||||
|
{
|
||||||
|
if(!state)
|
||||||
|
{
|
||||||
|
const m = model.value;
|
||||||
|
if(m)
|
||||||
|
{
|
||||||
|
const idx = m?.findIndex(e => e.id === config.id);
|
||||||
|
m[idx].state = false;
|
||||||
|
model.value = m;
|
||||||
|
}
|
||||||
|
setTimeout(() => model.value?.splice(model.value?.findIndex(e => e.id === config.id), 1), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ToastRoot[data-type='error'] {
|
||||||
|
@apply border-light-red;
|
||||||
|
@apply dark:border-dark-red;
|
||||||
|
@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-greenBack;
|
||||||
|
@apply dark:bg-dark-greenBack;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-state='open'] {
|
||||||
|
animation: slideIn .15s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.ToastRoot[data-state='closed'] {
|
||||||
|
animation: hide .1s ease-in;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='move'] {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='cancel'] {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform .2s ease-out;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='end'] {
|
||||||
|
animation: swipeRight .1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hide {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes swipeRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<TooltipRoot :delay-duration="delay" :disabled="disabled">
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span tabindex="0"><slot></slot></span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPortal>
|
||||||
|
<TooltipContent class="TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50" :side="side" :side-offset="['left', 'right'].includes(side ?? '') ? 8 : 0">
|
||||||
|
{{ message }}
|
||||||
|
<TooltipArrow class="fill-light-30 dark:fill-dark-30"></TooltipArrow>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
|
</TooltipRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { message, delay = 300, side } = defineProps<{
|
||||||
|
message: string
|
||||||
|
delay?: number,
|
||||||
|
disabled?: boolean
|
||||||
|
side?: 'left' | 'right' | 'top' | 'bottom'
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.TooltipContent {
|
||||||
|
animation-duration: .3s;
|
||||||
|
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.TooltipContent[data-side="top"] {
|
||||||
|
animation-name: slideUp;
|
||||||
|
}
|
||||||
|
.TooltipContent[data-side="bottom"] {
|
||||||
|
animation-name: slideDown;
|
||||||
|
}
|
||||||
|
.TooltipContent[data-side="left"] {
|
||||||
|
animation-name: slideLeft;
|
||||||
|
}
|
||||||
|
.TooltipContent[data-side="right"] {
|
||||||
|
animation-name: slideRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes slideRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideLeft {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDrag, usePinch, useWheel } from '@vueuse/gesture';
|
import { useDrag, usePinch, useWheel } from '@vueuse/gesture';
|
||||||
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import { clamp } from '#imports';
|
import { clamp } from '#imports';
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
|
|
@ -180,43 +181,26 @@ const wheelHandler = useWheel(({ event: Event, delta: [x, y] }: { event: Event,
|
||||||
<div id="canvas" ref="canvas" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none"
|
<div id="canvas" ref="canvas" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none"
|
||||||
:style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
:style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 absolute sm:top-2 top-10 left-2 z-30 overflow-hidden">
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 absolute sm:top-2 top-10 left-2 z-30 overflow-hidden">
|
||||||
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer"
|
<Tooltip message="Zoom avant" side="right">
|
||||||
aria-label="Zoom in" data-tooltip-position="left">
|
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
<Icon icon="radix-icons:plus" />
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round">
|
|
||||||
<path d="M5 12h14"></path>
|
|
||||||
<path d="M12 5v14"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div @click="zoom = 1" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" aria-label="Reset zoom"
|
</Tooltip>
|
||||||
data-tooltip-position="left">
|
<Tooltip message="Reset" side="right">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
<div @click="zoom = 1" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
<Icon icon="radix-icons:reload" />
|
||||||
stroke-linejoin="round">
|
|
||||||
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
|
|
||||||
<path d="M21 3v5h-5"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div @click="reset" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" aria-label="Zoom to fit"
|
</Tooltip>
|
||||||
data-tooltip-position="left">
|
<Tooltip message="Tout contenir" side="right">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
<div @click="reset" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
<Icon icon="radix-icons:corners" />
|
||||||
stroke-linejoin="round">
|
|
||||||
<path d="M8 3H5a2 2 0 0 0-2 2v3"></path>
|
|
||||||
<path d="M21 8V5a2 2 0 0 0-2-2h-3"></path>
|
|
||||||
<path d="M3 16v3a2 2 0 0 0 2 2h3"></path>
|
|
||||||
<path d="M16 21h3a2 2 0 0 0 2-2v-3"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div @click="zoom = clamp(zoom * 0.9, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer"
|
</Tooltip>
|
||||||
aria-label="Zoom out" data-tooltip-position="left">
|
<Tooltip message="Zoom arrière" side="right">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
<div @click="zoom = clamp(zoom * 0.9, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
<Icon icon="radix-icons:minus" />
|
||||||
stroke-linejoin="round">
|
|
||||||
<path d="M5 12h14"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-0 left-0 w-full h-full origin-center pointer-events-none *:pointer-events-auto *:select-none"
|
<div class="absolute top-0 left-0 w-full h-full origin-center pointer-events-none *:pointer-events-auto *:select-none"
|
||||||
:style="{transform: `scale(${zoom}) translate(${dispX}px, ${dispY}px)`}">
|
:style="{transform: `scale(${zoom}) translate(${dispX}px, ${dispY}px)`}">
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import type { CanvasNode } from '~/types/canvas';
|
import type { CanvasNode } from '~/types/canvas';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -41,15 +42,7 @@ const colors = computed(() => {
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex flex-1 justify-center items-center bg-light-30 dark:bg-dark-30">
|
<div class="flex flex-1 justify-center items-center bg-light-30 dark:bg-dark-30">
|
||||||
<div class="">
|
<Icon icon="radix-icons:text-align-left" class="w-8 h-8"/>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
||||||
class="svg-icon lucide-align-left">
|
|
||||||
<line x1="21" y1="6" x2="3" y2="6"></line>
|
|
||||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="17" y1="18" x2="3" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
comments: {
|
|
||||||
type: [Object],
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="comments !== undefined && comments !== null" class="site-body-right-column">
|
|
||||||
<div class="site-body-right-column-inner">
|
|
||||||
<div>
|
|
||||||
{{ comments.length }} commentaire{{ comments.length === 1 ? '' : 's' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const route = useRoute();
|
|
||||||
const project = computed(() => parseInt(route.params.projectId as string));
|
|
||||||
|
|
||||||
const { data: navigation, refresh } = await useFetch(() => `/api/project/${project.value}/navigation`, { immediate: false });
|
|
||||||
if(!isNaN(project.value))
|
|
||||||
{
|
|
||||||
await refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits(['navigate']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="relative flex-auto">
|
|
||||||
<div class="absolute top-0 bottom-0 left-0 right-0 overflow-auto xl:px-4 px-2">
|
|
||||||
<NavigationLink @navigate="e => emit('navigate')" class="ps-2" v-if="!!navigation" v-for="link of navigation" :project="project" :link="link" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Navigation } from '~/types/api';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
link: Navigation;
|
|
||||||
project?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
const hasChildren = computed(() => {
|
|
||||||
return props.link && props.link.children && props.link.children.length > 0 || false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const collapsed = ref(!unifySlug(useRoute().params.slug).startsWith(props.link.path));
|
|
||||||
|
|
||||||
const emit = defineEmits(['navigate']);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="py-1" v-if="project && !isNaN(project)">
|
|
||||||
<Accordion v-if="hasChildren" v-model="collapsed" class="data-[type]:after:content-[attr(data-type)] data-[type]:after:border data-[type]:after:border-light-35 data-[type]:dark:after:border-dark-35
|
|
||||||
data-[type]:after:text-sm data-[type]:after:px-1 data-[type]:after:bg-light-20 data-[type]:dark:after:bg-dark-20 data-[type]:after:[font-variant:all-petite-caps]">
|
|
||||||
<template #header>{{ link.title }}</template>
|
|
||||||
<template #content >
|
|
||||||
<NavigationLink @navigate="e => emit('navigate')" class="border-light-40 dark:border-dark-40 ms-2 ps-4 border-l" v-if="hasChildren" v-for="l of link.children" :link="l" :project="project" />
|
|
||||||
</template>
|
|
||||||
</Accordion>
|
|
||||||
<NuxtLink v-else @click="e => emit('navigate')" class="text-light-100 dark:text-dark-100 cursor-pointer hover:text-opacity-75 xl:text-base text-sm flex justify-between items-center
|
|
||||||
data-[type]:after:content-[attr(data-type)] data-[type]:after:border data-[type]:after:border-light-35 data-[type]:dark:after:border-dark-35
|
|
||||||
data-[type]:after:text-sm data-[type]:after:px-1 data-[type]:after:bg-light-20 data-[type]:dark:after:bg-dark-20 data-[type]:after:[font-variant:all-petite-caps]" :to="{ path: `/explorer/${project}/${link.path}`, force: true }"
|
|
||||||
active-class="!text-accent-blue relative before:border-l-2 before:border-accent-blue before:absolute before:-top-1 before:-bottom-1 before:-left-4 before:-mx-px" :data-type="(link.type === 'Canvas' ? 'graph' : (link.private ? 'privé' : undefined))">
|
|
||||||
<div>{{ link.title }}</div>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
<template>
|
|
||||||
<HoverPopup @before-show="fetch">
|
|
||||||
<template #content>
|
|
||||||
<Suspense suspensible>
|
|
||||||
<div :class="[{'is-loaded': fetched}, file?.type === 'Markdown' ? 'overflow-auto' : 'overflow-hidden']">
|
|
||||||
<div v-if="pending" class="loading w-[550px] h-[450px]"></div>
|
|
||||||
<template v-else-if="!!file">
|
|
||||||
<div v-if="file.type === 'Markdown'" class="p-6 ms-6">
|
|
||||||
<ProseH1>{{ file.title }}</ProseH1>
|
|
||||||
<Markdown :content="file.content"></Markdown>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="file.type === 'Canvas'" class="w-[550px] h-[450px] overflow-hidden">
|
|
||||||
<CanvasRenderer :canvas="JSON.parse(file.content) " />
|
|
||||||
</div>
|
|
||||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
|
||||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Fichier vide</div>
|
|
||||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est vide</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
|
||||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Impossible d'afficher</div>
|
|
||||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est impossible à traiter</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #fallback><div class="loading w-[550px] h-[450px]"></div></template>
|
|
||||||
</Suspense>
|
|
||||||
</template>
|
|
||||||
<template #default><slot name="default"></slot></template>
|
|
||||||
</HoverPopup>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { CommentedFile } from '~/types/api';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
project: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
anchor: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const file = ref<CommentedFile | null>();
|
|
||||||
const pending = ref(false), fetched = ref(false);
|
|
||||||
|
|
||||||
async function fetch()
|
|
||||||
{
|
|
||||||
if(fetched.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
fetched.value = true;
|
|
||||||
pending.value = true;
|
|
||||||
|
|
||||||
const data = await $fetch(`/api/project/${props.project}/file/${encodeURIComponent(props.path)}`);
|
|
||||||
|
|
||||||
pending.value = false;
|
|
||||||
file.value = data;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,50 +1,60 @@
|
||||||
<template>
|
<template>
|
||||||
<Suspense suspensible>
|
|
||||||
<NuxtLink no-prefetch class="text-accent-blue inline-flex items-center" v-if="data && data[0]"
|
<NuxtLink no-prefetch class="text-accent-blue inline-flex items-center" v-if="data && data[0]"
|
||||||
:to="{ path: `/explorer/${project}/${data[0].path}`, hash: hash }" :class="class">
|
:to="{ name: 'explore-path', params: { path: data[0].path }, hash: hash }" :class="class">
|
||||||
<PreviewContent :project="project" :path="data[0].path" :anchor="hash">
|
<HoverCard class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': data[0].type === 'canvas'}">
|
||||||
|
<template #content>
|
||||||
|
<template v-if="data[0].type === 'markdown'">
|
||||||
|
<div class="px-10">
|
||||||
|
<Markdown :content="data[0].content" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="data[0].type === 'canvas'">
|
||||||
|
<div class="w-[600px] h-[600px] relative">
|
||||||
|
<Canvas :canvas="JSON.parse(data[0].content)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<slot v-bind="$attrs"></slot>
|
<slot v-bind="$attrs"></slot>
|
||||||
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'Markdown'" :height="20" :width="20"
|
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'markdown'" :icon="iconByType[data[0].type]" />
|
||||||
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
|
||||||
</template>
|
</template>
|
||||||
</PreviewContent>
|
</HoverCard>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink no-prefetch v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
<NuxtLink no-prefetch v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
||||||
<slot v-bind="$attrs"></slot>
|
<slot v-bind="$attrs"></slot>
|
||||||
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'Markdown'" :height="20" :width="20"
|
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'markdown'" :height="20" :width="20"
|
||||||
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<slot :class="class" v-else v-bind="$attrs"></slot>
|
<slot :class="class" v-else v-bind="$attrs"></slot>
|
||||||
</Suspense>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { parseURL } from 'ufo';
|
import { parseURL } from 'ufo';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const iconByType: Record<string, string> = {
|
||||||
href: {
|
'folder': 'circum:folder-on',
|
||||||
type: String,
|
'canvas': 'ph:graph-light',
|
||||||
required: false,
|
'file': 'radix-icons:file',
|
||||||
},
|
}
|
||||||
class: {
|
const { href } = defineProps<{
|
||||||
type: String,
|
href: string
|
||||||
required: false,
|
class?: string
|
||||||
}
|
}>();
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
const { hash, pathname, protocol } = parseURL(href);
|
||||||
const { hash, pathname, protocol } = parseURL(props.href);
|
const data = ref(), loading = ref(false);
|
||||||
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
|
|
||||||
const data = ref();
|
|
||||||
|
|
||||||
if(!!pathname && !protocol)
|
if(!!pathname && !protocol)
|
||||||
{
|
{
|
||||||
data.value = await $fetch(`/api/project/${project.value}/file`, {
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
data.value = await $fetch(`/api/file`, {
|
||||||
query: {
|
query: {
|
||||||
search: `%${pathname}`
|
search: `%${pathname}`
|
||||||
},
|
},
|
||||||
ignoreResponseError: true,
|
|
||||||
});
|
});
|
||||||
|
} catch(e) { }
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -162,17 +162,14 @@ blockquote:empty
|
||||||
@apply w-6;
|
@apply w-6;
|
||||||
@apply h-6;
|
@apply h-6;
|
||||||
@apply stroke-2;
|
@apply stroke-2;
|
||||||
}
|
@apply float-start;
|
||||||
.callout-title
|
@apply me-2;
|
||||||
{
|
|
||||||
@apply flex;
|
|
||||||
@apply items-center;
|
|
||||||
@apply gap-2;
|
|
||||||
}
|
}
|
||||||
.callout-title-inner
|
.callout-title-inner
|
||||||
{
|
{
|
||||||
@apply inline-block;
|
@apply block;
|
||||||
@apply font-bold;
|
@apply font-bold;
|
||||||
|
@apply ps-8;
|
||||||
}
|
}
|
||||||
.callout > p
|
.callout > p
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2 relative sm:right-8 right-4">
|
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2 relative lg:right-8 sm:right-4 right-2">
|
||||||
<slot />
|
<slot />
|
||||||
</h1>
|
</h1>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2 relative sm:right-8 right-4">
|
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2 relative sm:right-4 right-2">
|
||||||
<slot />
|
<slot />
|
||||||
</h2>
|
</h2>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
<template>
|
<template>
|
||||||
<hr class="border-light-35 dark:border-dark-35 m-4">
|
<Separator class="border-light-35 dark:border-dark-35 m-4" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<HoverPopup @before-show="fetch">
|
<!-- <HoverPopup @before-show="fetch">
|
||||||
<template #content>
|
<template #content>
|
||||||
<Suspense suspensible>
|
<Suspense suspensible>
|
||||||
<div class="mw-[400px]">
|
<div class="mw-[400px]">
|
||||||
|
|
@ -27,10 +27,13 @@
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</HoverPopup>
|
</HoverPopup> -->
|
||||||
|
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<!-- <script setup lang="ts">
|
||||||
import type { Tag } from '~/types/api';
|
import type { Tag } from '~/types/api';
|
||||||
|
|
||||||
const { tag } = defineProps({
|
const { tag } = defineProps({
|
||||||
|
|
@ -51,4 +54,4 @@ async function fetch()
|
||||||
data.value = await $fetch(`/api/project/${project.value}/tags/${encodeURIComponent(tag)}`);
|
data.value = await $fetch(`/api/project/${project.value}/tags/${encodeURIComponent(tag)}`);
|
||||||
fetched.value = true;
|
fetched.value = true;
|
||||||
}
|
}
|
||||||
</script>
|
</script> -->
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<span class="text-accent-blue inline-flex items-center cursor-pointer">
|
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
<template>
|
|
||||||
<blockquote ref="el">
|
|
||||||
<slot />
|
|
||||||
</blockquote>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const attrs = useAttrs(), el = ref<HTMLQuoteElement>(), title = ref<Element | null>(null);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if(el && el.value && attrs.hasOwnProperty("dataCalloutFold"))
|
|
||||||
{
|
|
||||||
title.value = el.value.querySelector('.callout-title');
|
|
||||||
title.value?.addEventListener('click', toggle);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
title.value?.removeEventListener('click', toggle);
|
|
||||||
})
|
|
||||||
function toggle() {
|
|
||||||
el.value?.classList?.toggle('is-collapsed');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
blockquote:not(.callout)
|
|
||||||
{
|
|
||||||
@apply ps-4;
|
|
||||||
@apply my-4;
|
|
||||||
@apply relative;
|
|
||||||
@apply before:absolute;
|
|
||||||
@apply before:-top-1;
|
|
||||||
@apply before:-bottom-1;
|
|
||||||
@apply before:left-0;
|
|
||||||
@apply before:w-1;
|
|
||||||
@apply before:bg-light-30;
|
|
||||||
@apply dark:before:bg-dark-30;
|
|
||||||
}
|
|
||||||
blockquote:empty
|
|
||||||
{
|
|
||||||
@apply before:hidden;
|
|
||||||
}
|
|
||||||
.callout {
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
}
|
|
||||||
.callout.is-collapsible .callout-title
|
|
||||||
{
|
|
||||||
@apply cursor-pointer;
|
|
||||||
}
|
|
||||||
.callout .fold
|
|
||||||
{
|
|
||||||
@apply transition-transform;
|
|
||||||
}
|
|
||||||
.callout.is-collapsed .fold
|
|
||||||
{
|
|
||||||
@apply -rotate-90;
|
|
||||||
}
|
|
||||||
.callout.is-collapsed > p
|
|
||||||
{
|
|
||||||
@apply hidden;
|
|
||||||
}
|
|
||||||
.callout[datacallout="abstract"],
|
|
||||||
.callout[datacallout="summary"],
|
|
||||||
.callout[datacallout="tldr"] {
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[datacallout="info"] {
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
@apply text-light-blue;
|
|
||||||
@apply dark:text-dark-blue;
|
|
||||||
}
|
|
||||||
.callout[datacallout="todo"] {
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
@apply text-light-blue;
|
|
||||||
@apply dark:text-dark-blue;
|
|
||||||
}
|
|
||||||
.callout[datacallout="important"] {
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[datacallout="tip"],
|
|
||||||
.callout[datacallout="hint"] {
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[datacallout="success"],
|
|
||||||
.callout[datacallout="check"],
|
|
||||||
.callout[datacallout="done"] {
|
|
||||||
@apply bg-light-green;
|
|
||||||
@apply dark:bg-dark-green;
|
|
||||||
@apply text-light-green;
|
|
||||||
@apply dark:text-dark-green;
|
|
||||||
}
|
|
||||||
.callout[datacallout="question"],
|
|
||||||
.callout[datacallout="help"],
|
|
||||||
.callout[datacallout="faq"] {
|
|
||||||
@apply bg-light-orange;
|
|
||||||
@apply dark:bg-dark-orange;
|
|
||||||
@apply text-light-orange;
|
|
||||||
@apply dark:text-dark-orange;
|
|
||||||
}
|
|
||||||
.callout[datacallout="warning"],
|
|
||||||
.callout[datacallout="caution"],
|
|
||||||
.callout[datacallout="attention"] {
|
|
||||||
@apply bg-light-orange;
|
|
||||||
@apply dark:bg-dark-orange;
|
|
||||||
@apply text-light-orange;
|
|
||||||
@apply dark:text-dark-orange;
|
|
||||||
}
|
|
||||||
.callout[datacallout="failure"],
|
|
||||||
.callout[datacallout="fail"],
|
|
||||||
.callout[datacallout="missing"] {
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[datacallout="danger"],
|
|
||||||
.callout[datacallout="error"] {
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[datacallout="bug"] {
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[datacallout="example"] {
|
|
||||||
@apply bg-light-purple;
|
|
||||||
@apply dark:bg-dark-purple;
|
|
||||||
@apply text-light-purple;
|
|
||||||
@apply dark:text-dark-purple;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callout
|
|
||||||
{
|
|
||||||
@apply overflow-hidden;
|
|
||||||
@apply my-4;
|
|
||||||
@apply p-3;
|
|
||||||
@apply ps-6;
|
|
||||||
@apply bg-blend-lighten;
|
|
||||||
@apply !bg-opacity-25;
|
|
||||||
@apply border-l-4;
|
|
||||||
@apply inline-block;
|
|
||||||
@apply pe-8;
|
|
||||||
}
|
|
||||||
.callout-icon
|
|
||||||
{
|
|
||||||
@apply w-6;
|
|
||||||
@apply h-6;
|
|
||||||
@apply stroke-2;
|
|
||||||
}
|
|
||||||
.callout-title
|
|
||||||
{
|
|
||||||
@apply flex;
|
|
||||||
@apply items-center;
|
|
||||||
@apply gap-2;
|
|
||||||
}
|
|
||||||
.callout-title-inner
|
|
||||||
{
|
|
||||||
@apply inline-block;
|
|
||||||
@apply font-bold;
|
|
||||||
}
|
|
||||||
.callout > p
|
|
||||||
{
|
|
||||||
@apply mt-2;
|
|
||||||
@apply font-semibold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<template>
|
|
||||||
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2">
|
|
||||||
<slot />
|
|
||||||
</h1>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2">
|
|
||||||
<slot />
|
|
||||||
</h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<h3 :id="parseId(id)" class="text-2xl font-bold mt-2 mb-4">
|
|
||||||
<slot />
|
|
||||||
</h3>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<template>
|
|
||||||
<h4 :id="parseId(id)" class="text-xl font-semibold my-2" style="font-variant: small-caps;">
|
|
||||||
<slot />
|
|
||||||
</h4>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<h5 :id="parseId(id)" class="text-lg font-semibold my-1">
|
|
||||||
<slot />
|
|
||||||
</h5>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<h6 :id="parseId(id)">
|
|
||||||
<slot />
|
|
||||||
</h6>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
|
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const collapsed = defineModel<boolean>({
|
|
||||||
default: true,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div @click="collapsed = !collapsed" class="flex gap-2 items-center cursor-pointer hover:text-opacity-75 text-light-100 dark:text-dark-100" :class="$attrs.class">
|
|
||||||
<div class="w-4 h-4 transition-transform" :class="{'-rotate-90': collapsed}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M3 8L12 17L21 8"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="font-semibold xl:text-base text-sm flex-1"><slot name="header"></slot></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!collapsed">
|
|
||||||
<slot name="content"></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
<template>
|
|
||||||
<Teleport to="#teleports" v-if="visible">
|
|
||||||
<div @click.self="() => !focused && hide()" class="z-[100] absolute top-0 bottom-0 left-0 right-0 bg-light-0 dark:bg-dark-0 !bg-opacity-80 flex justify-center items-center">
|
|
||||||
<div class="relative border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 min-h-40 min-w-72">
|
|
||||||
<span v-if="closeIcon" class="cursor-pointer absolute top-1 right-1 flex hover:opacity-75" @click="() => hide()"><Icon :width="20" :height="20" icon="icons/close" /></span>
|
|
||||||
<div class="p-6" :class="$attrs.class">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
focused: {
|
|
||||||
type: Boolean,
|
|
||||||
defualt: false,
|
|
||||||
},
|
|
||||||
closeIcon: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['show', 'hide']);
|
|
||||||
const visible = ref(false);
|
|
||||||
|
|
||||||
function show()
|
|
||||||
{
|
|
||||||
visible.value = true;
|
|
||||||
emit('show');
|
|
||||||
}
|
|
||||||
function hide()
|
|
||||||
{
|
|
||||||
visible.value = false;
|
|
||||||
emit('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({hide, show});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
{{beforeText}}<span class="font-bold">{{matchedText}}</span>{{afterText}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
interface Prop
|
|
||||||
{
|
|
||||||
text: string;
|
|
||||||
matched: string;
|
|
||||||
}
|
|
||||||
const props = defineProps<Prop>();
|
|
||||||
|
|
||||||
const beforeText = computed(() => {
|
|
||||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase());
|
|
||||||
return props.text.substring(0, pos);
|
|
||||||
})
|
|
||||||
const matchedText = computed(() => {
|
|
||||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase());
|
|
||||||
return props.text.substring(pos, pos + props.matched.length);
|
|
||||||
})
|
|
||||||
const afterText = computed(() => {
|
|
||||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase()) + props.matched.length;
|
|
||||||
return props.text.substring(pos);
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<template>
|
|
||||||
<Teleport to="#teleports" v-if="display">
|
|
||||||
<div class="absolute border-2 border-light-35 dark:border-dark-35 max-w-[550px] max-h-[450px] bg-light-0 dark:bg-dark-0 text-light-100 dark:text-dark-100 overflow-hidden" :style="pos"
|
|
||||||
@mouseenter="debounce(show, 250)" @mouseleave="debounce(() => { emit('beforeHide'); display = false }, 250)">
|
|
||||||
<slot name="content"></slot>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
<span ref="el" :class="$attrs.class" @mouseenter="debounce(show, 250)" @mouseleave="debounce(() => { emit('beforeHide'); display = false }, 250)">
|
|
||||||
<slot name="default"></slot>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useTemplateRef } from 'vue';
|
|
||||||
|
|
||||||
const display = ref(false), fetched = ref(false);
|
|
||||||
const el = useTemplateRef('el'), pos = ref<Record<string, string>>();
|
|
||||||
const emit = defineEmits(['beforeShow', 'beforeHide']);
|
|
||||||
|
|
||||||
async function show()
|
|
||||||
{
|
|
||||||
if(display.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
emit('beforeShow');
|
|
||||||
|
|
||||||
const rect = (el.value as HTMLDivElement)?.getBoundingClientRect();
|
|
||||||
|
|
||||||
if(!rect)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const r: Record<string, string> = {};
|
|
||||||
if (rect.bottom + 450 < window.innerHeight)
|
|
||||||
r.top = (rect.bottom + 4) + "px";
|
|
||||||
else
|
|
||||||
r.bottom = (window.innerHeight - rect.top + 4) + "px";
|
|
||||||
if (rect.right + 550 < window.innerWidth)
|
|
||||||
r.left = rect.left + "px";
|
|
||||||
else
|
|
||||||
r.right = (window.innerWidth - rect.right + 4) + "px";
|
|
||||||
|
|
||||||
pos.value = r;
|
|
||||||
display.value = true;
|
|
||||||
}
|
|
||||||
let debounceFn: () => void, timeout: NodeJS.Timeout;
|
|
||||||
function debounce(fn: () => void, ms: number)
|
|
||||||
{
|
|
||||||
debounceFn = fn;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(debounceFn, ms);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
interface Prop
|
|
||||||
{
|
|
||||||
icon: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
defineProps<Prop>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span :class="$attrs.class" class="inline-block bg-light-100 dark:bg-dark-100 [mask-size:100%] [mask-repeat:no-repeat] [mask-position:center]" :style="`width: ${width}px; height: ${height}px; mask-image: url(/${icon}.svg)`"></span>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const emit = defineEmits(['input', 'change', 'focus', 'blur']);
|
|
||||||
const model = defineModel();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<input v-model="model" v-bind="$attrs" @input="e => emit('input', e)" @change="e => emit('change', e)" @focus="e => emit('focus', e)" @blur="e => emit('blur', e)"
|
|
||||||
class="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"/>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
interface Prop
|
|
||||||
{
|
|
||||||
error?: string | boolean;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
const props = defineProps<Prop>();
|
|
||||||
const model = defineModel<string>();
|
|
||||||
|
|
||||||
const err = ref<string | boolean | undefined>(props.error);
|
|
||||||
|
|
||||||
watchEffect(() => err.value = props.error);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="m-1 flex justify-between items-center gap-8">
|
|
||||||
<label v-if="title" class="pe-4">{{ title }}</label>
|
|
||||||
<Input class="flex-1" @input="err = false" :class="{ 'input-has-error': !!err }" v-model="model" v-bind="$attrs" />
|
|
||||||
</div>
|
|
||||||
<span v-if="err && typeof err === 'string'" class="text-light-red dark:text-dark-red block pb-2">{{ err }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<template>
|
|
||||||
<template
|
|
||||||
v-if="content && content.length > 0">
|
|
||||||
<Suspense :timeout="0">
|
|
||||||
<MarkdownRenderer #default :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
|
||||||
<template #fallback><div class="loading"></div></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>
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useTemplateRef } from 'vue';
|
|
||||||
|
|
||||||
const containerRef = useTemplateRef("container");
|
|
||||||
const { loggedIn, user } = useUserSession();
|
|
||||||
const { direction } = useSwipe(window, { threshold: 150 });
|
|
||||||
|
|
||||||
watch(direction, () => {
|
|
||||||
if(direction.value === 'right')
|
|
||||||
toggleNavigation(true);
|
|
||||||
if(direction.value === 'left')
|
|
||||||
toggleNavigation(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleNavigation(bool?: boolean)
|
|
||||||
{
|
|
||||||
containerRef.value?.setAttribute('aria-expanded', bool === undefined ? (containerRef.value?.getAttribute('aria-expanded') !== 'true').toString() : bool.toString());
|
|
||||||
}
|
|
||||||
const hideNavigation = () => toggleNavigation(false);
|
|
||||||
onMounted(() => {
|
|
||||||
const links = containerRef.value?.getElementsByTagName('a');
|
|
||||||
if(links)
|
|
||||||
{
|
|
||||||
for(const link of links)
|
|
||||||
{
|
|
||||||
link.addEventListener("click", hideNavigation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
const links = containerRef.value?.getElementsByTagName('a');
|
|
||||||
if(links)
|
|
||||||
{
|
|
||||||
for(const link of links)
|
|
||||||
{
|
|
||||||
link.addEventListener("click", hideNavigation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div ref="container" aria-expanded="false" class="group flex h-screen overflow-hidden">
|
|
||||||
<div class="z-50 sm:hidden block absolute top-0 left-0 p-2 border-e border-b border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25" @click="e => toggleNavigation()">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
||||||
class="w-4 h-4 fill-light-100 dark:fill-dark-100">
|
|
||||||
<line x1="21" y1="6" x2="3" y2="6"></line>
|
|
||||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="17" y1="18" x2="3" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="bg-light-0 sm:my-8 sm:py-3 dark:bg-dark-0 top-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:-left-full max-sm:group-aria-expanded:left-0 max-sm:transition-[left] py-8 max-sm:z-40">
|
|
||||||
<div class="relative bottom-6 flex flex-1 flex-col gap-4 xl:px-6 px-3">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<NuxtLink @click="hideNavigation" class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-sm:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }"><ThemeIcon class="inline" icon="logo" :width=56 :height=56 /></NuxtLink>
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<ThemeSwitch />
|
|
||||||
<NuxtLink @click="hideNavigation" class="" :to="{ path: '/user/profile', force: true }"><div class=" hover:border-opacity-70 flex border p-px border-light-70 dark:border-dark-70">
|
|
||||||
<Icon v-if="!loggedIn" icon="icons/user-login" :width=28 :height=28 />
|
|
||||||
<Picture v-else :src="`/users/${user?.id}/small.jpg`" :width=28 :height=28 class="flex" >
|
|
||||||
<Icon :icon="`icons/unknown`" :width=28 :height=28 ></Icon>
|
|
||||||
</Picture>
|
|
||||||
</div></NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex"><SearchView @navigate="hideNavigation" /></div>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
<div class="text-center xl:text-sm text-xs text-light-70 dark:text-dark-70">
|
|
||||||
<NuxtLink class="hover:underline italic" :to="{ path: '/third-party', force: true }">Mentions légales</NuxtLink>
|
|
||||||
<p>Copyright Peaceultime - 2024</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<template>
|
|
||||||
<span>
|
|
||||||
<img v-show="src && !fallback" @load="hideFallback" @error="showFallback" :src="src" :width="width" :height="height" />
|
|
||||||
<span v-show="!src || fallback"><slot></slot></span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
src: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const fallback = ref(false);
|
|
||||||
|
|
||||||
const showFallback = () => toggleFallback(true);
|
|
||||||
const hideFallback = () => toggleFallback(false);
|
|
||||||
|
|
||||||
function toggleFallback(toggle: boolean): void
|
|
||||||
{
|
|
||||||
console.log("Something happened")
|
|
||||||
fallback.value = toggle;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Search, File, Project, User } from '~/types/api';
|
|
||||||
|
|
||||||
const input = ref(''), results = ref<Search>({ projects: [], files: [], users: [] });
|
|
||||||
const pos = ref<DOMRect>(), loading = ref(false);
|
|
||||||
|
|
||||||
let timeout: NodeJS.Timeout;
|
|
||||||
|
|
||||||
const emit = defineEmits(['navigate']);
|
|
||||||
|
|
||||||
function search(e: Event)
|
|
||||||
{
|
|
||||||
pos.value = (e.currentTarget as HTMLElement)?.getBoundingClientRect();
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(debounced, 250);
|
|
||||||
}
|
|
||||||
async function debounced()
|
|
||||||
{
|
|
||||||
if(!input.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const fetch = await useFetch(`/api/search`, {
|
|
||||||
query: {
|
|
||||||
"search": `%${input.value}%`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
results.value = fetch.data.value;
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
results.value = { projects: [], files: [], users: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-full border border-light-30 dark:border-dark-30">
|
|
||||||
<Input class="w-full" type="text" placeholder="Recherche" v-model="input" @input="search" />
|
|
||||||
</div>
|
|
||||||
<Teleport to="#teleports" v-if="input !== '' && !!pos">
|
|
||||||
<div class="absolute z-50 border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 text-light-100 dark:text-dark-100 divide-y divide-light-35 dark:divide-dark-35 max-h-96 overflow-auto
|
|
||||||
scroll"
|
|
||||||
:style="{top: (pos.bottom + 4) + 'px', left: pos.left + 'px', width: pos.width + 'px'}">
|
|
||||||
<div class="loading" v-if="loading"></div>
|
|
||||||
<template v-else>
|
|
||||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.projects" :key="result.id"
|
|
||||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
|
||||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
|
||||||
@mousedown.prevent="navigateTo(`/explorer/${result.id}/${result.home}`); input = ''; emit('navigate');">
|
|
||||||
<div class="">
|
|
||||||
<Highlight class="text-lg" :text="result.name" :matched="input" />
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<div class="">{{ result.username }}</div>
|
|
||||||
<div class="">{{ result.pages }} pages</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.files" :key="result.id"
|
|
||||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
|
||||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
|
||||||
@mousedown.prevent="navigateTo(`/explorer/${result.project}/${result.path}`); input = ''; emit('navigate');">
|
|
||||||
<div class="">
|
|
||||||
<Highlight class="text-lg" :text="result.title" :matched="input" />
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<div class="">{{ result.username }}</div>
|
|
||||||
<div class="">{{ result.comments }} commentaires</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.users" :key="result.id"
|
|
||||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
|
||||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
|
||||||
@mousedown.prevent="navigateTo(`/users/${result.id}`); input = ''; emit('navigate');">
|
|
||||||
<div class="">
|
|
||||||
<Highlight class="text-lg" :text="result.username" :matched="input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=""
|
|
||||||
v-if="results.projects.length === 0 && results.files.length === 0 && results.users.length === 0">
|
|
||||||
Aucun résultat
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
interface Prop
|
|
||||||
{
|
|
||||||
icon: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
defineProps<Prop>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span>
|
|
||||||
<img :src="`/icons/${icon}.light.svg`" class="block dark:hidden" :width="width" :height="height" />
|
|
||||||
<img :src="`/icons/${icon}.dark.svg`" class="dark:block hidden" :width="width" :height="height" />
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<script setup>
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
|
|
||||||
const isDark = computed({
|
|
||||||
get() {
|
|
||||||
return colorMode.value === 'dark'
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex relative">
|
|
||||||
<span class="hidden dark:block absolute top-[3px] left-[2px] z-[1] px-[5px]">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 stroke-light-100 dark:stroke-dark-100">
|
|
||||||
<path d="M12 3a6.364 6.364 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<div class="
|
|
||||||
before:absolute before:top-0 before:left-0 before:right-0 before:bottom-0 before:opacity-0 before:block before:z-10
|
|
||||||
after:transition-all after:w-4 after:h-4 after:border after:border-light-30 dark:after:border-dark-30 after:block after:m-[3px] after:top-[-1px] after:pointer-events-none after:absolute after:left-0 after:bg-light-0 after:translate-x-[1px] dark:after:translate-x-[26px]
|
|
||||||
inline-block relative cursor-pointer w-[50px] h-[22px] select-none border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-30 dark:hover:border-dark-35" @click="isDark = !isDark"></div>
|
|
||||||
<span class="block dark:hidden absolute top-[3px] left-[22px] z-[1] px-[5px]">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 stroke-light-100 dark:stroke-dark-100 ">
|
|
||||||
<circle cx="12" cy="12" r="4"></circle>
|
|
||||||
<path d="M12 2v2"></path>
|
|
||||||
<path d="M12 20v2"></path>
|
|
||||||
<path d="m4.93 4.93 1.41 1.41"></path>
|
|
||||||
<path d="m17.66 17.66 1.41 1.41"></path>
|
|
||||||
<path d="M2 12h2"></path>
|
|
||||||
<path d="M20 12h2"></path>
|
|
||||||
<path d="m6.34 17.66-1.41 1.41"></path>
|
|
||||||
<path d="m19.07 4.93-1.41 1.41"></path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<template>
|
|
||||||
<label>
|
|
||||||
<slot></slot>
|
|
||||||
<input ref="input" :accept="accept" :multiple="multiple" type="file" class="hidden" @change.self="(e) => files = [...(e.target as HTMLInputElement)?.files ?? []]"/>
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
accept: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
multiple: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const input = useTemplateRef('input');
|
|
||||||
const files = ref<File[]>([]);
|
|
||||||
|
|
||||||
watch([files], () => emit('change', files.value));
|
|
||||||
|
|
||||||
const emit = defineEmits(['change']);
|
|
||||||
|
|
||||||
defineExpose({ files });
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
|
import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
|
import * as schema from '../db/schema';
|
||||||
|
|
||||||
let instance: Database | undefined;
|
let instance: BunSQLiteDatabase<typeof schema>;
|
||||||
|
export default function useDatabase()
|
||||||
|
{
|
||||||
|
if(!instance)
|
||||||
|
{
|
||||||
|
const database = useRuntimeConfig().database;
|
||||||
|
const sqlite = new Database(database);
|
||||||
|
instance = drizzle({ client: sqlite, schema });
|
||||||
|
|
||||||
export default function useDatabase(): Database {
|
instance.run("PRAGMA journal_mode = WAL;");
|
||||||
if(instance === undefined)
|
instance.run("PRAGMA foreign_keys = true;");
|
||||||
instance = getDatabase();
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDatabase(): Database
|
|
||||||
{
|
|
||||||
const { dbFile } = useRuntimeConfig();
|
|
||||||
|
|
||||||
const db = new Database(dbFile);
|
|
||||||
|
|
||||||
db.exec("PRAGMA journal_mode = WAL;");
|
|
||||||
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
|
|
@ -16,7 +16,7 @@ export default function useMarkdown(): (md: string) => Root
|
||||||
const parse = (markdown: string) => {
|
const parse = (markdown: string) => {
|
||||||
if (!processor)
|
if (!processor)
|
||||||
{
|
{
|
||||||
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter]);
|
processor = unified().use([RemarkParse, RemarkGfm , RemarkOfm , RemarkBreaks, RemarkFrontmatter]);
|
||||||
processor.use(RemarkRehype, { allowDangerousHtml: true });
|
processor.use(RemarkRehype, { allowDangerousHtml: true });
|
||||||
processor.use(RehypeRaw);
|
processor.use(RehypeRaw);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import type { Project } from "~/types/api";
|
|
||||||
|
|
||||||
export default function useProject()
|
|
||||||
{
|
|
||||||
const project = useCookie('project');
|
|
||||||
|
|
||||||
const id = useState<number>("projectId", () => parseInt(project.value ?? '0'));
|
|
||||||
const name = useState<string>("projectName", undefined);
|
|
||||||
const owner = useState<number>("projectOwner", undefined);
|
|
||||||
const home = useState<string | null>("projectHomepage", () => null);
|
|
||||||
const summary = useState<string | null>("projectSummary", () => null);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id, name, owner, home, summary, get, set
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function get(): Promise<boolean> {
|
|
||||||
const id = useState<number>("projectId");
|
|
||||||
|
|
||||||
if (!id.value)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await $fetch(`/api/project/${id.value}`) as Project;
|
|
||||||
|
|
||||||
const name = useState<string>("projectName");
|
|
||||||
const owner = useState<number>("projectOwner");
|
|
||||||
const home = useState<string | null>("projectHomepage");
|
|
||||||
const summary = useState<string | null>("projectSummary");
|
|
||||||
|
|
||||||
name.value = result.name;
|
|
||||||
owner.value = result.owner;
|
|
||||||
home.value = result.home;
|
|
||||||
summary.value = result.summary;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch(e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function set(id: number): Promise<boolean> {
|
|
||||||
const _id = useState<number>("projectId");
|
|
||||||
|
|
||||||
_id.value = id;
|
|
||||||
const project = useCookie('project');
|
|
||||||
project.value = id.toString();
|
|
||||||
|
|
||||||
return await get();
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
export interface ToastConfig
|
||||||
|
{
|
||||||
|
closeable?: boolean
|
||||||
|
duration: number
|
||||||
|
title?: string
|
||||||
|
content?: string
|
||||||
|
timer?: boolean
|
||||||
|
type?: ToastType
|
||||||
|
}
|
||||||
|
export type ToastType = 'info' | 'success' | 'error';
|
||||||
|
export type ExtraToastConfig = ToastConfig & { id: string, state: boolean };
|
||||||
|
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
const [provideToaster, useToast] = createInjectionState(() => {
|
||||||
|
const list = ref<ExtraToastConfig[]>([]);
|
||||||
|
|
||||||
|
function add(config: ToastConfig)
|
||||||
|
{
|
||||||
|
list.value.push({ ...config, id: (++id).toString(), state: true, });
|
||||||
|
}
|
||||||
|
function clear(type?: ToastType)
|
||||||
|
{
|
||||||
|
list.value.forEach(e => { if(e.type !== type) { e.state = false; } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { list, add, clear }
|
||||||
|
}, { injectionKey: Symbol('toaster') });
|
||||||
|
|
||||||
|
export { provideToaster, useToastWithDefault as useToast };
|
||||||
|
|
||||||
|
function useToastWithDefault()
|
||||||
|
{
|
||||||
|
const toasts = useToast();
|
||||||
|
if(!toasts)
|
||||||
|
{
|
||||||
|
return { list: ref<ExtraToastConfig[]>([]), add: () => {}, clear: () => {} };
|
||||||
|
}
|
||||||
|
return toasts;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { relations } from 'drizzle-orm';
|
||||||
|
import { int, text, sqliteTable, type SQLiteTableExtraConfig, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||||
|
|
||||||
|
export const usersTable = sqliteTable("users", {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
username: text().notNull().unique(),
|
||||||
|
email: text().notNull().unique(),
|
||||||
|
hash: text().notNull().unique(),
|
||||||
|
state: int().notNull().default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usersDataTable = sqliteTable("users_data", {
|
||||||
|
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSessionsTable = sqliteTable("user_sessions", {
|
||||||
|
id: int().notNull(),
|
||||||
|
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
|
}, (table): SQLiteTableExtraConfig => {
|
||||||
|
return {
|
||||||
|
pk: primaryKey({ columns: [table.id, table.user_id] }),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userPermissionsTable = sqliteTable("user_permissions", {
|
||||||
|
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
permission: text().notNull(),
|
||||||
|
}, (table): SQLiteTableExtraConfig => {
|
||||||
|
return {
|
||||||
|
pk: primaryKey({ columns: [table.id, table.permission] }),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const explorerContentTable = sqliteTable("explorer_content", {
|
||||||
|
path: text().primaryKey(),
|
||||||
|
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
||||||
|
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
||||||
|
session: many(userSessionsTable),
|
||||||
|
permission: many(userPermissionsTable),
|
||||||
|
content: many(explorerContentTable),
|
||||||
|
}));
|
||||||
|
export const usersDataRelation = relations(usersDataTable, ({ one }) => ({
|
||||||
|
users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }),
|
||||||
|
}));
|
||||||
|
export const userSessionsRelation = relations(userSessionsTable, ({ one }) => ({
|
||||||
|
users: one(usersTable, { fields: [userSessionsTable.user_id], references: [usersTable.id], }),
|
||||||
|
}));
|
||||||
|
export const userPermissionsRelation = relations(userPermissionsTable, ({ one }) => ({
|
||||||
|
users: one(usersTable, { fields: [userPermissionsTable.id], references: [usersTable.id], }),
|
||||||
|
}));
|
||||||
|
export const explorerContentRelation = relations(explorerContentTable, ({ one }) => ({
|
||||||
|
users: one(usersTable, { fields: [explorerContentTable.owner], references: [usersTable.id], }),
|
||||||
|
}));
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: './drizzle',
|
||||||
|
schema: './db/schema.ts',
|
||||||
|
dialect: 'sqlite',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DB_FILE!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
CREATE TABLE `explorer_content` (
|
||||||
|
`path` text PRIMARY KEY NOT NULL,
|
||||||
|
`owner` integer NOT NULL,
|
||||||
|
`title` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`content` blob,
|
||||||
|
`navigable` integer DEFAULT true,
|
||||||
|
`private` integer DEFAULT false,
|
||||||
|
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `user_sessions` (
|
||||||
|
`id` integer NOT NULL,
|
||||||
|
`user_id` integer NOT NULL,
|
||||||
|
`timestamp` integer NOT NULL,
|
||||||
|
PRIMARY KEY(`id`, `user_id`),
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `users_data` (
|
||||||
|
`id` integer PRIMARY KEY NOT NULL,
|
||||||
|
`signin` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`username` text NOT NULL,
|
||||||
|
`email` text NOT NULL,
|
||||||
|
`hash` text NOT NULL,
|
||||||
|
`state` integer DEFAULT 0 NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_hash_unique` ON `users` (`hash`);
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
CREATE TABLE `user_permissions` (
|
||||||
|
`id` integer NOT NULL,
|
||||||
|
`permissions` text NOT NULL,
|
||||||
|
PRIMARY KEY(`id`, `permissions`),
|
||||||
|
FOREIGN KEY (`id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_user_permissions` (
|
||||||
|
`id` integer NOT NULL,
|
||||||
|
`permission` text NOT NULL,
|
||||||
|
PRIMARY KEY(`id`, `permission`),
|
||||||
|
FOREIGN KEY (`id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_user_permissions`("id", "permission") SELECT "id", "permission" FROM `user_permissions`;--> statement-breakpoint
|
||||||
|
DROP TABLE `user_permissions`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_user_permissions` RENAME TO `user_permissions`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "f66f1f97-ceb3-46ed-988b-62828fe4a6a6",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"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_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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "854cab71-b937-4f4f-80b0-cbb09c7b5944",
|
||||||
|
"prevId": "f66f1f97-ceb3-46ed-988b-62828fe4a6a6",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"name": "permissions",
|
||||||
|
"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_permissions_pk": {
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"permissions"
|
||||||
|
],
|
||||||
|
"name": "user_permissions_id_permissions_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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,300 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "6da7ff20-0db8-4055-a353-bb0ea2fa5e0b",
|
||||||
|
"prevId": "854cab71-b937-4f4f-80b0-cbb09c7b5944",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"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": {
|
||||||
|
"\"user_permissions\".\"permissions\"": "\"user_permissions\".\"permission\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1730829864576,
|
||||||
|
"tag": "0000_needy_rictor",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1730832678255,
|
||||||
|
"tag": "0001_sticky_jack_flag",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1730985155814,
|
||||||
|
"tag": "0002_messy_solo",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { relations } from "drizzle-orm/relations";
|
||||||
|
import { users, explorerContent, userSessions, usersData, userPermissions } from "./schema";
|
||||||
|
|
||||||
|
export const explorerContentRelations = relations(explorerContent, ({one}) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [explorerContent.owner],
|
||||||
|
references: [users.id]
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const usersRelations = relations(users, ({many}) => ({
|
||||||
|
explorerContents: many(explorerContent),
|
||||||
|
userSessions: many(userSessions),
|
||||||
|
usersData: many(usersData),
|
||||||
|
userPermissions: many(userPermissions),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const userSessionsRelations = relations(userSessions, ({one}) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [userSessions.userId],
|
||||||
|
references: [users.id]
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const usersDataRelations = relations(usersData, ({one}) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [usersData.id],
|
||||||
|
references: [users.id]
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const userPermissionsRelations = relations(userPermissions, ({one}) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [userPermissions.id],
|
||||||
|
references: [users.id]
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { sqliteTable, AnySQLiteColumn, foreignKey, text, integer, blob, primaryKey, uniqueIndex } from "drizzle-orm/sqlite-core"
|
||||||
|
import { sql } from "drizzle-orm"
|
||||||
|
|
||||||
|
export const explorerContent = sqliteTable("explorer_content", {
|
||||||
|
path: text().primaryKey().notNull(),
|
||||||
|
owner: integer().notNull().references(() => users.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||||
|
title: text().notNull(),
|
||||||
|
type: text().notNull(),
|
||||||
|
content: blob(),
|
||||||
|
navigable: integer().default(true),
|
||||||
|
private: integer().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSessions = sqliteTable("user_sessions", {
|
||||||
|
id: integer().notNull(),
|
||||||
|
userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||||
|
timestamp: integer().notNull(),
|
||||||
|
},
|
||||||
|
(table) => {
|
||||||
|
return {
|
||||||
|
pk0: primaryKey({ columns: [table.id, table.userId], name: "user_sessions_id_user_id_pk"})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usersData = sqliteTable("users_data", {
|
||||||
|
id: integer().primaryKey().notNull().references(() => users.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||||
|
signin: integer().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const users = sqliteTable("users", {
|
||||||
|
id: integer().primaryKey({ autoIncrement: true }).notNull(),
|
||||||
|
username: text().notNull(),
|
||||||
|
email: text().notNull(),
|
||||||
|
hash: text().notNull(),
|
||||||
|
state: integer().default(0).notNull(),
|
||||||
|
},
|
||||||
|
(table) => {
|
||||||
|
return {
|
||||||
|
hashUnique: uniqueIndex("users_hash_unique").on(table.hash),
|
||||||
|
emailUnique: uniqueIndex("users_email_unique").on(table.email),
|
||||||
|
usernameUnique: uniqueIndex("users_username_unique").on(table.username),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userPermissions = sqliteTable("user_permissions", {
|
||||||
|
id: integer().notNull().references(() => users.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||||
|
permissions: text().notNull(),
|
||||||
|
},
|
||||||
|
(table) => {
|
||||||
|
return {
|
||||||
|
pk0: primaryKey({ columns: [table.id, table.permissions], name: "user_permissions_id_permissions_pk"})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const drizzleMigrations = sqliteTable("__drizzle_migrations", {
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden justify-center items-center flex-col gap-4">
|
||||||
|
<NuxtRouteAnnouncer/>
|
||||||
|
<div class="flex gap-4 items-center">
|
||||||
|
<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>
|
||||||
|
<NuxtLink :href="{ name: 'index' }"><Button>Revenir en lieu sûr</Button></NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NuxtError } from '#app'
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
error: Object as () => NuxtError
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleError = () => clearError({ redirect: '/' })
|
||||||
|
</script>
|
||||||
|
|
@ -1,9 +1,75 @@
|
||||||
<template>
|
<template>
|
||||||
<NavBar>
|
<CollapsibleRoot class="flex flex-1 flex-col" v-model:open="open">
|
||||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Projets" :to="{ path: `/explorer`, force: true }">Projets</NuxtLink>
|
<div class="z-50 md:hidden flex w-full items-center justify-between h-12 border-b border-light-35 dark:border-dark-35">
|
||||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Editeur" :to="{ path: '/editing', force: true }">Editeur</NuxtLink>
|
<div class="flex items-center px-2">
|
||||||
</NavBar>
|
<CollapsibleTrigger asChild>
|
||||||
<div class="flex-1 flex items-baseline overflow-auto py-8 sm:px-8 px-4 relative">
|
<Button icon class="ms-2 !bg-transparent group">
|
||||||
|
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
|
||||||
|
<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-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="{ name: 'user-profile' }">
|
||||||
|
<div class="hover:border-opacity-70 flex">
|
||||||
|
<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 h-screen overflow-hidden">
|
||||||
|
<CollapsibleContent asChild forceMount>
|
||||||
|
<div class="bg-light-0 md:py-11 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.dark.svg" class="dark:block hidden" />
|
||||||
|
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
||||||
|
</NuxtLink>
|
||||||
|
<div class="flex gap-4 items-center">
|
||||||
|
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
||||||
|
<Tooltip :message="loggedIn ? 'Mon profil' : 'Se connecter'" side="right">
|
||||||
|
<NuxtLink class="" :to="{ name: 'user-profile' }">
|
||||||
|
<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="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="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||||
|
<p>Copyright Peaceultime - 2024</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
</CollapsibleRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
const open = ref(true);
|
||||||
|
const { loggedIn } = useUserSession();
|
||||||
|
|
||||||
|
const { data: pages } = await useLazyFetch('/api/navigation', {
|
||||||
|
transform: transform,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(useRouter().currentRoute, () => {
|
||||||
|
open.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function transform(list: any[]): any[]
|
||||||
|
{
|
||||||
|
return list?.map(e => ({ label: e.title, children: transform(e.children), link: e.path, tag: e.private ? 'private' : e.type }))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<template>
|
|
||||||
<NavBar>
|
|
||||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Projets" :to="{ path: `/explorer`, force: true }">Projets</NuxtLink>
|
|
||||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Editeur" :to="{ path: '/editing', force: true }">Editeur</NuxtLink>
|
|
||||||
<hr class="border-light-35 dark:border-dark-35"/>
|
|
||||||
<ExplorerNavigation></ExplorerNavigation>
|
|
||||||
</NavBar>
|
|
||||||
<div class="flex-1 flex items-baseline overflow-auto py-8 sm:px-8 px-4 relative">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-1 items-center justify-center">
|
||||||
|
<div class="w-full md:w-auto h-full border-e border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 md:p-8 xl:p-16 flex justify-center items-center">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:block flex-auto h-full"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
const { loggedIn, ready, fetch } = useUserSession();
|
const { loggedIn, fetch, user } = useUserSession();
|
||||||
const meta = to.meta;
|
const meta = to.meta;
|
||||||
|
|
||||||
if(!ready)
|
|
||||||
await fetch();
|
await fetch();
|
||||||
|
|
||||||
if(!!meta.guestsGoesTo && !loggedIn.value)
|
if(!!meta.guestsGoesTo && !loggedIn.value)
|
||||||
|
|
@ -11,12 +10,27 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
}
|
}
|
||||||
else if(meta.requireAuth && !loggedIn.value)
|
else if(meta.requireAuth && !loggedIn.value)
|
||||||
{
|
{
|
||||||
return abortNavigation();
|
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
||||||
}
|
}
|
||||||
else if(!!meta.usersGoesTo && loggedIn.value)
|
else if(!!meta.usersGoesTo && loggedIn.value)
|
||||||
{
|
{
|
||||||
return navigateTo(meta.usersGoesTo);
|
return navigateTo(meta.usersGoesTo);
|
||||||
}
|
}
|
||||||
|
else if(!!meta.validState && (!loggedIn.value || (user.value?.state ?? 0) === 0))
|
||||||
|
{
|
||||||
|
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
||||||
|
}
|
||||||
|
else if(!!meta.rights)
|
||||||
|
{
|
||||||
|
if(!user.value)
|
||||||
|
{
|
||||||
|
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
||||||
|
}
|
||||||
|
else if(!hasPermissions(user.value.permissions, meta.rights))
|
||||||
|
{
|
||||||
|
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||||
|
import { Database } from "bun:sqlite";
|
||||||
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
|
|
||||||
|
const sqlite = new Database("db.sqlite");
|
||||||
|
const db = drizzle(sqlite);
|
||||||
|
await migrate(db, { migrationsFolder: "./drizzle" });
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import { defineNuxtModule } from '@nuxt/kit'
|
|
||||||
import { Database } from "bun:sqlite";
|
|
||||||
|
|
||||||
interface Schema
|
|
||||||
{
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
tbl_name: string;
|
|
||||||
sql: string;
|
|
||||||
}
|
|
||||||
interface Structure
|
|
||||||
{
|
|
||||||
cid: number;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
[key: string]: number | string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineNuxtModule({
|
|
||||||
setup (options, nuxt) {
|
|
||||||
nuxt.hook('build:done', () => {
|
|
||||||
console.log("Building database");
|
|
||||||
|
|
||||||
const template = new Database(nuxt.options.runtimeConfig.templateFile);
|
|
||||||
const schema = template.query(`SELECT * FROM sqlite_schema WHERE type = 'table'`).all() as Schema[];
|
|
||||||
const structure = template.query(`SELECT * FROM pragma_table_info(?1)`);
|
|
||||||
|
|
||||||
const db = new Database(nuxt.options.runtimeConfig.dbFile);
|
|
||||||
db.exec(`PRAGMA foreign_keys = OFF; PRAGMA defer_foreign_keys = OFF;`);
|
|
||||||
const oldSchema = db.query(`SELECT * FROM sqlite_schema WHERE type = 'table'`).all() as Schema[];
|
|
||||||
const oldStructure = db.query(`SELECT * FROM pragma_table_info(?1)`);
|
|
||||||
|
|
||||||
(db.transaction((tables: Schema[], oldTables: Schema[]) => {
|
|
||||||
for(const table of tables)
|
|
||||||
{
|
|
||||||
const oldIdx = oldTables.findIndex(e => e.name === table.name);
|
|
||||||
|
|
||||||
if(table.name === 'sqlite_sequence')
|
|
||||||
{
|
|
||||||
oldTables.splice(oldIdx, 1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = structure.all(table.name) as Structure[];
|
|
||||||
const oldColumns = oldStructure.all(table.name) as Structure[];
|
|
||||||
|
|
||||||
if(oldIdx !== -1)
|
|
||||||
{
|
|
||||||
oldTables.splice(oldIdx, 1);
|
|
||||||
|
|
||||||
const filteredColumns = oldColumns.filter(e => columns.find(f => e.name === f.name)).map(e => `"${e.name}"`).join(', ')
|
|
||||||
|
|
||||||
db.exec(table.sql.replace(`CREATE TABLE "${table.name}"`, `CREATE TABLE "${table.name}_new"`));
|
|
||||||
db.exec(`INSERT INTO ${table.name}_new (${filteredColumns}) SELECT ${filteredColumns} FROM ${table.name}`);
|
|
||||||
db.exec(`DROP TABLE ${table.name}`);
|
|
||||||
db.exec(`ALTER TABLE ${table.name}_new RENAME TO ${table.name}`);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
db.exec(table.sql);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(const table of oldTables)
|
|
||||||
{
|
|
||||||
db.exec(`DROP TABLE ${table.name}`);
|
|
||||||
}
|
|
||||||
})).immediate(schema, oldSchema);
|
|
||||||
|
|
||||||
db.exec(`PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;`);
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
template.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -1,38 +1,61 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
import DatabaseSync from './modules/database.sync/module';
|
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2024-04-03',
|
||||||
modules: [
|
modules: [
|
||||||
"@nuxtjs/color-mode",
|
'@nuxtjs/color-mode',
|
||||||
"nuxt-security",
|
'nuxt-security',
|
||||||
"@nuxtjs/tailwindcss",
|
'@nuxtjs/tailwindcss',
|
||||||
"@vueuse/nuxt",
|
'@vueuse/nuxt',
|
||||||
DatabaseSync
|
'radix-vue/nuxt',
|
||||||
],
|
],
|
||||||
runtimeConfig: {
|
|
||||||
dbFile: 'db.sqlite',
|
|
||||||
templateFile: 'template.sqlite',
|
|
||||||
session: {
|
|
||||||
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
viewer: false,
|
viewer: false,
|
||||||
config: {
|
config: {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
raw: '0 0 0 4px var(--tw-shadow-color)'
|
raw: '0 0 0 2px var(--tw-shadow-color)'
|
||||||
}
|
},
|
||||||
|
keyframes: {
|
||||||
|
slideDownAndFade: {
|
||||||
|
from: { opacity: '0', transform: 'translateY(-2px)' },
|
||||||
|
to: { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
slideLeftAndFade: {
|
||||||
|
from: { opacity: '0', transform: 'translateX(2px)' },
|
||||||
|
to: { opacity: '1', transform: 'translateX(0)' },
|
||||||
|
},
|
||||||
|
slideUpAndFade: {
|
||||||
|
from: { opacity: '0', transform: 'translateY(2px)' },
|
||||||
|
to: { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
slideRightAndFade: {
|
||||||
|
from: { opacity: '0', transform: 'translateX(-2px)' },
|
||||||
|
to: { opacity: '1', transform: 'translateX(0)' },
|
||||||
|
},
|
||||||
|
contentShow: {
|
||||||
|
from: { opacity: '0', transform: 'translate(-50%, -48%) scale(0.96)' },
|
||||||
|
to: { opacity: '1', transform: 'translate(-50%, -50%) scale(1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
contentShow: 'contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
transparent: 'transparent',
|
transparent: 'transparent',
|
||||||
current: 'currentColor',
|
current: 'currentColor',
|
||||||
light: {
|
light: {
|
||||||
red: '#e93147',
|
red: '#e93147',
|
||||||
|
redBack: '#F9C7CD',
|
||||||
orange: '#ec7500',
|
orange: '#ec7500',
|
||||||
yellow: '#e0ac00',
|
yellow: '#e0ac00',
|
||||||
green: '#08b94e',
|
green: '#08b94e',
|
||||||
|
greenBack: '#BCECCF',
|
||||||
cyan: '#00bfbc',
|
cyan: '#00bfbc',
|
||||||
blue: '#086ddd',
|
blue: '#086ddd',
|
||||||
purple: '#7852ee',
|
purple: '#7852ee',
|
||||||
|
|
@ -40,21 +63,23 @@ export default defineNuxtConfig({
|
||||||
0: "#ffffff",
|
0: "#ffffff",
|
||||||
5: "#fcfcfc",
|
5: "#fcfcfc",
|
||||||
10: "#fafafa",
|
10: "#fafafa",
|
||||||
20: "#f6f6f6",
|
20: "#f7f7f7",
|
||||||
25: "#e3e3e3",
|
25: "#e4e4e4",
|
||||||
30: "#e0e0e0",
|
30: "#dfdfdf",
|
||||||
35: "#d4d4d4",
|
35: "#d2d2d2",
|
||||||
40: "#bdbdbd",
|
40: "#bdbdbd",
|
||||||
50: "#ababab",
|
50: "#ababab",
|
||||||
60: "#707070",
|
60: "#707070",
|
||||||
70: "#5c5c5c",
|
70: "#5c5c5c",
|
||||||
100: "#222222",
|
100: "#202020",
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
red: '#fb464c',
|
red: '#fb464c',
|
||||||
|
redBack: '#5A292B',
|
||||||
orange: '#e9973f',
|
orange: '#e9973f',
|
||||||
yellow: '#e0de71',
|
yellow: '#e0de71',
|
||||||
green: '#44cf6e',
|
green: '#44cf6e',
|
||||||
|
greenBack: '#284E34',
|
||||||
cyan: '#53dfdd',
|
cyan: '#53dfdd',
|
||||||
blue: '#027aff',
|
blue: '#027aff',
|
||||||
purple: '#a882ff',
|
purple: '#a882ff',
|
||||||
|
|
@ -94,9 +119,12 @@ export default defineNuxtConfig({
|
||||||
experimental: {
|
experimental: {
|
||||||
tasks: true,
|
tasks: true,
|
||||||
},
|
},
|
||||||
scheduledTasks: {
|
},
|
||||||
'0 */1 * * *': ['sync']
|
runtimeConfig: {
|
||||||
}
|
session: {
|
||||||
|
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013'
|
||||||
|
},
|
||||||
|
database: 'db.sqlite'
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
rateLimiter: false,
|
rateLimiter: false,
|
||||||
|
|
@ -104,7 +132,7 @@ export default defineNuxtConfig({
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
"img-src": "'self' data: blob:"
|
"img-src": "'self' data: blob:"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
compatibilityDate: '2024-07-25',
|
xssValidator: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
47
package.json
47
package.json
|
|
@ -1,28 +1,29 @@
|
||||||
{
|
{
|
||||||
"devDependencies": {
|
"name": "d-any",
|
||||||
"@nuxtjs/color-mode": "^3.4.4",
|
"private": true,
|
||||||
"@nuxtjs/tailwindcss": "^6.12.1",
|
"type": "module",
|
||||||
"@types/bun": "^1.1.8",
|
"scripts": {
|
||||||
"@types/diff": "^5.2.2",
|
"predev": "bun i && bunx nuxi cleanup",
|
||||||
"@vueuse/gesture": "^2.0.0",
|
"dev": "bunx --bun nuxi dev"
|
||||||
"@vueuse/nuxt": "^11.0.3",
|
|
||||||
"hast-util-to-html": "^9.0.2",
|
|
||||||
"nuxt": "^3.13.1",
|
|
||||||
"nuxt-security": "^2.0.0-rc.9",
|
|
||||||
"remark-breaks": "^4.0.0",
|
|
||||||
"remark-ofm": "link:remark-ofm",
|
|
||||||
"vue": "^3.5.3",
|
|
||||||
"vue-router": "^4.4.3",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff": "^5.2.0",
|
"@iconify/vue": "^4.1.2",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"rehype-raw": "^7.0.0",
|
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||||
"remark-frontmatter": "^5.0.0",
|
"@vueuse/nuxt": "^11.1.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"codemirror": "^6.0.1",
|
||||||
"remark-parse": "^11.0.0",
|
"drizzle-orm": "^0.35.3",
|
||||||
"remark-rehype": "^11.1.0",
|
"nuxt": "^3.14.159",
|
||||||
"unified": "^11.0.5"
|
"nuxt-security": "^2.0.0",
|
||||||
|
"radix-vue": "^1.9.8",
|
||||||
|
"vue": "latest",
|
||||||
|
"vue-router": "latest",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.1.12",
|
||||||
|
"better-sqlite3": "^11.5.0",
|
||||||
|
"bun-types": "^1.1.34",
|
||||||
|
"drizzle-kit": "^0.26.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
<template>
|
<template>
|
||||||
<Head>
|
Page inconnue.
|
||||||
<Title>Inconnu</Title>
|
|
||||||
</Head>
|
|
||||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center">
|
|
||||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Introuvable</div>
|
|
||||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page n'existe pas</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
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();
|
||||||
|
async function fetch()
|
||||||
|
{
|
||||||
|
status.value = 'pending';
|
||||||
|
data.value = null;
|
||||||
|
error.value = null;
|
||||||
|
err.value = false;
|
||||||
|
success.value = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data.value = await $fetch(`/api/admin/jobs/${job.value}`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
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, });
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
status.value = 'error';
|
||||||
|
error.value = e;
|
||||||
|
err.value = true;
|
||||||
|
success.value = false;
|
||||||
|
|
||||||
|
toaster.add({ duration: 10000, content: error.value, type: 'error', timer: true, });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>Administration</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-col justify-start">
|
||||||
|
<ProseH2>Administration</ProseH2>
|
||||||
|
<Select label="Job" v-model="job">
|
||||||
|
<SelectItem label="Synchroniser" value="sync" />
|
||||||
|
<SelectItem label="Nettoyer la base" value="clear" disabled />
|
||||||
|
<SelectItem label="Reconstruire" value="rebuild" disabled />
|
||||||
|
</Select>
|
||||||
|
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
|
||||||
|
<span>Executer</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
<style>
|
|
||||||
.editor-container
|
|
||||||
{
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
.editor
|
|
||||||
{
|
|
||||||
width: 45%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Head>
|
|
||||||
<Title>Live Editing</Title>
|
|
||||||
</Head>
|
|
||||||
<div class="flex flex-1 flex-col justify-center items-center">
|
|
||||||
<h1 class="block flex-1 text-3xl">En cours de développement</h1>
|
|
||||||
<div class="flex flex-1">
|
|
||||||
<Suspense>
|
|
||||||
<template #fallback>
|
|
||||||
<div class="loading"></div>
|
|
||||||
</template>
|
|
||||||
<EditableMarkdown class="editor-preview" v-if="input.length > 0" v-model="input"></EditableMarkdown>
|
|
||||||
</Suspense>
|
|
||||||
<textarea class="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 w-1/2 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" v-model="input"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const input = ref(`## Liste de sorts provisoire
|
|
||||||
%% Equilibrage: Les sorts de dégâts plus cher ne doivent pas forcément proposer plus de dés de dégâts mais offrir plus d'options et avoir des dé de dégâts plus haut, pour synergiser avec les buffs de l'arbre de magie. %%
|
|
||||||
|
|
||||||
**Notation:**
|
|
||||||
- Nom #element (coût, durée d'incantation, portée, prérequis d'incantation)
|
|
||||||
> Effet
|
|
||||||
### Rang 1
|
|
||||||
- Trait de feu #element/feu (4 mana, tour, 12 cases, V/Ge/Gl)
|
|
||||||
>Tire un faisceau de flamme, infligeant 2d8 dégâts de [[Les types de dégâts#Feu|feu]].
|
|
||||||
|
|
||||||
- Echauffement #element/feu (2 mana, tour, V/Gl)
|
|
||||||
>Chauffe à blanc une arme ou un projectile. Jusqu'au début de votre prochain tour, les coups portés avec l'objet infligent 1d6 dégâts supplémentaire. Les dégâts de l'arme deviennent des dégâts de [[Les types de dégâts#Feu|feu]].
|
|
||||||
|
|
||||||
- #element/feu (mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- Corps ardent #element/feu(6 mana, tour, V/Ge/Gl/C)
|
|
||||||
>Pendant 5 tours, toute personne terminant son tour à une case de vous subit 1d10 dégâts de [[Les types de dégâts#Feu|feu]].
|
|
||||||
|
|
||||||
- #element/feu(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- Protection supérieure #element/glace (2 mana, réaction, V/Gl)
|
|
||||||
>L'armure subit l'intégralité des dégâts sur le prochain coup.
|
|
||||||
|
|
||||||
- Lames de glace #element/glace (4 mana, tour, 12 cases)
|
|
||||||
>Tire 2 projectiles infligeant 1d8 dégâts de [[Les types de dégâts#Glace|glace]]. Augmenter les dés de dégâts offre un projectile supplémentaire à la place. Chaque projectile demande un jet d'attaque séparé et peut viser une cible différente.
|
|
||||||
|
|
||||||
- #element/glace(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/glace(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/glace(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- Chaine de foudre #element/foudre (4 mana, tour, 12 cases)
|
|
||||||
>Frappe une cible visible puis rebondit sur jusqu'à 2 autres cibles à 1 case de la première. Inflige 1d8 dégâts de [[Les types de dégâts#Foudre|foudre]].
|
|
||||||
|
|
||||||
- Vitesse lumière #element/foudre (3 mana, tour)
|
|
||||||
>Se téléporte à 6 cases tant que vous pouvez voir et courir vers la destination.
|
|
||||||
|
|
||||||
- Décharge de foudre #element/foudre(3 mana, tour)
|
|
||||||
>Tire une décharge foudroyante d'énergie, infligeant 4d4[[Glossaire#Jet explosif|!]] dégâts de [[Les types de dégâts#Foudre|foudre]].
|
|
||||||
|
|
||||||
- Faisceau fulgurant #element/foudre(4 mana, tour)
|
|
||||||
>Lance un faisceau électrique qui peut contourner les obstacles pour toucher une cible à couvert. Inflige 3d4[[Glossaire#Jet explosif|!]] dégâts de [[Les types de dégâts#Foudre|foudre]].
|
|
||||||
>*Vous pouvez viser une case que vous ne voyez pas, mais le MJ ne doit pas vous informer si l'attaque pourrait toucher une cible.*
|
|
||||||
|
|
||||||
- #element/foudre(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/terre(2 mana, tour)
|
|
||||||
>Un pilier de matière est extirpé du sol pour aller frapper la cible, qui est alors déplacée d'une case. Si la cible est propulsée contre un mur, elle subit alors 3d12 dégâts [[Les types de dégâts#Contondant|contondant]].
|
|
||||||
|
|
||||||
- #element/terre(3 mana, tour)
|
|
||||||
>Propulse un projectile de matière sur la cible, infligeant 1d12 dégâts [[Les types de dégâts#Contondant|contondant]] et appliquant un [[Les effets#L'étourdissement|étourdissement]] (2/12).
|
|
||||||
|
|
||||||
- Bouclier tortue #element/terre(3 mana, tour)
|
|
||||||
>Vous gagnez un bonus de 2 en blocage, mais subissez également un malus de 2 en esquive et perdez 2 cases de vitesse de course durant 1 min.
|
|
||||||
|
|
||||||
- #element/terre(2 mana, réaction)
|
|
||||||
> Vous gagnez une résistance aux dégâts [[Les types de dégâts#Les dégâts physiques|physiques]] jusqu'à la fin de votre prochain tour.
|
|
||||||
|
|
||||||
- #element/terre(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- Enchantement mineur #element/arcane(2 mana, tour, V/Gl)
|
|
||||||
> Condense de l'énergie magique dans une arme ou un projectile. Vous faites une attaque immédiatement après avoir lancé ce sort sans dépenser d'action, infligeant 1d8 dégâts supplémentaire. Les dégâts de l'arme deviennent [[Les types de dégâts#Neutre|magique]].
|
|
||||||
|
|
||||||
- Rupture de force #element/arcane(3 mana, tour, V/Ge/Gl)
|
|
||||||
> Vous condensez une puissante énergie magique qui est propulsée directement sur votre cible. Vous lancez 2d20 et prenez le plus haut résultat pour infliger des dégâts [[Les types de dégâts#Neutre|magique]]. *Avoir un #avantage aux dégâts permet de lancer un autre d20.* *Augmenter les dégâts de ce sort permet d'infliger 5 dégâts [[Les types de dégâts#Neutre|magique]] supplémentaire.*
|
|
||||||
|
|
||||||
- #element/arcane(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/arcane(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/arcane(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- Foulée aérienne #element/air(3 mana, tour, 12 cases)
|
|
||||||
>La vitesse de course de votre cible augmente de 2 cases pendant 1 minute. Vous gagnez également un bonus de +1 à l'esquive.
|
|
||||||
|
|
||||||
- Pression forcée #element/air(5 mana, tour, 18 cases)
|
|
||||||
>Crée une imposante colonne d'air descendent de 3 cases de rayon sur 12 cases de haut. Les créatures à l'intérieur ont un malus de 1 à l'esquive. Les créatures volantes chutent de 3 cases par tour.
|
|
||||||
|
|
||||||
- #element/air(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/air(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/air(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- Conservation #element/nature (2 mana, 1 minute)
|
|
||||||
>Permet à jusqu'à 5 herbes ou préparations médicinales de se conserver 1 jour de plus. *Ne peux être utilisé qu'une seule fois par herbe/préparation.*
|
|
||||||
|
|
||||||
- #element/nature(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/nature(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/nature(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/nature(mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- Absorption radieuse #element/lumiere (3 mana, tour)
|
|
||||||
> Absorbe la lumière d'une zone de 4 cases de rayon, la faisant apparaitre comme plus sombre. #todo
|
|
||||||
|
|
||||||
- #element/lumiere (mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/lumiere (mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/lumiere (mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/lumiere (mana, tour)
|
|
||||||
>
|
|
||||||
|
|
||||||
- #element/psy(6 mana, tour)
|
|
||||||
>Envenime l'esprit de la cible, brouillant sa perception de la réalité et lui faisant voir des images subliminales de chaos. Applique un effet de [[Les effets#La peur|peur]] (4/12).
|
|
||||||
### Rang 2
|
|
||||||
- Trait de feu 2 #element/feu (5 mana, tour, 15 cases, V/Ge/Gl)
|
|
||||||
>Tire un faisceau de flamme, infligeant 3d8 de dégâts de feu.
|
|
||||||
|
|
||||||
- Lames de glace 2 #element/glace (5 mana, tour, 15 cases)
|
|
||||||
>Tire 3 projectiles à 1d8 de glace. Augmenter les dés de dégâts offre un projectile supplémentaire à la place. Chaque projectile demande un jet d'attaque séparé et peut viser une cible différente.
|
|
||||||
|
|
||||||
- Chaine de foudre 2 #element/foudre (5 mana, tour, 15 cases)
|
|
||||||
>Frappe une cible visible puis rebondit sur jusqu'à 3 autres cibles à 2 cases de la première. 1d8+3 de foudre.
|
|
||||||
|
|
||||||
- Décharge de foudre 2 #element/foudre(3 mana, tour)
|
|
||||||
>Tire une décharge foudroyante d'énergie, infligeant 6d4[[Glossaire#Jet explosif|!]] de dégâts de foudre.
|
|
||||||
|
|
||||||
- Conservation 2 #element/nature (4 mana, 1 minute)
|
|
||||||
>Permet à jusqu'à 8 herbes ou préparations médicinales de se conserver 3 jours de plus. *Ne peux être utilisé qu'une seule fois par herbe/préparation.*
|
|
||||||
|
|
||||||
- Boule de feu #element/feu (8 mana, tour, 12 cases)
|
|
||||||
>Projette une imposante boule de flamme explosant au contact d'une surface, infligeant ainsi 4d10 de feu sur 3 cases de rayon.
|
|
||||||
|
|
||||||
- Détonation #element/feu (4 mana, tour, 8 cases)
|
|
||||||
>Pointe un lieu visible. Une explosion de flamme jaillit subitement, infligeant 2d10 de feu sur 2 cases de rayon.
|
|
||||||
|
|
||||||
- Lance de givre #element/glace(4 mana, tour)
|
|
||||||
>Une lame de glace vient grandir le long de votre arme. Augmente votre portée d'une case. L'arme inflige des dégâts tranchants. Dure 1 min, casse après 8 coups réussis.
|
|
||||||
|
|
||||||
- Téléportation #element/foudre (4 mana, tour)
|
|
||||||
>Se téléporte à un point visible à 9 cases max.
|
|
||||||
|
|
||||||
- Apaisement #element/psy (3 mana, tour)
|
|
||||||
>En touchant la cible, vous pouvez faire un jet d'intelligence. Guérit l'influence, le charme et la peur, mais augmente les chances de ces effet de 1 niveau pendant 3 tours.
|
|
||||||
|
|
||||||
- Painshock #element/psy (6 mana, tour)
|
|
||||||
>*Ne fonctionne que si la cible touchée à subit des dégâts depuis votre dernier tour.* Vous touchez une plaie et intensifiez la douleur à l'extrême. Applique un effet d'[[Les effets#L'étourdissement|étourdissement]]. La difficulté est égale à 2/12 + 1 niveau pour chaque 10% de vie max retiré.
|
|
||||||
|
|
||||||
- Perturbateur #element/psy (4 mana, réaction, 9 cases, V/Ge)
|
|
||||||
>Lorsqu'un lanceur de sort termine son incantation, vous pouvez perturber les flux magiques pour lui imposer un malus de 3 au jet.
|
|
||||||
### Rang 3
|
|
||||||
- Rejet pur #divin (spécial, tour, 3 cases, Ge)
|
|
||||||
>Vous propulsez une énergie magique pure condensée sur votre adversaire avec une puissance absolue. Vous infligez 1d6!+4 dégâts [[Les types de dégâts#Neutre|magique]] tous les 3 mana dépensé. Vous pouvez dépenser jusqu'à 30 mana. Après avoir lancé ce sort, vous subissez un malus de 4 au lancer de sort pendant 1 tour.
|
|
||||||
### Sorts unique
|
|
||||||
Les sorts uniques sont des sorts obtenus uniquement avec des objets magiques ou en progressant dans l'arbre d'entrainement. Il n'existe **aucun** autre moyen d'obtenir des sorts.
|
|
||||||
|
|
||||||
- Dévastation #element/feu + #element/glace + #element/foudre (10 mana, tour, 12 cases)
|
|
||||||
>Inflige 10+3d10 dégâts. Vous pouvez choisir le type de dégâts entre feu, glace et foudre. Ignore les résistances et réduit les immunités en résistance. ^484fc3
|
|
||||||
|
|
||||||
- Soin #element/nature (8 mana, tour, toucher)
|
|
||||||
>Soigne 10+1d10 PV et guérit l'[[Les effets#L'étourdissement|étourdissement]], le [[Les effets#Le saignement|saignement]] et les [[Les effets#L'empoisonnement|poisons]]. ^068b55
|
|
||||||
|
|
||||||
- Contresort #element/arcane (4 mana, réaction, 12 cases)
|
|
||||||
>Perturbe les flux magique pour interrompre une canalisation en cours. Vous pouvez augmenter le coût du sort pour augmenter les chances de réussite. La difficulté est égale à 6 - le cout du sort à interrompre + le cout du contresort. ^a8f46f
|
|
||||||
|
|
||||||
- Focalisation destructrice #element/arcane (12 mana, tour)
|
|
||||||
>Vous focalisez les énergies magiques sur vous, rendant l'utilisation de sort plus complexe pour les autres. La densité d'énergie anormale vous fait subir 5 points de dégâts par tour. Pendant une minute, toute personne à 18 cases de vous essayant de lancer un sort ou de [[1.Règles/6.Les Aspects/index#Transformations|se transformer]] subit un malus de 4. ^73b8bd
|
|
||||||
|
|
||||||
- Domination mentale #element/psy (10 mana, tour, toucher)
|
|
||||||
>Applique un effet de [[Les effets#La possession|possession]] (6/12). ^5b38b6
|
|
||||||
### Sorts spéciaux
|
|
||||||
Les sorts spéciaux sont une liste de sorts que les joueurs peuvent obtenir durant certaines aventures. Selon les cas, un joueur peut demander au maitre du jeu de commencer avec un sort spécial si ça correspond à son passé. Les sorts spéciaux peuvent aussi être des sorts que les PNJ ont et qu'ils peuvent apprendre aux joueurs.`);
|
|
||||||
</script>
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>Editeur</Title>
|
||||||
|
</Head>
|
||||||
|
<Editor v-model="model" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
rights: ['admin', 'editor'],
|
||||||
|
})
|
||||||
|
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>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<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] - {{ page.title }}</Title>
|
||||||
|
</Head>
|
||||||
|
<template v-if="page.type === 'markdown'">
|
||||||
|
<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>
|
||||||
|
<NuxtLink :href="{ name: 'explore-edit-path', params: { path: path } }"><Button v-if="isOwner">Modifier</Button></NuxtLink>
|
||||||
|
</div>
|
||||||
|
<Markdown :content="page.content" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="page.type === 'canvas'">
|
||||||
|
<Canvas :canvas="JSON.parse(page.content)" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ProseH2 class="flex-1 text-center">Impossible d'afficher le contenu demandé</ProseH2>
|
||||||
|
</template>
|
||||||
|
</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 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,84 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="page" class="xl:p-12 lg:p-8 py-4 flex flex-1 flex-col items-start justify-start max-h-full">
|
||||||
|
<Head>
|
||||||
|
<Title>Modification de {{ page.title }}</Title>
|
||||||
|
</Head>
|
||||||
|
<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 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">
|
||||||
|
<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="my-4 flex-1 w-full max-h-full flex">
|
||||||
|
<template v-if="page.type === 'markdown'">
|
||||||
|
<SplitterGroup direction="horizontal" class="flex-1 w-full flex">
|
||||||
|
<SplitterPanel asChild>
|
||||||
|
<textarea v-model="page.content" class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto"></textarea>
|
||||||
|
</SplitterPanel>
|
||||||
|
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
||||||
|
<SplitterPanel asChild>
|
||||||
|
<div class="flex-1 max-h-full !overflow-y-auto px-8"><Markdown :content="page.content" /></div>
|
||||||
|
</SplitterPanel>
|
||||||
|
</SplitterGroup>
|
||||||
|
</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 { user, loggedIn } = useUserSession();
|
||||||
|
|
||||||
|
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 ]});
|
||||||
|
|
||||||
|
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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue