You've already forked obsidian-visualiser
Compare commits
37 Commits
9ca546f490
...
character
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8dcc47a1b | ||
| 996b9711e4 | |||
|
|
da5c1202ed | ||
| c33bd95b81 | |||
|
|
d5851499cd | ||
|
|
e78a60f771 | ||
|
|
9a6f91a341 | ||
|
|
218b68db60 | ||
|
|
42915d699f | ||
|
|
df3577f673 | ||
|
|
871861e66e | ||
| 1ee895ab42 | |||
| 3f58114091 | |||
|
|
878bcc0a16 | ||
| e924fdfe38 | |||
| 5e6f296c56 | |||
| ab2778c626 | |||
| 7a11c5382c | |||
|
|
4885479ac6 | ||
|
|
fd0603f916 | ||
| 0771d5ebd1 | |||
|
|
598cf54bc5 | ||
|
|
9352b3f0a1 | ||
|
|
a30f394ef7 | ||
|
|
32439b41f6 | ||
|
|
b8f547d3e9 | ||
|
|
3c412d1cbe | ||
|
|
1de2439a8a | ||
|
|
308c2974f1 | ||
|
|
cb00c093ff | ||
|
|
735dfb6980 | ||
| f599b561af | |||
| 7beeed8a61 | |||
| 403a65158a | |||
| fef7c8f57c | |||
| e5b53585aa | |||
| e7d0d69e55 |
139
app.vue
139
app.vue
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
||||||
<NuxtRouteAnnouncer/>
|
<NuxtRouteAnnouncer/>
|
||||||
|
<NuxtLoadingIndicator />
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full relative">
|
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
@@ -13,27 +14,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Content } from './shared/content.util';
|
|
||||||
import * as Floating from '#shared/floating.util';
|
|
||||||
|
|
||||||
provideToaster();
|
provideToaster();
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
Content.init();
|
|
||||||
Floating.init();
|
|
||||||
|
|
||||||
const unmount = useRouter().afterEach((to, from, failure) => {
|
|
||||||
if(failure) return;
|
|
||||||
|
|
||||||
document.querySelectorAll(`a[href="${from.path}"][data-active]`).forEach(e => e.classList.remove(e.getAttribute('data-active') ?? ''));
|
|
||||||
document.querySelectorAll(`a[href="${to.path}"][data-active]`).forEach(e => e.classList.add(e.getAttribute('data-active') ?? ''));
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unmount();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const { list } = useToast();
|
const { list } = useToast();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -57,121 +39,8 @@ const { list } = useToast();
|
|||||||
@apply bg-light-50;
|
@apply bg-light-50;
|
||||||
@apply dark:bg-dark-50;
|
@apply dark:bg-dark-50;
|
||||||
}
|
}
|
||||||
.callout[data-type="abstract"],
|
|
||||||
.callout[data-type="summary"],
|
|
||||||
.callout[data-type="tldr"]
|
|
||||||
{
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[data-type="info"]
|
|
||||||
{
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
@apply text-light-blue;
|
|
||||||
@apply dark:text-dark-blue;
|
|
||||||
}
|
|
||||||
.callout[data-type="todo"]
|
|
||||||
{
|
|
||||||
@apply bg-light-blue;
|
|
||||||
@apply dark:bg-dark-blue;
|
|
||||||
@apply text-light-blue;
|
|
||||||
@apply dark:text-dark-blue;
|
|
||||||
}
|
|
||||||
.callout[data-type="important"]
|
|
||||||
{
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[data-type="tip"],
|
|
||||||
.callout[data-type="hint"]
|
|
||||||
{
|
|
||||||
@apply bg-light-cyan;
|
|
||||||
@apply dark:bg-dark-cyan;
|
|
||||||
@apply text-light-cyan;
|
|
||||||
@apply dark:text-dark-cyan;
|
|
||||||
}
|
|
||||||
.callout[data-type="success"],
|
|
||||||
.callout[data-type="check"],
|
|
||||||
.callout[data-type="done"]
|
|
||||||
{
|
|
||||||
@apply bg-light-green;
|
|
||||||
@apply dark:bg-dark-green;
|
|
||||||
@apply text-light-green;
|
|
||||||
@apply dark:text-dark-green;
|
|
||||||
}
|
|
||||||
.callout[data-type="question"],
|
|
||||||
.callout[data-type="help"],
|
|
||||||
.callout[data-type="faq"]
|
|
||||||
{
|
|
||||||
@apply bg-light-orange;
|
|
||||||
@apply dark:bg-dark-orange;
|
|
||||||
@apply text-light-orange;
|
|
||||||
@apply dark:text-dark-orange;
|
|
||||||
}
|
|
||||||
.callout[data-type="warning"],
|
|
||||||
.callout[data-type="caution"],
|
|
||||||
.callout[data-type="attention"]
|
|
||||||
{
|
|
||||||
@apply bg-light-orange;
|
|
||||||
@apply dark:bg-dark-orange;
|
|
||||||
@apply text-light-orange;
|
|
||||||
@apply dark:text-dark-orange;
|
|
||||||
}
|
|
||||||
.callout[data-type="failure"],
|
|
||||||
.callout[data-type="fail"],
|
|
||||||
.callout[data-type="missing"]
|
|
||||||
{
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[data-type="danger"],
|
|
||||||
.callout[data-type="error"]
|
|
||||||
{
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[data-type="bug"]
|
|
||||||
{
|
|
||||||
@apply bg-light-red;
|
|
||||||
@apply dark:bg-dark-red;
|
|
||||||
@apply text-light-red;
|
|
||||||
@apply dark:text-dark-red;
|
|
||||||
}
|
|
||||||
.callout[data-type="example"]
|
|
||||||
{
|
|
||||||
@apply bg-light-purple;
|
|
||||||
@apply dark:bg-dark-purple;
|
|
||||||
@apply text-light-purple;
|
|
||||||
@apply dark:text-dark-purple;
|
|
||||||
}
|
|
||||||
.variant-cap
|
|
||||||
{
|
|
||||||
font-variant: small-caps;
|
|
||||||
}
|
|
||||||
.cm-editor
|
|
||||||
{
|
|
||||||
@apply bg-transparent;
|
|
||||||
@apply flex-1 h-full;
|
|
||||||
@apply font-sans;
|
|
||||||
|
|
||||||
@apply text-light-100 dark:text-dark-100;
|
::-webkit-scrollbar-corner {
|
||||||
}
|
@apply bg-transparent;
|
||||||
.cm-editor .cm-content
|
|
||||||
{
|
|
||||||
@apply caret-light-100 dark:caret-dark-100;
|
|
||||||
}
|
|
||||||
.cm-line
|
|
||||||
{
|
|
||||||
@apply text-base;
|
|
||||||
@apply font-sans;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -29,6 +29,12 @@ type EdgeEditor = InstanceType<typeof CanvasEdgeEditor>;
|
|||||||
|
|
||||||
const cancelEvent = (e: Event) => e.preventDefault();
|
const cancelEvent = (e: Event) => e.preventDefault();
|
||||||
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
||||||
|
function getID(length: number)
|
||||||
|
{
|
||||||
|
for (var id = [], i = 0; i < length; i++)
|
||||||
|
id.push((16 * Math.random() | 0).toString(16));
|
||||||
|
return id.join("");
|
||||||
|
}
|
||||||
function center(touches: TouchList): Position
|
function center(touches: TouchList): Position
|
||||||
{
|
{
|
||||||
const pos = { x: 0, y: 0 };
|
const pos = { x: 0, y: 0 };
|
||||||
@@ -52,7 +58,7 @@ function distance(touches: TouchList): number
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import { clamp, getID, ID_SIZE } from '#shared/general.util';
|
import { clamp } from '#shared/general.util';
|
||||||
import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas';
|
import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas';
|
||||||
|
|
||||||
const canvas = defineModel<CanvasContent>({ required: true });
|
const canvas = defineModel<CanvasContent>({ required: true });
|
||||||
@@ -69,11 +75,12 @@ const hints = ref<SnapHint[]>([]);
|
|||||||
const viewport = computed<Box>(() => {
|
const viewport = computed<Box>(() => {
|
||||||
const width = viewportSize.width.value / zoom.value, height = viewportSize.height.value / zoom.value;
|
const width = viewportSize.width.value / zoom.value, height = viewportSize.height.value / zoom.value;
|
||||||
const movementX = viewportSize.width.value - width, movementY = viewportSize.height.value - height;
|
const movementX = viewportSize.width.value - width, movementY = viewportSize.height.value - height;
|
||||||
return { x: -dispX.value + movementX / 2, y: -dispY.value + movementY / 2, width, height };
|
return { x: -dispX.value + movementX / 2, y: -dispY.value + movementY / 2, w: width, h: height };
|
||||||
});
|
});
|
||||||
const updateScaleVar = useDebounceFn(() => {
|
const updateScaleVar = useDebounceFn(() => {
|
||||||
if(transformRef.value)
|
if(transformRef.value)
|
||||||
{
|
{
|
||||||
|
console.log(zoom.value);
|
||||||
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
|
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
|
||||||
}
|
}
|
||||||
if(canvasRef.value)
|
if(canvasRef.value)
|
||||||
@@ -93,19 +100,10 @@ const historyPos = ref(-1);
|
|||||||
const historyCursor = computed(() => history.value.length > 0 && historyPos.value > -1 ? history.value[historyPos.value] : undefined);
|
const historyCursor = computed(() => history.value.length > 0 && historyPos.value > -1 ? history.value[historyPos.value] : undefined);
|
||||||
|
|
||||||
watch(props, () => {
|
watch(props, () => {
|
||||||
snapFinder = new SnapFinder(hints, viewport, { gridSize: 512, preferences: canvasSettings.value, threshold: 16, cellSize: 64 });
|
snapFinder = new SnapFinder(hints, viewport, { gridSize: 512, preferences: canvasSettings.value, threshold: 16, cellSize: 64 })
|
||||||
|
canvas.value.nodes?.forEach((e) => snapFinder.update(e));
|
||||||
canvas.value.nodes?.forEach((e) => {
|
|
||||||
snapFinder.update(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
focusing.value = undefined;
|
focusing.value = undefined;
|
||||||
editing.value = undefined;
|
editing.value = undefined;
|
||||||
|
|
||||||
dispX.value = 0;
|
|
||||||
dispY.value = 0;
|
|
||||||
zoom.value = 0.5;
|
|
||||||
|
|
||||||
history.value = [];
|
history.value = [];
|
||||||
historyPos.value = -1;
|
historyPos.value = -1;
|
||||||
fakeEdge.value = {};
|
fakeEdge.value = {};
|
||||||
@@ -336,10 +334,11 @@ function edit(element: Element)
|
|||||||
}
|
}
|
||||||
function createNode(e: MouseEvent)
|
function createNode(e: MouseEvent)
|
||||||
{
|
{
|
||||||
|
let box = canvasRef.value?.getBoundingClientRect()!;
|
||||||
const width = 250, height = 100;
|
const width = 250, height = 100;
|
||||||
const x = e.layerX / zoom.value - dispX.value - width / 2;
|
const x = (e.layerX / zoom.value) - dispX.value - (width / 2);
|
||||||
const y = e.layerY / zoom.value - dispY.value - height / 2;
|
const y = (e.layerY / zoom.value) - dispY.value - (height / 2);
|
||||||
const node: CanvasNode = { id: getID(ID_SIZE), x, y, width, height, type: 'text' };
|
const node: CanvasNode = { id: getID(16), x, y, width, height, type: 'text' };
|
||||||
|
|
||||||
if(!canvas.value.nodes)
|
if(!canvas.value.nodes)
|
||||||
canvas.value.nodes = [node];
|
canvas.value.nodes = [node];
|
||||||
@@ -405,7 +404,7 @@ function dragEndEdgeTo(e: MouseEvent): void
|
|||||||
if(fakeEdge.value.snapped)
|
if(fakeEdge.value.snapped)
|
||||||
{
|
{
|
||||||
const node = canvas.value.nodes!.find(e => e.id === fakeEdge.value.drag!.id)!;
|
const node = canvas.value.nodes!.find(e => e.id === fakeEdge.value.drag!.id)!;
|
||||||
const edge: CanvasEdge = { fromNode: fakeEdge.value.drag!.id, fromSide: fakeEdge.value.fromSide!, toNode: fakeEdge.value.snapped.node, toSide: fakeEdge.value.snapped.side, id: getID(ID_SIZE), color: node.color };
|
const edge: CanvasEdge = { fromNode: fakeEdge.value.drag!.id, fromSide: fakeEdge.value.fromSide!, toNode: fakeEdge.value.snapped.node, toSide: fakeEdge.value.snapped.side, id: getID(16), color: node.color };
|
||||||
canvas.value.edges?.push(edge);
|
canvas.value.edges?.push(edge);
|
||||||
|
|
||||||
addAction('create', [{ from: undefined, to: edge, element: { id: edge.id, type: 'edge' } }]);
|
addAction('create', [{ from: undefined, to: edge, element: { id: edge.id, type: 'edge' } }]);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, type DecorationSet } from '@codemirror/view';
|
<script lang="ts">
|
||||||
import { Annotation, EditorState, SelectionRange, type Range } from '@codemirror/state';
|
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
|
||||||
|
import { Annotation, EditorState, RangeValue, SelectionRange, type Range } from '@codemirror/state';
|
||||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||||
import { bracketMatching, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
|
import { bracketMatching, defaultHighlightStyle, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
|
||||||
import { search, searchKeymap } from '@codemirror/search';
|
import { search, searchKeymap } from '@codemirror/search';
|
||||||
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||||||
import { lintKeymap } from '@codemirror/lint';
|
import { lintKeymap } from '@codemirror/lint';
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
import { IterMode, Tree } from '@lezer/common';
|
import { IterMode, Tree } from '@lezer/common';
|
||||||
import { tags } from '@lezer/highlight';
|
import { tags } from '@lezer/highlight';
|
||||||
import { dom } from './dom.util';
|
|
||||||
const External = Annotation.define<boolean>();
|
const External = Annotation.define<boolean>();
|
||||||
const Hidden = Decoration.mark({ class: 'hidden' });
|
const Hidden = Decoration.mark({ class: 'hidden' });
|
||||||
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
|
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
|
||||||
@@ -99,16 +99,24 @@ class Decorator
|
|||||||
return decorations;
|
return decorations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
export class MarkdownEditor
|
<script setup lang="ts">
|
||||||
{
|
const { autofocus = false } = defineProps<{
|
||||||
private static _singleton: MarkdownEditor;
|
placeholder?: string
|
||||||
|
autofocus?: boolean
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string>();
|
||||||
|
|
||||||
private view: EditorView;
|
const editor = useTemplateRef('editor');
|
||||||
onChange?: (content: string) => void;
|
const view = ref<EditorView>();
|
||||||
constructor()
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(editor.value)
|
||||||
{
|
{
|
||||||
this.view = new EditorView({
|
view.value = new EditorView({
|
||||||
|
doc: model.value,
|
||||||
|
parent: editor.value,
|
||||||
extensions: [
|
extensions: [
|
||||||
markdown({
|
markdown({
|
||||||
base: markdownLanguage,
|
base: markdownLanguage,
|
||||||
@@ -156,7 +164,9 @@ export class MarkdownEditor
|
|||||||
]),
|
]),
|
||||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||||
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
||||||
this.onChange && this.onChange(viewUpdate.state.doc.toString());
|
{
|
||||||
|
model.value = viewUpdate.state.doc.toString();
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
EditorView.contentAttributes.of({spellcheck: "true"}),
|
EditorView.contentAttributes.of({spellcheck: "true"}),
|
||||||
ViewPlugin.fromClass(Decorator, {
|
ViewPlugin.fromClass(Decorator, {
|
||||||
@@ -164,88 +174,62 @@ export class MarkdownEditor
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
|
||||||
focus()
|
|
||||||
{
|
|
||||||
this.view.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
set content(value: string)
|
if(autofocus)
|
||||||
{
|
|
||||||
if (value === undefined)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const currentValue = this.view ? this.view.state.doc.toString() : "";
|
|
||||||
if (this.view && value !== currentValue)
|
|
||||||
{
|
{
|
||||||
this.view.dispatch({
|
view.value.focus();
|
||||||
changes: { from: 0, to: currentValue.length, insert: value || "" },
|
|
||||||
annotations: [External.of(true)],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
get content(): string
|
});
|
||||||
{
|
|
||||||
return this.view.state.doc.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
get dom()
|
onBeforeUnmount(() => {
|
||||||
|
if (view.value)
|
||||||
{
|
{
|
||||||
return this.view.dom;
|
view.value?.destroy();
|
||||||
|
view.value = undefined;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
static get singleton(): MarkdownEditor
|
watchEffect(() => {
|
||||||
{
|
if (model.value === void 0) {
|
||||||
if(!MarkdownEditor._singleton)
|
return;
|
||||||
MarkdownEditor._singleton = new MarkdownEditor();
|
|
||||||
return MarkdownEditor._singleton;
|
|
||||||
}
|
}
|
||||||
}
|
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 || "" },
|
||||||
|
annotations: [External.of(true)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export class FramedEditor
|
defineExpose({ focus: () => editor.value?.focus() });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="editor" class="flex flex-1 w-full justify-stretch items-stretch py-2 px-1.5 font-sans text-base"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.variant-cap
|
||||||
{
|
{
|
||||||
editor: MarkdownEditor;
|
font-variant: small-caps;
|
||||||
dom: HTMLIFrameElement;
|
|
||||||
|
|
||||||
private static _singleton: FramedEditor;
|
|
||||||
static get singleton()
|
|
||||||
{
|
|
||||||
if(!FramedEditor._singleton)
|
|
||||||
FramedEditor._singleton = new FramedEditor();
|
|
||||||
|
|
||||||
return FramedEditor._singleton;
|
|
||||||
}
|
|
||||||
private constructor()
|
|
||||||
{
|
|
||||||
this.editor = MarkdownEditor.singleton;
|
|
||||||
this.dom = dom('iframe');
|
|
||||||
this.dom.addEventListener('load', () => {
|
|
||||||
if(!this.dom.contentDocument)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.dom.contentDocument.documentElement.setAttribute('class', document.documentElement.getAttribute('class') ?? '');
|
|
||||||
this.dom.contentDocument.documentElement.setAttribute('style', document.documentElement.getAttribute('style') ?? '');
|
|
||||||
|
|
||||||
const base = this.dom.contentDocument.head.appendChild(this.dom.contentDocument.createElement('base'));
|
|
||||||
base.setAttribute('href', window.location.href);
|
|
||||||
|
|
||||||
for(let element of document.getElementsByTagName('link'))
|
|
||||||
{
|
|
||||||
if(element.getAttribute('rel') === 'stylesheet')
|
|
||||||
this.dom.contentDocument.head.appendChild(element.cloneNode(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
for(let element of document.getElementsByTagName('style'))
|
|
||||||
{
|
|
||||||
this.dom.contentDocument.head.appendChild(element.cloneNode(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dom.contentDocument.body.setAttribute('class', document.body.getAttribute('class') ?? '');
|
|
||||||
this.dom.contentDocument.body.setAttribute('style', document.body.getAttribute('style') ?? '');
|
|
||||||
|
|
||||||
this.dom.contentDocument.body.appendChild(this.editor.dom);
|
|
||||||
|
|
||||||
this.editor.focus();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.cm-editor
|
||||||
|
{
|
||||||
|
@apply bg-transparent;
|
||||||
|
@apply flex-1 h-full;
|
||||||
|
@apply font-sans;
|
||||||
|
|
||||||
|
@apply text-light-100 dark:text-dark-100;
|
||||||
|
}
|
||||||
|
.cm-editor .cm-content
|
||||||
|
{
|
||||||
|
@apply caret-light-100 dark:caret-dark-100;
|
||||||
|
}
|
||||||
|
.cm-line
|
||||||
|
{
|
||||||
|
@apply text-base;
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
40
components/FramedEditor.vue
Normal file
40
components/FramedEditor.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<Editor ref="editor" v-model="model" autofocus :gutters="false" />
|
||||||
|
<iframe ref="iframe" class="w-full h-full border-0" sandbox="allow-same-origin allow-scripts"></iframe>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const model = defineModel<string>();
|
||||||
|
const editor = useTemplateRef('editor'), iframe = useTemplateRef('iframe');
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(iframe.value && iframe.value.contentDocument && editor.value)
|
||||||
|
{
|
||||||
|
editor.value.$el.remove();
|
||||||
|
|
||||||
|
iframe.value.contentDocument.documentElement.setAttribute('class', document.documentElement.getAttribute('class') ?? '');
|
||||||
|
iframe.value.contentDocument.documentElement.setAttribute('style', document.documentElement.getAttribute('style') ?? '');
|
||||||
|
|
||||||
|
const base = iframe.value.contentDocument.head.appendChild(iframe.value.contentDocument.createElement('base'));
|
||||||
|
base.setAttribute('href', window.location.href);
|
||||||
|
|
||||||
|
for(let element of document.getElementsByTagName('link'))
|
||||||
|
{
|
||||||
|
if(element.getAttribute('rel') === 'stylesheet')
|
||||||
|
iframe.value.contentDocument.head.appendChild(element.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let element of document.getElementsByTagName('style'))
|
||||||
|
{
|
||||||
|
iframe.value.contentDocument.head.appendChild(element.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe.value.contentDocument.body.setAttribute('class', document.body.getAttribute('class') ?? '');
|
||||||
|
iframe.value.contentDocument.body.setAttribute('style', document.body.getAttribute('style') ?? '');
|
||||||
|
|
||||||
|
iframe.value.contentDocument.body.appendChild(editor.value.$el);
|
||||||
|
|
||||||
|
editor.value.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
49
components/MarkdownRenderer.vue
Normal file
49
components/MarkdownRenderer.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="content && content.length > 0">
|
||||||
|
<ProsesRenderer #default v-if="data" :node="data" :proses="proses" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
import { heading } from 'hast-util-heading';
|
||||||
|
import { headingRank } from 'hast-util-heading-rank';
|
||||||
|
import { parseId } from '~/shared/general.util';
|
||||||
|
import type { Root } from 'hast';
|
||||||
|
|
||||||
|
const { content, proses, filter } = defineProps<{
|
||||||
|
content: string
|
||||||
|
proses?: Record<string, string | Component>
|
||||||
|
filter?: string
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const parser = useMarkdown(), data = ref<Root>();
|
||||||
|
const node = computed(() => content ? parser(content) : undefined);
|
||||||
|
watch([node], () => {
|
||||||
|
if(!node.value)
|
||||||
|
data.value = undefined;
|
||||||
|
else if(!filter)
|
||||||
|
{
|
||||||
|
data.value = node.value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const start = node.value?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
|
||||||
|
|
||||||
|
if(start === -1)
|
||||||
|
data.value = node.value;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let end = start;
|
||||||
|
const rank = headingRank(node.value.children[start])!;
|
||||||
|
while(end < node.value.children.length)
|
||||||
|
{
|
||||||
|
end++;
|
||||||
|
if(heading(node.value.children[end]) && headingRank(node.value.children[end])! <= rank)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
data.value = { ...node.value, children: node.value.children.slice(start, end) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true, });
|
||||||
|
</script>
|
||||||
115
components/ProsesRenderer.vue
Normal file
115
components/ProsesRenderer.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { RootContent, Root } from 'hast';
|
||||||
|
import { Text, Comment } from 'vue';
|
||||||
|
|
||||||
|
import ProseP from '~/components/prose/ProseP.vue';
|
||||||
|
import ProseA from '~/components/prose/ProseA.vue';
|
||||||
|
import ProseBlockquote from '~/components/prose/ProseBlockquote.vue';
|
||||||
|
import ProseCallout from './prose/ProseCallout.vue';
|
||||||
|
import ProseCode from '~/components/prose/ProseCode.vue';
|
||||||
|
import ProsePre from '~/components/prose/ProsePre.vue';
|
||||||
|
import ProseEm from '~/components/prose/ProseEm.vue';
|
||||||
|
import ProseH1 from '~/components/prose/ProseH1.vue';
|
||||||
|
import ProseH2 from '~/components/prose/ProseH2.vue';
|
||||||
|
import ProseH3 from '~/components/prose/ProseH3.vue';
|
||||||
|
import ProseH4 from '~/components/prose/ProseH4.vue';
|
||||||
|
import ProseH5 from '~/components/prose/ProseH5.vue';
|
||||||
|
import ProseH6 from '~/components/prose/ProseH6.vue';
|
||||||
|
import ProseHr from '~/components/prose/ProseHr.vue';
|
||||||
|
import ProseImg from '~/components/prose/ProseImg.vue';
|
||||||
|
import ProseUl from '~/components/prose/ProseUl.vue';
|
||||||
|
import ProseOl from '~/components/prose/ProseOl.vue';
|
||||||
|
import ProseLi from '~/components/prose/ProseLi.vue';
|
||||||
|
import ProseSmall from './prose/ProseSmall.vue';
|
||||||
|
import ProseStrong from '~/components/prose/ProseStrong.vue';
|
||||||
|
import ProseTable from '~/components/prose/ProseTable.vue';
|
||||||
|
import ProseTag from '~/components/prose/ProseTag.vue';
|
||||||
|
import ProseThead from '~/components/prose/ProseThead.vue';
|
||||||
|
import ProseTbody from '~/components/prose/ProseTbody.vue';
|
||||||
|
import ProseTd from '~/components/prose/ProseTd.vue';
|
||||||
|
import ProseTh from '~/components/prose/ProseTh.vue';
|
||||||
|
import ProseTr from '~/components/prose/ProseTr.vue';
|
||||||
|
import ProseScript from '~/components/prose/ProseScript.vue';
|
||||||
|
|
||||||
|
const proseList = {
|
||||||
|
"p": ProseP,
|
||||||
|
"a": ProseA,
|
||||||
|
"blockquote": ProseBlockquote,
|
||||||
|
"callout": ProseCallout,
|
||||||
|
"code": ProseCode,
|
||||||
|
"pre": ProsePre,
|
||||||
|
"em": ProseEm,
|
||||||
|
"h1": ProseH1,
|
||||||
|
"h2": ProseH2,
|
||||||
|
"h3": ProseH3,
|
||||||
|
"h4": ProseH4,
|
||||||
|
"h5": ProseH5,
|
||||||
|
"h6": ProseH6,
|
||||||
|
"hr": ProseHr,
|
||||||
|
"img": ProseImg,
|
||||||
|
"ul": ProseUl,
|
||||||
|
"ol": ProseOl,
|
||||||
|
"li": ProseLi,
|
||||||
|
"small": ProseSmall,
|
||||||
|
"strong": ProseStrong,
|
||||||
|
"table": ProseTable,
|
||||||
|
"tag": ProseTag,
|
||||||
|
"thead": ProseThead,
|
||||||
|
"tbody": ProseTbody,
|
||||||
|
"td": ProseTd,
|
||||||
|
"th": ProseTh,
|
||||||
|
"tr": ProseTr,
|
||||||
|
"script": ProseScript
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'MarkdownRenderer',
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
proses: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setup(props) {
|
||||||
|
if(props.proses)
|
||||||
|
{
|
||||||
|
for(const prose of Object.keys(props.proses))
|
||||||
|
{
|
||||||
|
if(typeof props.proses[prose] === 'string')
|
||||||
|
props.proses[prose] = await resolveComponent(props.proses[prose]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { tags: Object.assign({}, proseList, props.proses) };
|
||||||
|
},
|
||||||
|
render(ctx: any) {
|
||||||
|
const { node, tags } = ctx;
|
||||||
|
|
||||||
|
if(!node)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return h('div', null, {default: () => (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderNode(node: RootContent, tags: Record<string, any>): VNode | undefined
|
||||||
|
{
|
||||||
|
if(node.type === 'text' && node.value.length > 0 && node.value !== '\n')
|
||||||
|
{
|
||||||
|
return h(Text, node.value);
|
||||||
|
}
|
||||||
|
else if(node.type === 'comment' && node.value.length > 0 && node.value !== '\n')
|
||||||
|
{
|
||||||
|
return h(Comment, node.value);
|
||||||
|
}
|
||||||
|
else if(node.type === 'element')
|
||||||
|
{
|
||||||
|
return h(tags[node.tagName] ?? node.tagName, { ...node.properties, class: node.properties.className }, { default: () => node.children.map(e => renderNode(e, tags)).filter(e => !!e) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
39
components/TrainingConfigEditor.vue
Normal file
39
components/TrainingConfigEditor.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CharacterConfig, MainStat, TrainingLevel } from '~/types/character';
|
||||||
|
import PreviewA from './prose/PreviewA.vue';
|
||||||
|
|
||||||
|
const { config } = defineProps<{
|
||||||
|
config: CharacterConfig
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selection = ref<{
|
||||||
|
stat: MainStat;
|
||||||
|
level: TrainingLevel;
|
||||||
|
option: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function focusTraining(stat: MainStat, level: TrainingLevel, option: number)
|
||||||
|
{
|
||||||
|
const s = selection.value;
|
||||||
|
if(s !== undefined && s.stat === stat && s.level === level && s.option === option)
|
||||||
|
{
|
||||||
|
selection.value = undefined;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selection.value = {
|
||||||
|
stat, level, option
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TrainingViewer :config="config" progress>
|
||||||
|
<template #default="{ stat, level, option }">
|
||||||
|
<div @click.capture="console.log" class="border border-light-40 dark:border-dark-40 hover:border-light-70 dark:hover:border-dark-70 cursor-pointer px-2 py-1 w-[400px]" :class="{ '!border-accent-blue': selection !== undefined && selection?.stat == stat && selection?.level == level && selection?.option == option }">
|
||||||
|
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="config.training[stat][level][option].description.map(e => e.text).join('\n')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TrainingViewer>
|
||||||
|
</template>
|
||||||
50
components/TrainingViewer.vue
Normal file
50
components/TrainingViewer.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { MAIN_STATS, mainStatTexts, type CharacterConfig } from '~/types/character';
|
||||||
|
|
||||||
|
const { config } = defineProps<{
|
||||||
|
config: CharacterConfig
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const position = ref(0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10 min-h-20">
|
||||||
|
<div class="flex flex-shrink gap-3 items-center relative w-48 ms-12">
|
||||||
|
<span v-for="(stat, i) of MAIN_STATS" :value="stat" class="block w-2.5 h-2.5 m-px outline outline-1 outline-transparent
|
||||||
|
hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer" @click="position = i"></span>
|
||||||
|
<span :style="{ 'left': position * 1.5 + 'em' }" :data-text="mainStatTexts[MAIN_STATS[position]]" class="rounded-full w-3 h-3 bg-accent-blue absolute transition-[left]
|
||||||
|
after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex">
|
||||||
|
<slot name="addin" :stat="MAIN_STATS[position]"></slot>
|
||||||
|
</div>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 px-8 overflow-hidden max-w-full">
|
||||||
|
<div class="relative cursor-grab active:cursor-grabbing select-none transition-[left] flex flex-1 flex-row max-w-full" :style="{ 'left': `-${position * 100}%` }">
|
||||||
|
<div class="flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-20" v-for="(stat, name) in config.training">
|
||||||
|
<template v-for="(options, level) of stat">
|
||||||
|
<div class="w-full flex h-px"><div class="border-t border-dashed border-light-50 dark:border-dark-50 w-full"></div><span class="relative left-4">{{ level }}</span></div>
|
||||||
|
<div class="flex flex-row gap-4 justify-center">
|
||||||
|
<template v-for="(option, i) in options">
|
||||||
|
<slot :stat="name" :level="level" :option="i"></slot>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-20" v-for="(stat, name) in config.training" >
|
||||||
|
<div class="flex flex-row gap-2 justify-center relative" v-for="(options, level) in stat">
|
||||||
|
<template v-if="progress">
|
||||||
|
<div class="absolute left-0 right-0 -top-2 h-px border-t border-light-30 dark:border-dark-30 border-dashed">
|
||||||
|
<span class="absolute right-0 p-1 text-end">{{ level }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-for="(option, i) in options">
|
||||||
|
<slot :stat="name" :level="level" :option="i"></slot>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<CollapsibleRoot v-model:open="model" :disabled="disabled" :defaultOpen="defaultOpen">
|
<CollapsibleRoot v-model:open="model" :disabled="disabled" :defaultOpen="defaultOpen">
|
||||||
|
<slot name="alwaysVisible"></slot>
|
||||||
<div class="flex flex-row justify-center items-center">
|
<div class="flex flex-row justify-center items-center">
|
||||||
<span v-if="!!label">{{ label }}</span>
|
<span>{{ label }}<slot name="label"></slot></span>
|
||||||
<CollapsibleTrigger class="ms-4" asChild>
|
<CollapsibleTrigger class="ms-4" asChild>
|
||||||
<Button icon :disabled="disabled">
|
<Button icon :disabled="disabled">
|
||||||
<Icon v-if="model" icon="radix-icons:cross-2" class="h-4 w-4" />
|
<Icon v-if="model" icon="radix-icons:cross-2" class="h-4 w-4" />
|
||||||
@@ -9,7 +10,6 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
</div>
|
</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]">
|
<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>
|
<slot></slot>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
|
|||||||
45
components/base/Combobox.vue
Normal file
45
components/base/Combobox.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<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>
|
||||||
|
<ComboboxRoot v-model:model-value="model" v-model:open="open" :multiple="multiple">
|
||||||
|
<ComboboxAnchor :disabled="disabled" class="mx-4 inline-flex min-w-[150px] 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">
|
||||||
|
<ComboboxTrigger class="flex flex-1 justify-between !cursor-pointer">
|
||||||
|
<span v-if="!multiple">{{ model !== undefined ? options.find(e => e[1] === model)![0] : "" }}</span>
|
||||||
|
<span class="flex gap-2" v-else><span v-if="model !== undefined">{{ options.find(e => e[1] === (model as T[])[0]) !== undefined ? options.find(e => e[1] === (model as T[])[0])![0] : undefined }}</span><span v-if="model !== undefined && (model as T[]).length > 1">{{((model as T[]).length > 1 ? `+${(model as T[]).length - 1}` : "") }}</span></span>
|
||||||
|
<Icon icon="radix-icons:caret-down" class="h-4 w-4" />
|
||||||
|
</ComboboxTrigger>
|
||||||
|
</ComboboxAnchor>
|
||||||
|
|
||||||
|
<ComboboxPortal :disabled="disabled">
|
||||||
|
<ComboboxContent :position="position" align="start" class="min-w-[150px] bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] z-50">
|
||||||
|
<ComboboxViewport>
|
||||||
|
<ComboboxItem v-for="[label, value] of options" :value="value" :disabled="disabled" class="text-base py-2 leading-none text-light-60 dark:text-dark-60 flex items-center px-6 relative Combobox-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">
|
||||||
|
<span class="">{{ label }}</span>
|
||||||
|
<ComboboxItemIndicator class="absolute left-1 w-4 inline-flex items-center justify-center">
|
||||||
|
<Icon icon="radix-icons:check" />
|
||||||
|
</ComboboxItemIndicator>
|
||||||
|
</ComboboxItem>
|
||||||
|
</ComboboxViewport>
|
||||||
|
</ComboboxContent>
|
||||||
|
</ComboboxPortal>
|
||||||
|
</ComboboxRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends string | number | boolean | Record<string, any>">
|
||||||
|
import { ComboboxInput, ComboboxTrigger, ComboboxViewport, ComboboxContent, ComboboxPortal, ComboboxRoot } from 'radix-vue'
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
const { disabled = false, position = 'popper', multiple = false } = defineProps<{
|
||||||
|
placeholder?: string
|
||||||
|
disabled?: boolean
|
||||||
|
position?: 'inline' | 'popper'
|
||||||
|
label?: string
|
||||||
|
multiple?: boolean
|
||||||
|
options: Array<[string, T]>
|
||||||
|
}>();
|
||||||
|
const open = ref(false);
|
||||||
|
const model = defineModel<T | T[]>();
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Label class="my-2 flex flex-1 items-center justify-between flex-col md:flex-row">
|
<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>
|
<span class="pb-1 md:p-0">{{ label }}</span>
|
||||||
<SelectRoot v-model="model">
|
<SelectRoot v-model="model" :default-value="defaultValue">
|
||||||
<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
|
<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
|
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
|
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
|
||||||
@@ -31,11 +31,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SelectContent, SelectPortal, SelectRoot, SelectScrollDownButton, SelectScrollUpButton, SelectTrigger, SelectValue, SelectViewport } from 'radix-vue'
|
import { SelectContent, SelectPortal, SelectRoot, SelectScrollDownButton, SelectScrollUpButton, SelectTrigger, SelectValue, SelectViewport } from 'radix-vue'
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
const { placeholder, disabled = false, position = 'popper', label } = defineProps<{
|
const { disabled = false, position = 'popper' } = defineProps<{
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
position?: 'item-aligned' | 'popper'
|
position?: 'item-aligned' | 'popper'
|
||||||
label?: string
|
label?: string
|
||||||
|
defaultValue?: string
|
||||||
}>();
|
}>();
|
||||||
const model = defineModel<string>();
|
const model = defineModel<string>();
|
||||||
</script>
|
</script>
|
||||||
@@ -12,7 +12,7 @@ import { Icon } from '@iconify/vue/dist/iconify.js';
|
|||||||
import { SelectItem, SelectItemIndicator, SelectItemText } from 'radix-vue'
|
import { SelectItem, SelectItemIndicator, SelectItemText } from 'radix-vue'
|
||||||
const { disabled = false, value } = defineProps<{
|
const { disabled = false, value } = defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
value: NonNullable<any>
|
value: NonNullable<string>
|
||||||
label: string
|
label: string
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Label class="flex justify-center items-center my-2">
|
<Label class="flex justify-center items-center my-2">
|
||||||
<span class="md:text-base text-sm">{{ label }}</span>
|
<span class="md:text-base text-sm">{{ label }}</span>
|
||||||
<SwitchRoot v-model:checked="model" :disabled="disabled"
|
<SwitchRoot v-model:checked="model" :disabled="disabled" :default-checked="defaultValue"
|
||||||
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
|
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-[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">
|
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative">
|
||||||
@@ -21,6 +21,7 @@ const { label, disabled, onIcon, offIcon } = defineProps<{
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onIcon?: string
|
onIcon?: string
|
||||||
offIcon?: string
|
offIcon?: string
|
||||||
|
defaultValue?: boolean
|
||||||
}>();
|
}>();
|
||||||
const model = defineModel<boolean>();
|
const model = defineModel<boolean>();
|
||||||
</script>
|
</script>
|
||||||
@@ -4,10 +4,12 @@
|
|||||||
<div class="grid grid-cols-8 px-3 pt-2 pb-2">
|
<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>
|
<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>
|
<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>
|
<ToastDescription v-if="toast.content" class="text-sm col-span-8 text-light-100 dark:text-dark-100" asChild><span>{{ toast.content }}</span></ToastDescription>
|
||||||
</div>
|
</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
|
<TimerProgress v-if="toast.timer" shape="thin" :delay="toast.duration" class="mb-0 mt-0 w-full
|
||||||
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)" />
|
group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green
|
||||||
|
group-data-[type=error]:bg-light-red dark:group-data-[type=error]:bg-dark-red group-data-[type=success]:bg-light-green dark:group-data-[type=success]:bg-dark-green !bg-opacity-50"
|
||||||
|
@finish="() => tryClose(toast, false)" />
|
||||||
</ToastRoot>
|
</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" />
|
<ToastViewport class="fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72" />
|
||||||
@@ -37,14 +39,16 @@ function tryClose(config: ExtraToastConfig, state: boolean)
|
|||||||
.ToastRoot[data-type='error'] {
|
.ToastRoot[data-type='error'] {
|
||||||
@apply border-light-red;
|
@apply border-light-red;
|
||||||
@apply dark:border-dark-red;
|
@apply dark:border-dark-red;
|
||||||
@apply bg-light-redBack;
|
@apply bg-light-red;
|
||||||
@apply dark:bg-dark-redBack;
|
@apply dark:bg-dark-red;
|
||||||
|
@apply !bg-opacity-50;
|
||||||
}
|
}
|
||||||
.ToastRoot[data-type='success'] {
|
.ToastRoot[data-type='success'] {
|
||||||
@apply border-light-green;
|
@apply border-light-green;
|
||||||
@apply dark:border-dark-green;
|
@apply dark:border-dark-green;
|
||||||
@apply bg-light-greenBack;
|
@apply bg-light-green;
|
||||||
@apply dark:bg-dark-greenBack;
|
@apply dark:bg-dark-green;
|
||||||
|
@apply !bg-opacity-50;
|
||||||
}
|
}
|
||||||
.ToastRoot[data-state='open'] {
|
.ToastRoot[data-state='open'] {
|
||||||
animation: slideIn .15s cubic-bezier(0.16, 1, 0.3, 1);
|
animation: slideIn .15s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<span tabindex="0"><slot></slot></span>
|
<span tabindex="0"><slot></slot></span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<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">
|
<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" :class="$attrs.class" :side="side" :align="align" :align-offset="-16" :side-offset="['left', 'right'].includes(side ?? '') ? 8 : 0">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
<TooltipArrow class="fill-light-30 dark:fill-dark-30"></TooltipArrow>
|
<TooltipArrow class="fill-light-30 dark:fill-dark-30"></TooltipArrow>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -15,9 +15,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { message, delay = 300, side } = defineProps<{
|
const { message, delay = 300, side } = defineProps<{
|
||||||
message: string
|
message: string
|
||||||
delay?: number,
|
delay?: number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
side?: 'left' | 'right' | 'top' | 'bottom'
|
side?: 'left' | 'right' | 'top' | 'bottom'
|
||||||
|
align?: 'start' | 'center' | 'end'
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="absolute" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
|
<div class="absolute" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
|
||||||
<div :class="[style.border]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
<div :class="[style.border]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
||||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg">
|
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg">
|
||||||
<div v-if="node.text && node.text.length > 0" class="flex items-center">
|
<div v-if="node.text?.length > 0" class="flex items-center">
|
||||||
<MarkdownRenderer :content="node.text" />
|
<MarkdownRenderer :content="node.text" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="absolute" ref="dom" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
|
<div class="absolute" ref="dom" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
|
||||||
<div v-if="!editing || node.type === 'group'" style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4 cursor-move': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
<div v-if="!editing || node.type === 'group'" style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4 cursor-move': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
||||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg" @click.left="(e) => { if(node.type !== 'group') selectNode(e) }" @dblclick.left="(e) => { if(node.type !== 'group') editNode(e) }">
|
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg" @click.left="(e) => { if(node.type !== 'group') selectNode(e) }" @dblclick.left="(e) => { if(node.type !== 'group') editNode(e) }">
|
||||||
<div v-if="node.text && node.text.length > 0" class="flex items-center">
|
<div v-if="node.text?.length > 0" class="flex items-center">
|
||||||
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +85,7 @@ function editNode(e: Event) {
|
|||||||
dom.value?.removeEventListener('mousedown', dragstart);
|
dom.value?.removeEventListener('mousedown', dragstart);
|
||||||
emit('edit', { type: 'node', id: node.id });
|
emit('edit', { type: 'node', id: node.id });
|
||||||
}
|
}
|
||||||
function resizeNode(e: MouseEvent, x: number, y: number, width: number, height: number) {
|
function resizeNode(e: MouseEvent, x: number, y: number, w: number, h: number) {
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
const startx = node.x, starty = node.y, startw = node.width, starth = node.height;
|
const startx = node.x, starty = node.y, startw = node.width, starth = node.height;
|
||||||
@@ -96,15 +96,15 @@ function resizeNode(e: MouseEvent, x: number, y: number, width: number, height:
|
|||||||
|
|
||||||
realx = realx + (e.movementX / zoom) * x;
|
realx = realx + (e.movementX / zoom) * x;
|
||||||
realy = realy + (e.movementY / zoom) * y;
|
realy = realy + (e.movementY / zoom) * y;
|
||||||
realw = Math.max(realw + (e.movementX / zoom) * width, 64);
|
realw = Math.max(realw + (e.movementX / zoom) * w, 64);
|
||||||
realh = Math.max(realh + (e.movementY / zoom) * height, 64);
|
realh = Math.max(realh + (e.movementY / zoom) * h, 64);
|
||||||
|
|
||||||
const result = e.altKey ? undefined : snap({ ...node, x: realx, y: realy, width: realw, height: realh }, { x, y, width, height });
|
const result = e.altKey ? undefined : snap({ ...node, x: realx, y: realy, width: realw, height: realh }, { x, y, w, h });
|
||||||
|
|
||||||
node.x = result?.x ?? realx;
|
node.x = result?.x ?? realx;
|
||||||
node.y = result?.y ?? realy;
|
node.y = result?.y ?? realy;
|
||||||
node.width = result?.width ?? realw;
|
node.width = result?.w ?? realw;
|
||||||
node.height = result?.height ?? realh;
|
node.height = result?.h ?? realh;
|
||||||
};
|
};
|
||||||
const resizeend = (e: MouseEvent) => {
|
const resizeend = (e: MouseEvent) => {
|
||||||
if(e.button !== 0)
|
if(e.button !== 0)
|
||||||
@@ -127,7 +127,7 @@ function dragEdge(e: MouseEvent, direction: Direction) {
|
|||||||
function unselect() {
|
function unselect() {
|
||||||
if(editing.value)
|
if(editing.value)
|
||||||
{
|
{
|
||||||
const text = node.type === 'group' ? node.label! : node.text!;
|
const text = node.type === 'group' ? node.label : node.text;
|
||||||
|
|
||||||
if(text !== oldText)
|
if(text !== oldText)
|
||||||
{
|
{
|
||||||
|
|||||||
38
components/character/editor/AbilityEditor.vue
Normal file
38
components/character/editor/AbilityEditor.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<template v-if="model && model.people !== undefined">
|
||||||
|
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10">
|
||||||
|
<Label class="flex items-center justify-between gap-2">
|
||||||
|
<span class="pb-1 mx-2 md:p-0">Points restants</span>
|
||||||
|
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||||
|
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
<Button @click="emit('next')">Suivant</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-wrap justify-center items-center flex-1 gap-12 mx-8 my-4 px-48">
|
||||||
|
<template v-for="ability of config.abilities">
|
||||||
|
<div class="flex flex-col border border-light-50 dark:border-dark-50 p-4 gap-2 w-[200px] relative">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<NumberFieldRoot :min="0" class="flex w-20 justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 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">
|
||||||
|
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
||||||
|
</NumberFieldRoot>
|
||||||
|
<Tooltip side="bottom" :message="`${mainStatTexts[ability.max[0]]} (0) + ${mainStatTexts[ability.max[1]]} (0) + 0`"><span class="text-lg text-end cursor-pointer">/ {{ 0 }}</span></Tooltip>
|
||||||
|
</div>
|
||||||
|
<span class="text-xl text-center font-bold">{{ ability.name }}</span>
|
||||||
|
<span class="absolute -bottom-px -left-px h-[3px] bg-accent-blue" :style="{ width: `200px` }"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { mainStatTexts, type Character, type CharacterConfig } from '~/types/character';
|
||||||
|
|
||||||
|
const { config } = defineProps<{
|
||||||
|
config: CharacterConfig,
|
||||||
|
}>();
|
||||||
|
const model = defineModel<Character>({ required: true });
|
||||||
|
|
||||||
|
const emit = defineEmits(['next']);
|
||||||
|
</script>
|
||||||
38
components/character/editor/AspectSelector.vue
Normal file
38
components/character/editor/AspectSelector.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<template v-if="model && model.people !== undefined">
|
||||||
|
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10">
|
||||||
|
<Label class="flex items-center justify-between gap-2">
|
||||||
|
<span class="pb-1 mx-2 md:p-0">Physique</span>
|
||||||
|
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||||
|
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex items-center justify-between gap-2">
|
||||||
|
<span class="pb-1 mx-2 md:p-0">Mental</span>
|
||||||
|
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||||
|
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex items-center justify-between gap-2">
|
||||||
|
<span class="pb-1 mx-2 md:p-0">Caractère</span>
|
||||||
|
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||||
|
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
<Button @click="emit('next')" :disabled="model.aspect === undefined">Enregistrer</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-1 gap-4 mx-8 my-4">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Character, CharacterConfig } from '~/types/character';
|
||||||
|
|
||||||
|
const { config } = defineProps<{
|
||||||
|
config: CharacterConfig,
|
||||||
|
}>();
|
||||||
|
const model = defineModel<Character>({ required: true });
|
||||||
|
|
||||||
|
const emit = defineEmits(['next']);
|
||||||
|
</script>
|
||||||
56
components/character/editor/LevelEditor.vue
Normal file
56
components/character/editor/LevelEditor.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<template v-if="model && model.character && model.character.people !== undefined">
|
||||||
|
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10">
|
||||||
|
<Label class="flex items-center justify-between gap-2">
|
||||||
|
<span class="pb-1 mx-2 md:p-0">Niveau</span>
|
||||||
|
<NumberFieldRoot :min="1" :max="20" v-model="model.character.level" @update:model-value="val => model.updateLevel(val as Level)" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 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">
|
||||||
|
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex items-center justify-between gap-2">
|
||||||
|
<span class="pb-1 mx-2 md:p-0">Attributions restantes</span>
|
||||||
|
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||||
|
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex items-center justify-between gap-2">
|
||||||
|
<span class="pb-1 mx-2 md:p-0">Vie</span>
|
||||||
|
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||||
|
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex items-center justify-between gap-2">
|
||||||
|
<span class="pb-1 mx-2 md:p-0">Mana</span>
|
||||||
|
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||||
|
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
<Button @click="emit('next')">Suivant</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-1 gap-4 mx-8 my-4">
|
||||||
|
<template v-for="(level, index) of config.peoples[model.character.people!].options">
|
||||||
|
<div class="w-full flex h-px"><div class="border-t border-dashed border-light-50 dark:border-dark-50 w-full" :class="{ 'opacity-30': index > model.character.level }"></div><span class="sticky top-0">{{ index }}</span></div>
|
||||||
|
<div class="flex flex-row gap-4 justify-center" :class="{ 'opacity-30': index > model.character.level }">
|
||||||
|
<template v-for="(option, i) of level">
|
||||||
|
<div class="flex border border-light-50 dark:border-dark-50 px-4 py-2 w-[400px]" @click="model.toggleLevelOption(parseInt(index as unknown as string, 10) as Level, i)"
|
||||||
|
:class="{ 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer': index <= model.character.level, '!border-accent-blue bg-accent-blue bg-opacity-20': model.character.leveling?.some(e => e[0] == index && e[1] === i) ?? false }">
|
||||||
|
<span class="text-wrap whitespace-pre">{{ option.description }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CharacterBuilder } from '~/shared/character';
|
||||||
|
import type { CharacterConfig, Level } from '~/types/character';
|
||||||
|
|
||||||
|
const { config } = defineProps<{
|
||||||
|
config: CharacterConfig,
|
||||||
|
}>();
|
||||||
|
const model = defineModel<CharacterBuilder>({ required: true });
|
||||||
|
|
||||||
|
const emit = defineEmits(['next']);
|
||||||
|
</script>
|
||||||
30
components/character/editor/PeopleSelector.vue
Normal file
30
components/character/editor/PeopleSelector.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<template v-if="model">
|
||||||
|
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center">
|
||||||
|
<TextInput label="Nom" v-model="model.character.name" class="flex-none"/>
|
||||||
|
<Switch label="Privé ?" :default-value="model.character.visibility === 'private'" @update:model-value="(e) => model!.character.visibility = e ? 'private' : 'public'" />
|
||||||
|
<Button @click="emit('next')">Suivant</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 gap-4 p-2 overflow-x-auto justify-center">
|
||||||
|
<div v-for="(people, i) of config.peoples" @click="model.character.people = i" class="flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35
|
||||||
|
cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]" :class="{ '!border-accent-blue outline-2 outline outline-accent-blue': model.character.people === i }">
|
||||||
|
<Avatar :src="people.name" :text="`Image placeholder`" class="h-[320px]" />
|
||||||
|
<span class="text-xl font-bold text-center">{{ people.name }}</span>
|
||||||
|
<span class="w-full border-b border-light-50 dark:border-dark-50"></span>
|
||||||
|
<span class="text-wrap word-break">{{ people.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CharacterBuilder } from '~/shared/character';
|
||||||
|
import type { CharacterConfig } from '~/types/character';
|
||||||
|
|
||||||
|
const { config } = defineProps<{
|
||||||
|
config: CharacterConfig,
|
||||||
|
}>();
|
||||||
|
const model = defineModel<CharacterBuilder>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['next']);
|
||||||
|
</script>
|
||||||
69
components/character/editor/TrainingEditor.vue
Normal file
69
components/character/editor/TrainingEditor.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<TrainingViewer :config="config">
|
||||||
|
<template #addin="{ stat }">
|
||||||
|
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10">
|
||||||
|
<Label class="flex items-center justify-between gap-2">
|
||||||
|
<span class="pb-1 mx-2 md:p-0">Points restants</span>
|
||||||
|
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||||
|
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
<Button @click="emit('next')">Suivant</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ stat, level, option }">
|
||||||
|
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50" @click="toggleOption(stat, parseInt(level as unknown as string, 10) as TrainingLevel, option)" :class="{ /*'opacity-30': level > maxTraining[stat] + 1, 'hover:border-light-60 dark:hover:border-dark-60': level <= maxTraining[stat] + 1, */'!border-accent-blue bg-accent-blue bg-opacity-20': level == 0 || (model.training[stat]?.some(e => e[0] == level && e[1] === option) ?? false) }">
|
||||||
|
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="config.training[stat][level][option].description.map(e => e.text).join('\n')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TrainingViewer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import PreviewA from '~/components/prose/PreviewA.vue';
|
||||||
|
import { MAIN_STATS, type Character, type CharacterConfig, type MainStat, type TrainingLevel } from '~/types/character';
|
||||||
|
|
||||||
|
const { config } = defineProps<{
|
||||||
|
config: CharacterConfig,
|
||||||
|
}>();
|
||||||
|
const model = defineModel<Character>({ required: true, });
|
||||||
|
|
||||||
|
const maxTraining = Object.fromEntries(MAIN_STATS.map(e => [e, 0]));
|
||||||
|
|
||||||
|
const emit = defineEmits(['next']);
|
||||||
|
|
||||||
|
function toggleOption(stat: MainStat, level: TrainingLevel, choice: number)
|
||||||
|
{
|
||||||
|
const character = model.value;
|
||||||
|
|
||||||
|
if(level == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for(let i = 1; i < level; i++) //Check previous levels as a requirement
|
||||||
|
{
|
||||||
|
if(!character.training[stat].some(e => e[0] == i))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(character.training[stat].some(e => e[0] == level))
|
||||||
|
{
|
||||||
|
if(character.training[stat].some(e => e[0] == level && e[1] === choice))
|
||||||
|
{
|
||||||
|
for(let i = 15; i >= level; i --) //Invalidate higher levels
|
||||||
|
{
|
||||||
|
const index = character.training[stat].findIndex(e => e[0] == i);
|
||||||
|
if(index !== -1)
|
||||||
|
character.training[stat].splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
character.training[stat].splice(character.training[stat].findIndex(e => e[0] == level), 1, [level, choice]);
|
||||||
|
}
|
||||||
|
else //if(trainingPoints.value && trainingPoints.value > 0)
|
||||||
|
{
|
||||||
|
character.training[stat].push([level, choice]);
|
||||||
|
}
|
||||||
|
|
||||||
|
model.value = character;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,4 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { type Position } from '#shared/canvas.util';
|
||||||
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
|
|
||||||
|
const cancelEvent = (e: Event) => e.preventDefault();
|
||||||
|
function center(touches: TouchList): Position
|
||||||
|
{
|
||||||
|
const pos = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
for(const touch of touches)
|
||||||
|
{
|
||||||
|
pos.x += touch.clientX;
|
||||||
|
pos.y += touch.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos.x /= touches.length;
|
||||||
|
pos.y /= touches.length;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
function distance(touches: TouchList): number
|
||||||
|
{
|
||||||
|
const [A, B] = touches;
|
||||||
|
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
stroke-light-red
|
stroke-light-red
|
||||||
@@ -66,8 +90,9 @@ dark:outline-dark-purple
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import { clamp } from '#shared/general.util';
|
||||||
import type { CanvasContent } from '~/types/content';
|
import type { CanvasContent } from '~/types/content';
|
||||||
import { Canvas } from '#shared/canvas.util';
|
|
||||||
|
|
||||||
const { path } = defineProps<{
|
const { path } = defineProps<{
|
||||||
path: string
|
path: string
|
||||||
@@ -77,31 +102,196 @@ const { user } = useUserSession();
|
|||||||
const { content, get } = useContent();
|
const { content, get } = useContent();
|
||||||
const overview = computed(() => content.value.find(e => e.path === path) as CanvasContent | undefined);
|
const overview = computed(() => content.value.find(e => e.path === path) as CanvasContent | undefined);
|
||||||
const isOwner = computed(() => user.value?.id === overview.value?.owner);
|
const isOwner = computed(() => user.value?.id === overview.value?.owner);
|
||||||
const element = useTemplateRef('element');
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
mount();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
if(overview.value && !overview.value.content)
|
if(overview.value && !overview.value.content)
|
||||||
{
|
{
|
||||||
|
loading.value = true;
|
||||||
await get(path);
|
await get(path);
|
||||||
mount();
|
loading.value = false;
|
||||||
|
}
|
||||||
|
const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined);
|
||||||
|
console.log(canvas.value);
|
||||||
|
|
||||||
|
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
||||||
|
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');
|
||||||
|
|
||||||
|
const updateScaleVar = useDebounceFn(() => {
|
||||||
|
if(transformRef.value)
|
||||||
|
{
|
||||||
|
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
|
||||||
|
}
|
||||||
|
if(canvasRef.value)
|
||||||
|
{
|
||||||
|
canvasRef.value.style.setProperty('--zoom-multiplier', (1 / Math.pow(zoom.value, 0.7)).toFixed(3));
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
const reset = (_: MouseEvent) => {
|
||||||
|
zoom.value = minZoom.value;
|
||||||
|
|
||||||
|
dispX.value = 0;
|
||||||
|
dispY.value = 0;
|
||||||
|
|
||||||
|
updateTransform();
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined);
|
onMounted(() => {
|
||||||
|
let lastX = 0, lastY = 0, lastDistance = 0;
|
||||||
|
let box = canvasRef.value?.getBoundingClientRect()!;
|
||||||
|
const dragMove = (e: MouseEvent) => {
|
||||||
|
dispX.value = dispX.value - (lastX - e.clientX) / zoom.value;
|
||||||
|
dispY.value = dispY.value - (lastY - e.clientY) / zoom.value;
|
||||||
|
|
||||||
function mount()
|
lastX = e.clientX;
|
||||||
|
lastY = e.clientY;
|
||||||
|
|
||||||
|
updateTransform();
|
||||||
|
};
|
||||||
|
const dragEnd = (e: MouseEvent) => {
|
||||||
|
window.removeEventListener('mouseup', dragEnd);
|
||||||
|
window.removeEventListener('mousemove', dragMove);
|
||||||
|
};
|
||||||
|
canvasRef.value?.addEventListener('mouseenter', () => {
|
||||||
|
window.addEventListener('wheel', cancelEvent, { passive: false });
|
||||||
|
document.addEventListener('gesturestart', cancelEvent);
|
||||||
|
document.addEventListener('gesturechange', cancelEvent);
|
||||||
|
|
||||||
|
canvasRef.value?.addEventListener('mouseleave', () => {
|
||||||
|
window.removeEventListener('wheel', cancelEvent);
|
||||||
|
document.removeEventListener('gesturestart', cancelEvent);
|
||||||
|
document.removeEventListener('gesturechange', cancelEvent);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
window.addEventListener('resize', () => box = canvasRef.value?.getBoundingClientRect()!);
|
||||||
|
canvasRef.value?.addEventListener('mousedown', (e) => {
|
||||||
|
lastX = e.clientX;
|
||||||
|
lastY = e.clientY;
|
||||||
|
|
||||||
|
window.addEventListener('mouseup', dragEnd, { passive: true });
|
||||||
|
window.addEventListener('mousemove', dragMove, { passive: true });
|
||||||
|
}, { passive: true });
|
||||||
|
canvasRef.value?.addEventListener('wheel', (e) => {
|
||||||
|
if((zoom.value >= 3 && e.deltaY < 0) || (zoom.value <= minZoom.value && e.deltaY > 0))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const diff = Math.exp(e.deltaY * -0.001);
|
||||||
|
const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2);
|
||||||
|
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
|
||||||
|
|
||||||
|
dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value);
|
||||||
|
dispY.value = dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value);
|
||||||
|
|
||||||
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3)
|
||||||
|
|
||||||
|
updateTransform();
|
||||||
|
}, { passive: true });
|
||||||
|
canvasRef.value?.addEventListener('touchstart', (e) => {
|
||||||
|
({ x: lastX, y: lastY } = center(e.touches));
|
||||||
|
|
||||||
|
if(e.touches.length > 1)
|
||||||
|
{
|
||||||
|
lastDistance = distance(e.touches);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasRef.value?.addEventListener('touchend', touchend, { passive: true });
|
||||||
|
canvasRef.value?.addEventListener('touchcancel', touchcancel, { passive: true });
|
||||||
|
canvasRef.value?.addEventListener('touchmove', touchmove, { passive: true });
|
||||||
|
}, { passive: true });
|
||||||
|
const touchend = (e: TouchEvent) => {
|
||||||
|
if(e.touches.length > 1)
|
||||||
|
{
|
||||||
|
({ x: lastX, y: lastY } = center(e.touches));
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasRef.value?.removeEventListener('touchend', touchend);
|
||||||
|
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
|
||||||
|
canvasRef.value?.removeEventListener('touchmove', touchmove);
|
||||||
|
};
|
||||||
|
const touchcancel = (e: TouchEvent) => {
|
||||||
|
if(e.touches.length > 1)
|
||||||
|
{
|
||||||
|
({ x: lastX, y: lastY } = center(e.touches));
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasRef.value?.removeEventListener('touchend', touchend);
|
||||||
|
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
|
||||||
|
canvasRef.value?.removeEventListener('touchmove', touchmove);
|
||||||
|
};
|
||||||
|
const touchmove = (e: TouchEvent) => {
|
||||||
|
const pos = center(e.touches);
|
||||||
|
dispX.value = dispX.value - (lastX - pos.x) / zoom.value;
|
||||||
|
dispY.value = dispY.value - (lastY - pos.y) / zoom.value;
|
||||||
|
lastX = pos.x;
|
||||||
|
lastY = pos.y;
|
||||||
|
|
||||||
|
if(e.touches.length === 2)
|
||||||
|
{
|
||||||
|
const dist = distance(e.touches);
|
||||||
|
const diff = dist / lastDistance;
|
||||||
|
lastDistance = dist;
|
||||||
|
|
||||||
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTransform();
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTransform();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateTransform()
|
||||||
{
|
{
|
||||||
if(element.value && canvas.value)
|
if(transformRef.value)
|
||||||
{
|
{
|
||||||
const c = new Canvas(canvas.value);
|
transformRef.value.style.transform = `scale3d(${zoom.value}, ${zoom.value}, 1) translate3d(${dispX.value}px, ${dispY.value}px, 0)`;
|
||||||
element.value.appendChild(c.container);
|
|
||||||
c.mount();
|
|
||||||
}
|
}
|
||||||
|
updateScaleVar();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="element"></div>
|
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none">
|
||||||
|
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4">
|
||||||
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||||
|
<Tooltip message="Zoom avant" side="right">
|
||||||
|
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
|
<Icon icon="radix-icons:plus" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip message="Reset" side="right">
|
||||||
|
<div @click="zoom = 1; updateTransform();" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
|
<Icon icon="radix-icons:reload" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip message="Tout contenir" side="right">
|
||||||
|
<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">
|
||||||
|
<Icon icon="radix-icons:corners" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip message="Zoom arrière" side="right">
|
||||||
|
<div @click="zoom = clamp(zoom / 1.1, minZoom, 3); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
|
<Icon icon="radix-icons:minus" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<NuxtLink v-if="overview && isOwner || hasPermissions(user?.permissions ?? [], ['admin', 'editor'])" :to="{ name: 'explore-edit', hash: `#${encodeURIComponent(overview!.path)}` }" class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||||
|
<Tooltip message="Modifier" side="right">
|
||||||
|
<Icon icon="radix-icons:pencil-1" class="w-8 h-8 p-2" />
|
||||||
|
</Tooltip>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div ref="transformRef" :style="{
|
||||||
|
'transform-origin': 'center center',
|
||||||
|
}" class="h-full">
|
||||||
|
<div v-if="canvas" class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
|
||||||
|
<div>
|
||||||
|
<CanvasNode v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CanvasEdge v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
30
components/prose/PreviewA.vue
Normal file
30
components/prose/PreviewA.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<span class="text-accent-blue inline-flex items-center" :class="class">
|
||||||
|
<HoverCard nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
|
||||||
|
<template #content>
|
||||||
|
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="decodeURIComponent(pathname)" :filter="hash.substring(1)" popover />
|
||||||
|
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="decodeURIComponent(pathname)" /></div></template>
|
||||||
|
</template>
|
||||||
|
<span>
|
||||||
|
<slot v-bind="$attrs"></slot>
|
||||||
|
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :icon="iconByType[overview.type]" />
|
||||||
|
</span>
|
||||||
|
</HoverCard>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { parseURL } from 'ufo';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import { iconByType } from '#shared/general.util';
|
||||||
|
|
||||||
|
const { href } = defineProps<{
|
||||||
|
href: string
|
||||||
|
class?: string
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { hash, pathname } = parseURL(href);
|
||||||
|
|
||||||
|
const { content } = useContent();
|
||||||
|
const overview = computed(() => content.value.find(e => e.path === decodeURIComponent(pathname)));
|
||||||
|
</script>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<NuxtLink class="text-accent-blue inline-flex items-center" :to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href" :class="class">
|
<NuxtLink class="text-accent-blue inline-flex items-center" :to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: decodeURIComponent(hash) } : href" :class="class">
|
||||||
<HoverCard nuxt-client class="min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
|
<HoverCard nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
|
||||||
<template #content>
|
<template #content>
|
||||||
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="pathname" :filter="hash.substring(1)" popover />
|
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="decodeURIComponent(pathname)" :filter="hash.substring(1)" popover />
|
||||||
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="pathname" /></div></template>
|
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="decodeURIComponent(pathname)" /></div></template>
|
||||||
</template>
|
</template>
|
||||||
<span>
|
<span>
|
||||||
<slot v-bind="$attrs"></slot>
|
<slot v-bind="$attrs"></slot>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { parseURL } from 'ufo';
|
import { parseURL } from 'ufo';
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import { iconByType } from '#shared/content.util';
|
import { iconByType } from '#shared/general.util';
|
||||||
|
|
||||||
const { href } = defineProps<{
|
const { href } = defineProps<{
|
||||||
href: string
|
href: string
|
||||||
@@ -26,7 +26,7 @@ const { href } = defineProps<{
|
|||||||
const { hash, pathname } = parseURL(href);
|
const { hash, pathname } = parseURL(href);
|
||||||
|
|
||||||
const { content } = useContent();
|
const { content } = useContent();
|
||||||
const overview = computed(() => content.value.find(e => e.path === pathname));
|
const overview = computed(() => content.value.find(e => e.path === decodeURIComponent(pathname)));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const defaultCalloutIcon = 'radix-icons:info-circled';
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
const { type, title, fold } = defineProps<{
|
const { type, title, fold } = defineProps<{
|
||||||
type: string;
|
type: string;
|
||||||
@@ -43,3 +44,103 @@ const { type, title, fold } = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
const disabled = computed(() => fold === undefined);
|
const disabled = computed(() => fold === undefined);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.callout[data-type="abstract"],
|
||||||
|
.callout[data-type="summary"],
|
||||||
|
.callout[data-type="tldr"]
|
||||||
|
{
|
||||||
|
@apply bg-light-cyan;
|
||||||
|
@apply dark:bg-dark-cyan;
|
||||||
|
@apply text-light-cyan;
|
||||||
|
@apply dark:text-dark-cyan;
|
||||||
|
}
|
||||||
|
.callout[data-type="info"]
|
||||||
|
{
|
||||||
|
@apply bg-light-blue;
|
||||||
|
@apply dark:bg-dark-blue;
|
||||||
|
@apply text-light-blue;
|
||||||
|
@apply dark:text-dark-blue;
|
||||||
|
}
|
||||||
|
.callout[data-type="todo"]
|
||||||
|
{
|
||||||
|
@apply bg-light-blue;
|
||||||
|
@apply dark:bg-dark-blue;
|
||||||
|
@apply text-light-blue;
|
||||||
|
@apply dark:text-dark-blue;
|
||||||
|
}
|
||||||
|
.callout[data-type="important"]
|
||||||
|
{
|
||||||
|
@apply bg-light-cyan;
|
||||||
|
@apply dark:bg-dark-cyan;
|
||||||
|
@apply text-light-cyan;
|
||||||
|
@apply dark:text-dark-cyan;
|
||||||
|
}
|
||||||
|
.callout[data-type="tip"],
|
||||||
|
.callout[data-type="hint"]
|
||||||
|
{
|
||||||
|
@apply bg-light-cyan;
|
||||||
|
@apply dark:bg-dark-cyan;
|
||||||
|
@apply text-light-cyan;
|
||||||
|
@apply dark:text-dark-cyan;
|
||||||
|
}
|
||||||
|
.callout[data-type="success"],
|
||||||
|
.callout[data-type="check"],
|
||||||
|
.callout[data-type="done"]
|
||||||
|
{
|
||||||
|
@apply bg-light-green;
|
||||||
|
@apply dark:bg-dark-green;
|
||||||
|
@apply text-light-green;
|
||||||
|
@apply dark:text-dark-green;
|
||||||
|
}
|
||||||
|
.callout[data-type="question"],
|
||||||
|
.callout[data-type="help"],
|
||||||
|
.callout[data-type="faq"]
|
||||||
|
{
|
||||||
|
@apply bg-light-orange;
|
||||||
|
@apply dark:bg-dark-orange;
|
||||||
|
@apply text-light-orange;
|
||||||
|
@apply dark:text-dark-orange;
|
||||||
|
}
|
||||||
|
.callout[data-type="warning"],
|
||||||
|
.callout[data-type="caution"],
|
||||||
|
.callout[data-type="attention"]
|
||||||
|
{
|
||||||
|
@apply bg-light-orange;
|
||||||
|
@apply dark:bg-dark-orange;
|
||||||
|
@apply text-light-orange;
|
||||||
|
@apply dark:text-dark-orange;
|
||||||
|
}
|
||||||
|
.callout[data-type="failure"],
|
||||||
|
.callout[data-type="fail"],
|
||||||
|
.callout[data-type="missing"]
|
||||||
|
{
|
||||||
|
@apply bg-light-red;
|
||||||
|
@apply dark:bg-dark-red;
|
||||||
|
@apply text-light-red;
|
||||||
|
@apply dark:text-dark-red;
|
||||||
|
}
|
||||||
|
.callout[data-type="danger"],
|
||||||
|
.callout[data-type="error"]
|
||||||
|
{
|
||||||
|
@apply bg-light-red;
|
||||||
|
@apply dark:bg-dark-red;
|
||||||
|
@apply text-light-red;
|
||||||
|
@apply dark:text-dark-red;
|
||||||
|
}
|
||||||
|
.callout[data-type="bug"]
|
||||||
|
{
|
||||||
|
@apply bg-light-red;
|
||||||
|
@apply dark:bg-dark-red;
|
||||||
|
@apply text-light-red;
|
||||||
|
@apply dark:text-dark-red;
|
||||||
|
}
|
||||||
|
.callout[data-type="example"]
|
||||||
|
{
|
||||||
|
@apply bg-light-purple;
|
||||||
|
@apply dark:bg-dark-purple;
|
||||||
|
@apply text-light-purple;
|
||||||
|
@apply dark:text-dark-purple;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<HoverCard nuxt-client class="min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]">
|
<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">
|
||||||
<template #content>
|
<slot></slot>
|
||||||
<Markdown class="!px-6" path="tags" :filter="tag" popover />
|
</span>
|
||||||
</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>
|
|
||||||
</HoverCard>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -19,12 +14,3 @@
|
|||||||
@apply bg-accent-blue bg-opacity-10 text-accent-blue text-sm pb-0.5 pe-1 rounded-r-[12px] !rounded-se-none border border-l-0 border-accent-blue border-opacity-30;
|
@apply bg-accent-blue bg-opacity-10 text-accent-blue text-sm pb-0.5 pe-1 rounded-r-[12px] !rounded-se-none border border-l-0 border-accent-blue border-opacity-30;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const { tag } = defineProps<{
|
|
||||||
tag: string
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { content } = useContent();
|
|
||||||
const overview = computed(() => content.value.find(e => e.path === "tags"));
|
|
||||||
</script>
|
|
||||||
@@ -1,45 +1,40 @@
|
|||||||
import { Content } from '~/shared/content.util';
|
|
||||||
import type { ExploreContent, ContentComposable, TreeItem } from '~/types/content';
|
import type { ExploreContent, ContentComposable, TreeItem } from '~/types/content';
|
||||||
|
|
||||||
const useContentState = () => useState<ExploreContent[]>('content', () => []);
|
const useContentState = () => useState<ExploreContent[]>('content', () => []);
|
||||||
|
|
||||||
export function useContent(): ContentComposable {
|
export function useContent(): ContentComposable {
|
||||||
const contentState = useContentState();
|
const contentState = useContentState();
|
||||||
|
return {
|
||||||
return {
|
content: contentState,
|
||||||
content: contentState,
|
tree: computed(() => {
|
||||||
tree: computed(() => {
|
const arr: TreeItem[] = [];
|
||||||
const arr: TreeItem[] = [];
|
for(const element of contentState.value)
|
||||||
for(const element of contentState.value)
|
{
|
||||||
{
|
addChild(arr, element);
|
||||||
addChild(arr, element);
|
}
|
||||||
}
|
return arr;
|
||||||
return arr;
|
}),
|
||||||
}),
|
fetch,
|
||||||
fetch,
|
get,
|
||||||
get,
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetch(force: boolean = false) {
|
async function fetch(force: boolean) {
|
||||||
const content = useContentState();
|
const content = useContentState();
|
||||||
if(content.value.length === 0 || force)
|
if(content.value.length === 0 || force)
|
||||||
content.value = await useRequestFetch()('/api/file/overview');
|
content.value = await useRequestFetch()('/api/file/overview');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(path: string, force: boolean = false): Promise<ExploreContent | undefined> {
|
async function get(path: string) {
|
||||||
const content = useContentState()
|
const content = useContentState()
|
||||||
const value = content.value;
|
const value = content.value;
|
||||||
const item = value.find(e => e.path === path);
|
const item = value.find(e => e.path === path);
|
||||||
|
if(item)
|
||||||
if(item && !item.content)
|
|
||||||
{
|
{
|
||||||
item.content = await useRequestFetch()(`/api/file/content/${encodeURIComponent(path)}`);
|
item.content = await useRequestFetch()(`/api/file/content/${encodeURIComponent(path)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
content.value = value;
|
content.value = value;
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addChild(arr: TreeItem[], e: ExploreContent): void {
|
function addChild(arr: TreeItem[], e: ExploreContent): void {
|
||||||
|
|||||||
@@ -8,14 +8,9 @@ import RemarkGfm from 'remark-gfm';
|
|||||||
import RemarkBreaks from 'remark-breaks';
|
import RemarkBreaks from 'remark-breaks';
|
||||||
import RemarkFrontmatter from 'remark-frontmatter';
|
import RemarkFrontmatter from 'remark-frontmatter';
|
||||||
|
|
||||||
interface Parser
|
export default function useMarkdown(): (md: string) => Root
|
||||||
{
|
{
|
||||||
parse: (md: string) => Promise<Root>;
|
let processor: Processor;
|
||||||
parseSync: (md: string) => Root
|
|
||||||
}
|
|
||||||
export default function useMarkdown(): Parser
|
|
||||||
{
|
|
||||||
let processor: Processor, processorSync: Processor;
|
|
||||||
|
|
||||||
const parse = (markdown: string) => {
|
const parse = (markdown: string) => {
|
||||||
if (!processor)
|
if (!processor)
|
||||||
@@ -24,20 +19,9 @@ export default function useMarkdown(): Parser
|
|||||||
processor.use(RemarkRehype);
|
processor.use(RemarkRehype);
|
||||||
}
|
}
|
||||||
|
|
||||||
const processed = processor.run(processor.parse(markdown)) as Promise<Root>;
|
|
||||||
return processed;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseSync = (markdown: string) => {
|
|
||||||
if (!processor)
|
|
||||||
{
|
|
||||||
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter]);
|
|
||||||
processor.use(RemarkRehype);
|
|
||||||
}
|
|
||||||
|
|
||||||
const processed = processor.runSync(processor.parse(markdown)) as Root;
|
const processed = processor.runSync(processor.parse(markdown)) as Root;
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { parse, parseSync };
|
return parse;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { UserSession, UserSessionComposable } from '~/types/auth'
|
import type { UserSession, UserSessionComposable } from '~/types/auth'
|
||||||
|
import { useContent } from './useContent'
|
||||||
|
|
||||||
const useSessionState = () => useState<UserSession>('nuxt-session', () => ({}))
|
const useSessionState = () => useState<UserSession>('nuxt-session', () => ({}))
|
||||||
const useAuthReadyState = () => useState('nuxt-auth-ready', () => false)
|
const useAuthReadyState = () => useState('nuxt-auth-ready', () => false)
|
||||||
|
const useContentFetch = (force: boolean) => useContent().fetch(force);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable to get back the user session and utils around it.
|
* Composable to get back the user session and utils around it.
|
||||||
|
|||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
107
db/schema.ts
107
db/schema.ts
@@ -1,7 +1,8 @@
|
|||||||
import { relations } from 'drizzle-orm';
|
import { relations } from 'drizzle-orm';
|
||||||
import { int, text, sqliteTable as table, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
import { int, text, sqliteTable, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||||
|
import { ABILITIES, MAIN_STATS } from '~/shared/character';
|
||||||
|
|
||||||
export const usersTable = table("users", {
|
export const usersTable = sqliteTable("users", {
|
||||||
id: int().primaryKey({ autoIncrement: true }),
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
username: text().notNull().unique(),
|
username: text().notNull().unique(),
|
||||||
email: text().notNull().unique(),
|
email: text().notNull().unique(),
|
||||||
@@ -9,50 +10,93 @@ export const usersTable = table("users", {
|
|||||||
state: int().notNull().default(0),
|
state: int().notNull().default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const usersDataTable = table("users_data", {
|
export const usersDataTable = sqliteTable("users_data", {
|
||||||
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
|
logCount: int().notNull().default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userSessionsTable = table("user_sessions", {
|
export const userSessionsTable = sqliteTable("user_sessions", {
|
||||||
id: int().notNull(),
|
id: int().notNull(),
|
||||||
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
}, (table) => [ primaryKey({ columns: [table.id, table.user_id] }) ]);
|
}, (table) => [primaryKey({ columns: [table.id, table.user_id] })]);
|
||||||
|
|
||||||
export const userPermissionsTable = table("user_permissions", {
|
export const userPermissionsTable = sqliteTable("user_permissions", {
|
||||||
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
permission: text().notNull(),
|
permission: text().notNull(),
|
||||||
}, (table) => [ primaryKey({ columns: [table.id, table.permission] }) ]);
|
}, (table) => [primaryKey({ columns: [table.id, table.permission] })]);
|
||||||
|
|
||||||
export const projectFilesTable = table("project_files", {
|
export const explorerContentTable = sqliteTable("explorer_content", {
|
||||||
id: text().primaryKey(),
|
path: text().primaryKey(),
|
||||||
path: text().notNull().unique(),
|
|
||||||
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
title: text().notNull(),
|
title: text().notNull(),
|
||||||
type: text({ enum: ['file', 'folder', 'markdown', 'canvas', 'map'] }).notNull(),
|
type: text({ enum: ['file', 'folder', 'markdown', 'canvas', 'map'] }).notNull(),
|
||||||
|
content: blob({ mode: 'buffer' }),
|
||||||
navigable: int({ mode: 'boolean' }).notNull().default(true),
|
navigable: int({ mode: 'boolean' }).notNull().default(true),
|
||||||
private: int({ mode: 'boolean' }).notNull().default(false),
|
private: int({ mode: 'boolean' }).notNull().default(false),
|
||||||
order: int().notNull(),
|
order: int().notNull(),
|
||||||
|
visit: int().notNull().default(0),
|
||||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const projectContentTable = table("project_content", {
|
export const emailValidationTable = sqliteTable("email_validation", {
|
||||||
id: text().primaryKey(),
|
|
||||||
content: blob({ mode: 'buffer' }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const emailValidationTable = table("email_validation", {
|
|
||||||
id: text().primaryKey(),
|
id: text().primaryKey(),
|
||||||
timestamp: int({ mode: 'timestamp' }).notNull(),
|
timestamp: int({ mode: 'timestamp' }).notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const characterTable = sqliteTable("character", {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
name: text().notNull(),
|
||||||
|
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
people: int().notNull(),
|
||||||
|
level: int().notNull().default(1),
|
||||||
|
aspect: int(),
|
||||||
|
notes: text(),
|
||||||
|
health: int().notNull().default(0),
|
||||||
|
mana: int().notNull().default(0),
|
||||||
|
|
||||||
|
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
|
||||||
|
thumbnail: blob(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const characterTrainingTable = sqliteTable("character_training", {
|
||||||
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
stat: text({ enum: MAIN_STATS }).notNull(),
|
||||||
|
level: int().notNull(),
|
||||||
|
choice: int().notNull(),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]);
|
||||||
|
|
||||||
|
export const characterLevelingTable = sqliteTable("character_leveling", {
|
||||||
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
level: int().notNull(),
|
||||||
|
choice: int().notNull(),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.character, table.level] })]);
|
||||||
|
|
||||||
|
export const characterAbilitiesTable = sqliteTable("character_abilities", {
|
||||||
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
ability: text({ enum: ABILITIES }).notNull(),
|
||||||
|
value: int().notNull().default(0),
|
||||||
|
max: int().notNull().default(0),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.character, table.ability] })]);
|
||||||
|
|
||||||
|
export const characterModifiersTable = sqliteTable("character_modifiers", {
|
||||||
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
modifier: text({ enum: MAIN_STATS }).notNull(),
|
||||||
|
value: int().notNull().default(0),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.character, table.modifier] })]);
|
||||||
|
|
||||||
|
export const characterSpellsTable = sqliteTable("character_spell", {
|
||||||
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
value: text().notNull(),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.character, table.value] })]);
|
||||||
|
|
||||||
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
||||||
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
||||||
session: many(userSessionsTable),
|
session: many(userSessionsTable),
|
||||||
permission: many(userPermissionsTable),
|
permission: many(userPermissionsTable),
|
||||||
files: many(projectFilesTable),
|
content: many(explorerContentTable),
|
||||||
}));
|
}));
|
||||||
export const usersDataRelation = relations(usersDataTable, ({ one }) => ({
|
export const usersDataRelation = relations(usersDataTable, ({ one }) => ({
|
||||||
users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }),
|
users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }),
|
||||||
@@ -63,9 +107,30 @@ export const userSessionsRelation = relations(userSessionsTable, ({ one }) => ({
|
|||||||
export const userPermissionsRelation = relations(userPermissionsTable, ({ one }) => ({
|
export const userPermissionsRelation = relations(userPermissionsTable, ({ one }) => ({
|
||||||
users: one(usersTable, { fields: [userPermissionsTable.id], references: [usersTable.id], }),
|
users: one(usersTable, { fields: [userPermissionsTable.id], references: [usersTable.id], }),
|
||||||
}));
|
}));
|
||||||
export const projectFilesRelation = relations(projectFilesTable, ({ one }) => ({
|
export const explorerContentRelation = relations(explorerContentTable, ({ one }) => ({
|
||||||
users: one(usersTable, { fields: [projectFilesTable.owner], references: [usersTable.id], }),
|
users: one(usersTable, { fields: [explorerContentTable.owner], references: [usersTable.id], }),
|
||||||
}));
|
}));
|
||||||
export const projectContentRelation = relations(projectContentTable, ({ one }) => ({
|
export const characterRelation = relations(characterTable, ({ one, many }) => ({
|
||||||
files: one(projectFilesTable, { fields: [projectContentTable.id], references: [projectFilesTable.id], }),
|
user: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }),
|
||||||
|
training: many(characterTrainingTable),
|
||||||
|
levels: many(characterLevelingTable),
|
||||||
|
abilities: many(characterAbilitiesTable),
|
||||||
|
modifiers: many(characterModifiersTable),
|
||||||
|
spells: many(characterSpellsTable)
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({
|
||||||
|
character: one(characterTable, { fields: [characterTrainingTable.character], references: [characterTable.id] })
|
||||||
|
}));
|
||||||
|
export const characterLevelingRelation = relations(characterLevelingTable, ({ one }) => ({
|
||||||
|
character: one(characterTable, { fields: [characterLevelingTable.character], references: [characterTable.id] })
|
||||||
|
}));
|
||||||
|
export const characterAbilitiesRelation = relations(characterAbilitiesTable, ({ one }) => ({
|
||||||
|
character: one(characterTable, { fields: [characterAbilitiesTable.character], references: [characterTable.id] })
|
||||||
|
}));
|
||||||
|
export const characterModifierRelation = relations(characterModifiersTable, ({ one }) => ({
|
||||||
|
character: one(characterTable, { fields: [characterModifiersTable.character], references: [characterTable.id] })
|
||||||
|
}));
|
||||||
|
export const characterSpellsRelation = relations(characterSpellsTable, ({ one }) => ({
|
||||||
|
character: one(characterTable, { fields: [characterSpellsTable.character], references: [characterTable.id] })
|
||||||
}));
|
}));
|
||||||
7
drizzle/0006_clever_marvex.sql
Normal file
7
drizzle/0006_clever_marvex.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE `character` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`owner` integer NOT NULL,
|
||||||
|
`options` text NOT NULL,
|
||||||
|
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
CREATE TABLE `project_content` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`content` blob
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `project_files` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`path` text NOT NULL,
|
|
||||||
`owner` integer NOT NULL,
|
|
||||||
`title` text NOT NULL,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`navigable` integer DEFAULT true NOT NULL,
|
|
||||||
`private` integer DEFAULT false NOT NULL,
|
|
||||||
`order` integer NOT NULL,
|
|
||||||
`timestamp` integer NOT NULL,
|
|
||||||
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `project_files_path_unique` ON `project_files` (`path`);--> statement-breakpoint
|
|
||||||
DROP TABLE `explorer_content`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `users_data` DROP COLUMN `logCount`;
|
|
||||||
14
drizzle/0007_tearful_true_believers.sql
Normal file
14
drizzle/0007_tearful_true_believers.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_character` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`owner` integer NOT NULL,
|
||||||
|
`progress` text NOT NULL,
|
||||||
|
`thumbnail` blob,
|
||||||
|
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_character`("id", "name", "owner", "progress", "thumbnail") SELECT "id", "name", "owner", "progress", "thumbnail" FROM `character`;--> statement-breakpoint
|
||||||
|
DROP TABLE `character`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_character` RENAME TO `character`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
1
drizzle/0008_glorious_johnny_blaze.sql
Normal file
1
drizzle/0008_glorious_johnny_blaze.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `character` ADD `values` text DEFAULT '{}' NOT NULL;
|
||||||
1
drizzle/0009_thin_omega_sentinel.sql
Normal file
1
drizzle/0009_thin_omega_sentinel.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `character` ADD `visibility` text DEFAULT 'private' NOT NULL;
|
||||||
47
drizzle/0010_bored_sabra.sql
Normal file
47
drizzle/0010_bored_sabra.sql
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
CREATE TABLE `character_abilities` (
|
||||||
|
`character` integer NOT NULL,
|
||||||
|
`ability` text NOT NULL,
|
||||||
|
`value` integer DEFAULT 0 NOT NULL,
|
||||||
|
PRIMARY KEY(`character`, `ability`),
|
||||||
|
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `character_leveling` (
|
||||||
|
`character` integer NOT NULL,
|
||||||
|
`level` integer NOT NULL,
|
||||||
|
`choice` integer NOT NULL,
|
||||||
|
PRIMARY KEY(`character`, `level`),
|
||||||
|
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `character_modifiers` (
|
||||||
|
`character` integer NOT NULL,
|
||||||
|
`modifier` text NOT NULL,
|
||||||
|
`value` integer DEFAULT 0 NOT NULL,
|
||||||
|
PRIMARY KEY(`character`, `modifier`),
|
||||||
|
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `character_spell` (
|
||||||
|
`character` integer PRIMARY KEY NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `character_training` (
|
||||||
|
`character` integer NOT NULL,
|
||||||
|
`stat` text NOT NULL,
|
||||||
|
`level` integer NOT NULL,
|
||||||
|
`choice` integer NOT NULL,
|
||||||
|
PRIMARY KEY(`character`, `stat`, `level`),
|
||||||
|
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` ADD `people` integer NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` ADD `level` integer DEFAULT 1 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` ADD `aspect` integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` ADD `notes` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` ADD `health` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` ADD `mana` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` DROP COLUMN `progress`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` DROP COLUMN `values`;
|
||||||
1
drizzle/0011_demonic_titania.sql
Normal file
1
drizzle/0011_demonic_titania.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `character_abilities` ADD `max` integer DEFAULT 0 NOT NULL;
|
||||||
12
drizzle/0012_graceful_energizer.sql
Normal file
12
drizzle/0012_graceful_energizer.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_character_spell` (
|
||||||
|
`character` integer NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
PRIMARY KEY(`character`, `value`),
|
||||||
|
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_character_spell`("character", "value") SELECT "character", "value" FROM `character_spell`;--> statement-breakpoint
|
||||||
|
DROP TABLE `character_spell`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_character_spell` RENAME TO `character_spell`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -1,9 +1,61 @@
|
|||||||
{
|
{
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "b24a44ad-b43a-4aba-be9c-350fd41bed04",
|
"id": "4e31a794-f0ae-4c44-a846-6e1bafa4b247",
|
||||||
"prevId": "a2731c1f-4150-4423-946e-670d794f8961",
|
"prevId": "a2731c1f-4150-4423-946e-670d794f8961",
|
||||||
"tables": {
|
"tables": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"name": "options",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_owner_users_id_fk": {
|
||||||
|
"name": "character_owner_users_id_fk",
|
||||||
|
"tableFrom": "character",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
"email_validation": {
|
"email_validation": {
|
||||||
"name": "email_validation",
|
"name": "email_validation",
|
||||||
"columns": {
|
"columns": {
|
||||||
@@ -28,44 +80,13 @@
|
|||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
"checkConstraints": {}
|
"checkConstraints": {}
|
||||||
},
|
},
|
||||||
"project_content": {
|
"explorer_content": {
|
||||||
"name": "project_content",
|
"name": "explorer_content",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"content": {
|
|
||||||
"name": "content",
|
|
||||||
"type": "blob",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"project_files": {
|
|
||||||
"name": "project_files",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"path": {
|
"path": {
|
||||||
"name": "path",
|
"name": "path",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": true,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
@@ -90,6 +111,13 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
"navigable": {
|
"navigable": {
|
||||||
"name": "navigable",
|
"name": "navigable",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -113,6 +141,14 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"visit": {
|
||||||
|
"name": "visit",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
"timestamp": {
|
"timestamp": {
|
||||||
"name": "timestamp",
|
"name": "timestamp",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -121,19 +157,11 @@
|
|||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {},
|
||||||
"project_files_path_unique": {
|
|
||||||
"name": "project_files_path_unique",
|
|
||||||
"columns": [
|
|
||||||
"path"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"project_files_owner_users_id_fk": {
|
"explorer_content_owner_users_id_fk": {
|
||||||
"name": "project_files_owner_users_id_fk",
|
"name": "explorer_content_owner_users_id_fk",
|
||||||
"tableFrom": "project_files",
|
"tableFrom": "explorer_content",
|
||||||
"tableTo": "users",
|
"tableTo": "users",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"owner"
|
"owner"
|
||||||
@@ -271,6 +299,14 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"logCount": {
|
||||||
|
"name": "logCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
|
|||||||
420
drizzle/meta/0007_snapshot.json
Normal file
420
drizzle/meta/0007_snapshot.json
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "15ea15e0-3d44-4dff-a4cd-f8666c4aa5ed",
|
||||||
|
"prevId": "4e31a794-f0ae-4c44-a846-6e1bafa4b247",
|
||||||
|
"tables": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"name": "progress",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_owner_users_id_fk": {
|
||||||
|
"name": "character_owner_users_id_fk",
|
||||||
|
"tableFrom": "character",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"email_validation": {
|
||||||
|
"name": "email_validation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"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": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visit": {
|
||||||
|
"name": "visit",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": 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
|
||||||
|
},
|
||||||
|
"lastTimestamp": {
|
||||||
|
"name": "lastTimestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"logCount": {
|
||||||
|
"name": "logCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"\"character\".\"options\"": "\"character\".\"progress\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
426
drizzle/meta/0008_snapshot.json
Normal file
426
drizzle/meta/0008_snapshot.json
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "eb68cf2f-c7e2-4111-910d-a26b0fc438cc",
|
||||||
|
"prevId": "15ea15e0-3d44-4dff-a4cd-f8666c4aa5ed",
|
||||||
|
"tables": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"name": "progress",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"name": "values",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'{}'"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_owner_users_id_fk": {
|
||||||
|
"name": "character_owner_users_id_fk",
|
||||||
|
"tableFrom": "character",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"email_validation": {
|
||||||
|
"name": "email_validation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"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": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visit": {
|
||||||
|
"name": "visit",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": 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
|
||||||
|
},
|
||||||
|
"lastTimestamp": {
|
||||||
|
"name": "lastTimestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"logCount": {
|
||||||
|
"name": "logCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
434
drizzle/meta/0009_snapshot.json
Normal file
434
drizzle/meta/0009_snapshot.json
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "bffde16c-d716-40ec-9d92-cb49814815d7",
|
||||||
|
"prevId": "eb68cf2f-c7e2-4111-910d-a26b0fc438cc",
|
||||||
|
"tables": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"name": "progress",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"name": "values",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'{}'"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"name": "visibility",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'private'"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_owner_users_id_fk": {
|
||||||
|
"name": "character_owner_users_id_fk",
|
||||||
|
"tableFrom": "character",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"email_validation": {
|
||||||
|
"name": "email_validation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"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": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visit": {
|
||||||
|
"name": "visit",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": 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
|
||||||
|
},
|
||||||
|
"lastTimestamp": {
|
||||||
|
"name": "lastTimestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"logCount": {
|
||||||
|
"name": "logCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
724
drizzle/meta/0010_snapshot.json
Normal file
724
drizzle/meta/0010_snapshot.json
Normal file
@@ -0,0 +1,724 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "af3d9e4f-cea6-42fa-8f8b-d743d97b9c37",
|
||||||
|
"prevId": "bffde16c-d716-40ec-9d92-cb49814815d7",
|
||||||
|
"tables": {
|
||||||
|
"character_abilities": {
|
||||||
|
"name": "character_abilities",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ability": {
|
||||||
|
"name": "ability",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_abilities_character_character_id_fk": {
|
||||||
|
"name": "character_abilities_character_character_id_fk",
|
||||||
|
"tableFrom": "character_abilities",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_abilities_character_ability_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"ability"
|
||||||
|
],
|
||||||
|
"name": "character_abilities_character_ability_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_leveling": {
|
||||||
|
"name": "character_leveling",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"choice": {
|
||||||
|
"name": "choice",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_leveling_character_character_id_fk": {
|
||||||
|
"name": "character_leveling_character_character_id_fk",
|
||||||
|
"tableFrom": "character_leveling",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_leveling_character_level_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"level"
|
||||||
|
],
|
||||||
|
"name": "character_leveling_character_level_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_modifiers": {
|
||||||
|
"name": "character_modifiers",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"modifier": {
|
||||||
|
"name": "modifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_modifiers_character_character_id_fk": {
|
||||||
|
"name": "character_modifiers_character_character_id_fk",
|
||||||
|
"tableFrom": "character_modifiers",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_modifiers_character_modifier_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"modifier"
|
||||||
|
],
|
||||||
|
"name": "character_modifiers_character_modifier_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_spell": {
|
||||||
|
"name": "character_spell",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_spell_character_character_id_fk": {
|
||||||
|
"name": "character_spell_character_character_id_fk",
|
||||||
|
"tableFrom": "character_spell",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"people": {
|
||||||
|
"name": "people",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"aspect": {
|
||||||
|
"name": "aspect",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"name": "notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"health": {
|
||||||
|
"name": "health",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"mana": {
|
||||||
|
"name": "mana",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"name": "visibility",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'private'"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_owner_users_id_fk": {
|
||||||
|
"name": "character_owner_users_id_fk",
|
||||||
|
"tableFrom": "character",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_training": {
|
||||||
|
"name": "character_training",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"stat": {
|
||||||
|
"name": "stat",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"choice": {
|
||||||
|
"name": "choice",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_training_character_character_id_fk": {
|
||||||
|
"name": "character_training_character_character_id_fk",
|
||||||
|
"tableFrom": "character_training",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_training_character_stat_level_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"stat",
|
||||||
|
"level"
|
||||||
|
],
|
||||||
|
"name": "character_training_character_stat_level_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"email_validation": {
|
||||||
|
"name": "email_validation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"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": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visit": {
|
||||||
|
"name": "visit",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": 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
|
||||||
|
},
|
||||||
|
"lastTimestamp": {
|
||||||
|
"name": "lastTimestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"logCount": {
|
||||||
|
"name": "logCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
732
drizzle/meta/0011_snapshot.json
Normal file
732
drizzle/meta/0011_snapshot.json
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "e0aaebf1-54e4-4f61-804b-7cce23c88069",
|
||||||
|
"prevId": "af3d9e4f-cea6-42fa-8f8b-d743d97b9c37",
|
||||||
|
"tables": {
|
||||||
|
"character_abilities": {
|
||||||
|
"name": "character_abilities",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ability": {
|
||||||
|
"name": "ability",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"name": "max",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_abilities_character_character_id_fk": {
|
||||||
|
"name": "character_abilities_character_character_id_fk",
|
||||||
|
"tableFrom": "character_abilities",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_abilities_character_ability_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"ability"
|
||||||
|
],
|
||||||
|
"name": "character_abilities_character_ability_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_leveling": {
|
||||||
|
"name": "character_leveling",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"choice": {
|
||||||
|
"name": "choice",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_leveling_character_character_id_fk": {
|
||||||
|
"name": "character_leveling_character_character_id_fk",
|
||||||
|
"tableFrom": "character_leveling",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_leveling_character_level_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"level"
|
||||||
|
],
|
||||||
|
"name": "character_leveling_character_level_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_modifiers": {
|
||||||
|
"name": "character_modifiers",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"modifier": {
|
||||||
|
"name": "modifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_modifiers_character_character_id_fk": {
|
||||||
|
"name": "character_modifiers_character_character_id_fk",
|
||||||
|
"tableFrom": "character_modifiers",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_modifiers_character_modifier_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"modifier"
|
||||||
|
],
|
||||||
|
"name": "character_modifiers_character_modifier_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_spell": {
|
||||||
|
"name": "character_spell",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_spell_character_character_id_fk": {
|
||||||
|
"name": "character_spell_character_character_id_fk",
|
||||||
|
"tableFrom": "character_spell",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"people": {
|
||||||
|
"name": "people",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"aspect": {
|
||||||
|
"name": "aspect",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"name": "notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"health": {
|
||||||
|
"name": "health",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"mana": {
|
||||||
|
"name": "mana",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"name": "visibility",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'private'"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_owner_users_id_fk": {
|
||||||
|
"name": "character_owner_users_id_fk",
|
||||||
|
"tableFrom": "character",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_training": {
|
||||||
|
"name": "character_training",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"stat": {
|
||||||
|
"name": "stat",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"choice": {
|
||||||
|
"name": "choice",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_training_character_character_id_fk": {
|
||||||
|
"name": "character_training_character_character_id_fk",
|
||||||
|
"tableFrom": "character_training",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_training_character_stat_level_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"stat",
|
||||||
|
"level"
|
||||||
|
],
|
||||||
|
"name": "character_training_character_stat_level_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"email_validation": {
|
||||||
|
"name": "email_validation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"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": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visit": {
|
||||||
|
"name": "visit",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": 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
|
||||||
|
},
|
||||||
|
"lastTimestamp": {
|
||||||
|
"name": "lastTimestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"logCount": {
|
||||||
|
"name": "logCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
740
drizzle/meta/0012_snapshot.json
Normal file
740
drizzle/meta/0012_snapshot.json
Normal file
@@ -0,0 +1,740 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "cb7a2b9c-1392-4f23-9fc2-9ce8de2e0231",
|
||||||
|
"prevId": "e0aaebf1-54e4-4f61-804b-7cce23c88069",
|
||||||
|
"tables": {
|
||||||
|
"character_abilities": {
|
||||||
|
"name": "character_abilities",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ability": {
|
||||||
|
"name": "ability",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"name": "max",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_abilities_character_character_id_fk": {
|
||||||
|
"name": "character_abilities_character_character_id_fk",
|
||||||
|
"tableFrom": "character_abilities",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_abilities_character_ability_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"ability"
|
||||||
|
],
|
||||||
|
"name": "character_abilities_character_ability_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_leveling": {
|
||||||
|
"name": "character_leveling",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"choice": {
|
||||||
|
"name": "choice",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_leveling_character_character_id_fk": {
|
||||||
|
"name": "character_leveling_character_character_id_fk",
|
||||||
|
"tableFrom": "character_leveling",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_leveling_character_level_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"level"
|
||||||
|
],
|
||||||
|
"name": "character_leveling_character_level_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_modifiers": {
|
||||||
|
"name": "character_modifiers",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"modifier": {
|
||||||
|
"name": "modifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_modifiers_character_character_id_fk": {
|
||||||
|
"name": "character_modifiers_character_character_id_fk",
|
||||||
|
"tableFrom": "character_modifiers",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_modifiers_character_modifier_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"modifier"
|
||||||
|
],
|
||||||
|
"name": "character_modifiers_character_modifier_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_spell": {
|
||||||
|
"name": "character_spell",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_spell_character_character_id_fk": {
|
||||||
|
"name": "character_spell_character_character_id_fk",
|
||||||
|
"tableFrom": "character_spell",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_spell_character_value_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"name": "character_spell_character_value_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"people": {
|
||||||
|
"name": "people",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"aspect": {
|
||||||
|
"name": "aspect",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"name": "notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"health": {
|
||||||
|
"name": "health",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"mana": {
|
||||||
|
"name": "mana",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"name": "visibility",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'private'"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_owner_users_id_fk": {
|
||||||
|
"name": "character_owner_users_id_fk",
|
||||||
|
"tableFrom": "character",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_training": {
|
||||||
|
"name": "character_training",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"stat": {
|
||||||
|
"name": "stat",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"choice": {
|
||||||
|
"name": "choice",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_training_character_character_id_fk": {
|
||||||
|
"name": "character_training_character_character_id_fk",
|
||||||
|
"tableFrom": "character_training",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_training_character_stat_level_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"stat",
|
||||||
|
"level"
|
||||||
|
],
|
||||||
|
"name": "character_training_character_stat_level_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"email_validation": {
|
||||||
|
"name": "email_validation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"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": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visit": {
|
||||||
|
"name": "visit",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": 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
|
||||||
|
},
|
||||||
|
"lastTimestamp": {
|
||||||
|
"name": "lastTimestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"logCount": {
|
||||||
|
"name": "logCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,8 +47,50 @@
|
|||||||
{
|
{
|
||||||
"idx": 6,
|
"idx": 6,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1743344302223,
|
"when": 1745072860245,
|
||||||
"tag": "0006_luxuriant_blade",
|
"tag": "0006_clever_marvex",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1745074613379,
|
||||||
|
"tag": "0007_tearful_true_believers",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1745675022171,
|
||||||
|
"tag": "0008_glorious_johnny_blaze",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1745920443528,
|
||||||
|
"tag": "0009_thin_omega_sentinel",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1746014143374,
|
||||||
|
"tag": "0010_bored_sabra",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1746017162319,
|
||||||
|
"tag": "0011_demonic_titania",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1746027790969,
|
||||||
|
"tag": "0012_graceful_energizer",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,12 +14,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NuxtError } from '#app'
|
import type { NuxtError } from '#app';
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
error: Object as () => NuxtError
|
error: Object as () => NuxtError
|
||||||
})
|
});
|
||||||
|
|
||||||
const handleError = () => clearError({ redirect: '/' })
|
const handleError = () => clearError({ redirect: '/' });
|
||||||
</script>
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<CollapsibleRoot class="flex flex-1 flex-col" v-model:open="open">
|
<CollapsibleRoot class="flex flex-1 flex-col" v-model:open="open">
|
||||||
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
<div class="z-50 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||||
<div class="flex items-center px-2 gap-4">
|
<div class="flex items-center px-2 gap-4">
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button icon class="!bg-transparent group md:hidden">
|
<Button icon class="!bg-transparent group md:hidden">
|
||||||
@@ -14,10 +14,21 @@
|
|||||||
<span class="text-xl max-md:hidden">d[any]</span>
|
<span class="text-xl max-md:hidden">d[any]</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-8 max-md:hidden">
|
<NavigationMenuRoot class="relative">
|
||||||
<Tooltip message="Developpement en cours" side="bottom"><NuxtLink href="#" class="text-light-70 dark:text-dark-70">Parcourir les projets</NuxtLink></Tooltip>
|
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
|
||||||
<Tooltip message="Developpement en cours" side="bottom"><NuxtLink href="#" class="text-light-70 dark:text-dark-70">Créer du contenu</NuxtLink></Tooltip>
|
<NavigationMenuItem>
|
||||||
</div>
|
<NavigationMenuTrigger>
|
||||||
|
<NuxtLink :href="{ name: 'character' }" class="text-light-70 dark:text-dark-70" active-class="!text-accent-blue"><span class="pl-3 py-1 flex-1 truncate">Personnages</span></NuxtLink>
|
||||||
|
</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent class="absolute top-0 left-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30">
|
||||||
|
<NuxtLink :href="{ name: 'character-list' }" class="text-light-70 dark:text-dark-70" active-class="!text-accent-blue"><span class="py-2 px-3 flex-1 truncate">Tous les personnages</span></NuxtLink>
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
<div class="absolute top-full left-0 flex w-full justify-center my-4">
|
||||||
|
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] overflow-hidden rounded-[10px] bg-white transition-[width,_height] duration-300 sm:w-[var(--radix-navigation-menu-viewport-width)]" />
|
||||||
|
</div>
|
||||||
|
</NavigationMenuRoot>
|
||||||
<div class="flex items-center px-2 gap-4">
|
<div class="flex items-center px-2 gap-4">
|
||||||
<template v-if="!loggedIn">
|
<template v-if="!loggedIn">
|
||||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">Se connecter</NuxtLink>
|
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">Se connecter</NuxtLink>
|
||||||
@@ -28,23 +39,32 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
|
<div class="flex flex-1 flex-row relative h-screen w-screen overflow-hidden">
|
||||||
<!-- <CollapsibleContent asChild forceMount> -->
|
<CollapsibleContent asChild forceMount>
|
||||||
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
|
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
|
||||||
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="treeParent">
|
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden">
|
||||||
<div class="flex flex-row flex-1 justify-between items-center py-4 px-2">
|
<div v-if="user" class="flex flex-1 py-4 px-2 flex-row flex-1 justify-between items-center">
|
||||||
<NuxtLink :href="{ name: 'explore-path', params: { path: 'index' } }" class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
<NuxtLink v-if="hasPermissions(user.permissions, ['admin', 'editor'])" :to="{ name: 'explore-edit' }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink>
|
||||||
<span class="pl-3 py-1 flex-1 truncate">Projet</span>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink v-if="user && hasPermissions(user.permissions, ['admin', 'editor'])" :to="{ name: 'explore-edit' }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Tree v-if="pages" v-model="pages" :getKey="(item) => item.path" class="ps-4">
|
||||||
|
<template #default="{ item, isExpanded }">
|
||||||
|
<NuxtLink :href="item.value.path && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.path } } : undefined" class="flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full" :class="{ 'font-medium': item.hasChildren }" active-class="text-accent-blue" :data-private="item.value.private">
|
||||||
|
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level / 2 - 1.5}em` }" />
|
||||||
|
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="w-5 h-5" />
|
||||||
|
<div class="pl-1.5 py-1.5 flex-1 truncate">
|
||||||
|
{{ item.value.title }}
|
||||||
|
</div>
|
||||||
|
<Tooltip message="Privé" side="right"><Icon v-show="item.value.private" class="mx-1" icon="radix-icons:lock-closed" /></Tooltip>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
</Tree>
|
||||||
</div>
|
</div>
|
||||||
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60">
|
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60">
|
||||||
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||||
<p>Copyright Peaceultime - 2025</p>
|
<p>Copyright Peaceultime - 2025</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- </CollapsibleContent> -->
|
</CollapsibleContent>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleRoot>
|
</CollapsibleRoot>
|
||||||
@@ -52,14 +72,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import { iconByType } from '#shared/general.util';
|
||||||
import type { DropdownOption } from '~/components/base/DropdownMenu.vue';
|
import type { DropdownOption } from '~/components/base/DropdownMenu.vue';
|
||||||
import { hasPermissions } from '#shared/auth.util';
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
import { TreeDOM } from '#shared/tree';
|
import type { TreeItem } from '~/types/content';
|
||||||
import { Content, iconByType } from '#shared/content.util';
|
|
||||||
import { dom, icon, text } from '#shared/dom.util';
|
|
||||||
import { unifySlug } from '#shared/general.util';
|
|
||||||
import { popper } from '#shared/floating.util';
|
|
||||||
import { link } from '#shared/proses';
|
|
||||||
|
|
||||||
const options = ref<DropdownOption[]>([{
|
const options = ref<DropdownOption[]>([{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
@@ -78,43 +94,16 @@ const { fetch } = useContent();
|
|||||||
await fetch(false);
|
await fetch(false);
|
||||||
|
|
||||||
const route = useRouter().currentRoute;
|
const route = useRouter().currentRoute;
|
||||||
const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined);
|
const path = computed(() => route.value.params.path ? Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path : undefined);
|
||||||
|
|
||||||
await Content.init();
|
|
||||||
const tree = new TreeDOM((item, depth) => {
|
|
||||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [
|
|
||||||
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }),
|
|
||||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
|
||||||
item.private ? popper(dom('span', { class: 'flex' }, [icon('radix-icons:lock-closed', { class: 'mx-1' })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], 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' }) : undefined,
|
|
||||||
])]);
|
|
||||||
}, (item, depth) => {
|
|
||||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [link({ class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined, [
|
|
||||||
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
|
||||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
|
||||||
item.private ? popper(dom('span', { class: 'flex' }, [icon('radix-icons:lock-closed', { class: 'mx-1' })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], 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' }) : undefined,
|
|
||||||
])]);
|
|
||||||
}, (item) => item.navigable);
|
|
||||||
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(e, true));
|
|
||||||
const treeParent = useTemplateRef('treeParent');
|
|
||||||
|
|
||||||
const unmount = useRouter().afterEach((to, from, failure) => {
|
|
||||||
if(failure)
|
|
||||||
return;
|
|
||||||
|
|
||||||
to.name === 'explore-path' && (unifySlug(to.params.path).split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(e, true));
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
const { tree } = useContent();
|
||||||
if(treeParent.value)
|
const pages = computed(() => transform(tree.value));
|
||||||
{
|
function transform(list: TreeItem[] | undefined): TreeItem[] | undefined
|
||||||
treeParent.value.appendChild(tree.container);
|
{
|
||||||
}
|
return list?.filter(e => e.navigable)?.map(e => ({ ...e, open: path.value?.startsWith(e.path), children: transform(e.children) }));
|
||||||
})
|
}
|
||||||
onUnmounted(() => {
|
|
||||||
unmount();
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
@@ -5,6 +5,7 @@ import path from 'node:path'
|
|||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2024-04-03',
|
compatibilityDate: '2024-04-03',
|
||||||
|
ssr: false,
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxtjs/color-mode',
|
'@nuxtjs/color-mode',
|
||||||
'nuxt-security',
|
'nuxt-security',
|
||||||
@@ -16,11 +17,6 @@ export default defineNuxtConfig({
|
|||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
viewer: false,
|
viewer: false,
|
||||||
config: {
|
config: {
|
||||||
content: {
|
|
||||||
files: [
|
|
||||||
"./shared/**/*.{vue,js,jsx,mjs,ts,tsx}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
@@ -61,14 +57,14 @@ export default defineNuxtConfig({
|
|||||||
current: 'currentColor',
|
current: 'currentColor',
|
||||||
light: {
|
light: {
|
||||||
red: '#e93147',
|
red: '#e93147',
|
||||||
redBack: '#F9C7CD',
|
orange: '#FF9800',
|
||||||
orange: '#ec7500',
|
yellow: '#FFEB3B',
|
||||||
yellow: '#e0ac00',
|
green: '#388E3C',
|
||||||
green: '#08b94e',
|
indigo: '#7986CB',
|
||||||
greenBack: '#BCECCF',
|
|
||||||
cyan: '#00bfbc',
|
cyan: '#00bfbc',
|
||||||
|
lime: '#8BC34A',
|
||||||
blue: '#086ddd',
|
blue: '#086ddd',
|
||||||
purple: '#7852ee',
|
purple: '#AB47BC',
|
||||||
pink: '#d53984',
|
pink: '#d53984',
|
||||||
0: "#ffffff",
|
0: "#ffffff",
|
||||||
5: "#fcfcfc",
|
5: "#fcfcfc",
|
||||||
@@ -119,7 +115,6 @@ export default defineNuxtConfig({
|
|||||||
pageTransition: false,
|
pageTransition: false,
|
||||||
layoutTransition: false
|
layoutTransition: false
|
||||||
},
|
},
|
||||||
ssr: false,
|
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
path: '~/components',
|
path: '~/components',
|
||||||
@@ -131,6 +126,9 @@ export default defineNuxtConfig({
|
|||||||
experimental: {
|
experimental: {
|
||||||
tasks: true,
|
tasks: true,
|
||||||
},
|
},
|
||||||
|
watchOptions: {
|
||||||
|
usePolling: true,
|
||||||
|
},
|
||||||
rollupConfig: {
|
rollupConfig: {
|
||||||
external: ['bun'],
|
external: ['bun'],
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -141,12 +139,13 @@ export default defineNuxtConfig({
|
|||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
session: {
|
session: {
|
||||||
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013',
|
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013',
|
||||||
maxAge: 60 * 60 * 24 * 30,
|
maxAge: 60 * 60 * 24 *30,
|
||||||
},
|
},
|
||||||
database: 'db.sqlite',
|
database: 'db.sqlite',
|
||||||
mail: {
|
mail: {
|
||||||
host: '',
|
host: '',
|
||||||
port: '',
|
port: '',
|
||||||
|
proxy: '',
|
||||||
user: '',
|
user: '',
|
||||||
passwd: '',
|
passwd: '',
|
||||||
dkim: '',
|
dkim: '',
|
||||||
@@ -167,6 +166,7 @@ export default defineNuxtConfig({
|
|||||||
sources: ['/api/__sitemap__/urls']
|
sources: ['/api/__sitemap__/urls']
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
buildCache: true,
|
||||||
componentIslands: {
|
componentIslands: {
|
||||||
selectiveClient: true,
|
selectiveClient: true,
|
||||||
},
|
},
|
||||||
@@ -185,9 +185,19 @@ export default defineNuxtConfig({
|
|||||||
cert: fs.readFileSync(path.resolve(__dirname, 'localhost+1.pem')).toString('utf-8'),
|
cert: fs.readFileSync(path.resolve(__dirname, 'localhost+1.pem')).toString('utf-8'),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
vue: {
|
devtools: {
|
||||||
compilerOptions: {
|
enabled: false,
|
||||||
isCustomElement: (tag) => tag === 'iconify-icon',
|
},
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
watch: {
|
||||||
|
usePolling: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watchers: {
|
||||||
|
chokidar: {
|
||||||
|
usePolling: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
48
package.json
48
package.json
@@ -4,59 +4,55 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "bun i",
|
"predev": "bun i",
|
||||||
"dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 bunx --bun nuxi dev"
|
"dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 bunx --bun nuxi dev --no-f"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.5.0",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.0",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
"@codemirror/lang-markdown": "^6.3.3",
|
||||||
"@codemirror/lang-markdown": "^6.3.2",
|
|
||||||
"@floating-ui/dom": "^1.6.13",
|
|
||||||
"@iconify/vue": "^4.3.0",
|
"@iconify/vue": "^4.3.0",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@markdoc/markdoc": "^0.5.1",
|
"@markdoc/markdoc": "^0.5.2",
|
||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"@nuxtjs/sitemap": "^7.2.5",
|
"@nuxtjs/sitemap": "^7.4.2",
|
||||||
"@nuxtjs/tailwindcss": "^6.13.1",
|
"@nuxtjs/tailwindcss": "^6.13.1",
|
||||||
"@types/codemirror": "^5.60.15",
|
|
||||||
"@vueuse/gesture": "^2.0.0",
|
"@vueuse/gesture": "^2.0.0",
|
||||||
"@vueuse/math": "^12.7.0",
|
"@vueuse/math": "^13.4.0",
|
||||||
"@vueuse/nuxt": "^12.7.0",
|
"@vueuse/nuxt": "^13.4.0",
|
||||||
"codemirror": "6.0.1",
|
"codemirror": "^6.0.2",
|
||||||
"drizzle-orm": "^0.39.3",
|
"drizzle-orm": "^0.44.2",
|
||||||
"hast": "^1.0.0",
|
"hast": "^1.0.0",
|
||||||
"hast-util-heading": "^3.0.0",
|
"hast-util-heading": "^3.0.0",
|
||||||
"hast-util-heading-rank": "^3.0.0",
|
"hast-util-heading-rank": "^3.0.0",
|
||||||
"iconify-icon": "^2.3.0",
|
|
||||||
"lodash.capitalize": "^4.2.1",
|
"lodash.capitalize": "^4.2.1",
|
||||||
"mdast-util-find-and-replace": "^3.0.2",
|
"mdast-util-find-and-replace": "^3.0.2",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^7.0.3",
|
||||||
"nuxt": "3.15.4",
|
"nuxt": "3.17.5",
|
||||||
"nuxt-security": "^2.1.5",
|
"nuxt-security": "^2.2.0",
|
||||||
"radix-vue": "^1.9.15",
|
"radix-vue": "^1.9.17",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-frontmatter": "^5.0.0",
|
"remark-frontmatter": "^5.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-ofm": "link:remark-ofm",
|
"remark-ofm": "link:remark-ofm",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.1",
|
"remark-rehype": "^11.1.2",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-vue": "^6.0.0",
|
"rollup-plugin-vue": "^6.0.0",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.1",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.2.2",
|
"@types/bun": "^1.2.17",
|
||||||
"@types/lodash.capitalize": "^4.2.9",
|
"@types/lodash.capitalize": "^4.2.9",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/unist": "^3.0.3",
|
"@types/unist": "^3.0.3",
|
||||||
"better-sqlite3": "^11.8.1",
|
"better-sqlite3": "^12.1.1",
|
||||||
"bun-types": "^1.2.2",
|
"bun-types": "^1.2.17",
|
||||||
"drizzle-kit": "^0.30.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"rehype-stringify": "^10.0.1"
|
"rehype-stringify": "^10.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { format } from '~/shared/general.util';
|
import { format, iconByType } from '~/shared/general.util';
|
||||||
import { iconByType } from '~/shared/content.util';
|
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
interface File
|
interface File
|
||||||
|
|||||||
115
pages/character/[id]/edit.client.vue
Normal file
115
pages/character/[id]/edit.client.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import characterConfig from '#shared/character-config.json';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import { CharacterBuilder, defaultCharacter } from '~/shared/character';
|
||||||
|
import type { Character, CharacterConfig } from '~/types/character';
|
||||||
|
|
||||||
|
const stepTexts: Record<number, string> = {
|
||||||
|
0: 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.',
|
||||||
|
1: 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.',
|
||||||
|
2: 'Spécialisez votre personnage en attribuant vos points d\'entrainement parmi les 7 branches disponibles.\nChaque paliers de 3 points augmentent votre modifieur.',
|
||||||
|
3: 'Diversifiez vos possibilités en affectant vos points dans les différentes compétences disponibles.',
|
||||||
|
4: 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.'
|
||||||
|
};
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
guestsGoesTo: '/user/login',
|
||||||
|
});
|
||||||
|
let id = useRouter().currentRoute.value.params.id;
|
||||||
|
const { add } = useToast();
|
||||||
|
const config = characterConfig as CharacterConfig;
|
||||||
|
const data = ref<Character>({ ...defaultCharacter });
|
||||||
|
const builder = markRaw(new CharacterBuilder(data.value));
|
||||||
|
|
||||||
|
const step = ref(0);
|
||||||
|
|
||||||
|
if(id !== 'new')
|
||||||
|
{
|
||||||
|
const character = await useRequestFetch()(`/api/character/${id}`);
|
||||||
|
|
||||||
|
if(!character)
|
||||||
|
{
|
||||||
|
throw new Error('Donnée du personnage introuvables');
|
||||||
|
}
|
||||||
|
|
||||||
|
data.value = Object.assign(defaultCharacter, data.value, character);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(leave: boolean)
|
||||||
|
{
|
||||||
|
if(data.value.name === '' || data.value.people === undefined || data.value.people === -1)
|
||||||
|
{
|
||||||
|
add({ title: 'Données manquantes', content: "Merci de saisir un nom et une race avant de pouvoir enregistrer votre personnage", type: 'error', duration: 25000, timer: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(id === 'new')
|
||||||
|
{
|
||||||
|
//@ts-ignore
|
||||||
|
id = await useRequestFetch()(`/api/character`, {
|
||||||
|
method: 'post',
|
||||||
|
body: data.value,
|
||||||
|
onResponseError: (e) => {
|
||||||
|
add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
|
||||||
|
useRouter().replace({ name: 'character-id-edit', params: { id: id } })
|
||||||
|
if(leave) useRouter().push({ name: 'character-id', params: { id: id } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//@ts-ignore
|
||||||
|
await useRequestFetch()(`/api/character/${id}`, {
|
||||||
|
method: 'post',
|
||||||
|
body: data.value,
|
||||||
|
onResponseError: (e) => {
|
||||||
|
add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
|
||||||
|
if(leave) useRouter().push({ name: 'character-id', params: { id: id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useShortcuts({
|
||||||
|
"Meta_S": () =>save(false),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Edition de {{ data.name || 'nouveau personnage' }}</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-1 max-w-full flex-col align-center">
|
||||||
|
<StepperRoot class="flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden" v-model="step">
|
||||||
|
<div class="flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20">
|
||||||
|
<div></div>
|
||||||
|
<div class="flex w-full flex-row gap-4 items-center justify-center relative">
|
||||||
|
<StepperItem :step="0" class="group"><StepperTrigger class="px-2 py-1 border-b border-transparent hover:border-accent-blue group-data-[state=active]:text-accent-blue">Peuples</StepperTrigger></StepperItem>
|
||||||
|
<StepperItem :disabled="data.people === undefined" :step="1" class="group flex items-center"><Icon icon="radix-icons:chevron-right" class="w-6 h-6 group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" /><StepperTrigger class="px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue">Niveaux</StepperTrigger></StepperItem>
|
||||||
|
<StepperItem :disabled="data.people === undefined" :step="2" class="group flex items-center"><Icon icon="radix-icons:chevron-right" class="w-6 h-6 group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" /><StepperTrigger class="px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue">Entrainement</StepperTrigger></StepperItem>
|
||||||
|
<StepperItem :disabled="data.people === undefined" :step="3" class="group flex items-center"><Icon icon="radix-icons:chevron-right" class="w-6 h-6 group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" /><StepperTrigger class="px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue">Compétences</StepperTrigger></StepperItem>
|
||||||
|
<StepperItem :disabled="data.people === undefined" :step="4" class="group flex items-center"><Icon icon="radix-icons:chevron-right" class="w-6 h-6 group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" /><StepperTrigger class="px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue">Aspect</StepperTrigger></StepperItem>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tooltip class="max-w-96" side="bottom" align="end" :message="stepTexts[step]"><Icon icon="radix-icons:question-mark-circled" class="w-5 h-5" /></Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 outline-none max-w-full w-full overflow-y-auto" v-show="step === 0">
|
||||||
|
<PeopleSelector v-model="builder" :config="config" @next="step = 1" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 outline-none max-w-full w-full overflow-y-auto" v-show="step === 1">
|
||||||
|
<LevelEditor v-model="builder" :config="config" @next="step = 2" />
|
||||||
|
</div>
|
||||||
|
<!-- <div class="flex-1 outline-none max-w-full w-full h-full max-h-full overflow-y-auto" v-show="step === 2">
|
||||||
|
<TrainingEditor v-model="builder" :config="config" @next="step = 3" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 outline-none max-w-full w-fulloverflow-y-auto" v-show="step === 3">
|
||||||
|
<AbilityEditor v-model="builder" :config="config" @next="step = 4" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 outline-none max-w-full w-full overflow-y-auto" v-show="step === 4">
|
||||||
|
<AspectSelector v-model="builder" :config="config" @next="save(true)" />
|
||||||
|
</div> -->
|
||||||
|
</StepperRoot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
193
pages/character/[id]/index.client.vue
Normal file
193
pages/character/[id]/index.client.vue
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import config from '#shared/character-config.json';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import PreviewA from '~/components/prose/PreviewA.vue';
|
||||||
|
import { clamp } from '~/shared/general.util';
|
||||||
|
import type { SpellConfig } from '~/types/character';
|
||||||
|
import { elementTexts, spellTypeTexts, type CharacterConfig } from '~/types/character';
|
||||||
|
|
||||||
|
const characterConfig = config as CharacterConfig;
|
||||||
|
|
||||||
|
const id = useRouter().currentRoute.value.params.id;
|
||||||
|
const { user } = useUserSession();
|
||||||
|
const { add } = useToast();
|
||||||
|
|
||||||
|
const { data: character, status, error } = await useFetch(`/api/character/${id}/compiled`);
|
||||||
|
|
||||||
|
/*
|
||||||
|
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
|
||||||
|
text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue
|
||||||
|
text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow
|
||||||
|
text-light-orange dark:text-dark-orange border-light-orange dark:border-dark-orange bg-light-orange dark:bg-dark-orange
|
||||||
|
text-light-indigo dark:text-dark-indigo border-light-indigo dark:border-dark-indigo bg-light-indigo dark:bg-dark-indigo
|
||||||
|
text-light-lime dark:text-dark-lime border-light-lime dark:border-dark-lime bg-light-lime dark:bg-dark-lime
|
||||||
|
text-light-green dark:text-dark-green border-light-green dark:border-dark-green bg-light-green dark:bg-dark-green
|
||||||
|
text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow
|
||||||
|
text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="status === 'pending'">
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Chargement ...</Title>
|
||||||
|
</Head>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="status === 'success' && character && !error">
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - {{ character.name }}</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-row gap-4 justify-between">
|
||||||
|
<div></div>
|
||||||
|
<div class="flex lg:flex-row flex-col gap-6 items-center justify-center">
|
||||||
|
<div class="flex gap-6 items-center">
|
||||||
|
<Avatar src="" icon="radix-icons:person" size="large" />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-xl font-bold">{{ character.name }}</span>
|
||||||
|
<span class="text-sm">De {{ character.username }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold">Niveau {{ character.level }}</span>
|
||||||
|
<span>{{ character.race === -1 ? "Race inconnue" : characterConfig.peoples[character.race].name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col lg:border-l border-light-30 dark:border-dark-30 py-4 ps-4">
|
||||||
|
<span class="flex flex-row items-center gap-2">PV: {{ character.health - character.values.health }}/{{ character.health }}</span>
|
||||||
|
<span class="flex flex-row items-center gap-2">Mana: {{ character.mana - character.values.mana }}/{{ character.mana }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="self-center">
|
||||||
|
<Tooltip side="right" message="Modifier" v-if="user && user.id === character.owner"><NuxtLink :to="{ name: 'character-id-edit', params: { id: character.id } }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink></Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 flex-col justify-center gap-4 *:py-2">
|
||||||
|
<div class="grid 2xl:grid-cols-10 grid-cols-1 gap-4 items-center border-b border-light-30 dark:border-dark-30 me-4 pe-4">
|
||||||
|
<div class="flex relative justify-between ps-4 gap-2 2xl:col-span-6">
|
||||||
|
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.strength }}</span><span class="text-sm 2xl:text-base">Force</span></div>
|
||||||
|
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.dexterity }}</span><span class="text-sm 2xl:text-base">Dextérité</span></div>
|
||||||
|
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.constitution }}</span><span class="text-sm 2xl:text-base">Constitution</span></div>
|
||||||
|
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.intelligence }}</span><span class="text-sm 2xl:text-base">Intelligence</span></div>
|
||||||
|
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.curiosity }}</span><span class="text-sm 2xl:text-base">Curiosité</span></div>
|
||||||
|
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.charisma }}</span><span class="text-sm 2xl:text-base">Charisme</span></div>
|
||||||
|
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.psyche }}</span><span class="text-sm 2xl:text-base">Psyché</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 relative 2xl:border-l border-light-30 dark:border-dark-30 ps-4 2xl:col-span-4 flex-row items-center justify-between">
|
||||||
|
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.initiative }}</span><span>Initiative</span></div>
|
||||||
|
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">{{ character.speed === false ? "Aucun déplacement" : `${character.speed} cases` }}</span><span>Course</span></div>
|
||||||
|
<Icon icon="ph:shield-checkered" class="w-8 h-8" />
|
||||||
|
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.passivedodge + character.defense.passiveparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Passive</span></div>
|
||||||
|
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.passivedodge + character.defense.activeparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Blocage</span></div>
|
||||||
|
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.activedodge + character.defense.passiveparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Esquive</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 px-8">
|
||||||
|
<div class="flex flex-col pe-8 gap-4 py-8 w-80 border-r border-light-30 dark:border-dark-30">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'arme</span>
|
||||||
|
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
|
||||||
|
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes légères">Arme légère</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes de jet">Arme de jet</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes naturelles">Arme naturelle</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes">Arme standard</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes improvisées">Arme improvisée</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.strength > 2" href="1. Règles/99. Annexes/4. Équipement#Les armes lourdes">Arme lourde</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.strength > 3" href="1. Règles/99. Annexes/4. Équipement#Les armes à deux mains">Arme à deux mains</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.dexterity > 0 && character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes maniables">Arme maniable</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes à projectiles">Arme à projectiles</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 2" href="1. Règles/99. Annexes/4. Équipement#Les armes longues">Arme longue</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.shield > 0" href="1. Règles/99. Annexes/4. Équipement#Les boucliers">Bouclier</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.shield > 0 && character.mastery.strength > 3" href="1. Règles/99. Annexes/4. Équipement#Les boucliers à deux mains">Bouclier à deux mains</PreviewA>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="character.mastery.armor > 0" class="flex flex-col">
|
||||||
|
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'armure</span>
|
||||||
|
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
|
||||||
|
<PreviewA v-if="character.mastery.armor > 0" href="1. Règles/99. Annexes/4. Équipement#Les armures légères">Armure légère</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.armor > 1" href="1. Règles/99. Annexes/4. Équipement#Les armures">Armure standard</PreviewA>
|
||||||
|
<PreviewA v-if="character.mastery.armor > 2" href="1. Règles/99. Annexes/4. Équipement#Les armures lourdes">Armure lourde</PreviewA>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise de sorts</span>
|
||||||
|
<span>Sorts de précision: <span class="font-bold">{{ character.spellranks.precision }}</span></span>
|
||||||
|
<span>Sorts de savoir: <span class="font-bold">{{ character.spellranks.knowledge }}</span></span>
|
||||||
|
<span>Sorts d'instinct: <span class="font-bold">{{ character.spellranks.instinct }}</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30 mb-2">Compétences</span>
|
||||||
|
<div class="grid grid-cols-3 gap-1">
|
||||||
|
<div class="flex flex-col px-2 items-center text-sm text-light-70 dark:text-dark-70" v-for="(value, ability) of character.abilities"><span class="font-bold text-base text-light-100 dark:text-dark-100">+{{ value }}</span><span>{{ characterConfig.abilities[ability].name }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TabsRoot default-value="features" class="w-[60rem]">
|
||||||
|
<TabsList class="flex flex-row relative px-4 gap-4">
|
||||||
|
<TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator>
|
||||||
|
<TabsTrigger value="features" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Aptitudes</TabsTrigger>
|
||||||
|
<TabsTrigger value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Sorts</TabsTrigger>
|
||||||
|
<TabsTrigger value="notes" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Notes</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="features">
|
||||||
|
<div class="flex flex-1 flex-col ps-8 gap-4 py-8">
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-lg font-semibold">Actions</span>
|
||||||
|
<span class="text-sm text-light-70 dark:text-dark-70">Attaquer - Saisir - Faire chuter - Déplacer - Courir - Pas de coté - Lancer un sort - S'interposer - Se transformer - Utiliser un objet - Anticiper une action - Improviser</span>
|
||||||
|
<MarkdownRenderer :content="character.features.action.join('\n')" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-lg font-semibold">Réactions</span>
|
||||||
|
<span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span>
|
||||||
|
<MarkdownRenderer :content="character.features.reaction.join('\n')" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-lg font-semibold">Actions libre</span>
|
||||||
|
<span class="text-sm text-light-70 dark:text-dark-70">Analyser une situation - Communiquer</span>
|
||||||
|
<MarkdownRenderer :content="character.features.freeaction.join('\n')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-lg font-semibold">Aptitudes</span>
|
||||||
|
<MarkdownRenderer :content="character.features.misc.map(e => `> ${e}`).join('\n\n')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent v-if="character.spells.length > 0" value="spells">
|
||||||
|
<div class="flex flex-1 flex-col ps-8 gap-4 py-8">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="pb-4 px-2 mt-4 border-b last:border-none border-light-30 dark:border-dark-30 flex flex-col" v-for="spell of character.spells.map(e => characterConfig.spells.find((f: SpellConfig) => f.id === e)).filter(e => !!e)">
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<span class="text-lg font-bold">{{ spell.name }}</span>
|
||||||
|
<div class="flex flex-row items-center gap-6">
|
||||||
|
<div class="flex flex-row text-sm gap-2">
|
||||||
|
<span v-for="element of spell.elements" :class="elementTexts[element].class" class="border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px">{{ elementTexts[element].text }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row text-sm gap-1">
|
||||||
|
<span class="">Rang {{ spell.rank }}</span><span>/</span>
|
||||||
|
<span class="">{{ spellTypeTexts[spell.type] }}</span><span>/</span>
|
||||||
|
<span class="">{{ spell.cost }} mana</span><span>/</span>
|
||||||
|
<span class="">{{ typeof spell.speed === 'string' ? spell.speed : `${spell.speed} minutes` }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MarkdownRenderer :content="spell.effect" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="notes">
|
||||||
|
<div class="flex flex-1 flex-col ps-8 gap-4 py-8">
|
||||||
|
<MarkdownRenderer :content="character.notes" />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</TabsRoot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Erreur</Title>
|
||||||
|
</Head>
|
||||||
|
<div>Erreur de chargement</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
95
pages/character/index.client.vue
Normal file
95
pages/character/index.client.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
guestsGoesTo: '/user/login',
|
||||||
|
})
|
||||||
|
const { add } = useToast();
|
||||||
|
const { user } = useUserSession();
|
||||||
|
|
||||||
|
const { data: characters, error, status } = await useFetch(`/api/character`);
|
||||||
|
|
||||||
|
async function deleteCharacter(id: number)
|
||||||
|
{
|
||||||
|
status.value = "pending";
|
||||||
|
await useRequestFetch()(`/api/character/${id}`, { method: 'delete' });
|
||||||
|
status.value = "success";
|
||||||
|
add({ content: 'Personnage supprimé', type: 'info', duration: 25000, timer: true, });
|
||||||
|
characters.value = characters.value?.filter(e => e.id !== id);
|
||||||
|
}
|
||||||
|
async function duplicateCharacter(id: number)
|
||||||
|
{
|
||||||
|
status.value = "pending";
|
||||||
|
const newId = await useRequestFetch()(`/api/character/${id}/duplicate`, { method: 'post' });
|
||||||
|
status.value = "success";
|
||||||
|
add({ content: 'Personnage dupliqué', type: 'info', duration: 25000, timer: true, });
|
||||||
|
useRouter().push({ name: 'character-id', params: { id: newId } });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Mes personnages</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex align-center justify-center">
|
||||||
|
<NuxtLink v-if="user?.state === 1" :to="{ name: 'character-id-edit', params: { id: 'new' } }"><Button>Nouveau personnage</Button></NuxtLink>
|
||||||
|
<Tooltip v-else side="top" message="Veuillez valider votre email avant de pouvoir créer un personnage."><Button disabled>Nouveau personnage</Button></Tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-if="status === 'pending'" class="flex flex-1 justify-center align-center">
|
||||||
|
<Loading size="large" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="status === 'success'" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
|
||||||
|
<div class="border border-light-30 dark:border-dark-30 p-3 flex flex-row gap-4" v-for="character of characters">
|
||||||
|
<Avatar size="large" icon="radix-icons:person" src="" />
|
||||||
|
<div class="flex flex-1 flex-shrink flex-col truncate">
|
||||||
|
<NuxtLink class="text-xl font-bold hover:text-accent-blue truncate" :to="{ name: 'character-id', params: { id: character.id } }" :title="character.name">{{ character.name }}</NuxtLink>
|
||||||
|
<span class="text-sm truncate">Niveau {{ character.level }}</span>
|
||||||
|
</div>
|
||||||
|
<AlertDialogRoot>
|
||||||
|
<DropdownMenuRoot>
|
||||||
|
<DropdownMenuTrigger class="self-start">
|
||||||
|
<Button icon><Icon icon="radix-icons:dots-vertical" /></Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuContent align="end" side="bottom" class="z-50 outline-none bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade border border-light-35 dark:border-dark-35">
|
||||||
|
<DropdownMenuItem @select="useRouter().push({ name: 'character-id-edit', params: { id: character.id } })" class="cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-baseline py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
|
||||||
|
<Icon icon="radix-icons:pencil-1" class="absolute left-1.5" />
|
||||||
|
<span>Editer</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @select="duplicateCharacter(character.id)" class="cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
|
||||||
|
<Icon icon="radix-icons:clipboard-copy" class="absolute left-1.5" />
|
||||||
|
<span>Dupliquer</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<AlertDialogTrigger>
|
||||||
|
<DropdownMenuItem class="cursor-pointer text-base text-light-red dark:text-dark-red leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-red dark:data-[highlighted]:bg-dark-red data-[highlighted]:bg-opacity-30 dark:data-[highlighted]:bg-opacity-30">
|
||||||
|
<Icon icon="radix-icons:trash" class="absolute left-1.5" />
|
||||||
|
<span>Supprimer</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
|
||||||
|
<DropdownMenuArrow class="fill-light-35 dark:fill-dark-35" />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuRoot>
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||||
|
<AlertDialogContent
|
||||||
|
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||||
|
<AlertDialogTitle class="text-3xl font-light relative -top-2">Supprimer {{ character.name }} ?</AlertDialogTitle>
|
||||||
|
<div class="flex flex-1 justify-end gap-4">
|
||||||
|
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
|
||||||
|
<AlertDialogAction asChild><Button @click="() => deleteCharacter(character.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||||
|
</div>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
</AlertDialogRoot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span>Erreur de chargement</span>
|
||||||
|
<span>{{ error?.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
27
pages/character/list.client.vue
Normal file
27
pages/character/list.client.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { data: characters, error, status } = await useFetch(`/api/character`, { params: { visibility: "public" } });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Liste des personnages</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div v-if="status === 'pending'" class="flex flex-1 justify-center align-center">
|
||||||
|
<Loading size="large" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="status === 'success'" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
|
||||||
|
<div class="border border-light-30 dark:border-dark-30 p-3 flex flex-row gap-4" v-for="character of characters">
|
||||||
|
<Avatar size="large" icon="radix-icons:person" src="" />
|
||||||
|
<div class="flex flex-1 flex-shrink flex-col truncate">
|
||||||
|
<NuxtLink class="text-xl font-bold hover:text-accent-blue truncate" :to="{ name: 'character-id', params: { id: character.id } }" :title="character.name">{{ character.name }}</NuxtLink>
|
||||||
|
<span class="text-sm truncate">Niveau {{ character.progress.level }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span>Erreur de chargement</span>
|
||||||
|
<span>{{ error?.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
42
pages/character/manage.client.vue
Normal file
42
pages/character/manage.client.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import characterConfig from '#shared/character-config.json';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import type { CharacterConfig } from '~/types/character';
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const config = ref<CharacterConfig>(characterConfig);
|
||||||
|
|
||||||
|
function copy()
|
||||||
|
{
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(config.value));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Edition de données</Title>
|
||||||
|
</Head>
|
||||||
|
<TabsRoot class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full" default-value="training">
|
||||||
|
<TabsList class="flex flex-row gap-4 self-center relative px-4">
|
||||||
|
<TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator>
|
||||||
|
<TabsTrigger value="peoples" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Peuples</TabsTrigger>
|
||||||
|
<TabsTrigger value="training" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Entrainement</TabsTrigger>
|
||||||
|
<TabsTrigger value="abilities" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Compétences</TabsTrigger>
|
||||||
|
<TabsTrigger value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Sorts</TabsTrigger>
|
||||||
|
<Tooltip message="Copier le JSON" side="right"><Button icon @click="copy" class="p-2"><Icon icon="radix-icons:clipboard-copy" /></Button></Tooltip>
|
||||||
|
</TabsList>
|
||||||
|
<div class="flex-1 outline-none max-w-full w-full">
|
||||||
|
<TabsContent value="peoples">
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="training">
|
||||||
|
<TrainingConfigEditor :config="config" />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="abilities">
|
||||||
|
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="spells">
|
||||||
|
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</TabsRoot>
|
||||||
|
</template>
|
||||||
@@ -1,23 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-1 justify-start items-start" ref="element">
|
<div class="flex flex-1 justify-start items-start" v-if="overview">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - {{ overview?.title ?? "Erreur" }}</Title>
|
<Title>d[any] - {{ overview.title }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
|
<Markdown v-if="overview.type === 'markdown'" :path="path" />
|
||||||
|
<Canvas v-else-if="overview.type === 'canvas'" :path="path" />
|
||||||
|
<ProseH2 v-else class="flex-1 text-center">Impossible d'afficher le contenu demandé</ProseH2>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Erreur</Title>
|
||||||
|
</Head>
|
||||||
|
<div><ProseH2>Impossible d'afficher le contenu demandé</ProseH2></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Content } from '#shared/content.util';
|
|
||||||
import { unifySlug } from '#shared/general.util';
|
|
||||||
|
|
||||||
const element = useTemplateRef('element'), overview = ref();
|
|
||||||
const route = useRouter().currentRoute;
|
const route = useRouter().currentRoute;
|
||||||
const path = computed(() => unifySlug(route.value.params.path));
|
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||||
|
|
||||||
onMounted(async () => {
|
const { content } = useContent();
|
||||||
if(element.value && path.value && await Content.ready)
|
const overview = computed(() => content.value.find(e => e.path === path.value));
|
||||||
{
|
|
||||||
overview.value = Content.render(element.value, path.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
@@ -2,113 +2,228 @@
|
|||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Modification</Title>
|
<Title>d[any] - Modification</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-1 flex-col xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3 overflow-hidden">
|
<ClientOnly>
|
||||||
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
<CollapsibleRoot asChild class="flex flex-1 flex-col xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3 overflow-hidden" v-model="open">
|
||||||
<div class="flex items-center px-2 gap-4">
|
<div>
|
||||||
<!-- <CollapsibleTrigger asChild>
|
<div class="z-50 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||||
<Button icon class="!bg-transparent group md:hidden">
|
<div class="flex items-center px-2 gap-4">
|
||||||
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
|
<CollapsibleTrigger asChild>
|
||||||
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
|
<Button icon class="!bg-transparent group md:hidden">
|
||||||
</Button>
|
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
|
||||||
</CollapsibleTrigger> -->
|
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
|
||||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-opacity-70 m-2 flex items-center gap-4" aria-label="Accueil" :to="{ path: '/', force: true }">
|
</Button>
|
||||||
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
</CollapsibleTrigger>
|
||||||
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-opacity-70 m-2 flex items-center gap-4" aria-label="Accueil" :to="{ path: '/', force: true }">
|
||||||
<span class="text-xl max-md:hidden">d[any]</span>
|
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
||||||
</NuxtLink>
|
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
||||||
</div>
|
<span class="text-xl max-md:hidden">d[any]</span>
|
||||||
<div class="flex items-center px-2 gap-4">
|
</NuxtLink>
|
||||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
</div>
|
||||||
</div>
|
<div class="flex items-center px-2 gap-4">
|
||||||
</div>
|
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
||||||
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
|
</div>
|
||||||
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
|
</div>
|
||||||
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="tree"></div>
|
<div class="flex flex-1 flex-row relative overflow-hidden">
|
||||||
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60">
|
<CollapsibleContent asChild forceMount>
|
||||||
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
|
||||||
<p>Copyright Peaceultime - 2025</p>
|
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden">
|
||||||
|
<div class="flex flex-row justify-between items-center pt-2 pb-4 mb-2 px-2 gap-4 border-b border-light-35 dark:border-dark-35">
|
||||||
|
<Button @click="router.push({ name: 'explore-path', params: { path: selected ? getPath(selected) : 'index' } })">Quitter</Button>
|
||||||
|
<Button @click="save(true);">Enregistrer</Button>
|
||||||
|
<Tooltip side="top" message="Nouveau">
|
||||||
|
<DropdownMenu align="end" side="bottom" :options="[{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Markdown',
|
||||||
|
kbd: 'Ctrl+N',
|
||||||
|
icon: 'radix-icons:file-text',
|
||||||
|
select: () => add('markdown'),
|
||||||
|
}, {
|
||||||
|
type: 'item',
|
||||||
|
label: 'Dossier',
|
||||||
|
kbd: 'Ctrl+Shift+N',
|
||||||
|
icon: 'lucide:folder',
|
||||||
|
select: () => add('folder'),
|
||||||
|
}, {
|
||||||
|
type: 'item',
|
||||||
|
label: 'Canvas',
|
||||||
|
icon: 'ph:graph-light',
|
||||||
|
select: () => add('canvas'),
|
||||||
|
}, {
|
||||||
|
type: 'item',
|
||||||
|
label: 'Carte',
|
||||||
|
icon: 'lucide:map',
|
||||||
|
select: () => add('map'),
|
||||||
|
}, {
|
||||||
|
type: 'item',
|
||||||
|
label: 'Fichier',
|
||||||
|
icon: 'radix-icons:file',
|
||||||
|
select: () => add('file'),
|
||||||
|
}]">
|
||||||
|
<Button icon><Icon class="w-5 h-5" icon="radix-icons:plus" /></Button>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<DraggableTree class="ps-4 text-sm" :items="navigation ?? undefined" :get-key="(item: Partial<TreeItemEditable>) => item.path !== undefined ? getPath(item as TreeItemEditable) : ''" @updateTree="drop"
|
||||||
|
v-model="selected" :defaultExpanded="defaultExpanded" :get-children="(item: Partial<TreeItemEditable>) => item.type === 'folder' ? item.children : undefined" >
|
||||||
|
<template #default="{ handleToggle, handleSelect, isExpanded, isDragging, item }">
|
||||||
|
<div class="flex flex-1 items-center overflow-hidden" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level / 2 - 0.5}em` }">
|
||||||
|
<div class="flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple group-data-[selected]:text-accent-blue">
|
||||||
|
<Icon @click="handleToggle" 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 / 2 - 1.5}em` }" />
|
||||||
|
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="w-5 h-5" @click="handleSelect" />
|
||||||
|
<div class="pl-1.5 py-1.5 flex-1 truncate" :title="item.value.title" @click="handleSelect" :class="{ 'font-semibold': item.hasChildren }">
|
||||||
|
{{ item.value.title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span @click="item.value.private = !item.value.private">
|
||||||
|
<Icon v-if="item.value.private" icon="radix-icons:lock-closed" />
|
||||||
|
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:lock-open-2" />
|
||||||
|
</span>
|
||||||
|
<span @click="item.value.navigable = !item.value.navigable">
|
||||||
|
<Icon v-if="item.value.navigable" icon="radix-icons:eye-open" />
|
||||||
|
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:eye-none" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #hint="{ instruction }">
|
||||||
|
<div v-if="instruction" class="absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50" :style="{
|
||||||
|
width: `calc(100% - ${instruction.currentLevel / 2 - 1.5}em)`
|
||||||
|
}" :class="{
|
||||||
|
'!border-b-4': instruction?.type === 'reorder-below',
|
||||||
|
'!border-t-4': instruction?.type === 'reorder-above',
|
||||||
|
'!border-4': instruction?.type === 'make-child',
|
||||||
|
}"></div>
|
||||||
|
</template>
|
||||||
|
</DraggableTree>
|
||||||
|
</div>
|
||||||
|
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60">
|
||||||
|
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||||
|
<p>Copyright Peaceultime - 2025</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
<div class="flex flex-1 flex-row max-h-full overflow-hidden">
|
||||||
|
<div v-if="selected" class="flex flex-1 flex-col items-start justify-start max-h-full relative">
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Modification de {{ selected.title }}</Title>
|
||||||
|
</Head>
|
||||||
|
<CollapsibleRoot v-model:open="topOpen" class="group data-[state=open]:mt-4 w-full relative">
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Button class="absolute left-1/2 -translate-x-1/2 group-data-[state=open]:-bottom-3 group-data-[state=closed]:-bottom-6 z-30" icon>
|
||||||
|
<Icon v-if="topOpen" icon="radix-icons:caret-up" class="h-4 w-4" />
|
||||||
|
<Icon v-else icon="radix-icons:caret-down" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent class="xl:px-12 lg:px-8 px-6">
|
||||||
|
<div class="pb-2 grid lg:grid-cols-2 grid-cols-1 lg:items-center justify-between gap-x-4 flex-1 border-b border-light-35 dark:border-dark-35">
|
||||||
|
<input type="text" v-model="selected.title" @input="() => {
|
||||||
|
if(selected && !selected.customPath)
|
||||||
|
{
|
||||||
|
selected.name = parsePath(selected.title);
|
||||||
|
rebuildPath(selected.children, getPath(selected));
|
||||||
|
}
|
||||||
|
}" placeholder="Titre" style="line-height: normal;" class="flex-1 md:text-5xl text-4xl md:h-14 h-12 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 pb-3 font-thin bg-transparent"/>
|
||||||
|
<div class="flex flex-row justify-between items-center gap-x-4">
|
||||||
|
<div v-if="selected.customPath" class="flex lg:items-center truncate">
|
||||||
|
<pre class="md:text-base text-sm truncate" style="direction: rtl">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}</pre>
|
||||||
|
<TextInput v-model="selected.name" @input="(e: Event) => {
|
||||||
|
if(selected && selected.customPath)
|
||||||
|
{
|
||||||
|
selected.name = parsePath(selected.name);
|
||||||
|
rebuildPath(selected.children, getPath(selected));
|
||||||
|
}
|
||||||
|
}" class="mx-0 font-mono"/>
|
||||||
|
</div>
|
||||||
|
<pre v-else class="md:text-base text-sm truncate" style="direction: rtl">{{ getPath(selected) }}/</pre>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<Dialog :title="`Supprimer '${selected.title}'${selected.children?.length ?? 0 > 0 ? ' et ses enfants' : ''}`">
|
||||||
|
<template #trigger><Button icon class="bg-light-red dark:bg-dark-red !bg-opacity-40 border-light-red dark:border-dark-red hover:bg-light-red dark:hover:bg-dark-red hover:!bg-opacity-70 hover:border-light-red dark:hover:border-dark-red"><Icon icon="radix-icons:trash" /></Button></template>
|
||||||
|
<template #default>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<DialogClose><Button @click="navigation = tree.remove(navigation, getPath(selected)); selected = undefined;" class="bg-light-red dark:bg-dark-red !bg-opacity-40 border-light-red dark:border-dark-red hover:bg-light-red dark:hover:bg-dark-red hover:!bg-opacity-70 hover:border-light-red dark:hover:border-dark-red">Oui</Button></DialogClose>
|
||||||
|
<DialogClose><Button>Non</Button></DialogClose>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
<Dialog title="Préférences Markdown" v-if="selected.type === 'markdown'">
|
||||||
|
<template #trigger><Button icon><Icon icon="radix-icons:gear" /></Button></template>
|
||||||
|
<template #default>
|
||||||
|
<Select label="Editeur de markdown" :modelValue="preferences.markdown.editing" @update:model-value="v => preferences.markdown.editing = (v as 'reading' | 'editing' | 'split')">
|
||||||
|
<SelectItem label="Mode lecture" value="reading" />
|
||||||
|
<SelectItem label="Mode edition" value="editing" />
|
||||||
|
<SelectItem label="Ecran partagé" value="split" />
|
||||||
|
</Select>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
<DropdownMenu align="end" :options="[{
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'URL custom',
|
||||||
|
select: (state: boolean) => { selected!.customPath = state; if(!state) selected!.name = parsePath(selected!.title) },
|
||||||
|
checked: selected.customPath
|
||||||
|
}]">
|
||||||
|
<Button icon><Icon icon="radix-icons:dots-vertical"/></Button>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</CollapsibleRoot>
|
||||||
|
<div class="py-4 flex-1 w-full max-h-full flex overflow-hidden xl:px-12 lg:px-8 px-6 relative">
|
||||||
|
<template v-if="selected.type === 'markdown'">
|
||||||
|
<div v-if="contentStatus === 'pending'" class="flex flex-1 justify-center items-center">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
<span v-else-if="contentError">{{ contentError }}</span>
|
||||||
|
<template v-else-if="preferences.markdown.editing === 'editing'">
|
||||||
|
<Editor v-model="selected.content" autofocus class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto lg:mx-16 xl:mx-32 2xl:mx-64" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="preferences.markdown.editing === 'reading'">
|
||||||
|
<div class="flex-1 max-h-full !overflow-y-auto px-4 xl:px-32 2xl:px-64"><MarkdownRenderer :content="(debounced as string)" :proses="{ 'a': FakeA }" /></div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="preferences.markdown.editing === 'split'">
|
||||||
|
<SplitterGroup direction="horizontal" class="flex-1 w-full flex">
|
||||||
|
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }" :defaultSize="50">
|
||||||
|
<Editor v-model="selected.content" autofocus class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto" :class="{ 'hidden': isCollapsed }" />
|
||||||
|
</SplitterPanel>
|
||||||
|
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
||||||
|
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }">
|
||||||
|
<div class="flex-1 max-h-full !overflow-y-auto px-8" :class="{ 'hidden': isCollapsed }"><MarkdownRenderer :content="(debounced as string)" :proses="{ 'a': FakeA }" /></div>
|
||||||
|
</SplitterPanel>
|
||||||
|
</SplitterGroup>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="selected.type === 'canvas'">
|
||||||
|
<CanvasEditor v-if="selected.content" :modelValue="selected.content" :path="getPath(selected)" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="selected.type === 'map'">
|
||||||
|
<span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de carte en cours de développement</ProseH3></span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="selected.type === 'file'">
|
||||||
|
<span>Modifier le contenu :</span><input type="file" @change="(e: Event) => console.log((e.target as HTMLInputElement).files?.length)" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-row max-h-full overflow-hidden" ref="container"></div>
|
</CollapsibleRoot>
|
||||||
</div>
|
</ClientOnly>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Content, Editor } from '#shared/content.util';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import { button, loading } from '#shared/proses';
|
|
||||||
import { dom, icon, text } from '~/shared/dom.util';
|
|
||||||
import { modal, popper } from '~/shared/floating.util';
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
rights: ['admin', 'editor'],
|
|
||||||
layout: 'null',
|
|
||||||
});
|
|
||||||
|
|
||||||
const toaster = useToast();
|
|
||||||
const { user } = useUserSession();
|
|
||||||
const tree = useTemplateRef('tree'), container = useTemplateRef('container');
|
|
||||||
let editor: Editor;
|
|
||||||
|
|
||||||
function pull()
|
|
||||||
{
|
|
||||||
Content.pull().then(e => {
|
|
||||||
toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
|
|
||||||
}).catch(e => {
|
|
||||||
toaster.add({ type: 'success', content: 'Une erreur est survenue durant la récupération des données.', timer: true, duration: 7500 });
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function push()
|
|
||||||
{
|
|
||||||
const { close } = modal([dom('div', { class: 'flex flex-col gap-4 justify-center items-center' }, [ dom('div', { class: 'text-xl', text: 'Mise à jour des données' }), loading('large') ])], { priority: false, closeWhenOutside: true, });
|
|
||||||
Content.push().then(e => {
|
|
||||||
close();
|
|
||||||
toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
|
|
||||||
}).catch(e => {
|
|
||||||
close();
|
|
||||||
toaster.add({ type: 'success', content: 'Une erreur est survenue durant l\'enregistrement des données.', timer: true, duration: 7500 });
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if(tree.value && container.value && await Content.ready)
|
|
||||||
{
|
|
||||||
const load = loading('normal');
|
|
||||||
tree.value.appendChild(load);
|
|
||||||
|
|
||||||
const content = dom('div', { class: 'flex flex-row justify-start items-center gap-4 p-2' }, [
|
|
||||||
popper(button(icon('ph:cloud-arrow-down', { height: 20, width: 20 }), pull, 'p-1'), { placement: 'top', offset: 4, delay: 120, arrow: true, content: [text('Actualiser')], 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' }),
|
|
||||||
popper(button(icon('ph:cloud-arrow-up', { height: 20, width: 20 }), push, 'p-1'), { placement: 'top', offset: 4, delay: 120, arrow: true, content: [text('Enregistrer')], 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' }),
|
|
||||||
])
|
|
||||||
|
|
||||||
tree.value.insertBefore(content, load);
|
|
||||||
|
|
||||||
editor = new Editor();
|
|
||||||
|
|
||||||
tree.value.replaceChild(editor.tree.container, load);
|
|
||||||
container.value.appendChild(editor.container);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
editor?.unmount();
|
|
||||||
})
|
|
||||||
/* import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
||||||
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
||||||
import type { FileType, LocalContent, TreeItem } from '#shared/content.util';
|
import { iconByType, convertContentFromText, convertContentToText, DEFAULT_CONTENT,parsePath } from '#shared/general.util';
|
||||||
import { DEFAULT_CONTENT, iconByType, Content, getPath } from '#shared/content.util';
|
import type { ExploreContent, FileType, TreeItem } from '~/types/content';
|
||||||
|
import FakeA from '~/components/prose/FakeA.vue';
|
||||||
import type { Preferences } from '~/types/general';
|
import type { Preferences } from '~/types/general';
|
||||||
import { fakeA as proseA } from '#shared/proses';
|
|
||||||
import { parsePath } from '~/shared/general.util';
|
|
||||||
import type { CanvasContent } from '~/types/canvas';
|
|
||||||
|
|
||||||
export type TreeItemEditable = LocalContent &
|
export type TreeItemEditable = TreeItem &
|
||||||
{
|
{
|
||||||
parent?: string;
|
parent: string;
|
||||||
name?: string;
|
name: string;
|
||||||
customPath?: boolean;
|
customPath: boolean;
|
||||||
children?: TreeItemEditable[];
|
children?: TreeItemEditable[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,73 +240,225 @@ const open = ref(true), topOpen = ref(true);
|
|||||||
const toaster = useToast();
|
const toaster = useToast();
|
||||||
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
let navigation = Content.tree as TreeItemEditable[];
|
const { content: complete, tree: project } = useContent();
|
||||||
const selected = ref<TreeItemEditable>();
|
const navigation = ref<TreeItemEditable[]>(transform(JSON.parse(JSON.stringify(project.value)))!);
|
||||||
|
const selected = ref<TreeItemEditable>(), edited = ref(false);
|
||||||
|
const contentStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), contentError = ref<string>();
|
||||||
|
|
||||||
const preferences = useCookie<Preferences>('preferences', { default: () => ({ markdown: { editing: 'split' }, canvas: { gridSnap: true, neighborSnap: true, spacing: 32 } }), watch: true, maxAge: 60*60*24*31 });
|
const preferences = useCookie<Preferences>('preferences', { default: () => ({ markdown: { editing: 'split' }, canvas: { snap: true, size: 32 } }), watch: true, maxAge: 60*60*24*31 });
|
||||||
|
|
||||||
watch(selected, async (value, old) => {
|
watch(selected, async (value, old) => {
|
||||||
if(selected.value)
|
if(selected.value)
|
||||||
{
|
{
|
||||||
if(!selected.value.content && selected.value.path)
|
if(!selected.value.content && selected.value.path)
|
||||||
{
|
{
|
||||||
selected.value = await Content.content(selected.value.path);
|
contentStatus.value = 'pending';
|
||||||
}
|
try
|
||||||
|
{
|
||||||
|
const storedEdit = sessionStorage.getItem(`editing:${encodeURIComponent(selected.value.path)}`);
|
||||||
|
|
||||||
router.replace({ hash: '#' + encodeURIComponent(selected.value!.path || getPath(selected.value!)) });
|
if(storedEdit)
|
||||||
|
{
|
||||||
|
selected.value.content = convertContentFromText(selected.value.type, storedEdit);
|
||||||
|
contentStatus.value = 'success';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selected.value.content = (await $fetch(`/api/file/content/${encodeURIComponent(selected.value.path)}`, { query: { type: 'editing'} }));
|
||||||
|
contentStatus.value = 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
debounced.value = selected.value.content ?? '';
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
contentError.value = (e as Error).message;
|
||||||
|
contentStatus.value = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//@ts-ignore
|
||||||
|
debounced.value = selected.value.content ?? '';
|
||||||
|
}
|
||||||
|
router.replace({ hash: '#' + selected.value.path || getPath(selected.value) });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
router.replace({ hash: '' });
|
router.replace({ hash: '' });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const debouncedSave = useDebounceFn(save, 60000, { maxWait: 180000 });
|
const content = computed(() => selected.value?.content ?? '');
|
||||||
|
const debounced = useDebounce(content, 250, { maxWait: 500 });
|
||||||
|
|
||||||
|
watch(debounced, () => {
|
||||||
|
if(selected.value && debounced.value)
|
||||||
|
sessionStorage.setItem(`editing:${encodeURIComponent(selected.value.path)}`, typeof debounced.value === 'string' ? debounced.value : JSON.stringify(debounced.value));
|
||||||
|
});
|
||||||
useShortcuts({
|
useShortcuts({
|
||||||
//meta_s: { usingInput: true, handler: () => save(), prevent: true },
|
meta_s: { usingInput: true, handler: () => save(false), prevent: true },
|
||||||
meta_n: { usingInput: true, handler: () => add('markdown'), prevent: true },
|
meta_n: { usingInput: true, handler: () => add('markdown'), prevent: true },
|
||||||
meta_shift_n: { usingInput: true, handler: () => add('folder'), prevent: true },
|
meta_shift_n: { usingInput: true, handler: () => add('folder'), prevent: true },
|
||||||
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore-path', params: { path: 'index' } }), prevent: true }
|
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore-path', params: { path: 'index' } }), prevent: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tree = {
|
||||||
|
remove(data: TreeItemEditable[], id: string): TreeItemEditable[] {
|
||||||
|
return data
|
||||||
|
.filter(item => getPath(item) !== id)
|
||||||
|
.map((item) => {
|
||||||
|
if (tree.hasChildren(item)) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
children: tree.remove(item.children ?? [], id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
insertBefore(data: TreeItemEditable[], targetId: string, newItem: TreeItemEditable): TreeItemEditable[] {
|
||||||
|
return data.flatMap((item) => {
|
||||||
|
if (getPath(item) === targetId)
|
||||||
|
return [newItem, item];
|
||||||
|
|
||||||
|
if (tree.hasChildren(item)) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
children: tree.insertBefore(item.children ?? [], targetId, newItem),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
insertAfter(data: TreeItemEditable[], targetId: string, newItem: TreeItemEditable): TreeItemEditable[] {
|
||||||
|
return data.flatMap((item) => {
|
||||||
|
if (getPath(item) === targetId)
|
||||||
|
return [item, newItem];
|
||||||
|
|
||||||
|
if (tree.hasChildren(item)) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
children: tree.insertAfter(item.children ?? [], targetId, newItem),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
insertChild(data: TreeItemEditable[], targetId: string, newItem: TreeItemEditable): TreeItemEditable[] {
|
||||||
|
return data.flatMap((item) => {
|
||||||
|
if (getPath(item) === targetId) {
|
||||||
|
// already a parent: add as first child
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
// opening item so you can see where item landed
|
||||||
|
isOpen: true,
|
||||||
|
children: [newItem, ...item.children ?? []],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tree.hasChildren(item))
|
||||||
|
return item;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
children: tree.insertChild(item.children ?? [], targetId, newItem),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
find(data: TreeItemEditable[], itemId: string): TreeItemEditable | undefined {
|
||||||
|
for (const item of data) {
|
||||||
|
if (getPath(item) === itemId)
|
||||||
|
return item;
|
||||||
|
|
||||||
|
if (tree.hasChildren(item)) {
|
||||||
|
const result = tree.find(item.children ?? [], itemId);
|
||||||
|
if (result)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
search(data: TreeItemEditable[], prop: keyof TreeItemEditable, value: string): TreeItemEditable[] {
|
||||||
|
const arr = [];
|
||||||
|
|
||||||
|
for (const item of data)
|
||||||
|
{
|
||||||
|
if (item[prop]?.toString().toLowerCase()?.startsWith(value.toLowerCase()))
|
||||||
|
arr.push(item);
|
||||||
|
|
||||||
|
if (tree.hasChildren(item)) {
|
||||||
|
arr.push(...tree.search(item.children ?? [], prop, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
},
|
||||||
|
getPathToItem({
|
||||||
|
current,
|
||||||
|
targetId,
|
||||||
|
parentIds = [],
|
||||||
|
}: {
|
||||||
|
current: TreeItemEditable[]
|
||||||
|
targetId: string
|
||||||
|
parentIds?: string[]
|
||||||
|
}): string[] | undefined {
|
||||||
|
for (const item of current) {
|
||||||
|
if (getPath(item) === targetId)
|
||||||
|
return parentIds;
|
||||||
|
|
||||||
|
const nested = tree.getPathToItem({
|
||||||
|
current: (item.children ?? []),
|
||||||
|
targetId,
|
||||||
|
parentIds: [...parentIds, getPath(item)],
|
||||||
|
});
|
||||||
|
if (nested)
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasChildren(item: TreeItemEditable): boolean {
|
||||||
|
return (item.children ?? []).length > 0;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
function add(type: FileType): void
|
function add(type: FileType): void
|
||||||
{
|
{
|
||||||
if(!navigation)
|
if(!navigation.value)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const news = [...tree.search(navigation, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i);
|
const news = [...tree.search(navigation.value, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i);
|
||||||
const title = `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`;
|
const title = `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`;
|
||||||
const item: TreeItemEditable = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: [], customPath: false, content: DEFAULT_CONTENT[type], owner: -1, timestamp: new Date(), visit: 0 };
|
const item: TreeItemEditable = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: [], customPath: false, content: DEFAULT_CONTENT[type], owner: -1, timestamp: new Date(), visit: 0 };
|
||||||
|
|
||||||
if(!selected.value)
|
if(!selected.value)
|
||||||
{
|
{
|
||||||
navigation = [...navigation, item];
|
navigation.value = [...navigation.value, item];
|
||||||
}
|
}
|
||||||
else if(selected.value?.children)
|
else if(selected.value?.children)
|
||||||
{
|
{
|
||||||
item.parent = getPath(selected.value);
|
item.parent = getPath(selected.value);
|
||||||
navigation = tree.insertChild(navigation, item.parent, item);
|
navigation.value = tree.insertChild(navigation.value, item.parent, item);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
navigation = tree.insertAfter(navigation, getPath(selected.value), item);
|
navigation.value = tree.insertAfter(navigation.value, getPath(selected.value), item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function updateTree(instruction: Instruction, itemId: string, targetId: string) : TreeItemEditable[] | undefined {
|
function updateTree(instruction: Instruction, itemId: string, targetId: string) : TreeItemEditable[] | undefined {
|
||||||
if(!navigation)
|
if(!navigation.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const item = tree.find(navigation, itemId);
|
const item = tree.find(navigation.value, itemId);
|
||||||
const target = tree.find(navigation, targetId);
|
const target = tree.find(navigation.value, targetId);
|
||||||
|
|
||||||
if(!item)
|
if(!item)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (instruction.type === 'reparent') {
|
if (instruction.type === 'reparent') {
|
||||||
const path = tree.getPathToItem({
|
const path = tree.getPathToItem({
|
||||||
current: navigation,
|
current: navigation.value,
|
||||||
targetId: targetId,
|
targetId: targetId,
|
||||||
});
|
});
|
||||||
if (!path) {
|
if (!path) {
|
||||||
@@ -200,23 +467,23 @@ function updateTree(instruction: Instruction, itemId: string, targetId: string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const desiredId = path[instruction.desiredLevel];
|
const desiredId = path[instruction.desiredLevel];
|
||||||
let result = tree.remove(navigation, itemId);
|
let result = tree.remove(navigation.value, itemId);
|
||||||
result = tree.insertAfter(result, desiredId, item);
|
result = tree.insertAfter(result, desiredId, item);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// the rest of the actions require you to drop on something else
|
// the rest of the actions require you to drop on something else
|
||||||
if (itemId === targetId)
|
if (itemId === targetId)
|
||||||
return navigation;
|
return navigation.value;
|
||||||
|
|
||||||
if (instruction.type === 'reorder-above') {
|
if (instruction.type === 'reorder-above') {
|
||||||
let result = tree.remove(navigation, itemId);
|
let result = tree.remove(navigation.value, itemId);
|
||||||
result = tree.insertBefore(result, targetId, item);
|
result = tree.insertBefore(result, targetId, item);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instruction.type === 'reorder-below') {
|
if (instruction.type === 'reorder-below') {
|
||||||
let result = tree.remove(navigation, itemId);
|
let result = tree.remove(navigation.value, itemId);
|
||||||
result = tree.insertAfter(result, targetId, item);
|
result = tree.insertAfter(result, targetId, item);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -225,13 +492,13 @@ function updateTree(instruction: Instruction, itemId: string, targetId: string)
|
|||||||
if(!target || target.type !== 'folder')
|
if(!target || target.type !== 'folder')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let result = tree.remove(navigation, itemId);
|
let result = tree.remove(navigation.value, itemId);
|
||||||
result = tree.insertChild(result, targetId, item);
|
result = tree.insertChild(result, targetId, item);
|
||||||
rebuildPath([item], targetId);
|
rebuildPath([item], targetId);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return navigation;
|
return navigation.value;
|
||||||
}
|
}
|
||||||
function transform(items: TreeItem[] | undefined): TreeItemEditable[] | undefined
|
function transform(items: TreeItem[] | undefined): TreeItemEditable[] | undefined
|
||||||
{
|
{
|
||||||
@@ -249,7 +516,7 @@ function flatten(items: TreeItemEditable[] | undefined): TreeItemEditable[]
|
|||||||
}
|
}
|
||||||
function drop(instruction: Instruction, itemId: string, targetId: string)
|
function drop(instruction: Instruction, itemId: string, targetId: string)
|
||||||
{
|
{
|
||||||
navigation = updateTree(instruction, itemId, targetId) ?? navigation ?? [];
|
navigation.value = updateTree(instruction, itemId, targetId) ?? navigation.value ?? [];
|
||||||
}
|
}
|
||||||
function rebuildPath(tree: TreeItemEditable[] | null | undefined, parentPath: string)
|
function rebuildPath(tree: TreeItemEditable[] | null | undefined, parentPath: string)
|
||||||
{
|
{
|
||||||
@@ -261,14 +528,38 @@ function rebuildPath(tree: TreeItemEditable[] | null | undefined, parentPath: st
|
|||||||
rebuildPath(e.children, getPath(e));
|
rebuildPath(e.children, getPath(e));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function save()
|
async function save(redirect: boolean): Promise<void>
|
||||||
{
|
{
|
||||||
if(selected.value && selected.value.content)
|
//@ts-ignore
|
||||||
{
|
const map = (e: TreeItemEditable[]): TreeItemEditable[] => e.map(f => ({ ...f, content: f.content ? convertContentToText(f.type, f.content) : undefined, children: f.children ? map(f.children) : undefined }));
|
||||||
selected.value.path = getPath(selected.value);
|
saveStatus.value = 'pending';
|
||||||
Content.save(selected.value);
|
try {
|
||||||
|
const result = await $fetch(`/api/project`, {
|
||||||
|
method: 'post',
|
||||||
|
body: map(navigation.value),
|
||||||
|
});
|
||||||
|
saveStatus.value = 'success';
|
||||||
|
edited.value = false;
|
||||||
|
sessionStorage.clear();
|
||||||
|
|
||||||
|
toaster.clear('error');
|
||||||
|
toaster.add({ type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000 });
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
complete.value = result as ExploreContent[];
|
||||||
|
if(redirect) router.go(-1);
|
||||||
|
} catch(e: any) {
|
||||||
|
toaster.add({
|
||||||
|
type: 'error', content: e.message, timer: true, duration: 10000
|
||||||
|
})
|
||||||
|
console.error(e);
|
||||||
|
saveStatus.value = 'error';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function getPath(item: TreeItemEditable): string
|
||||||
|
{
|
||||||
|
return [item.parent, parsePath(item.customPath ? item.name : item.title)].filter(e => !!e).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
const defaultExpanded = computed(() => {
|
const defaultExpanded = computed(() => {
|
||||||
if(router.currentRoute.value.hash)
|
if(router.currentRoute.value.hash)
|
||||||
@@ -277,11 +568,11 @@ const defaultExpanded = computed(() => {
|
|||||||
split.forEach((e, i) => { if(i !== 0) split[i] = split[i - 1] + '/' + e });
|
split.forEach((e, i) => { if(i !== 0) split[i] = split[i - 1] + '/' + e });
|
||||||
return split;
|
return split;
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
watch(router.currentRoute, (value) => {
|
/*watch(router.currentRoute, (value) => {
|
||||||
if(value && value.hash && navigation)
|
if(value && value.hash && navigation.value)
|
||||||
selected.value = tree.find(navigation, decodeURIComponent(value.hash.substring(1)));
|
selected.value = tree.find(navigation.value, value.hash.substring(1));
|
||||||
else
|
else
|
||||||
selected.value = undefined;
|
selected.value = undefined;
|
||||||
}, { immediate: true }); */
|
}, { immediate: true });*/
|
||||||
</script>
|
</script>
|
||||||
16
schemas/file.ts
Normal file
16
schemas/file.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const fileType = z.enum(['folder', 'file', 'markdown', 'canvas', 'map']);
|
||||||
|
export const schema = z.object({
|
||||||
|
path: z.string(),
|
||||||
|
owner: z.number().finite(),
|
||||||
|
title: z.string(),
|
||||||
|
type: fileType,
|
||||||
|
content: z.string(),
|
||||||
|
navigable: z.boolean(),
|
||||||
|
private: z.boolean(),
|
||||||
|
order: z.number().finite(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FileType = z.infer<typeof fileType>;
|
||||||
|
export type File = z.infer<typeof schema>;
|
||||||
16
schemas/navigation.ts
Normal file
16
schemas/navigation.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { fileType } from "./file";
|
||||||
|
|
||||||
|
export const single = z.object({
|
||||||
|
path: z.string(),
|
||||||
|
owner: z.number().finite(),
|
||||||
|
title: z.string(),
|
||||||
|
type: fileType,
|
||||||
|
navigable: z.boolean(),
|
||||||
|
private: z.boolean(),
|
||||||
|
order: z.number().finite(),
|
||||||
|
});
|
||||||
|
export const table = z.array(single);
|
||||||
|
|
||||||
|
export type Navigation = z.infer<typeof table>;
|
||||||
|
export type NavigationItem = z.infer<typeof single>;
|
||||||
@@ -1,16 +1,22 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { projectFilesTable } from "~/db/schema";
|
import { fileType } from "./file";
|
||||||
|
|
||||||
export const Project = z.array(z.object({
|
const baseItem = z.object({
|
||||||
id: z.string(),
|
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
|
parent: z.string(),
|
||||||
|
name: z.string(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
type: z.enum(projectFilesTable.type.enumValues),
|
type: fileType,
|
||||||
navigable: z.boolean(),
|
navigable: z.boolean(),
|
||||||
private: z.boolean(),
|
private: z.boolean(),
|
||||||
order: z.number().finite(),
|
order: z.number().finite(),
|
||||||
timestamp: z.string(),
|
content: z.string().optional().or(z.null()),
|
||||||
}));
|
});
|
||||||
|
export const item: z.ZodType<ProjectItem> = baseItem.extend({
|
||||||
|
children: z.lazy(() => item.array().optional()),
|
||||||
|
});
|
||||||
|
export const project = z.array(item);
|
||||||
|
|
||||||
export type ProjectType = z.infer<typeof Project>;
|
export type ProjectItem = z.infer<typeof baseItem> & {
|
||||||
export type ProjectItemType = ProjectType[number];
|
children?: ProjectItem[]
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { SitemapUrlInput } from '#sitemap/types'
|
import type { SitemapUrlInput } from '#sitemap/types'
|
||||||
import { projectFilesTable as files } from '~/db/schema';
|
import { explorerContentTable } from '~/db/schema';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
|
||||||
export default defineSitemapEventHandler(() => {
|
export default defineSitemapEventHandler(() => {
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
const pages = db.select({ path: files.path, lastMod: files.timestamp, navigable: files.navigable, private: files.private, type: files.type }).from(files).all();
|
const pages = db.select({ path: explorerContentTable.path, lastMod: explorerContentTable.timestamp, navigable: explorerContentTable.navigable, private: explorerContentTable.private, type: explorerContentTable.type }).from(explorerContentTable).all();
|
||||||
|
|
||||||
return pages.filter(e => e.type !== 'folder' && e.navigable && !e.private && e.path.split('/').map((_, i, a) => a.slice(0, i).join('/')).every(p => !pages.find(_p => _p.path === p)?.private)).map(e => ({
|
return pages.filter(e => e.type !== 'folder' && e.navigable && !e.private && e.path.split('/').map((_, i, a) => a.slice(0, i).join('/')).every(p => !pages.find(_p => _p.path === p)?.private)).map(e => ({
|
||||||
loc: `/explore/${encodeURIComponent(e.path)}`,
|
loc: `/explore/${encodeURIComponent(e.path)}`,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { ne, sql } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { projectFilesTable } from '~/db/schema';
|
import { explorerContentTable } from '~/db/schema';
|
||||||
import { hasPermissions } from '#shared/auth.util';
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const session = await getUserSession(e);
|
const session = await getUserSession(e);
|
||||||
@@ -15,15 +16,17 @@ export default defineEventHandler(async (e) => {
|
|||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
const content = db.select({
|
const content = db.select({
|
||||||
path: projectFilesTable.path,
|
path: explorerContentTable.path,
|
||||||
owner: projectFilesTable.owner,
|
owner: explorerContentTable.owner,
|
||||||
title: projectFilesTable.title,
|
title: explorerContentTable.title,
|
||||||
type: projectFilesTable.type,
|
type: explorerContentTable.type,
|
||||||
navigable: projectFilesTable.navigable,
|
size: sql<number>`CASE WHEN ${explorerContentTable.content} IS NULL THEN 0 ELSE length(${explorerContentTable.content}) END`.as('size'),
|
||||||
private: projectFilesTable.private,
|
navigable: explorerContentTable.navigable,
|
||||||
order: projectFilesTable.order,
|
private: explorerContentTable.private,
|
||||||
timestamp: projectFilesTable.timestamp,
|
order: explorerContentTable.order,
|
||||||
}).from(projectFilesTable).all();
|
visit: explorerContentTable.visit,
|
||||||
|
timestamp: explorerContentTable.timestamp,
|
||||||
|
}).from(explorerContentTable).all();
|
||||||
|
|
||||||
content.sort((a, b) => {
|
content.sort((a, b) => {
|
||||||
return a.path.split('/').length - b.path.split('/').length;
|
return a.path.split('/').length - b.path.split('/').length;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { sql } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { userSessionsTable } from '~/db/schema';
|
||||||
import { hasPermissions } from '~/shared/auth.util';
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
|||||||
}
|
}
|
||||||
}) as UserSessionRequired);
|
}) as UserSessionRequired);
|
||||||
|
|
||||||
|
db.update(usersDataTable).set({ logCount: user.data.logCount + 1 }).where(eq(usersDataTable.id, user.id)).run();
|
||||||
|
|
||||||
setResponseStatus(e, 201);
|
setResponseStatus(e, 201);
|
||||||
return { success: true, session: data };
|
return { success: true, session: data };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { usersDataTable, usersTable } from '~/db/schema';
|
|||||||
import { schema } from '~/schemas/registration';
|
import { schema } from '~/schemas/registration';
|
||||||
import { checkSession, logSession } from '~/server/utils/user';
|
import { checkSession, logSession } from '~/server/utils/user';
|
||||||
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||||
|
import sendMail from '~/server/tasks/mail';
|
||||||
|
|
||||||
interface SuccessHandler
|
interface SuccessHandler
|
||||||
{
|
{
|
||||||
@@ -71,7 +72,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
|||||||
|
|
||||||
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
||||||
|
|
||||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date() } }) as UserSessionRequired);
|
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date(), logCount: 1 } }) as UserSessionRequired);
|
||||||
|
|
||||||
const emailId = Bun.hash('register' + id.id + hash, Date.now());
|
const emailId = Bun.hash('register' + id.id + hash, Date.now());
|
||||||
const timestamp = Date.now() + 1000 * 60 * 60;
|
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||||
@@ -82,7 +83,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
|||||||
id: emailId, timestamp,
|
id: emailId, timestamp,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await runTask('mail', {
|
await sendMail({
|
||||||
payload: {
|
payload: {
|
||||||
type: 'mail',
|
type: 'mail',
|
||||||
to: [body.data.email],
|
to: [body.data.email],
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { eq, or } from 'drizzle-orm';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { usersTable } from '~/db/schema';
|
import { usersTable } from '~/db/schema';
|
||||||
|
import sendMail from '~/server/tasks/mail';
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
profile: z.string(),
|
profile: z.string(),
|
||||||
@@ -32,7 +33,7 @@ export default defineEventHandler(async (e) => {
|
|||||||
id, timestamp,
|
id, timestamp,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await runTask('mail', {
|
await sendMail({
|
||||||
payload: {
|
payload: {
|
||||||
type: 'mail',
|
type: 'mail',
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { usersDataTable, usersTable } from '~/db/schema';
|
|||||||
import { schema } from '~/schemas/registration';
|
import { schema } from '~/schemas/registration';
|
||||||
import { checkSession, logSession } from '~/server/utils/user';
|
import { checkSession, logSession } from '~/server/utils/user';
|
||||||
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||||
|
import sendMail from '~/server/tasks/mail';
|
||||||
|
|
||||||
interface SuccessHandler
|
interface SuccessHandler
|
||||||
{
|
{
|
||||||
@@ -71,9 +72,9 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
|||||||
|
|
||||||
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
||||||
|
|
||||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date() } }) as UserSessionRequired);
|
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date(), logCount: 1 } }) as UserSessionRequired);
|
||||||
|
|
||||||
await runTask('mail', {
|
await sendMail({
|
||||||
payload: {
|
payload: {
|
||||||
type: 'mail',
|
type: 'mail',
|
||||||
to: [body.data.email],
|
to: [body.data.email],
|
||||||
|
|||||||
95
server/api/character.get.ts
Normal file
95
server/api/character.get.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { and, eq, SQL, sql, type Operators } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { characterTable, userPermissionsTable } from '~/db/schema';
|
||||||
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
|
import { group } from '~/shared/general.util';
|
||||||
|
import type { Character, DoubleIndex, Level, MainStat, TrainingLevel } from '~/types/character';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
let { visibility } = getQuery(e) as { visibility?: "public" | "own" | "admin" };
|
||||||
|
|
||||||
|
if(!visibility)
|
||||||
|
{
|
||||||
|
visibility = "own";
|
||||||
|
}
|
||||||
|
|
||||||
|
let where: ((character: typeof characterTable._.config.columns, sql: Operators) => SQL | undefined) | undefined = undefined;
|
||||||
|
const db = useDatabase();
|
||||||
|
|
||||||
|
if(visibility === "own")
|
||||||
|
{
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
if(!session.user)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
where = (character, { eq, and }) => and(eq(character.owner, session.user!.id), eq(character.visibility, "private"));
|
||||||
|
}
|
||||||
|
else if(visibility === 'public')
|
||||||
|
{
|
||||||
|
where = (character, { eq, and }) => eq(character.visibility, "public");
|
||||||
|
}
|
||||||
|
else if(visibility === 'admin')
|
||||||
|
{
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
if(!session.user)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const db = useDatabase();
|
||||||
|
|
||||||
|
const rights = db.select({ right: userPermissionsTable.permission }).from(userPermissionsTable).where(eq(userPermissionsTable.id, session.user.id)).all();
|
||||||
|
if(rights.length === 0 || !hasPermissions(rights.map(e => e.right), ['admin']))
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
where = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const characters = db.query.characterTable.findMany({
|
||||||
|
with: {
|
||||||
|
abilities: true,
|
||||||
|
levels: true,
|
||||||
|
modifiers: true,
|
||||||
|
spells: true,
|
||||||
|
training: true,
|
||||||
|
user: {
|
||||||
|
columns: { username: true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
where: where,
|
||||||
|
}).sync();
|
||||||
|
|
||||||
|
if(characters !== undefined)
|
||||||
|
{
|
||||||
|
return characters.map(character => ({
|
||||||
|
id: character.id,
|
||||||
|
|
||||||
|
name: character.name,
|
||||||
|
people: character.people,
|
||||||
|
level: character.level,
|
||||||
|
aspect: character.aspect,
|
||||||
|
notes: character.notes,
|
||||||
|
health: character.health,
|
||||||
|
mana: character.mana,
|
||||||
|
|
||||||
|
training: character.training.reduce((p, v) => { if(!(v.stat in p)) p[v.stat] = []; p[v.stat].push([v.level as TrainingLevel, v.choice]); return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>),
|
||||||
|
leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex<Level>),
|
||||||
|
abilities: group(character.abilities.map(e => ({ ...e, value: [e.value, e.max] as [number, number] })), "ability", "value"),
|
||||||
|
spells: character.spells.map(e => e.value),
|
||||||
|
modifiers: group(character.modifiers, "modifier", "value"),
|
||||||
|
|
||||||
|
owner: character.owner,
|
||||||
|
username: character.user.username,
|
||||||
|
visibility: character.visibility,
|
||||||
|
} as Character));
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
});
|
||||||
67
server/api/character.post.ts
Normal file
67
server/api/character.post.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||||
|
import { CharacterValidation } from '~/shared/character';
|
||||||
|
import { type Ability, type MainStat } from '~/types/character';
|
||||||
|
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const body = await readValidatedBody(e, CharacterValidation.extend({ id: z.unknown(), }).safeParse);
|
||||||
|
if(!body.success)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return body.error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
if(!session.user || session.user.state !== 1)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const id = db.transaction((tx) => {
|
||||||
|
const id = tx.insert(characterTable).values({
|
||||||
|
name: body.data.name,
|
||||||
|
owner: session.user!.id,
|
||||||
|
people: body.data.people!,
|
||||||
|
level: body.data.level,
|
||||||
|
aspect: body.data.aspect,
|
||||||
|
notes: body.data.notes,
|
||||||
|
health: body.data.health,
|
||||||
|
mana: body.data.mana,
|
||||||
|
visibility: body.data.visibility,
|
||||||
|
thumbnail: body.data.thumbnail,
|
||||||
|
}).returning({ id: characterTable.id }).get().id;
|
||||||
|
|
||||||
|
if(body.data.leveling.length > 0) tx.insert(characterLevelingTable).values(body.data.leveling.map(e => ({ character: id, level: e[0], choice: e[1] }))).run();
|
||||||
|
|
||||||
|
const training = Object.entries(body.data.training).flatMap(e => e[1].map(_e => ({ character: id, stat: e[0] as MainStat, level: _e[0], choice: _e[1] })));
|
||||||
|
if(training.length > 0) tx.insert(characterTrainingTable).values(training).run();
|
||||||
|
|
||||||
|
const modifiers = Object.entries(body.data.modifiers).map((e) => ({ character: id, modifier: e[0] as MainStat, value: e[1] }));
|
||||||
|
if(modifiers.length > 0) tx.insert(characterModifiersTable).values(modifiers).run();
|
||||||
|
|
||||||
|
if(body.data.spells.length > 0) tx.insert(characterSpellsTable).values(body.data.spells.map(e => ({ character: id, value: e }))).run();
|
||||||
|
|
||||||
|
const abilities = Object.entries(body.data.abilities).map(e => ({ character: id, ability: e[0] as Ability, value: e[1][0], max: e[1][1] }));
|
||||||
|
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
|
||||||
|
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
|
||||||
|
setResponseStatus(e, 201);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
catch(_e)
|
||||||
|
{
|
||||||
|
console.error(_e);
|
||||||
|
|
||||||
|
setResponseStatus(e, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
33
server/api/character/[id].delete.ts
Normal file
33
server/api/character/[id].delete.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { characterTable } from '~/db/schema';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const id = getRouterParam(e, "id");
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const old = db.select({ id: characterTable.id, owner: characterTable.owner }).from(characterTable).where(eq(characterTable.id, id)).get();
|
||||||
|
|
||||||
|
if(!old)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
if(!session.user || old.owner !== session.user.id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.delete(characterTable).where(eq(characterTable.id, id)).run();
|
||||||
|
|
||||||
|
setResponseStatus(e, 200);
|
||||||
|
return;
|
||||||
|
});
|
||||||
66
server/api/character/[id].get.ts
Normal file
66
server/api/character/[id].get.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { and, eq, sql } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { characterTable } from '~/db/schema';
|
||||||
|
import { group } from '~/shared/general.util';
|
||||||
|
import type { Character, DoubleIndex, Level, MainStat, TrainingLevel } from '~/types/character';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const id = getRouterParam(e, "id");
|
||||||
|
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!session.user)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const character = db.query.characterTable.findFirst({
|
||||||
|
with: {
|
||||||
|
abilities: true,
|
||||||
|
levels: true,
|
||||||
|
modifiers: true,
|
||||||
|
spells: true,
|
||||||
|
training: true,
|
||||||
|
user: {
|
||||||
|
columns: { username: true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
where: (character, { eq, and }) => and(eq(character.id, parseInt(id, 10)), eq(characterTable.owner, session.user!.id)),
|
||||||
|
}).sync();
|
||||||
|
|
||||||
|
if(character !== undefined)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
id: character.id,
|
||||||
|
|
||||||
|
name: character.name,
|
||||||
|
people: character.people,
|
||||||
|
level: character.level,
|
||||||
|
aspect: character.aspect,
|
||||||
|
notes: character.notes,
|
||||||
|
health: character.health,
|
||||||
|
mana: character.mana,
|
||||||
|
|
||||||
|
training: character.training.reduce((p, v) => { if(!(v.stat in p)) p[v.stat] = []; p[v.stat].push([v.level as TrainingLevel, v.choice]); return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>),
|
||||||
|
leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex<Level>),
|
||||||
|
abilities: group(character.abilities.map(e => ({ ...e, value: [e.value, e.max] as [number, number] })), "ability", "value"),
|
||||||
|
spells: character.spells.map(e => e.value),
|
||||||
|
modifiers: group(character.modifiers, "modifier", "value"),
|
||||||
|
|
||||||
|
owner: character.owner,
|
||||||
|
username: character.user.username,
|
||||||
|
visibility: character.visibility,
|
||||||
|
} as Character;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
});
|
||||||
76
server/api/character/[id].post.ts
Normal file
76
server/api/character/[id].post.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||||
|
import { CharacterValidation } from '~/shared/character';
|
||||||
|
import { type Ability, type MainStat } from '~/types/character';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const params = getRouterParam(e, "id");
|
||||||
|
if(!params)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = parseInt(params, 10);
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, CharacterValidation.safeParse);
|
||||||
|
if(!body.success)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return body.error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const old = db.select({ id: characterTable.id, owner: characterTable.owner }).from(characterTable).where(eq(characterTable.id, id)).get();
|
||||||
|
|
||||||
|
if(!old)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
if(!session.user || old.owner !== session.user.id || session.user.state !== 1)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.transaction((tx) => {
|
||||||
|
tx.update(characterTable).set({
|
||||||
|
name: body.data.name,
|
||||||
|
people: body.data.people!,
|
||||||
|
level: body.data.level,
|
||||||
|
aspect: body.data.aspect,
|
||||||
|
notes: body.data.notes,
|
||||||
|
health: body.data.health,
|
||||||
|
mana: body.data.mana,
|
||||||
|
visibility: body.data.visibility,
|
||||||
|
thumbnail: body.data.thumbnail,
|
||||||
|
}).where(eq(characterTable.id, id)).run();
|
||||||
|
|
||||||
|
tx.delete(characterLevelingTable).where(eq(characterLevelingTable.character, id)).run();
|
||||||
|
tx.delete(characterTrainingTable).where(eq(characterTrainingTable.character, id)).run();
|
||||||
|
tx.delete(characterModifiersTable).where(eq(characterModifiersTable.character, id)).run();
|
||||||
|
tx.delete(characterSpellsTable).where(eq(characterSpellsTable.character, id)).run();
|
||||||
|
tx.delete(characterAbilitiesTable).where(eq(characterAbilitiesTable.character, id)).run();
|
||||||
|
|
||||||
|
if(body.data.leveling.length > 0) tx.insert(characterLevelingTable).values(body.data.leveling.map(e => ({ character: id, level: e[0], choice: e[1] }))).run();
|
||||||
|
|
||||||
|
const training = Object.entries(body.data.training).flatMap(e => e[1].map(_e => ({ character: id, stat: e[0] as MainStat, level: _e[0], choice: _e[1] })));
|
||||||
|
if(training.length > 0) tx.insert(characterTrainingTable).values(training).run();
|
||||||
|
|
||||||
|
const modifiers = Object.entries(body.data.modifiers).map((e) => ({ character: id, modifier: e[0] as MainStat, value: e[1] }));
|
||||||
|
if(modifiers.length > 0) tx.insert(characterModifiersTable).values(modifiers).run();
|
||||||
|
|
||||||
|
if(body.data.spells.length > 0) tx.insert(characterSpellsTable).values(body.data.spells.map(e => ({ character: id, value: e }))).run();
|
||||||
|
|
||||||
|
const abilities = Object.entries(body.data.abilities).map(e => ({ character: id, ability: e[0] as Ability, value: e[1][0], max: e[1][1] }));
|
||||||
|
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
|
||||||
|
});
|
||||||
|
|
||||||
|
await useStorage('cache').removeItem(`nitro:functions:character:${id}.json`);
|
||||||
|
|
||||||
|
setResponseStatus(e, 200);
|
||||||
|
return;
|
||||||
|
});
|
||||||
149
server/api/character/[id]/compiled.get.ts
Normal file
149
server/api/character/[id]/compiled.get.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { type Character, type CharacterConfig, type CompiledCharacter, type DoubleIndex, type Level, type MainStat, type TrainingLevel, type TrainingOption } from '~/types/character';
|
||||||
|
import characterData from '#shared/character-config.json';
|
||||||
|
import { group } from '~/shared/general.util';
|
||||||
|
import { defaultCharacter, MAIN_STATS } from '~/shared/character';
|
||||||
|
|
||||||
|
export default defineCachedEventHandler(async (e) => {
|
||||||
|
const id = getRouterParam(e, "id");
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const character = db.query.characterTable.findFirst({
|
||||||
|
with: {
|
||||||
|
abilities: true,
|
||||||
|
levels: true,
|
||||||
|
modifiers: true,
|
||||||
|
spells: true,
|
||||||
|
training: true,
|
||||||
|
user: {
|
||||||
|
columns: { username: true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
where: (character, { eq }) => eq(character.id, parseInt(id, 10)),
|
||||||
|
}).sync();
|
||||||
|
|
||||||
|
if(character !== undefined)
|
||||||
|
{
|
||||||
|
return compileCharacter(Object.assign(defaultCharacter, {
|
||||||
|
id: character.id,
|
||||||
|
|
||||||
|
name: character.name,
|
||||||
|
people: character.people,
|
||||||
|
level: character.level,
|
||||||
|
aspect: character.aspect,
|
||||||
|
notes: character.notes,
|
||||||
|
health: character.health,
|
||||||
|
mana: character.mana,
|
||||||
|
|
||||||
|
training: character.training.reduce((p, v) => { if(!(v.stat in p)) p[v.stat] = []; p[v.stat].push([v.level as TrainingLevel, v.choice]); return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>),
|
||||||
|
leveling: character.levels.map(e => [e.level as Level, e.choice] as DoubleIndex<Level>),
|
||||||
|
abilities: group(character.abilities.map(e => ({ ...e, value: [e.value, e.max] as [number, number] })), "ability", "value"),
|
||||||
|
spells: character.spells.map(e => e.value),
|
||||||
|
modifiers: group(character.modifiers, "modifier", "value"),
|
||||||
|
|
||||||
|
owner: character.owner,
|
||||||
|
username: character.user.username,
|
||||||
|
visibility: character.visibility,
|
||||||
|
} as Character) as Character);
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}, { name: "character", getKey: (e) => getRouterParam(e, "id") || 'error' });
|
||||||
|
|
||||||
|
function compileCharacter(character: Character & { username?: string }): CompiledCharacter
|
||||||
|
{
|
||||||
|
const config = characterData as CharacterConfig;
|
||||||
|
const race = character.people !== undefined ? config.peoples[character.people] : undefined;
|
||||||
|
const raceOptions = race ? character.leveling!.map(e => race.options[e[0]][e[1]]) : [];
|
||||||
|
const features = Object.entries(config.training).map(e => [e[0], getFeaturesOf(e[0] as MainStat, character.training[e[0] as MainStat])]) as [MainStat, TrainingOption[]][];
|
||||||
|
|
||||||
|
const compiled: CompiledCharacter = {
|
||||||
|
id: character.id,
|
||||||
|
owner: character.owner,
|
||||||
|
username: character.username,
|
||||||
|
name: character.name,
|
||||||
|
health: raceOptions.reduce((p, v) => p + (v.health ?? 0), 0),
|
||||||
|
mana: raceOptions.reduce((p, v) => p + (v.mana ?? 0), 0),
|
||||||
|
race: character.people!,
|
||||||
|
modifier: features.map(e => [e[0], Math.floor((e[1].length - 1) / 3) + (character.modifiers[e[0]] ?? 0)] as [MainStat, number]).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<MainStat, number>),
|
||||||
|
level: character.level,
|
||||||
|
values: {
|
||||||
|
health: character.health,
|
||||||
|
mana: character.mana
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
action: [],
|
||||||
|
reaction: [],
|
||||||
|
freeaction: [],
|
||||||
|
passive: [],
|
||||||
|
},
|
||||||
|
abilities: {
|
||||||
|
athletics: 0,
|
||||||
|
acrobatics: 0,
|
||||||
|
intimidation: 0,
|
||||||
|
sleightofhand: 0,
|
||||||
|
stealth: 0,
|
||||||
|
survival: 0,
|
||||||
|
investigation: 0,
|
||||||
|
history: 0,
|
||||||
|
religion: 0,
|
||||||
|
arcana: 0,
|
||||||
|
understanding: 0,
|
||||||
|
perception: 0,
|
||||||
|
performance: 0,
|
||||||
|
medecine: 0,
|
||||||
|
persuasion: 0,
|
||||||
|
animalhandling: 0,
|
||||||
|
deception: 0
|
||||||
|
},
|
||||||
|
spellslots: 0,
|
||||||
|
artslots: 0,
|
||||||
|
spellranks: {
|
||||||
|
instinct: 0,
|
||||||
|
knowledge: 0,
|
||||||
|
precision: 0,
|
||||||
|
arts: 0,
|
||||||
|
},
|
||||||
|
spells: character.spells ?? [],
|
||||||
|
speed: false,
|
||||||
|
defense: {
|
||||||
|
hardcap: Infinity,
|
||||||
|
static: 6,
|
||||||
|
activeparry: 0,
|
||||||
|
activedodge: 0,
|
||||||
|
passiveparry: 0,
|
||||||
|
passivedodge: 0,
|
||||||
|
},
|
||||||
|
mastery: {
|
||||||
|
strength: 0,
|
||||||
|
dexterity: 0,
|
||||||
|
shield: 0,
|
||||||
|
armor: 0,
|
||||||
|
multiattack: 1,
|
||||||
|
magicpower: 0,
|
||||||
|
magicspeed: 0,
|
||||||
|
magicelement: 0,
|
||||||
|
magicinstinct: 0,
|
||||||
|
},
|
||||||
|
resistance: {},//Object.fromEntries(MAIN_STATS.map(e => [e as MainStat, [0, 0]])) as Record<MainStat, [number, number]>,
|
||||||
|
initiative: 0,
|
||||||
|
aspect: "",
|
||||||
|
notes: character.notes ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
//features.forEach(e => e[1].forEach(_e => _e.features?.forEach(f => applyFeature(compiled, f))));
|
||||||
|
|
||||||
|
return compiled;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFeaturesOf(stat: MainStat, progression: DoubleIndex<TrainingLevel>[]): TrainingOption[]
|
||||||
|
{
|
||||||
|
const config = characterData as CharacterConfig;
|
||||||
|
return progression.map(e => config.training[stat][e[0]][e[1]]);
|
||||||
|
}
|
||||||
71
server/api/character/[id]/duplicate.post.ts
Normal file
71
server/api/character/[id]/duplicate.post.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const id = getRouterParam(e, "id");
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const old = db.select().from(characterTable).where(eq(characterTable.id, parseInt(id, 10))).get();
|
||||||
|
|
||||||
|
if(!old)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
if(!session.user || old.owner !== session.user.id || session.user.state !== 1)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const _id = db.transaction((tx) => {
|
||||||
|
const _id = tx.insert(characterTable).values({
|
||||||
|
name: old.name,
|
||||||
|
owner: session.user!.id,
|
||||||
|
people: old.people!,
|
||||||
|
level: old.level,
|
||||||
|
aspect: old.aspect,
|
||||||
|
notes: old.notes,
|
||||||
|
health: old.health,
|
||||||
|
mana: old.mana,
|
||||||
|
visibility: old.visibility,
|
||||||
|
thumbnail: old.thumbnail,
|
||||||
|
}).returning({ id: characterTable.id }).get().id;
|
||||||
|
|
||||||
|
const leveling = tx.select().from(characterLevelingTable).where(eq(characterLevelingTable.character, parseInt(id, 10))).all();
|
||||||
|
if(leveling.length > 0) tx.insert(characterLevelingTable).values(leveling.map(e => ({ character: _id, level: e.level, choice: e.choice }))).run();
|
||||||
|
|
||||||
|
const training = tx.select().from(characterTrainingTable).where(eq(characterTrainingTable.character, parseInt(id, 10))).all();
|
||||||
|
if(training.length > 0) tx.insert(characterTrainingTable).values(training.map(e => ({ character: _id, stat: e.stat, level: e.level, choice: e.choice }))).run();
|
||||||
|
|
||||||
|
const modifiers = tx.select().from(characterModifiersTable).where(eq(characterModifiersTable.character, parseInt(id, 10))).all();
|
||||||
|
if(modifiers.length > 0) tx.insert(characterModifiersTable).values(modifiers.map(e => ({ character: _id, modifier: e.modifier, value: e.value }))).run();
|
||||||
|
|
||||||
|
const spells = tx.select().from(characterSpellsTable).where(eq(characterSpellsTable.character, parseInt(id, 10))).all();
|
||||||
|
if(spells.length > 0) tx.insert(characterSpellsTable).values(spells.map(e => ({ character: _id, value: e.value }))).run();
|
||||||
|
|
||||||
|
const abilities = tx.select().from(characterAbilitiesTable).where(eq(characterAbilitiesTable.character, parseInt(id, 10))).all();
|
||||||
|
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities.map(e => ({ character: _id, ability: e.ability, value: e.value, max: e.max }))).run();
|
||||||
|
|
||||||
|
return _id;
|
||||||
|
});
|
||||||
|
|
||||||
|
setResponseStatus(e, 201);
|
||||||
|
return _id;
|
||||||
|
}
|
||||||
|
catch(_e)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 201);
|
||||||
|
throw _e;
|
||||||
|
}
|
||||||
|
});
|
||||||
35
server/api/character/[id]/values.get.ts
Normal file
35
server/api/character/[id]/values.get.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { and, eq, sql } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { characterTable } from '~/db/schema';
|
||||||
|
import type { Character, CharacterValues } from '~/types/character';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const id = getRouterParam(e, "id");
|
||||||
|
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
if(!session.user)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const character = db.select({
|
||||||
|
health: characterTable.health,
|
||||||
|
mana: characterTable.mana,
|
||||||
|
}).from(characterTable).where(and(eq(characterTable.id, parseInt(id, 10)), eq(characterTable.owner, session.user.id))).get();
|
||||||
|
|
||||||
|
if(character !== undefined)
|
||||||
|
{
|
||||||
|
return character as CharacterValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
});
|
||||||
44
server/api/character/[id]/values.post.ts
Normal file
44
server/api/character/[id]/values.post.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { characterTable } from '~/db/schema';
|
||||||
|
import type { CharacterValues } from '~/types/character';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const id = getRouterParam(e, "id");
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody(e) as CharacterValues;
|
||||||
|
if(!body)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const old = db.select({ id: characterTable.id, owner: characterTable.owner }).from(characterTable).where(eq(characterTable.id, parseInt(id, 10))).get();
|
||||||
|
|
||||||
|
if(!old)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
if(!session.user || old.owner !== session.user.id || session.user.state !== 1)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update(characterTable).set({
|
||||||
|
health: body.health,
|
||||||
|
mana: body.mana,
|
||||||
|
}).where(eq(characterTable.id, parseInt(id, 10))).run();
|
||||||
|
|
||||||
|
setResponseStatus(e, 200);
|
||||||
|
return;
|
||||||
|
});
|
||||||
37
server/api/file.post.ts
Normal file
37
server/api/file.post.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
import { schema } from '~/schemas/file';
|
||||||
|
import { parsePath } from '~/shared/general.util';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const body = await readValidatedBody(e, schema.safeParse);
|
||||||
|
if(!body.success)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 403);
|
||||||
|
throw body.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
|
||||||
|
const buffer = Buffer.from(convertToStorableLinks(body.data.content, db.select({ path: explorerContentTable.path }).from(explorerContentTable).all().map(e => e.path)), 'utf-8');
|
||||||
|
|
||||||
|
const content = db.insert(explorerContentTable).values({ ...body.data, content: buffer }).onConflictDoUpdate({ target: explorerContentTable.path, set: { ...body.data, content: buffer, timestamp: new Date() } });
|
||||||
|
|
||||||
|
if(content !== undefined)
|
||||||
|
{
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function convertToStorableLinks(content: string, path: string[]): string
|
||||||
|
{
|
||||||
|
return content.replaceAll(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (e: string, a1?: string, a2?: string , a3?: string) => {
|
||||||
|
const parsed = parsePath(a1 ?? '%%%%----%%%%----%%%%');
|
||||||
|
const replacer = path.find(e => e.endsWith(parsed));
|
||||||
|
const value = `[[${a1 ? (replacer ?? '') : ''}${a2 ?? ''}${(!a3 && a1 && replacer !== parsed ? '|' + a1 : a3) ?? ''}]]`;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { eq, sql } from 'drizzle-orm';
|
|
||||||
import useDatabase from '~/composables/useDatabase';
|
|
||||||
import { projectFilesTable as files, projectContentTable as content } from '~/db/schema';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const id = getRouterParam(e, "id") ?? '';
|
|
||||||
if(!id)
|
|
||||||
{
|
|
||||||
setResponseStatus(e, 404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = useDatabase();
|
|
||||||
const data = db.select({ content: sql<string>`cast(${content.content} as TEXT)`.as('content'), private: files.private, owner: files.owner, }).from(content).leftJoin(files, eq(content.id, files.id)).where(eq(content.id, id)).get();
|
|
||||||
|
|
||||||
if(data && data.content)
|
|
||||||
{
|
|
||||||
const session = await getUserSession(e);
|
|
||||||
|
|
||||||
if(!session || !session.user || session.user.id !== data.owner)
|
|
||||||
{
|
|
||||||
if(data.private)
|
|
||||||
{
|
|
||||||
setResponseStatus(e, 404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return data.content.replace(/%%(.+?)%%/g, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch(_e)
|
|
||||||
{
|
|
||||||
console.error(_e);
|
|
||||||
setResponseStatus(e, 500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import useDatabase from '~/composables/useDatabase';
|
|
||||||
import { hasPermissions } from '#shared/auth.util';
|
|
||||||
import { projectContentTable, projectFilesTable } from '~/db/schema';
|
|
||||||
import { eq } from 'drizzle-orm';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
|
||||||
const { user } = await getUserSession(e);
|
|
||||||
|
|
||||||
if(!user || !hasPermissions(user.permissions, ['admin', 'editor']))
|
|
||||||
{
|
|
||||||
throw createError({ statusCode: 401, statusText: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const id = getRouterParam(e, "id") ?? '';
|
|
||||||
const body = await readRawBody(e);
|
|
||||||
|
|
||||||
if(!id)
|
|
||||||
{
|
|
||||||
setResponseStatus(e, 404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = useDatabase();
|
|
||||||
const item = db.select({ id: projectFilesTable.id }).from(projectFilesTable).where(eq(projectFilesTable.id, id)).get();
|
|
||||||
|
|
||||||
if(!item)
|
|
||||||
{
|
|
||||||
setResponseStatus(e, 404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.insert(projectContentTable).values({ id: id, content: body }).onConflictDoUpdate({ set: { content: body }, target: projectContentTable.id }).run();
|
|
||||||
db.update(projectFilesTable).set({ timestamp: new Date() }).where(eq(projectFilesTable.id, id)).run()
|
|
||||||
}
|
|
||||||
catch(_e)
|
|
||||||
{
|
|
||||||
console.error(_e);
|
|
||||||
setResponseStatus(e, 500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
66
server/api/file/content/[path].get.ts
Normal file
66
server/api/file/content/[path].get.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { eq, sql } from 'drizzle-orm';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
import { convertContentFromText } from '~/shared/general.util';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const path = decodeURIComponent(getRouterParam(e, "path") ?? '');
|
||||||
|
const query = getQuery(e);
|
||||||
|
|
||||||
|
if(!path)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const content = db.select({
|
||||||
|
'content': sql<string>`cast(${explorerContentTable.content} as TEXT)`.as('content'),
|
||||||
|
'private': explorerContentTable.private,
|
||||||
|
'type': explorerContentTable.type,
|
||||||
|
'owner': explorerContentTable.owner,
|
||||||
|
'visit': explorerContentTable.visit,
|
||||||
|
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
|
||||||
|
|
||||||
|
if(content !== undefined)
|
||||||
|
{
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!session || !session.user || session.user.id !== content.owner)
|
||||||
|
{
|
||||||
|
if(content.private)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
content.content = content.content.replace(/%%(.+)%%/g, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(query.type === 'view')
|
||||||
|
{
|
||||||
|
db.update(explorerContentTable).set({ visit: content.visit + 1 }).where(eq(explorerContentTable.path, path)).run();
|
||||||
|
}
|
||||||
|
if(query.type === 'editing')
|
||||||
|
{
|
||||||
|
content.content = convertFromStorableLinks(content.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertContentFromText(content.type, content.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function convertFromStorableLinks(content: string): string
|
||||||
|
{
|
||||||
|
/*return content.replaceAll(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (e: string, a1?: string, a2?: string , a3?: string) => {
|
||||||
|
const parsed = parsePath(a1 ?? '%%%%----%%%%----%%%%');
|
||||||
|
const replacer = path.find(e => e.endsWith(parsed)) ?? parsed;
|
||||||
|
const value = `[[${replacer}${a2 ?? ''}${(!a3 && replacer !== parsed ? '|' + a1 : a3) ?? ''}]]`;
|
||||||
|
return value;
|
||||||
|
});*/
|
||||||
|
return content;
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { projectFilesTable } from '~/db/schema';
|
import { getTableColumns } from 'drizzle-orm';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const { user } = await getUserSession(e);
|
const { user } = await getUserSession(e);
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
const content = db.select().from(projectFilesTable).all();
|
const { content: _, ...columns } = getTableColumns(explorerContentTable);
|
||||||
|
const content = db.select(columns).from(explorerContentTable).all();
|
||||||
|
|
||||||
content.sort((a, b) => {
|
content.sort((a, b) => {
|
||||||
return a.path.split('/').length - b.path.split('/').length;
|
return a.path.split('/').length - b.path.split('/').length;
|
||||||
@@ -34,5 +36,6 @@ export default defineEventHandler(async (e) => {
|
|||||||
return content.filter(e => !!e);
|
return content.filter(e => !!e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import useDatabase from '~/composables/useDatabase';
|
|
||||||
import { hasPermissions } from "#shared/auth.util";
|
|
||||||
import { eq, sql } from "drizzle-orm";
|
|
||||||
import { projectFilesTable } from "~/db/schema";
|
|
||||||
import { Project } from "~/schemas/project";
|
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
|
||||||
const { user } = await getUserSession(e);
|
|
||||||
|
|
||||||
if(!user || !hasPermissions(user.permissions, ['admin', 'editor']))
|
|
||||||
{
|
|
||||||
throw createError({ statusCode: 401, statusText: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await readValidatedBody(e, Project.safeParse);
|
|
||||||
|
|
||||||
if(!body.success)
|
|
||||||
{
|
|
||||||
throw body.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = useDatabase(), items = body.data, blocked: string[] = [];
|
|
||||||
|
|
||||||
db.transaction((tx) => {
|
|
||||||
const data = tx.select({ id: projectFilesTable.id, timestamp: projectFilesTable.timestamp }).from(projectFilesTable).all();
|
|
||||||
const deletion = tx.delete(projectFilesTable).where(eq(projectFilesTable.id, sql.placeholder('id'))).prepare();
|
|
||||||
|
|
||||||
for(let i = 0; i < items.length; i++)
|
|
||||||
{
|
|
||||||
const item = items[i];
|
|
||||||
|
|
||||||
const index = data.findIndex(e => e.id === item.id);
|
|
||||||
|
|
||||||
if(index !== -1)
|
|
||||||
{
|
|
||||||
if(data[index].timestamp > new Date(item.timestamp))
|
|
||||||
{
|
|
||||||
blocked.push(item.id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.insert(projectFilesTable).values({
|
|
||||||
id: item.id,
|
|
||||||
path: item.path,
|
|
||||||
owner: user.id,
|
|
||||||
title: item.title,
|
|
||||||
type: item.type,
|
|
||||||
navigable: item.navigable,
|
|
||||||
private: item.private,
|
|
||||||
order: item.order,
|
|
||||||
}).onConflictDoUpdate({
|
|
||||||
set: {
|
|
||||||
id: item.id,
|
|
||||||
path: item.path,
|
|
||||||
title: item.title,
|
|
||||||
type: item.type,
|
|
||||||
navigable: item.navigable,
|
|
||||||
private: item.private,
|
|
||||||
order: item.order,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
target: projectFilesTable.id,
|
|
||||||
}).run();
|
|
||||||
}
|
|
||||||
|
|
||||||
for(let i = 0; i < data.length; i++)
|
|
||||||
{
|
|
||||||
deletion.run({ id: data[i].id });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return blocked;
|
|
||||||
});
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { eq, sql } from 'drizzle-orm';
|
import { eq, sql } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { projectFilesTable } from '~/db/schema';
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
|
||||||
export default defineCachedEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const id = getRouterParam(e, "id") ?? '';
|
const path = decodeURIComponent(getRouterParam(e, "path") ?? '');
|
||||||
|
|
||||||
if(!id)
|
if(!path)
|
||||||
{
|
{
|
||||||
setResponseStatus(e, 404);
|
setResponseStatus(e, 404);
|
||||||
return;
|
return;
|
||||||
@@ -14,14 +14,13 @@ export default defineCachedEventHandler(async (e) => {
|
|||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
|
|
||||||
const content = db.select({
|
const content = db.select({
|
||||||
'id': projectFilesTable.id,
|
'path': explorerContentTable.path,
|
||||||
'path': projectFilesTable.path,
|
'owner': explorerContentTable.owner,
|
||||||
'owner': projectFilesTable.owner,
|
'title': explorerContentTable.title,
|
||||||
'title': projectFilesTable.title,
|
'type': explorerContentTable.type,
|
||||||
'type': projectFilesTable.type,
|
'navigable': explorerContentTable.navigable,
|
||||||
'navigable': projectFilesTable.navigable,
|
'private': explorerContentTable.private,
|
||||||
'private': projectFilesTable.private,
|
}).from(explorerContentTable).where(eq(explorerContentTable.path, sql.placeholder('path'))).prepare().get({ path });
|
||||||
}).from(projectFilesTable).where(eq(projectFilesTable.id, sql.placeholder('id'))).prepare().get({ id });
|
|
||||||
|
|
||||||
if(content !== undefined)
|
if(content !== undefined)
|
||||||
{
|
{
|
||||||
@@ -48,4 +47,4 @@ export default defineCachedEventHandler(async (e) => {
|
|||||||
|
|
||||||
setResponseStatus(e, 404);
|
setResponseStatus(e, 404);
|
||||||
return;
|
return;
|
||||||
}, { getKey: (e) => getRouterParam(e, "id") ?? '', });
|
});
|
||||||
22
server/api/homebrew/[id].get.ts
Normal file
22
server/api/homebrew/[id].get.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const id = getRouterParam(e, "id");
|
||||||
|
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!session.user)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(id);
|
||||||
|
|
||||||
|
setResponseStatus(e, 200);
|
||||||
|
return {};
|
||||||
|
});
|
||||||
23
server/api/homebrew/[id].post.ts
Normal file
23
server/api/homebrew/[id].post.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const id = getRouterParam(e, "id");
|
||||||
|
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!session.user)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(id);
|
||||||
|
console.log(await readBody(e));
|
||||||
|
|
||||||
|
setResponseStatus(e, 200);
|
||||||
|
return;
|
||||||
|
});
|
||||||
77
server/api/navigation.get.ts
Normal file
77
server/api/navigation.get.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
import type { NavigationItem } from '~/schemas/navigation';
|
||||||
|
|
||||||
|
export type NavigationTreeItem = NavigationItem & { children?: NavigationTreeItem[] };
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const { user } = await getUserSession(e);
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const content = db.select({
|
||||||
|
path: explorerContentTable.path,
|
||||||
|
type: explorerContentTable.type,
|
||||||
|
owner: explorerContentTable.owner,
|
||||||
|
title: explorerContentTable.title,
|
||||||
|
navigable: explorerContentTable.navigable,
|
||||||
|
private: explorerContentTable.private,
|
||||||
|
order: explorerContentTable.order,
|
||||||
|
}).from(explorerContentTable).all();
|
||||||
|
|
||||||
|
content.sort((a, b) => {
|
||||||
|
return a.path.split('/').length - b.path.split('/').length;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(content.length > 0)
|
||||||
|
{
|
||||||
|
const navigation: NavigationTreeItem[] = [];
|
||||||
|
|
||||||
|
for(const idx in content)
|
||||||
|
{
|
||||||
|
const item = content[idx];
|
||||||
|
if(!!item.private && (user?.id ?? -1) !== item.owner || !item.navigable)
|
||||||
|
{
|
||||||
|
delete content[idx];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = item.path.includes('/') ? item.path.substring(0, item.path.lastIndexOf('/')) : undefined;
|
||||||
|
|
||||||
|
if(parent && !content.find(e => e && e.path === parent))
|
||||||
|
{
|
||||||
|
delete content[idx];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(const item of content.filter(e => !!e))
|
||||||
|
{
|
||||||
|
addChild(navigation, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
function addChild(arr: NavigationTreeItem[], e: NavigationItem): void
|
||||||
|
{
|
||||||
|
const parent = arr.find(f => e.path.startsWith(f.path));
|
||||||
|
|
||||||
|
if(parent)
|
||||||
|
{
|
||||||
|
if(!parent.children)
|
||||||
|
parent.children = [];
|
||||||
|
|
||||||
|
addChild(parent.children, e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
arr.push({ ...e });
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
if(a.order !== b.order)
|
||||||
|
return a.order - b.order;
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
80
server/api/project.get.ts
Normal file
80
server/api/project.get.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
import type { NavigationItem } from '~/schemas/navigation';
|
||||||
|
import type { ProjectItem, Project } from '~/schemas/project';
|
||||||
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const { user } = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!user || !hasPermissions(user.permissions, ['editor', 'admin']))
|
||||||
|
{
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const content = db.select({
|
||||||
|
path: explorerContentTable.path,
|
||||||
|
type: explorerContentTable.type,
|
||||||
|
owner: explorerContentTable.owner,
|
||||||
|
title: explorerContentTable.title,
|
||||||
|
navigable: explorerContentTable.navigable,
|
||||||
|
private: explorerContentTable.private,
|
||||||
|
order: explorerContentTable.order,
|
||||||
|
}).from(explorerContentTable).prepare().all();
|
||||||
|
|
||||||
|
content.sort((a, b) => {
|
||||||
|
return a.path.split('/').length - b.path.split('/').length;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(content.length > 0)
|
||||||
|
{
|
||||||
|
const project: Project = {
|
||||||
|
items: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const item of content.filter(e => !!e))
|
||||||
|
{
|
||||||
|
addChild(project.items, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
function addChild(arr: ProjectItem[], e: NavigationItem): void
|
||||||
|
{
|
||||||
|
const parent = arr.find(f => e.path.startsWith(f.path));
|
||||||
|
|
||||||
|
if(parent)
|
||||||
|
{
|
||||||
|
if(!parent.children)
|
||||||
|
parent.children = [];
|
||||||
|
|
||||||
|
addChild(parent.children, e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
arr.push({
|
||||||
|
path: e.path,
|
||||||
|
parent: e.path.substring(0, e.path.lastIndexOf('/')),
|
||||||
|
name: e.path.substring(e.path.lastIndexOf('/') + 1),
|
||||||
|
title: e.title,
|
||||||
|
type: e.type,
|
||||||
|
navigable: e.navigable,
|
||||||
|
private: e.private,
|
||||||
|
order: e.order,
|
||||||
|
});
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
if(a.order !== b.order)
|
||||||
|
return a.order - b.order;
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
106
server/api/project.post.ts
Normal file
106
server/api/project.post.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { hasPermissions } from "#shared/auth.util";
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
import { project, type ProjectItem } from '~/schemas/project';
|
||||||
|
import { parsePath } from "#shared/general.util";
|
||||||
|
import { eq, getTableColumns, sql } from "drizzle-orm";
|
||||||
|
import type { ExploreContent, TreeItem } from "~/types/content";
|
||||||
|
import type { TreeItemEditable } from "~/pages/explore/edit/index.vue";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const { user } = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!user || !hasPermissions(user.permissions, ['admin', 'editor']))
|
||||||
|
{
|
||||||
|
throw createError({ statusCode: 401, statusText: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, project.safeParse);
|
||||||
|
|
||||||
|
if(!body.success)
|
||||||
|
{
|
||||||
|
throw body.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = buildOrder(body.data) as Array<TreeItemEditable & { match?: number }>;
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const { ...cols } = getTableColumns(explorerContentTable);
|
||||||
|
const full = db.select(cols).from(explorerContentTable).all() as Record<string, any>[];
|
||||||
|
|
||||||
|
for(let i = full.length - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
const item = items.find(e => (e.path === '' ? [e.parent, parsePath(e.name === '' ? e.title : e.name)].filter(e => !!e).join('/') : e.path) === full[i].path);
|
||||||
|
if(item)
|
||||||
|
{
|
||||||
|
item.match = i;
|
||||||
|
full[i].include = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.transaction((tx) => {
|
||||||
|
for(let i = 0; i < items.length; i++)
|
||||||
|
{
|
||||||
|
const item = items[i];
|
||||||
|
const old = full[item.match!];
|
||||||
|
|
||||||
|
const path = [item.parent, parsePath(item.name === '' ? item.title : item.name)].filter(e => !!e).join('/');
|
||||||
|
|
||||||
|
tx.insert(explorerContentTable).values({
|
||||||
|
path: item.path || path,
|
||||||
|
owner: user.id,
|
||||||
|
title: item.title,
|
||||||
|
type: item.type,
|
||||||
|
navigable: item.navigable,
|
||||||
|
private: item.private,
|
||||||
|
order: item.order,
|
||||||
|
content: item.content ?? old?.content ?? null,
|
||||||
|
}).onConflictDoUpdate({
|
||||||
|
set: {
|
||||||
|
path: path,
|
||||||
|
title: item.title,
|
||||||
|
type: item.type,
|
||||||
|
navigable: item.navigable,
|
||||||
|
private: item.private,
|
||||||
|
order: item.order,
|
||||||
|
timestamp: new Date(),
|
||||||
|
content: item.content ?? old?.content ?? null,
|
||||||
|
},
|
||||||
|
target: explorerContentTable.path,
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
if(item.path !== path && !old)
|
||||||
|
{
|
||||||
|
tx.update(explorerContentTable).set({ content: sql`replace(${explorerContentTable.content}, ${sql.placeholder('old')}, ${sql.placeholder('new')})` }).prepare().run({ 'old': item.path, 'new': path });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(let i = 0; i < full.length; i++)
|
||||||
|
{
|
||||||
|
if(full[i].include !== true)
|
||||||
|
tx.delete(explorerContentTable).where(eq(explorerContentTable.path, full[i].path)).run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return items.map(e => ({
|
||||||
|
path: e.path,
|
||||||
|
owner: e.owner,
|
||||||
|
title: e.title,
|
||||||
|
type: e.type,
|
||||||
|
content: e.content,
|
||||||
|
navigable: e.navigable,
|
||||||
|
private: e.private,
|
||||||
|
order: e.order,
|
||||||
|
visit: e.visit,
|
||||||
|
timestamp: e.timestamp,
|
||||||
|
})) as ExploreContent[];
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildOrder(items: ProjectItem[]): ProjectItem[]
|
||||||
|
{
|
||||||
|
items.forEach((e, i) => {
|
||||||
|
e.order = i;
|
||||||
|
if(e.children) e.children = buildOrder(e.children);
|
||||||
|
});
|
||||||
|
|
||||||
|
return items.flatMap(e => [e, ...(e.children ?? [])]);
|
||||||
|
}
|
||||||
20
server/api/search.get.ts
Normal file
20
server/api/search.get.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const query = getQuery(e);
|
||||||
|
|
||||||
|
if (query.search) {
|
||||||
|
const db = useDatabase();
|
||||||
|
|
||||||
|
const files = db.query(`SELECT f.*, u.username, count(c.path) as comments FROM explorer_files f LEFT JOIN users u ON f.owner = u.id LEFT JOIN explorer_comments c ON c.project = f.project AND c.path = f.path WHERE title LIKE ?1 AND private = 0 AND type != "Folder" GROUP BY f.project, f.path`).all(query.search) as FileSearch[];
|
||||||
|
const users = db.query(`SELECT id, username FROM users WHERE username LIKE ?1`).all(query.search) as UserSearch[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
|
files,
|
||||||
|
users
|
||||||
|
} as Search;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import { hash } from "bun";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import useDatabase from "~/composables/useDatabase";
|
import useDatabase from "~/composables/useDatabase";
|
||||||
import { usersTable } from "~/db/schema";
|
import { usersTable } from "~/db/schema";
|
||||||
|
import sendMail from '~/server/tasks/mail';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const session = await getUserSession(e);
|
const session = await getUserSession(e);
|
||||||
@@ -56,7 +57,7 @@ export default defineEventHandler(async (e) => {
|
|||||||
id: emailId, timestamp,
|
id: emailId, timestamp,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await runTask('mail', {
|
await sendMail({
|
||||||
payload: {
|
payload: {
|
||||||
type: 'mail',
|
type: 'mail',
|
||||||
to: [data.email],
|
to: [data.email],
|
||||||
|
|||||||
@@ -28,69 +28,78 @@ const transport = nodemailer.createTransport({
|
|||||||
pool: true,
|
pool: true,
|
||||||
host: config.mail.host,
|
host: config.mail.host,
|
||||||
port: config.mail.port,
|
port: config.mail.port,
|
||||||
secure: true,
|
secure: config.mail.port === "465",
|
||||||
auth: {
|
auth: {
|
||||||
user: config.mail.user,
|
user: config.mail.user,
|
||||||
pass: config.mail.passwd,
|
pass: config.mail.passwd,
|
||||||
},
|
},
|
||||||
requireTLS: true,
|
tls: { rejectUnauthorized: false },
|
||||||
dkim: {
|
dkim: {
|
||||||
domainName: domain,
|
domainName: domain,
|
||||||
keySelector: selector,
|
keySelector: selector,
|
||||||
privateKey: dkim,
|
privateKey: dkim,
|
||||||
},
|
},
|
||||||
|
proxy: config.mail.proxy,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineTask({
|
if(process.env.NODE_ENV === 'production')
|
||||||
meta: {
|
{
|
||||||
name: 'mail',
|
transport.verify((error) => {
|
||||||
description: 'Send email',
|
if(error)
|
||||||
},
|
|
||||||
async run(e) {
|
|
||||||
try {
|
|
||||||
if(e.payload.type !== 'mail')
|
|
||||||
{
|
|
||||||
throw new Error(`Données inconnues`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = e.payload as MailPayload;
|
|
||||||
const template = templates[payload.template];
|
|
||||||
|
|
||||||
if(!template)
|
|
||||||
{
|
|
||||||
throw new Error(`Modèle de mail ${payload.template} inconnu`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.time('Generating HTML');
|
|
||||||
const mail: Mail.Options = {
|
|
||||||
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
|
|
||||||
to: payload.to,
|
|
||||||
html: await render(template.component, payload.data),
|
|
||||||
subject: template.subject,
|
|
||||||
textEncoding: 'quoted-printable',
|
|
||||||
};
|
|
||||||
console.timeEnd('Generating HTML');
|
|
||||||
|
|
||||||
if(mail.html === '')
|
|
||||||
return { result: false, error: new Error("Invalid content") };
|
|
||||||
|
|
||||||
console.time('Sending Mail');
|
|
||||||
const status = await transport.sendMail(mail);
|
|
||||||
console.timeEnd('Sending Mail');
|
|
||||||
|
|
||||||
if(status.rejected.length > 0)
|
|
||||||
{
|
|
||||||
return { result: false, error: status.response, details: status.rejectedErrors };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { result: true };
|
|
||||||
}
|
|
||||||
catch(e)
|
|
||||||
{
|
{
|
||||||
return { result: false, error: e };
|
console.log('Mail server cannot be reached');
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
},
|
else
|
||||||
})
|
console.log("Mail server is reachable and ready to communicate");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function(e: TaskEvent) {
|
||||||
|
try {
|
||||||
|
if(e.payload.type !== 'mail')
|
||||||
|
{
|
||||||
|
throw new Error(`Données inconnues`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = e.payload as MailPayload;
|
||||||
|
const template = templates[payload.template];
|
||||||
|
|
||||||
|
if(!template)
|
||||||
|
{
|
||||||
|
throw new Error(`Modèle de mail ${payload.template} inconnu`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.time('Generating HTML');
|
||||||
|
const mail: Mail.Options = {
|
||||||
|
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
|
||||||
|
to: payload.to,
|
||||||
|
html: await render(template.component, payload.data),
|
||||||
|
subject: template.subject,
|
||||||
|
textEncoding: 'quoted-printable',
|
||||||
|
};
|
||||||
|
console.timeEnd('Generating HTML');
|
||||||
|
|
||||||
|
if(mail.html === '')
|
||||||
|
return { result: false, error: new Error("Invalid content") };
|
||||||
|
|
||||||
|
console.time('Sending Mail');
|
||||||
|
const status = await transport.sendMail(mail);
|
||||||
|
console.timeEnd('Sending Mail');
|
||||||
|
|
||||||
|
if(status.rejected.length > 0)
|
||||||
|
{
|
||||||
|
return { result: false, error: status.response, details: status.rejectedErrors };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { result: true };
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
console.error(e);
|
||||||
|
return { result: false, error: e };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function render(component: any, data: Record<string, any>): Promise<string>
|
async function render(component: any, data: Record<string, any>): Promise<string>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import useDatabase from "~/composables/useDatabase";
|
import useDatabase from "~/composables/useDatabase";
|
||||||
import { extname, basename } from 'node:path';
|
import { extname, basename } from 'node:path';
|
||||||
|
import type { FileType } from '~/types/content';
|
||||||
import type { CanvasColor, CanvasContent } from "~/types/canvas";
|
import type { CanvasColor, CanvasContent } from "~/types/canvas";
|
||||||
import type { FileType, ProjectContent } from "#shared/content.util";
|
import { explorerContentTable } from "~/db/schema";
|
||||||
import { getID, ID_SIZE, parsePath } from "#shared/general.util";
|
import { convertToStorableLinks } from "../api/file.post";
|
||||||
import { projectContentTable, projectFilesTable } from "~/db/schema";
|
import { parsePath } from "~/shared/general.util";
|
||||||
|
|
||||||
const typeMapping: Record<string, FileType> = {
|
const typeMapping: Record<string, FileType> = {
|
||||||
".md": "markdown",
|
".md": "markdown",
|
||||||
@@ -29,22 +30,20 @@ export default defineTask({
|
|||||||
}
|
}
|
||||||
}) as { tree: any[] } & Record<string, any>;
|
}) as { tree: any[] } & Record<string, any>;
|
||||||
|
|
||||||
const files: ProjectContent[] = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e, i) => {
|
const files: typeof explorerContentTable.$inferInsert[] = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e, i) => {
|
||||||
if(e.type === 'tree')
|
if(e.type === 'tree')
|
||||||
{
|
{
|
||||||
const title = basename(e.path);
|
const title = basename(e.path);
|
||||||
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
|
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
|
||||||
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
|
|
||||||
return {
|
return {
|
||||||
id: getID(ID_SIZE),
|
path: e.path,
|
||||||
path: parsePath(path),
|
|
||||||
order: i,
|
order: i,
|
||||||
title: order && order[2] ? order[2] : title,
|
title: title,
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
content: null,
|
content: null,
|
||||||
owner: 1,
|
owner: 1,
|
||||||
navigable: true,
|
navigable: true,
|
||||||
private: e.path.startsWith('98.Privé'),
|
private: e.path === '98. Privé',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,32 +51,29 @@ export default defineTask({
|
|||||||
const extension = extname(e.path);
|
const extension = extname(e.path);
|
||||||
const title = basename(e.path, extension);
|
const title = basename(e.path, extension);
|
||||||
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
|
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
|
||||||
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
|
|
||||||
const content = (await $fetch(`https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/raw/${encodeURIComponent(e.path)}`));
|
const content = (await $fetch(`https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/raw/${encodeURIComponent(e.path)}`));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: getID(ID_SIZE),
|
path: extension === '.md' ? e.path.replace(extension, '') : e.path,
|
||||||
path: parsePath(extension === '.md' ? path.replace(extension, '') : path),
|
|
||||||
order: i,
|
order: i,
|
||||||
title: order && order[2] ? order[2] : title,
|
title: title,
|
||||||
type: (typeMapping[extension] ?? 'file'),
|
type: (typeMapping[extension] ?? 'file'),
|
||||||
content: reshapeContent(content as string, typeMapping[extension] ?? 'File'),
|
content: reshapeContent(content as string, typeMapping[extension] ?? 'File'),
|
||||||
owner: 1,
|
owner: 1,
|
||||||
navigable: true,
|
navigable: true,
|
||||||
private: e.path.startsWith('98.Privé'),
|
private: e.path === '98. Privé',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
files.forEach(e => e.content = reshapeLinks(e.content as string | null, files) ?? null);
|
files.forEach(e => {
|
||||||
|
const content = reshapeLinks(e.content as string | null, files) ?? null;
|
||||||
|
e.content = content ? Buffer.from(content, 'utf-8') : null;
|
||||||
|
});
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
db.transaction(tx => {
|
db.delete(explorerContentTable).run();
|
||||||
db.delete(projectFilesTable).run();
|
db.insert(explorerContentTable).values(files).run();
|
||||||
db.insert(projectFilesTable).values(files.map(e => {const { content, ...rest } = e; return rest; })).run();
|
|
||||||
db.delete(projectContentTable).run();
|
|
||||||
db.insert(projectContentTable).values(files.map(e => ({ content: e.content ? Buffer.from(e.content as string) : null, id: e.id }))).run();
|
|
||||||
});
|
|
||||||
|
|
||||||
return { result: true };
|
return { result: true };
|
||||||
}
|
}
|
||||||
@@ -89,11 +85,10 @@ export default defineTask({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
function reshapeLinks(content: string | null, all: typeof explorerContentTable.$inferInsert[])
|
||||||
function reshapeLinks(content: string | null, all: ProjectContent[])
|
|
||||||
{
|
{
|
||||||
return content?.replace(/\[\[(.*?)?(#.*?)?(\|.*?)?\]\]/g, (str, link, header, title) => {
|
return content?.replace(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (str, link, header, title) => {
|
||||||
return `[[${link ? parsePath(all.find(e => e.path.endsWith(parsePath(link)))?.path ?? parsePath(link)) : ''}${header ?? ''}${title ?? ''}]]`;
|
return `[[${link ? all.find(e => e.path.endsWith(link))?.path ?? link : ''}${header ?? ''}${title ?? ''}]]`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +97,6 @@ function reshapeContent(content: string, type: FileType): string | null
|
|||||||
switch(type)
|
switch(type)
|
||||||
{
|
{
|
||||||
case "markdown":
|
case "markdown":
|
||||||
return content;
|
|
||||||
case "file":
|
case "file":
|
||||||
return content;
|
return content;
|
||||||
case "canvas":
|
case "canvas":
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import useDatabase from "~/composables/useDatabase";
|
import useDatabase from "~/composables/useDatabase";
|
||||||
import { projectFilesTable, projectContentTable } from "~/db/schema";
|
import type { FileType } from '~/types/content';
|
||||||
import { eq } from "drizzle-orm";
|
import { explorerContentTable } from "~/db/schema";
|
||||||
|
import { eq, ne } from "drizzle-orm";
|
||||||
|
|
||||||
|
const typeMapping: Record<string, FileType> = {
|
||||||
|
".md": "markdown",
|
||||||
|
".canvas": "canvas"
|
||||||
|
};
|
||||||
|
|
||||||
export default defineTask({
|
export default defineTask({
|
||||||
meta: {
|
meta: {
|
||||||
@@ -9,10 +15,19 @@ export default defineTask({
|
|||||||
},
|
},
|
||||||
async run(event) {
|
async run(event) {
|
||||||
try {
|
try {
|
||||||
|
const tree = await $fetch('https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/git/trees/master', {
|
||||||
|
method: 'get',
|
||||||
|
headers: {
|
||||||
|
accept: 'application/json',
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
recursive: true,
|
||||||
|
per_page: 1000,
|
||||||
|
}
|
||||||
|
}) as any;
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
const files = db.select().from(projectFilesTable).leftJoin(projectContentTable, eq(projectContentTable.id, projectFilesTable.id)).all();
|
const files = db.select().from(explorerContentTable).where(ne(explorerContentTable.type, 'folder')).all();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { result: true };
|
return { result: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default defineTask({
|
|||||||
name: 'validation',
|
name: 'validation',
|
||||||
description: 'Add email ID to DB',
|
description: 'Add email ID to DB',
|
||||||
},
|
},
|
||||||
async run(e) {
|
run(e) {
|
||||||
try {
|
try {
|
||||||
if(e.payload.type !== 'validation')
|
if(e.payload.type !== 'validation')
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user