Project list and starting editing

This commit is contained in:
Peaceultime 2024-08-06 15:16:48 +02:00
parent a3d0b3b5bd
commit aba56bb034
24 changed files with 1189 additions and 943 deletions

27
app.vue
View File

@ -9,6 +9,15 @@ function hideLeftPanel(_: Event): void {
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
const { id: project, home, get } = useProject();
if(project.value !== 0 && home.value === null)
{
const { data: useless } = await useAsyncData(`project:get:${project}`, get);
}
const toggled = ref(false);
onMounted(() => { onMounted(() => {
icon = document.querySelector('.site-nav-bar .clickable-icon'); icon = document.querySelector('.site-nav-bar .clickable-icon');
icon?.removeEventListener('click', toggleLeftPanel); icon?.removeEventListener('click', toggleLeftPanel);
@ -30,12 +39,18 @@ onMounted(() => {
</svg> </svg>
</div> </div>
<div class="gapx-3 flex align-stretch"> <div class="gapx-3 flex align-stretch">
<NuxtLink @click="hideLeftPanel" class="site-nav-bar-text" aria-label="Accueil" to="/"> <NuxtLink @click="hideLeftPanel" class="site-nav-bar-text" aria-label="Accueil" :to="{ path: '/', force: true }">
<ThemeIcon icon="logo" :width=40 :height=40 /> <ThemeIcon icon="logo" :width=40 :height=40 />
</NuxtLink> </NuxtLink>
<NuxtLink class="site-nav-bar-text mobile-hidden" aria-label="Projets" to="/explorer" <div class="site-nav-bar-text mobile-hidden">
:class="{'mod-active': $route.path.startsWith('/explorer')}">Projets</NuxtLink> <NuxtLink aria-label="Projet" :to="{ path: project === 0 ? `/explorer` : `/explorer/${project}${home}`, force: true }"
<NuxtLink class="site-nav-bar-text mobile-hidden" aria-label="Outils" to="/tools" :class="{'mod-active': $route.path.startsWith('/explorer')}">Projet</NuxtLink>
<div class="arrow-down" :class="{active: toggled}" @click="toggled = !toggled"></div>
<div class="arrow-group" @click="toggled = false">
<NuxtLink class="arrow-group-item" aria-label="Projets" :to="{ path: '/explorer', force: true }">Liste des projets</NuxtLink>
</div>
</div>
<NuxtLink class="site-nav-bar-text mobile-hidden" aria-label="Outils" :to="{ path: '/tools', force: true }"
:class="{'mod-active': $route.path.startsWith('/tools')}">Outils</NuxtLink> :class="{'mod-active': $route.path.startsWith('/tools')}">Outils</NuxtLink>
</div> </div>
</div> </div>
@ -44,7 +59,7 @@ onMounted(() => {
</div> </div>
<div class="ps-1 gapx-1 flex align-center"> <div class="ps-1 gapx-1 flex align-center">
<ThemeSwitch class="mobile-hidden" /> <ThemeSwitch class="mobile-hidden" />
<NuxtLink class="site-login" to="/user/profile"> <NuxtLink class="site-login" :to="{ path: '/user/profile', force: true }">
<ThemeIcon icon="user" :width=32 :height=32 /> <ThemeIcon icon="user" :width=32 :height=32 />
</NuxtLink> </NuxtLink>
</div> </div>
@ -55,7 +70,7 @@ onMounted(() => {
</div> </div>
<div class="site-footer"> <div class="site-footer">
<p>Copyright Peaceultime - 2024</p> <p>Copyright Peaceultime - 2024</p>
<NuxtLink href="/third-party">Applications tierces et crédits</NuxtLink> <NuxtLink :to="{ path: '/third-party', force: true }">Applications tierces et crédits</NuxtLink>
</div> </div>
</div> </div>
</template> </template>

View File

@ -286,6 +286,88 @@ html.light-mode .light-block {
padding: 0 4px; padding: 0 4px;
border-radius: 4px; border-radius: 4px;
font-size: var(--font-smaller); font-size: var(--font-smaller);
/* font-style: italic; */
font-variant: all-petite-caps; font-variant: all-petite-caps;
}
.suggestion-subtext {
display: flex;
justify-content: space-between;
font-size: var(--font-smaller);
font-style: italic;
}
.arrow-down {
border: 2px solid var(--background-modifier-border-focus);
border-top-color: transparent;
border-left-color: transparent;
width: 10px;
height: 10px;
transform: rotate(45deg);
position: relative;
left: 8px;
top: -3px;
}
.projects-container {
flex: 1 1;
padding: 2em 4em;
}
.project-list {
display: flex;
}
.project-item {
margin: 1em;
padding: .5em;
border: 1px solid var(--background-modifier-border);
width: 45%;
}
.project-title {
font-size: var(--font-ui-large);
font-weight: var(--font-bold);
display: block;
}
.project-user {
font-size: var(--font-small);
display: inline;
}
.project-pages {
display: inline;
float: inline-end;
font-size: var(--font-small);
}
.arrow-group {
position: absolute;
left: -1em;
top: calc(100% + 7px);
border: 1px solid var(--background-modifier-border);
background-color: var(--background-primary);
z-index: 99;
text-wrap: nowrap;
display: none;
flex-direction: column;
}
.arrow-down.active + .arrow-group {
display: flex;
}
.arrow-group .arrow-group-item {
padding: 8px 1em;
font-weight: var(--font-medium);
color: var(--text-normal);
border-bottom: 1px solid var(--background-modifier-border);
}
.arrow-group .arrow-group-item:last-child {
border-bottom: none;
}
.arrow-group .arrow-group-item:hover {
background-color: var(--background-primary-alt);
} }

View File

@ -2636,7 +2636,7 @@ body:not(.native-scrollbars) * {
} }
.suggestion-item { .suggestion-item {
cursor: var(--cursor); cursor: pointer;
padding: var(--size-2-3) var(--size-4-3); padding: var(--size-2-3) var(--size-4-3);
padding-left: 12px; padding-left: 12px;
white-space: pre-wrap; white-space: pre-wrap;

View File

@ -24,6 +24,4 @@ const afterText = computed(() => {
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase()) + props.matched.length; const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase()) + props.matched.length;
return props.text.substring(pos); return props.text.substring(pos);
}) })
console.log(props, beforeText.value, afterText.value);
</script> </script>

View File

@ -5,14 +5,27 @@ function hideLeftPanel(_: Event)
} }
const route = useRoute(); const route = useRoute();
const showing = ref(false);
const project = parseInt(Array.isArray(route.params.projectId) ? '' : route.params.projectId); const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
const { data: navigation } = await useFetch(() => isNaN(project) ? '' : `/api/project/${project}/navigation`); const { data: navigation, execute, status, error } = await useFetch(() => `/api/project/${project.value}/navigation`, {
immediate: false,
});
if(route.params.projectId && project.value !== 0)
{
showing.value = true;
execute();
}
else
{
showing.value = false;
}
</script> </script>
<template> <template>
<div class="site-body-left-column"> <div :class="{'desktop-hidden': !showing}" class="site-body-left-column">
<div class="site-body-left-column-inner"> <div class="site-body-left-column-inner">
<div class="nav-view-outer"> <div class="nav-view-outer">
<div class="nav-view"> <div class="nav-view">

View File

@ -1,3 +1,32 @@
<template>
<slot
:data="data?.data"
:body="data?.body"
:toc="data?.toc"
:excerpt="data?.excerpt"
:error="error"
>
<MDCRenderer
v-if="body"
:tag="tag"
:class="props.class"
:body="body"
:data="data?.data"
:unwrap="props.unwrap"
:components="{
a: ProseA,
h1: ProseH1,
h2: ProseH2,
h3: ProseH3,
h4: ProseH4,
h5: ProseH5,
h6: ProseH6,
blockquote: ProseBlockquote,
}"
/>
</slot>
</template>
<script setup lang="ts"> <script setup lang="ts">
import ProseA from "~/components/content/prose/ProseA.vue"; import ProseA from "~/components/content/prose/ProseA.vue";
import ProseH1 from "~/components/content/prose/ProseH1.vue"; import ProseH1 from "~/components/content/prose/ProseH1.vue";
@ -7,27 +36,54 @@ import ProseH4 from "~/components/content/prose/ProseH4.vue";
import ProseH5 from "~/components/content/prose/ProseH5.vue"; import ProseH5 from "~/components/content/prose/ProseH5.vue";
import ProseH6 from "~/components/content/prose/ProseH6.vue"; import ProseH6 from "~/components/content/prose/ProseH6.vue";
import ProseBlockquote from "~/components/content/prose/ProseBlockquote.vue"; import ProseBlockquote from "~/components/content/prose/ProseBlockquote.vue";
const props = defineProps<{
content: string
}>();
const parser = useMarkdown();
const ast = await parser(props.content);
</script>
<template> import { hash } from 'ohash'
<Suspense> import { useAsyncData } from 'nuxt/app'
<template #fallback> import { watch, computed } from 'vue'
<div class="loading"></div>
</template> const props = defineProps({
<MDCRenderer :body="ast?.body" :data="ast?.data" :components="{ tag: {
a: ProseA, type: [String, Boolean],
h1: ProseH1, default: 'div'
h2: ProseH2, },
h3: ProseH3, content: {
h4: ProseH4, type: [String, Object],
h5: ProseH5, required: true
h6: ProseH6, },
blockquote: ProseBlockquote, excerpt: {
}" /> type: Boolean,
</Suspense> default: false
</template> },
class: {
type: [String, Array, Object],
default: ''
},
unwrap: {
type: [Boolean, String],
default: false
}
})
const model = defineModel<number>({
default: 0,
});
const parser = useMarkdown();
const key = computed(() => hash(props.content))
const { data, refresh, error } = await useAsyncData(key.value, async () => {
const timer = performance.now();
if (typeof props.content !== 'string') {
model.value = performance.now() - timer;
return props.content
}
const result = await parser(props.content);
model.value = performance.now() - timer;
return result;
})
const body = computed(() => props.excerpt ? data.value?.excerpt : data.value?.body)
watch(() => props.content, () => {
refresh()
})
</script>

View File

@ -54,8 +54,10 @@ async function debounced()
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')" @mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
@mousedown.prevent="navigateTo(`/explorer/${result.id}${result.home}`); input = ''"> @mousedown.prevent="navigateTo(`/explorer/${result.id}${result.home}`); input = ''">
<div class="suggestion-content"> <div class="suggestion-content">
<div class="suggestion-title"> <BoldContent class="suggestion-title" :text="result.name" :matched="input" />
<BoldContent :text="result.name" :matched="input" /> <div class="suggestion-subtext">
<div class="suggestion-text">{{ result.username }}</div>
<div class="suggestion-text">{{ result.pages }} pages</div>
</div> </div>
</div> </div>
</div> </div>
@ -64,10 +66,10 @@ async function debounced()
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')" @mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
@mousedown.prevent="navigateTo(`/explorer/${result.project}${result.path}`); input = ''"> @mousedown.prevent="navigateTo(`/explorer/${result.project}${result.path}`); input = ''">
<div class="suggestion-content"> <div class="suggestion-content">
<div class="suggestion-title"> <BoldContent class="suggestion-title" :text="result.title" :matched="input" />
<ProseA :href="result.path" :project="result.project"> <div class="suggestion-subtext">
<BoldContent :text="result.title" :matched="input" /> <div class="suggestion-text">{{ result.username }}</div>
</ProseA> <div class="suggestion-text">{{ result.comments }} commentaires</div>
</div> </div>
</div> </div>
</div> </div>
@ -76,11 +78,7 @@ async function debounced()
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')" @mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
@mousedown.prevent="navigateTo(`/user/${result.id}`); input = ''"> @mousedown.prevent="navigateTo(`/user/${result.id}`); input = ''">
<div class="suggestion-content"> <div class="suggestion-content">
<div class="suggestion-title"> <BoldContent class="suggestion-title" :text="result.username" :matched="input" />
<div>
<BoldContent :text="result.username" :matched="input" />
</div>
</div>
</div> </div>
</div> </div>
<div class="suggestion-empty" <div class="suggestion-empty"

View File

@ -26,7 +26,7 @@
<div v-else class="loading"></div> <div v-else class="loading"></div>
</div> </div>
</Teleport> </Teleport>
<NuxtLink :to="path" :class="class" noPrefetch @mouseenter="(e) => showPreview(e, true)" @mouseleave="hidePreview"> <NuxtLink :to="{ path, force: true }" :class="class" noPrefetch @mouseenter="(e) => showPreview(e, true)" @mouseleave="hidePreview">
<slot v-bind="$attrs"></slot> <slot v-bind="$attrs"></slot>
</NuxtLink> </NuxtLink>
</template> </template>
@ -74,22 +74,30 @@ if (parseURL(props.href).protocol !== undefined)
let id = ref(props.project); let id = ref(props.project);
if(id.value === undefined) if(id.value === undefined)
{ {
id = useProject().id; id.value = useProject().id.value;
} }
const { data: page, status, error, execute } = await useLazyFetch(`/api/project/${id.value}/file`, { const { data: page, status, error, execute } = await useLazyFetch(`/api/project/${id.value}/file`, {
query: { query: {
search: "%" + parseURL(props.href).pathname, search: "%" + parseURL(props.href).pathname,
} },
immediate: false,
dedupe: 'defer',
}); });
if(external.value) if(!external.value)
{ {
execute(); await execute();
}
if(status.value === 'error')
{
console.error(error.value);
} }
if (page.value && page.value[0]) if (page.value && page.value[0])
{ {
path.value = `/explorer/${id.value}${page.value[0].path}`; path.value = `/explorer/${id.value}${page.value[0].path}`;
console.log(path.value);
} }
</script> </script>

View File

@ -51,7 +51,7 @@ function hideLeftPanel(_: Event)
<NavigationLink v-if="hasChildren" v-for="l of link.children" :link="l" :project="project" /> <NavigationLink v-if="hasChildren" v-for="l of link.children" :link="l" :project="project" />
</div> </div>
</template> </template>
<NuxtLink @click="hideLeftPanel" v-else class="tree-item-self" :to="`/explorer/${project}${link.path}`" <NuxtLink @click="hideLeftPanel" v-else class="tree-item-self" :to="{ path: `/explorer/${project}${link.path}`, force: true }"
:active-class="'mod-active'" :data-type="link.type === 'Canvas' ? 'graph' : undefined"> :active-class="'mod-active'" :data-type="link.type === 'Canvas' ? 'graph' : undefined">
<div class="tree-item-inner">{{ link.title }}</div> <div class="tree-item-inner">{{ link.title }}</div>
</NuxtLink> </NuxtLink>

View File

@ -19,7 +19,7 @@ const hasChildren = computed(() => {
<template> <template>
<div class="tree-item"> <div class="tree-item">
<div class="tree-item-self" :class="{'is-clickable': hasChildren}" data-path="{{ props.link.title }}"> <div class="tree-item-self" :class="{'is-clickable': hasChildren}" data-path="{{ props.link.title }}">
<NuxtLink no-prefetch class="tree-item-inner" :href="{hash: '#' + props.link.id}">{{ props.link.text }}</NuxtLink> <NuxtLink no-prefetch class="tree-item-inner" :to="{ hash: '#' + props.link.id, force: true }">{{ props.link.text }}</NuxtLink>
</div> </div>
<div class="tree-item-children"> <div class="tree-item-children">
<TocLink v-if="hasChildren" v-for="link of props.link.children" :link="link" /> <TocLink v-if="hasChildren" v-for="link of props.link.children" :link="link" />

View File

@ -1,37 +1,50 @@
import type { Project } from "~/server/api/project.get"; import type { Project } from "~/types/api";
export default function useProject() export default function useProject()
{ {
const id = useState<number>("projectId", () => 1); const project = useCookie('project');
const id = useState<number>("projectId", () => parseInt(project.value ?? '0'));
const name = useState<string>("projectName", undefined); const name = useState<string>("projectName", undefined);
const owner = useState<number>("projectOwner", undefined); const owner = useState<number>("projectOwner", undefined);
const home = useState<string | null>("projectHomepage", () => null); const home = useState<string | null>("projectHomepage", () => null);
const summary = useState<string | null>("projectSummary", () => null);
return { return {
id, name, owner, home, get, set id, name, owner, home, summary, get, set
} };
} }
async function get(): Promise<void> { async function get(): Promise<boolean> {
const id = useState<number>("projectId"); const id = useState<number>("projectId");
if (!id.value) if (!id.value)
return; return false;
try { try {
const result = await $fetch(`/api/project/${id}`) as Project; const result = await $fetch(`/api/project/${id.value}`) as Project;
const name = useState<string>("projectName"); const name = useState<string>("projectName");
const owner = useState<number>("projectOwner"); const owner = useState<number>("projectOwner");
const home = useState<string | null>("projectHomepage"); const home = useState<string | null>("projectHomepage");
const summary = useState<string | null>("projectSummary");
name.value = result.name; name.value = result.name;
owner.value = result.owner; owner.value = result.owner;
home.value = result.home; home.value = result.home;
} catch(e) {} summary.value = result.summary;
return true;
} catch(e) {
return false;
}
} }
function set(id: number): void { async function set(id: number): Promise<boolean> {
const _id = useState<number>("projectId"); const _id = useState<number>("projectId");
_id.value = id; _id.value = id;
const project = useCookie('project');
project.value = id.toString();
return await get();
} }

BIN
db.sqlite

Binary file not shown.

18
pages/editing.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<div class="column">
<textarea v-model="input"></textarea>
<pre>{{ timing }}ms</pre>
</div>
<div class="column">
<Suspense>
<template #fallback>
<div class="loading"></div>
</template>
<Markdown v-if="input.length > 0" :content="input" v-model="timing"/>
</Suspense>
</div>
</template>
<script setup lang="ts">
const input = ref(""), timing = ref(0);
</script>

View File

@ -6,6 +6,10 @@ const { data: content } = await useFetch(`/api/project/${route.params.projectId}
path: unifySlug(route.params.slug) path: unifySlug(route.params.slug)
} }
}); });
const { set } = useProject();
await set(parseInt(route.params.projectId as string));
</script> </script>
<template> <template>

View File

@ -69,7 +69,7 @@ onMounted(() => {
<ContentRenderer :value="getContent(descriptions)" /> <ContentRenderer :value="getContent(descriptions)" />
<h2>Pages contenant le tag</h2> <h2>Pages contenant le tag</h2>
<div class="card-container"> <div class="card-container">
<NuxtLink :key="item._id" :href="'/explorer' + item._path" v-for="item of list" class="tag"> {{ item.title }} </NuxtLink> <NuxtLink :key="item._id" :to="{ path: '/explorer' + item._path, force: true }" v-for="item of list" class="tag"> {{ item.title }} </NuxtLink>
</div> </div>
</div> </div>
</div> </div>

24
pages/explorer/index.vue Normal file
View File

@ -0,0 +1,24 @@
<script setup lang="ts">
const { id: project } = useProject();
const { data: projects, status, error } = await useFetch('/api/project');
</script>
<template>
<Head>
<Title>Liste des projets</Title>
</Head>
<div class="site-body-center-column">
<div class="projects-container">
<div v-if="status === 'success'" class="project-list">
<div v-for="p of projects" class="project-item">
<NuxtLink class="project-title" :to="{ path: `/explorer/${p.id}${p.home}`, force: true }">{{ p.name }}</NuxtLink>
<div class="project-user">Par {{ p.username }}</div>
<div class="project-pages">{{ p.pages }} pages</div>
<div class="project-summary">{{ p.summary ?? "Sans contenu" }}</div>
</div>
</div>
<div v-else-if="status === 'pending'" class="loading"></div>
</div>
</div>
</template>

View File

@ -59,7 +59,7 @@ async function submit()
placeholder="" title="Mot de passe" placeholder="" title="Mot de passe"
:error="passwordError" /> :error="passwordError" />
<button>Se connecter</button> <button>Se connecter</button>
<NuxtLink to="/user/register">Pas de compte ?</NuxtLink> <NuxtLink :to="{ path: `/user/register`, force: true }">Pas de compte ?</NuxtLink>
</form> </form>
<div v-else-if="status === AuthStatus.loading" class="input-form"><div class="loading"></div></div> <div v-else-if="status === AuthStatus.loading" class="input-form"><div class="loading"></div></div>
<div v-else class="not-found-container"> <div v-else class="not-found-container">

View File

@ -87,7 +87,7 @@ async function submit()
title="Confirmer le mot de passe" title="Confirmer le mot de passe"
:error="confirmPassword === '' || confirmPassword === state.password ? '' : 'Les mots de passe saisies ne sont pas identique'" /> :error="confirmPassword === '' || confirmPassword === state.password ? '' : 'Les mots de passe saisies ne sont pas identique'" />
<button>S'inscrire</button> <button>S'inscrire</button>
<NuxtLink to="/user/login">Se connecter</NuxtLink> <NuxtLink :to="{ path: `/user/login`, force: true }">Se connecter</NuxtLink>
</form> </form>
<div v-else-if="status === AuthStatus.loading" class="input-form"><div class="loading"></div></div> <div v-else-if="status === AuthStatus.loading" class="input-form"><div class="loading"></div></div>
<div v-else class="not-found-container"> <div v-else class="not-found-container">

859
server/api/import.get.ts Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,8 +3,8 @@ import useDatabase from '~/composables/useDatabase';
export default defineEventHandler(async (e) => { export default defineEventHandler(async (e) => {
const project = getRouterParam(e, "projectId"); const project = getRouterParam(e, "projectId");
const where = ["project = $project"]; const where = ["id = $id"];
const criteria: Record<string, any> = { $project: project }; const criteria: Record<string, any> = { $id: project };
if (!!project) { if (!!project) {
const db = useDatabase(); const db = useDatabase();

View File

@ -6,9 +6,9 @@ export default defineEventHandler(async (e) => {
if (query.search) { if (query.search) {
const db = useDatabase(); const db = useDatabase();
const projects = db.query(`SELECT * FROM explorer_projects WHERE name LIKE ?1`).all(query.search) as Project[]; const projects = db.query(`SELECT p.*, u.username, COUNT(f.path) as pages FROM explorer_projects p LEFT JOIN users u ON p.owner = u.id LEFT JOIN explorer_files f ON f.project = p.id WHERE name LIKE ?1 AND f.type != "Folder" GROUP BY p.id`).all(query.search) as ProjectSearch[];
const files = db.query(`SELECT * FROM explorer_files WHERE title LIKE ?1 `).all(query.search) as File[]; 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 User[]; const users = db.query(`SELECT id, username FROM users WHERE username LIKE ?1`).all(query.search) as UserSearch[];
return { return {
projects, projects,

View File

@ -3,6 +3,7 @@ export interface Project {
name: string; name: string;
owner: number; owner: number;
home: string; home: string;
summary: string;
} }
export interface Navigation { export interface Navigation {
title: string; title: string;
@ -32,8 +33,25 @@ export interface User {
id: number; id: number;
username: string; username: string;
} }
export interface ProjectSearch extends Project
{
pages: number;
username: string;
}
export interface FileSearch extends File
{
comments: number;
username: string;
}
export interface CommentSearch extends Comment
{
username: string;
}
export interface UserSearch extends User
{
}
export interface Search { export interface Search {
projects: Project[]; projects: ProjectSearch[];
files: File[]; files: FileSearch[];
users: User[]; users: UserSearch[];
} }

0
types/cookies.ts Normal file
View File