✨ Frontend move to union feed
This commit is contained in:
33
pkg/views/src/components/posts/ArticleContent.vue
Normal file
33
pkg/views/src/components/posts/ArticleContent.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<section v-if="!props.contentOnly" class="mb-2">
|
||||
<h1 class="text-lg font-bold">{{ props.item?.title }}</h1>
|
||||
<div class="text-sm">{{ props.item?.description }}</div>
|
||||
</section>
|
||||
|
||||
<div v-if="props.brief">
|
||||
<router-link
|
||||
:to="{ name: 'posts.details', params: { postType: 'articles', alias: props.item?.alias ?? 'not-found' } }"
|
||||
append-icon="mdi-arrow-right"
|
||||
class="link underline text-primary font-medium"
|
||||
>
|
||||
Read more...
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<article class="prose max-w-none" v-html="parseContent(props.item?.content ?? '')" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dompurify from "dompurify";
|
||||
import { parse } from "marked";
|
||||
|
||||
const props = defineProps<{ item: any, brief?: boolean, contentOnly?: boolean }>();
|
||||
|
||||
function parseContent(src: string): string {
|
||||
return dompurify().sanitize(parse(src) as string);
|
||||
}
|
||||
</script>
|
20
pkg/views/src/components/posts/MomentContent.vue
Normal file
20
pkg/views/src/components/posts/MomentContent.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<article class="prose prose-moment" v-html="parseContent(props.item.content)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dompurify from "dompurify";
|
||||
import { parse } from "marked";
|
||||
|
||||
const props = defineProps<{ item: any }>();
|
||||
|
||||
function parseContent(src: string): string {
|
||||
return dompurify().sanitize(parse(src) as string);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.prose.prose-moment, p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
</style>
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card :loading="props.loading">
|
||||
<template #text>
|
||||
<div class="flex gap-3">
|
||||
<div>
|
||||
@ -11,9 +11,12 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="font-bold">{{ props.item?.author.nick }}</div>
|
||||
<div class="prose prose-post" v-html="parseContent(props.item.content)"></div>
|
||||
|
||||
<div v-if="props.item?.modal_type === 'article'" class="text-xs text-grey-darken-4 mb-2">Published an article</div>
|
||||
|
||||
<component :is="renderer[props.item?.model_type]" v-bind="props" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -21,14 +24,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dompurify from "dompurify";
|
||||
import { parse } from "marked";
|
||||
import type { Component } from "vue";
|
||||
import ArticleContent from "@/components/posts/ArticleContent.vue";
|
||||
import MomentContent from "@/components/posts/MomentContent.vue";
|
||||
|
||||
const props = defineProps<{ item: any }>();
|
||||
const props = defineProps<{ item: any, brief?: boolean, loading?: boolean }>();
|
||||
|
||||
function parseContent(src: string): string {
|
||||
return dompurify().sanitize(parse(src) as string);
|
||||
}
|
||||
const renderer: { [id: string]: Component } = {
|
||||
article: ArticleContent,
|
||||
moment: MomentContent
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -36,9 +41,3 @@ function parseContent(src: string): string {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.prose.prose-post, p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
</style>
|
@ -7,7 +7,7 @@
|
||||
<v-infinite-scroll :items="props.posts" :onLoad="props.loader">
|
||||
<template v-for="item in props.posts" :key="item">
|
||||
<div class="mb-3 px-1">
|
||||
<post-item :item="item" />
|
||||
<post-item :item="item" brief />
|
||||
</div>
|
||||
</template>
|
||||
</v-infinite-scroll>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-dialog v-model="editor.show" class="max-w-[540px]">
|
||||
<v-card title="New post">
|
||||
<v-dialog v-model="editor.show.moment" class="max-w-[540px]">
|
||||
<v-card title="Record a moment">
|
||||
<v-form>
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
@ -32,7 +32,7 @@
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn type="reset" color="grey" @click="editor.show = false">Cancel</v-btn>
|
||||
<v-btn type="reset" color="grey-darken-3" @click="editor.show.moment = false">Cancel</v-btn>
|
||||
<v-btn type="submit" @click.prevent>Publish</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
<v-tooltip v-for="item in navigationMenu" :text="item.name" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn flat v-bind="props" :to="{ name: item.to }" size="small" :icon="item.icon" />
|
||||
<v-btn flat exact v-bind="props" :to="{ name: item.to }" size="small" :icon="item.icon" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
@ -26,14 +26,30 @@
|
||||
<router-view />
|
||||
</v-main>
|
||||
|
||||
<v-fab
|
||||
class="editor-fab"
|
||||
icon="mdi-pencil"
|
||||
color="primary"
|
||||
size="64"
|
||||
appear
|
||||
@click="editor.show = true"
|
||||
/>
|
||||
<v-menu
|
||||
open-on-hover
|
||||
open-on-click
|
||||
:open-delay="0"
|
||||
:close-delay="1850"
|
||||
location="top"
|
||||
transition="scroll-y-reverse-transition"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-fab
|
||||
v-bind="props"
|
||||
class="editor-fab"
|
||||
icon="mdi-pencil"
|
||||
color="primary"
|
||||
size="64"
|
||||
appear
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col items-center gap-4 mb-4">
|
||||
<v-btn variant="elevated" color="secondary" icon="mdi-newspaper-variant" @click="editor.show.article = true" />
|
||||
<v-btn variant="elevated" color="accent" icon="mdi-camera-iris" @click="editor.show.moment = true" />
|
||||
</div>
|
||||
</v-menu>
|
||||
|
||||
<post-editor />
|
||||
</template>
|
||||
@ -43,7 +59,7 @@ import { ref } from "vue";
|
||||
import { useEditor } from "@/stores/editor";
|
||||
import PostEditor from "@/components/publish/PostEditor.vue";
|
||||
|
||||
const editor = useEditor()
|
||||
const editor = useEditor();
|
||||
const navigationMenu = [
|
||||
{ name: "Explore", icon: "mdi-compass", to: "explore" }
|
||||
];
|
||||
|
@ -12,6 +12,12 @@ const router = createRouter({
|
||||
path: "/",
|
||||
name: "explore",
|
||||
component: () => import("@/views/explore.vue")
|
||||
},
|
||||
|
||||
{
|
||||
path: "/p/:postType/:alias",
|
||||
name: "posts.details",
|
||||
component: () => import("@/views/posts/details.vue")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { reactive, ref } from "vue";
|
||||
|
||||
export const useEditor = defineStore("editor", () => {
|
||||
const show = ref(false);
|
||||
const show = reactive({
|
||||
moment: false,
|
||||
article: false,
|
||||
});
|
||||
|
||||
return { show };
|
||||
});
|
@ -4,7 +4,7 @@
|
||||
<post-list :loading="loading" :posts="posts" :loader="readMore" />
|
||||
</div>
|
||||
|
||||
<div class="aside sticky top-0 w-full h-fit md:min-w-[280px] md:max-w-[320px]">
|
||||
<div class="aside sticky top-0 w-full h-fit md:min-w-[280px] md:max-w-[320px] max-md:order-first">
|
||||
<v-card title="Categories">
|
||||
<v-list density="compact">
|
||||
</v-list>
|
||||
@ -18,28 +18,27 @@ import PostList from "@/components/posts/PostList.vue";
|
||||
import { reactive, ref } from "vue";
|
||||
import { request } from "@/scripts/request";
|
||||
|
||||
const error = ref<string | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const posts = ref<any[]>([]);
|
||||
|
||||
async function readPosts() {
|
||||
loading.value = true;
|
||||
const res = await request(`/api/posts?` + new URLSearchParams({
|
||||
const res = await request(`/api/feed?` + new URLSearchParams({
|
||||
take: pagination.pageSize.toString(),
|
||||
offset: ((pagination.page - 1) * pagination.pageSize).toString()
|
||||
}));
|
||||
if (res.status !== 200) {
|
||||
loading.value = false;
|
||||
error.value = await res.text();
|
||||
} else {
|
||||
error.value = null;
|
||||
loading.value = false;
|
||||
const data = await res.json();
|
||||
pagination.total = data["count"];
|
||||
posts.value.push(...data["data"]);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function readMore({ done }: any) {
|
||||
|
58
pkg/views/src/views/posts/details.vue
Normal file
58
pkg/views/src/views/posts/details.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<v-container class="flex max-md:flex-col gap-3 overflow-auto max-h-[calc(100vh-64px)] no-scrollbar">
|
||||
<div class="timeline flex-grow-1 max-w-[75ch]">
|
||||
<v-card :loading="loading">
|
||||
<article>
|
||||
<v-card-title>{{ post?.title }}</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<div class="text-sm">{{ post?.description }}</div>
|
||||
|
||||
<v-divider class="mt-5 mx-[-16px] border-opacity-50" />
|
||||
|
||||
<article-content :item="post" content-only />
|
||||
</v-card-text>
|
||||
</article>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div class="aside sticky top-0 w-full h-fit md:min-w-[280px]">
|
||||
<v-card title="Comments">
|
||||
<v-list density="compact">
|
||||
</v-list>
|
||||
</v-card>
|
||||
|
||||
<v-card title="Reactions" class="mt-3">
|
||||
<v-list density="compact">
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { request } from "@/scripts/request";
|
||||
import { useRoute } from "vue-router";
|
||||
import ArticleContent from "@/components/posts/ArticleContent.vue";
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const post = ref<any>(null);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
async function readPost() {
|
||||
loading.value = true;
|
||||
const res = await request(`/api/${route.params.postType}/${route.params.alias}?`);
|
||||
if (res.status !== 200) {
|
||||
error.value = await res.text();
|
||||
} else {
|
||||
error.value = null;
|
||||
post.value = await res.json();
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
readPost();
|
||||
</script>
|
Reference in New Issue
Block a user