Compare commits

..

2 Commits

Author SHA1 Message Date
c14d3f70a3 Editable realm 2024-03-19 22:03:58 +08:00
2f87f9bc32 More places will show menu 2024-03-19 20:35:05 +08:00
25 changed files with 391 additions and 385 deletions

8
.idea/.gitignore generated vendored
View File

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/Interactive.iml generated
View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -1,53 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

12
.idea/dataSources.xml generated
View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="hy_interactive@localhost" uuid="2e2101b2-4037-47ee-88ed-456dc2cb4423">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/hy_interactive</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@ -1,11 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="54" name="TypeScript" />
</Languages>
</inspection_tool>
<inspection_tool class="SqlDialectInspection" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Interactive.iml" filepath="$PROJECT_DIR$/.idea/Interactive.iml" />
</modules>
</component>
</project>

7
.idea/sqldialects.xml generated
View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/pkg/server/moments_api.go" dialect="PostgreSQL" />
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/2e2101b2-4037-47ee-88ed-456dc2cb4423/console.sql" dialect="PostgreSQL" />
</component>
</project>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -11,5 +11,9 @@ module.exports = {
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'vue/multi-word-component-names': 'off',
'vue/valid-v-for': 'off'
}
}

View File

@ -0,0 +1,43 @@
<template>
<v-menu>
<template #activator="{ props }">
<v-btn v-bind="props" icon="mdi-dots-vertical" variant="text" size="x-small" />
</template>
<v-list density="compact" lines="one">
<v-list-item disabled append-icon="mdi-flag" title="Report" />
<v-list-item v-if="isOwned" append-icon="mdi-pencil" title="Edit" @click="editPost" />
<v-list-item v-if="isOwned" append-icon="mdi-delete" title="Delete" @click="deletePost" />
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import { useEditor } from "@/stores/editor"
import { useUserinfo } from "@/stores/userinfo";
import { computed } from "vue"
const id = useUserinfo()
const editor = useEditor()
const props = defineProps<{ item: any }>()
const isOwned = computed(() => props.item?.author_id === id.userinfo.data.id)
function editPost() {
editor.related.edit_to = props.item
if (editor.show.hasOwnProperty(props.item.model_type)) {
// @ts-ignore
editor.show[props.item.model_type] = true
}
if (props.item.model_type === "comment") {
editor.related.comment_to = props.item
}
}
function deletePost() {
editor.related.delete_to = JSON.parse(JSON.stringify(props.item))
editor.related.delete_to.model_type = props.item.model_type + "s"
editor.show.delete = true
}
</script>

View File

@ -9,7 +9,7 @@
/>
</div>
<div class="flex-grow-1 relative">
<div class="flex-grow-1">
<div class="font-bold">{{ props.item?.author.nick }}</div>
<div v-if="props.item?.model_type === 'article'" class="text-xs text-grey-darken-4 mb-2">
@ -36,66 +36,35 @@
<div class="mt-1 text-xs opacity-80 flex gap-2 items-center">
<span>Posted at {{ new Date(props.item?.created_at).toLocaleString() }}</span>
</div>
</div>
<v-menu>
<template #activator="{ props }">
<div class="absolute right-0 top-0">
<v-btn v-bind="props" icon="mdi-dots-vertical" variant="text" size="x-small" />
</div>
</template>
<v-list density="compact" lines="one">
<v-list-item disabled append-icon="mdi-flag" title="Report" />
<v-list-item v-if="isOwned" append-icon="mdi-pencil" title="Edit" @click="editPost" />
<v-list-item v-if="isOwned" append-icon="mdi-delete" title="Delete" @click="deletePost" />
</v-list>
</v-menu>
<div>
<post-action :item="props.item" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed, type Component } from "vue"
import { type Component } from "vue"
import { useUserinfo } from "@/stores/userinfo"
import { useEditor } from "@/stores/editor"
import ArticleContent from "@/components/posts/ArticleContent.vue"
import MomentContent from "@/components/posts/MomentContent.vue"
import CommentContent from "@/components/posts/CommentContent.vue"
import PostAttachment from "./PostAttachment.vue"
import PostAttachment from "@/components/posts/PostAttachment.vue"
import PostReaction from "@/components/posts/PostReaction.vue"
import PostAction from "@/components/posts/PostAction.vue"
const id = useUserinfo()
const props = defineProps<{ item: any; brief?: boolean }>()
const emits = defineEmits(["update:item"])
const editor = useEditor()
const renderer: { [id: string]: Component } = {
article: ArticleContent,
moment: MomentContent,
comment: CommentContent
}
const isOwned = computed(() => props.item?.author_id === id.userinfo.data.id)
function editPost() {
editor.related.edit_to = props.item
if (editor.show.hasOwnProperty(props.item.model_type)) {
// @ts-ignore
editor.show[props.item.model_type] = true
}
if (props.item.model_type === "comment") {
editor.related.comment_to = props.item
}
}
function deletePost() {
editor.related.delete_to = JSON.parse(JSON.stringify(props.item))
editor.related.delete_to.model_type = props.item.model_type + "s"
editor.show.delete = true
}
function updateReactions(symbol: string, num: number) {
const item = JSON.parse(JSON.stringify(props.item))
if (item.reaction_list == null) {

View File

@ -0,0 +1,36 @@
<template>
<v-menu>
<template #activator="{ props }">
<v-btn v-bind="props" icon="mdi-dots-vertical" variant="text" size="x-small" />
</template>
<v-list density="compact" lines="one">
<v-list-item disabled append-icon="mdi-flag" title="Report" />
<v-list-item v-if="isOwned" append-icon="mdi-pencil" title="Edit" @click="editRealm" />
<v-list-item v-if="isOwned" append-icon="mdi-delete" title="Delete" @click="deleteRealm" />
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import { useRealms } from "@/stores/realms";
import { useUserinfo } from "@/stores/userinfo";
import { computed } from "vue"
const id = useUserinfo()
const realms = useRealms()
const props = defineProps<{ item: any }>()
const isOwned = computed(() => props.item?.account_id === id.userinfo.data.id)
function editRealm() {
realms.related.edit_to = props.item
realms.show.editor = true
}
function deleteRealm() {
realms.related.delete_to = props.item
realms.show.delete = true
}
</script>

View File

@ -0,0 +1,88 @@
<template>
<v-card title="Organize a realm" prepend-icon="mdi-account-multiple" :loading="loading">
<v-form @submit.prevent="submit">
<v-card-text>
<v-text-field label="Name" variant="outlined" density="comfortable" v-model="data.name" />
<v-textarea label="Description" variant="outlined" density="comfortable" v-model="data.description" />
<v-select
label="Realm type"
item-title="label"
item-value="value"
variant="outlined"
density="comfortable"
:items="realmTypeOptions"
v-model="data.realm_type"
/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn type="reset" color="grey-darken-3" @click="realms.show.editor = false">Cancel</v-btn>
<v-btn type="submit" :disabled="loading">Save</v-btn>
</v-card-actions>
</v-form>
</v-card>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
import { ref, watch } from "vue"
import { getAtk } from "@/stores/userinfo"
import { useRealms } from "@/stores/realms"
const emits = defineEmits(["relist"])
const realms = useRealms()
const realmTypeOptions = [
{ label: "Public Realm", value: 0 },
{ label: "Restricted Realm", value: 1 },
{ label: "Private Realm", value: 2 }
]
const error = ref<null | string>(null)
const loading = ref(false)
const data = ref({
name: "",
description: "",
realm_type: 0
})
async function submit(evt: SubmitEvent) {
const form = evt.target as HTMLFormElement
const payload = data.value
if (!payload.name) return
const url = realms.related.edit_to ? `/api/realms/${realms.related.edit_to?.id}` : "/api/moments";
const method = realms.related.edit_to ? "PUT" : "POST";
loading.value = true
const res = await fetch(url, {
method: method,
headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` },
body: JSON.stringify(payload)
})
if (res.status !== 200) {
error.value = await res.text()
} else {
emits("relist")
form.reset()
realms.done = true
realms.show.editor = false
}
loading.value = false
}
watch(
realms.related,
(val) => {
if (val.edit_to) {
data.value = JSON.parse(JSON.stringify(val.edit_to))
}
},
{ immediate: true }
)
</script>

View File

@ -2,126 +2,38 @@
<v-list density="comfortable">
<v-list-subheader>
Realms
<v-badge
color="warning"
content="Alpha"
inline
/>
<v-badge color="warning" content="Alpha" inline />
</v-list-subheader>
<v-list-item
v-for="item in realms"
v-for="item in realms.available"
exact
prepend-icon="mdi-account-multiple"
:to="{ name: 'realms.details', params: { realmId: item.id } }"
:to="{ name: 'realms.page', params: { realmId: item.id } }"
:title="item.name"
/>
<v-divider v-if="realms.length > 0" class="border-opacity-75 my-2" />
<v-divider v-if="realms.available.length > 0" class="border-opacity-75 my-2" />
<v-list-item
prepend-icon="mdi-plus"
title="Create a realm"
:disabled="!id.userinfo.isLoggedIn"
@click="creating = true"
@click="createRealm"
/>
</v-list>
<v-dialog v-model="creating" class="max-w-[540px]">
<v-card title="Create a realm" prepend-icon="mdi-account-multiple-plus" :loading="loading">
<v-form @submit.prevent="submit">
<v-card-text>
<v-text-field
label="Name"
variant="outlined"
density="comfortable"
v-model="requestData.name"
/>
<v-textarea
label="Description"
variant="outlined"
density="comfortable"
v-model="requestData.description"
/>
<v-select
label="Realm type"
item-title="label"
item-value="value"
variant="outlined"
density="comfortable"
:items="realmTypeOptions"
v-model="requestData.realm_type"
/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn type="reset" color="grey-darken-3" @click="creating = false">Cancel</v-btn>
<v-btn type="submit" :disabled="loading">Save</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { getAtk, useUserinfo } from "@/stores/userinfo";
import { useEditor } from "@/stores/editor";
import { useUserinfo } from "@/stores/userinfo"
import { useRealms } from "@/stores/realms"
const id = useUserinfo();
const editor = useEditor();
const id = useUserinfo()
const realms = useRealms()
const realms = computed(() => editor.availableRealms);
const requestData = ref({
name: "",
description: "",
realm_type: 0
});
const realmTypeOptions = [
{ label: "Public Realm", value: 0 },
{ label: "Restricted Realm", value: 1 },
{ label: "Private Realm", value: 2 }
];
const creating = ref(false);
const error = ref<string | null>(null);
const reverting = ref(false);
const loading = ref(false);
async function list() {
reverting.value = true;
try {
await editor.listRealms();
} catch (err) {
error.value = (err as Error).message;
}
reverting.value = false;
}
async function submit(evt: SubmitEvent) {
const form = evt.target as HTMLFormElement;
const payload = requestData.value;
if (!payload.name) return;
loading.value = true;
const res = await fetch("/api/realms", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` },
body: JSON.stringify(payload)
});
if (res.status !== 200) {
error.value = await res.text();
} else {
await list();
form.reset();
creating.value = false;
}
loading.value = false;
function createRealm() {
realms.related.edit_to = null
realms.related.delete_to = null
realms.show.editor = true
}
</script>

View File

@ -0,0 +1,12 @@
<template>
<v-dialog v-model="realms.show.editor" class="max-w-[540px]">
<realm-editor @relist="realms.list" />
</v-dialog>
</template>
<script setup lang="ts">
import { useRealms } from "@/stores/realms"
import RealmEditor from "@/components/realms/RealmEditor.vue"
const realms = useRealms()
</script>

View File

@ -88,7 +88,8 @@
</div>
</v-menu>
<post-action />
<post-tools />
<realm-tools />
</template>
<script setup lang="ts">
@ -96,7 +97,8 @@ import { computed, ref } from "vue"
import { useEditor } from "@/stores/editor"
import { useUserinfo } from "@/stores/userinfo"
import { useWellKnown } from "@/stores/wellKnown"
import PostAction from "@/components/publish/PostAction.vue"
import PostTools from "@/components/publish/PostTools.vue"
import RealmTools from "@/components/realms/RealmTools.vue"
import RealmList from "@/components/realms/RealmList.vue";
const id = useUserinfo()

View File

@ -28,8 +28,8 @@ const router = createRouter({
{
path: "/realms/:realmId",
name: "realms.details",
component: () => import("@/views/realms/details.vue")
name: "realms.page",
component: () => import("@/views/realms/page.vue")
}
]
}

View File

@ -1,16 +1,15 @@
import { defineStore } from "pinia";
import { reactive, ref } from "vue";
import { checkLoggedIn, getAtk } from "@/stores/userinfo";
import { defineStore } from "pinia"
import { reactive, ref } from "vue"
export const useEditor = defineStore("editor", () => {
const done = ref(false);
const done = ref(false)
const show = reactive({
moment: false,
article: false,
comment: false,
delete: false
});
})
const related = reactive<{
edit_to: any
@ -24,24 +23,7 @@ export const useEditor = defineStore("editor", () => {
reply_to: null,
repost_to: null,
delete_to: null
});
})
const availableRealms = ref<any[]>([]);
async function listRealms() {
if (!checkLoggedIn()) return;
const res = await fetch("/api/realms/me/available", {
headers: { Authorization: `Bearer ${getAtk()}` }
});
if (res.status !== 200) {
throw new Error(await res.text());
} else {
availableRealms.value = await res.json();
}
}
listRealms().then(() => console.log("[STARTUP HOOK] Fetch available realm successes."));
return { show, related, availableRealms, listRealms, done };
});
return { show, related, done }
})

View File

@ -0,0 +1,36 @@
import { reactive, ref } from "vue"
import { defineStore } from "pinia"
import { checkLoggedIn, getAtk } from "@/stores/userinfo"
export const useRealms = defineStore("realms", () => {
const done = ref(false)
const show = reactive({
editor: false,
delete: false
})
const related_to = reactive<{ edit_to: any; delete_to: any }>({
edit_to: null,
delete_to: null
})
const available = ref<any[]>([])
async function list() {
if (!checkLoggedIn()) return
const res = await fetch("/api/realms/me/available", {
headers: { Authorization: `Bearer ${getAtk()}` }
})
if (res.status !== 200) {
throw new Error(await res.text())
} else {
available.value = await res.json()
}
}
list().then(() => console.log("[STARTUP HOOK] Fetch available realm successes."))
return { done, show, related: related_to, available, list }
})

View File

@ -4,9 +4,15 @@
<v-card :loading="loading">
<article>
<v-card-text>
<div class="px-3">
<h1 class="text-lg font-medium">{{ post?.title }}</h1>
<p class="text-sm">{{ post?.description }}</p>
<div class="flex justify-between px-3">
<div>
<h1 class="text-lg font-medium">{{ post?.title }}</h1>
<p class="text-sm">{{ post?.description }}</p>
</div>
<div>
<post-action :item="post" />
</div>
</div>
<v-divider class="my-5 mx-[-16px] border-opacity-50" />
@ -56,11 +62,12 @@
<script setup lang="ts">
import { ref } from "vue"
import { useRoute } from "vue-router"
import { request } from "@/scripts/request"
import ArticleContent from "@/components/posts/ArticleContent.vue"
import PostReaction from "@/components/posts/PostReaction.vue"
import PostAction from "@/components/posts/PostAction.vue"
import CommentList from "@/components/comments/CommentList.vue"
import { useRoute } from "vue-router"
const loading = ref(false)
const error = ref<string | null>(null)

View File

@ -4,19 +4,25 @@
<v-card :loading="loading">
<article>
<v-card-text>
<div class="flex gap-2">
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card"
:image="post?.author.avatar"
/>
<div class="flex justify-between px-3">
<div class="flex gap-1">
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card me-2"
:image="post?.author.avatar"
/>
<div>
<p class="font-bold">{{ post?.author.nick }}</p>
<p class="opacity-80">
{{ post?.author.description ? post?.author.description : "No description yet." }}
</p>
</div>
</div>
<div>
<p class="font-bold">{{ post?.author.nick }}</p>
<p class="opacity-80">
{{ post?.author.description ? post?.author.description : "No description yet." }}
</p>
<post-action :item="post" />
</div>
</div>
@ -69,6 +75,7 @@ import MomentContent from "@/components/posts/MomentContent.vue"
import PostReaction from "@/components/posts/PostReaction.vue"
import CommentList from "@/components/comments/CommentList.vue"
import PostAttachment from "@/components/posts/PostAttachment.vue"
import PostAction from "@/components/posts/PostAction.vue"
const loading = ref(false)
const error = ref<string | null>(null)

View File

@ -1,83 +0,0 @@
<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 mt-[-16px]">
<post-list v-model:posts="posts" :loader="readMore" />
</div>
<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="Realm Info" :loading="loading">
<template #text>
<h2 class="font-medium">Name</h2>
<p>{{ metadata?.name }}</p>
<h2 class="font-medium mt-2">Description</h2>
<p>{{ metadata?.description }}</p>
</template>
</v-card>
</div>
</v-container>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue";
import { request } from "@/scripts/request";
import { useRoute } from "vue-router";
import PostList from "@/components/posts/PostList.vue";
const route = useRoute();
const loading = ref(false);
const error = ref<string | null>(null);
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
const metadata = ref<any>(null);
const posts = ref<any[]>([]);
async function readMetadata() {
loading.value = true;
const res = await request(`/api/realms/${route.params.realmId}`);
if (res.status !== 200) {
error.value = await res.text();
} else {
error.value = null;
metadata.value = await res.json();
}
loading.value = false;
}
async function readPosts() {
const res = await request(`/api/feed?` + new URLSearchParams({
take: pagination.pageSize.toString(),
offset: ((pagination.page - 1) * pagination.pageSize).toString(),
realmId: route.params.realmId as string
}));
if (res.status !== 200) {
error.value = await res.text();
} else {
error.value = null;
const data = await res.json();
pagination.total = data["count"];
posts.value.push(...(data["data"] ?? []));
}
}
async function readMore({ done }: any) {
// Reach the end of data
if (pagination.total <= pagination.page * pagination.pageSize) {
done("empty");
return;
}
pagination.page++;
await readPosts();
if (error.value != null) done("error");
else {
if (pagination.total > 0) done("ok");
else done("empty");
}
}
readMetadata();
readPosts();
</script>

View File

@ -0,0 +1,110 @@
<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 mt-[-16px]">
<post-list v-model:posts="posts" :loader="readMore" />
</div>
<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="Realm Info" :loading="loading">
<template #title>
<div class="flex justify-between">
<span>Realm Info</span>
<realm-action :item="metadata" />
</div>
</template>
<template #text>
<div>
<h2 class="font-medium">Name</h2>
<p>{{ metadata?.name }}</p>
<h2 class="font-medium mt-2">Description</h2>
<div v-html="parseContent(metadata?.description ?? '')"></div>
</div>
</template>
</v-card>
</div>
</v-container>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from "vue"
import { request } from "@/scripts/request"
import { useRealms } from "@/stores/realms"
import { useRoute } from "vue-router"
import { parse } from "marked"
import dompurify from "dompurify"
import PostList from "@/components/posts/PostList.vue"
import RealmAction from "@/components/realms/RealmAction.vue"
const route = useRoute()
const realms = useRealms()
const loading = ref(false)
const error = ref<string | null>(null)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const metadata = ref<any>(null)
const posts = ref<any[]>([])
async function readMetadata() {
loading.value = true
const res = await request(`/api/realms/${route.params.realmId}`)
if (res.status !== 200) {
error.value = await res.text()
} else {
error.value = null
metadata.value = await res.json()
}
loading.value = false
}
async function readPosts() {
const res = await request(
`/api/feed?` +
new URLSearchParams({
take: pagination.pageSize.toString(),
offset: ((pagination.page - 1) * pagination.pageSize).toString(),
realmId: route.params.realmId as string
})
)
if (res.status !== 200) {
error.value = await res.text()
} else {
error.value = null
const data = await res.json()
pagination.total = data["count"]
posts.value.push(...(data["data"] ?? []))
}
}
async function readMore({ done }: any) {
// Reach the end of data
if (pagination.total <= pagination.page * pagination.pageSize) {
done("empty")
return
}
pagination.page++
await readPosts()
if (error.value != null) done("error")
else {
if (pagination.total > 0) done("ok")
else done("empty")
}
}
readMetadata()
readPosts()
watch(realms, (val) => {
if (val.done) {
readMetadata().then(() => (realms.done = false))
}
})
function parseContent(src: string): string {
return dompurify().sanitize(parse(src) as string)
}
</script>