Compare commits

...

2 Commits

Author SHA1 Message Date
ea460c3623 🚀 Realm alpha 2024-03-17 22:59:31 +08:00
d954cb87e6 Realm page 2024-03-17 22:43:31 +08:00
9 changed files with 267 additions and 74 deletions

View File

@ -1,9 +1,5 @@
<template> <template>
<div class="post-list"> <div class="post-list">
<div v-if="props.loading" class="text-center py-8">
<v-progress-circular indeterminate />
</div>
<v-infinite-scroll :items="props.posts" :onLoad="props.loader"> <v-infinite-scroll :items="props.posts" :onLoad="props.loader">
<template v-for="(item, idx) in props.posts" :key="item"> <template v-for="(item, idx) in props.posts" :key="item">
<div class="mb-3 px-1"> <div class="mb-3 px-1">
@ -21,7 +17,7 @@
<script setup lang="ts"> <script setup lang="ts">
import PostItem from "@/components/posts/PostItem.vue"; import PostItem from "@/components/posts/PostItem.vue";
const props = defineProps<{ loading: boolean, posts: any[], loader: (opts: any) => Promise<any> }>(); const props = defineProps<{ posts: any[], loader: (opts: any) => Promise<any> }>();
const emits = defineEmits(["update:posts"]); const emits = defineEmits(["update:posts"]);
function updateItem(idx: number, data: any) { function updateItem(idx: number, data: any) {

View File

@ -66,7 +66,7 @@
<div> <div>
<p class="text-xs">Your content will visible for public at</p> <p class="text-xs">Your content will visible for public at</p>
<p class="text-lg font-medium"> <p class="text-lg font-medium">
{{ data.publishedAt ? new Date(data.publishedAt).toLocaleString() : new Date().toLocaleString() }} {{ data.published_at ? new Date(data.published_at).toLocaleString() : new Date().toLocaleString() }}
</p> </p>
</div> </div>
<v-btn size="small" icon="mdi-pencil" variant="text" @click="dialogs.plan = true" /> <v-btn size="small" icon="mdi-pencil" variant="text" @click="dialogs.plan = true" />
@ -85,14 +85,27 @@
</div> </div>
</template> </template>
</v-expansion-panel> </v-expansion-panel>
<v-expansion-panel title="Publish area">
<template #text>
<div class="flex justify-between items-center">
<div>
<p class="text-xs">This article will publish in</p>
<p class="text-lg font-medium">{{ currentRealm?.name ?? "No realm" }}</p>
</div>
<v-btn size="small" icon="mdi-account-group" variant="text" @click="dialogs.area = true" />
</div>
</template>
</v-expansion-panel>
</v-expansion-panels> </v-expansion-panels>
</v-container> </v-container>
</v-card-text> </v-card-text>
</v-form> </v-form>
</v-card> </v-card>
<planned-publish v-model:show="dialogs.plan" v-model:value="data.publishedAt" /> <planned-publish v-model:show="dialogs.plan" v-model:value="data.published_at" />
<media ref="media" v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" /> <media ref="media" v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" />
<publish-area v-model:show="dialogs.area" v-model:value="data.realm_id" />
<v-snackbar v-model="success" :timeout="3000">Your article has been published.</v-snackbar> <v-snackbar v-model="success" :timeout="3000">Your article has been published.</v-snackbar>
<v-snackbar v-model="uploading" :timeout="-1"> <v-snackbar v-model="uploading" :timeout="-1">
@ -108,27 +121,38 @@
import { request } from "@/scripts/request" import { request } from "@/scripts/request"
import { useEditor } from "@/stores/editor" import { useEditor } from "@/stores/editor"
import { getAtk } from "@/stores/userinfo" import { getAtk } from "@/stores/userinfo"
import { reactive, ref, watch } from "vue" import { computed, reactive, ref, watch } from "vue";
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
import Media from "@/components/publish/parts/Media.vue" import Media from "@/components/publish/parts/Media.vue"
import PublishArea from "@/components/publish/parts/PublishArea.vue";
const editor = useEditor() const editor = useEditor()
const dialogs = reactive({ const dialogs = reactive({
plan: false, plan: false,
categories: false, categories: false,
media: false media: false,
area: false,
}) })
const data = ref<any>({ const data = ref<any>({
title: "", title: "",
content: "", content: "",
description: "", description: "",
realm_id: null,
published_at: null, published_at: null,
attachments: [] attachments: []
}) })
const currentRealm = computed(() => {
if(data.value.realm_id) {
return editor.availableRealms.find((e) => e.id === data.value.realm_id)
} else {
return null
}
})
const router = useRouter() const router = useRouter()
const error = ref<string | null>(null) const error = ref<string | null>(null)
@ -146,7 +170,8 @@ async function postArticle(evt: SubmitEvent) {
console.log(payload) console.log(payload)
if (!payload.content) return if (!payload.content) return
if (!payload.title || !payload.description) return if (!payload.title || !payload.description) return
if (!payload.publishedAt) payload.publishedAt = new Date().toISOString() if (!payload.published_at) payload.published_at = new Date().toISOString()
if (!payload.realm_id) payload.realm_id = undefined
const url = editor.related.edit_to ? `/api/p/articles/${editor.related.edit_to?.id}` : "/api/p/articles" const url = editor.related.edit_to ? `/api/p/articles/${editor.related.edit_to?.id}` : "/api/p/articles"
const method = editor.related.edit_to ? "PUT" : "POST" const method = editor.related.edit_to ? "PUT" : "POST"

View File

@ -40,6 +40,18 @@
/> />
</template> </template>
</v-tooltip> </v-tooltip>
<v-tooltip text="Publish area" location="start">
<template #activator="{ props }">
<v-btn
v-bind="props"
type="button"
variant="text"
icon="mdi-account-group"
size="small"
@click="dialogs.area = true"
/>
</template>
</v-tooltip>
</div> </div>
</v-card-text> </v-card-text>
@ -54,6 +66,7 @@
<planned-publish v-model:show="dialogs.plan" v-model:value="data.published_at" /> <planned-publish v-model:show="dialogs.plan" v-model:value="data.published_at" />
<media v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" /> <media v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" />
<publish-area v-model:show="dialogs.area" v-model:value="data.realm_id" />
<v-snackbar v-model="success" :timeout="3000">Your post has been published.</v-snackbar> <v-snackbar v-model="success" :timeout="3000">Your post has been published.</v-snackbar>
<v-snackbar v-model="uploading" :timeout="-1"> <v-snackbar v-model="uploading" :timeout="-1">
@ -66,66 +79,70 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { request } from "@/scripts/request" import { request } from "@/scripts/request";
import { useEditor } from "@/stores/editor" import { useEditor } from "@/stores/editor";
import { getAtk } from "@/stores/userinfo" import { getAtk } from "@/stores/userinfo";
import { reactive, ref, watch } from "vue" import { reactive, ref, watch } from "vue";
import { useRouter } from "vue-router" import { useRouter } from "vue-router";
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue";
import Media from "@/components/publish/parts/Media.vue" import Media from "@/components/publish/parts/Media.vue";
import PublishArea from "@/components/publish/parts/PublishArea.vue";
const editor = useEditor() const editor = useEditor();
const dialogs = reactive({ const dialogs = reactive({
plan: false, plan: false,
media: false media: false,
}) area: false
});
const data = ref<any>({ const data = ref<any>({
content: "", content: "",
realm_id: null,
published_at: null, published_at: null,
attachments: [] attachments: []
}) });
const error = ref<string | null>(null) const error = ref<string | null>(null);
const success = ref(false) const success = ref(false);
const loading = ref(false) const loading = ref(false);
const uploading = ref(false) const uploading = ref(false);
const router = useRouter() const router = useRouter();
async function postMoment(evt: SubmitEvent) { async function postMoment(evt: SubmitEvent) {
const form = evt.target as HTMLFormElement const form = evt.target as HTMLFormElement;
const payload = data.value const payload = data.value;
if (!payload.content) return if (!payload.content) return;
if (!payload.published_at) payload.published_at = new Date().toISOString() if (!payload.published_at) payload.published_at = new Date().toISOString();
if (!payload.realm_id) payload.realm_id = undefined;
const url = editor.related.edit_to ? `/api/p/moments/${editor.related.edit_to?.id}` : "/api/p/moments" const url = editor.related.edit_to ? `/api/p/moments/${editor.related.edit_to?.id}` : "/api/p/moments";
const method = editor.related.edit_to ? "PUT" : "POST" const method = editor.related.edit_to ? "PUT" : "POST";
loading.value = true loading.value = true;
const res = await request(url, { const res = await request(url, {
method: method, method: method,
headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` }, headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}) });
if (res.status === 200) { if (res.status === 200) {
form.reset() form.reset();
const data = await res.json() const data = await res.json();
success.value = true success.value = true;
editor.show.moment = false editor.show.moment = false;
router.push({ name: "posts.details.moments", params: { alias: data.alias } }) router.push({ name: "posts.details.moments", params: { alias: data.alias } });
} else { } else {
error.value = await res.text() error.value = await res.text();
} }
loading.value = false loading.value = false;
} }
watch(editor.related, (val) => { watch(editor.related, (val) => {
if (val.edit_to && val.edit_to.model_type === "moment") { if (val.edit_to && val.edit_to.model_type === "moment") {
data.value = val.edit_to data.value = val.edit_to;
} }
}) });
</script> </script>
<style> <style>

View File

@ -0,0 +1,37 @@
<template>
<v-dialog
eager
class="max-w-[540px]"
:model-value="props.show"
@update:model-value="(val) => emits('update:show', val)"
>
<v-card title="Change your audiences">
<template #text>
<v-select
clearable
class="mt-2"
label="Realm"
hint="This field will only show realms you joined. Leave blank to publish this post in public area."
variant="solo-filled"
item-title="name"
item-value="id"
:items="editor.availableRealms"
:model-value="props.value"
@update:model-value="(val) => emits('update:value', val)"
/>
</template>
<template #actions>
<v-btn class="ms-auto" text="Ok" @click="emits('update:show', false)"></v-btn>
</template>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { useEditor } from "@/stores/editor";
const editor = useEditor();
const props = defineProps<{ show: boolean; value: string | null }>();
const emits = defineEmits(["update:show", "update:value"]);
</script>

View File

@ -1,7 +1,21 @@
<template> <template>
<v-list density="comfortable"> <v-list density="comfortable">
<v-list-subheader>Realms</v-list-subheader> <v-list-subheader>
<v-list-item v-for="item in realms" prepend-icon="mdi-account-multiple" :title="item.name" /> Realms
<v-badge
color="warning"
content="Alpha"
inline
/>
</v-list-subheader>
<v-list-item
v-for="item in realms"
exact
prepend-icon="mdi-account-multiple"
:to="{ name: 'realms.details', 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.length > 0" class="border-opacity-75 my-2" />
@ -54,12 +68,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { computed, ref } from "vue";
import { getAtk, useUserinfo } from "@/stores/userinfo"; import { getAtk, useUserinfo } from "@/stores/userinfo";
import { useEditor } from "@/stores/editor";
const id = useUserinfo(); const id = useUserinfo();
const editor = useEditor();
const realms = ref<any[]>([]); const realms = computed(() => editor.availableRealms);
const requestData = ref({ const requestData = ref({
name: "", name: "",
description: "", description: "",
@ -80,13 +96,10 @@ const loading = ref(false);
async function list() { async function list() {
reverting.value = true; reverting.value = true;
const res = await fetch("/api/realms/me/available", { try {
headers: { Authorization: `Bearer ${getAtk()}` } await editor.listRealms();
}); } catch (err) {
if (res.status !== 200) { error.value = (err as Error).message;
error.value = await res.text();
} else {
realms.value = await res.json();
} }
reverting.value = false; reverting.value = false;
} }
@ -111,6 +124,4 @@ async function submit(evt: SubmitEvent) {
} }
loading.value = false; loading.value = false;
} }
list();
</script> </script>

View File

@ -1,5 +1,5 @@
import { createRouter, createWebHistory } from "vue-router" import { createRouter, createWebHistory } from "vue-router";
import MasterLayout from "@/layouts/master.vue" import MasterLayout from "@/layouts/master.vue";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -24,10 +24,16 @@ const router = createRouter({
path: "/p/articles/:alias", path: "/p/articles/:alias",
name: "posts.details.articles", name: "posts.details.articles",
component: () => import("@/views/posts/articles.vue") component: () => import("@/views/posts/articles.vue")
},
{
path: "/realms/:realmId",
name: "realms.details",
component: () => import("@/views/realms/details.vue")
} }
] ]
} }
] ]
}) });
export default router export default router;

View File

@ -1,16 +1,16 @@
import { defineStore } from "pinia" import { defineStore } from "pinia";
import { reactive, ref } from "vue" import { reactive, ref } from "vue";
import { getAtk } from "@/stores/userinfo" import { checkLoggedIn, getAtk } from "@/stores/userinfo";
export const useEditor = defineStore("editor", () => { export const useEditor = defineStore("editor", () => {
const done = ref(false) const done = ref(false);
const show = reactive({ const show = reactive({
moment: false, moment: false,
article: false, article: false,
comment: false, comment: false,
delete: false delete: false
}) });
const related = reactive<{ const related = reactive<{
edit_to: any edit_to: any
@ -24,7 +24,24 @@ export const useEditor = defineStore("editor", () => {
reply_to: null, reply_to: null,
repost_to: null, repost_to: null,
delete_to: null delete_to: null
}) });
return { show, related, done } 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 };
});

View File

@ -1,7 +1,7 @@
<template> <template>
<v-container class="flex max-md:flex-col gap-3 overflow-auto max-h-[calc(100vh-64px)] no-scrollbar"> <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]"> <div class="timeline flex-grow-1 mt-[-16px]">
<post-list v-model:posts="posts" :loading="loading" :loader="readMore" /> <post-list v-model:posts="posts" :loader="readMore" />
</div> </div>
<div class="aside sticky top-0 w-full h-fit md:min-w-[280px] md:max-w-[320px] max-md:order-first"> <div class="aside sticky top-0 w-full h-fit md:min-w-[280px] md:max-w-[320px] max-md:order-first">
@ -19,14 +19,12 @@ import PostList from "@/components/posts/PostList.vue";
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { request } from "@/scripts/request"; import { request } from "@/scripts/request";
const loading = ref(false);
const error = ref<string | null>(null); const error = ref<string | null>(null);
const pagination = reactive({ page: 1, pageSize: 10, total: 0 }); const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
const posts = ref<any[]>([]); const posts = ref<any[]>([]);
async function readPosts() { async function readPosts() {
loading.value = true;
const res = await request(`/api/feed?` + new URLSearchParams({ const res = await request(`/api/feed?` + new URLSearchParams({
take: pagination.pageSize.toString(), take: pagination.pageSize.toString(),
offset: ((pagination.page - 1) * pagination.pageSize).toString() offset: ((pagination.page - 1) * pagination.pageSize).toString()
@ -37,9 +35,8 @@ async function readPosts() {
error.value = null; error.value = null;
const data = await res.json(); const data = await res.json();
pagination.total = data["count"]; pagination.total = data["count"];
posts.value.push(...data["data"]); posts.value.push(...(data["data"] ?? []));
} }
loading.value = false;
} }
async function readMore({ done }: any) { async function readMore({ done }: any) {
@ -52,7 +49,11 @@ async function readMore({ done }: any) {
pagination.page++; pagination.page++;
await readPosts(); await readPosts();
done("ok"); if (error.value != null) done("error");
else {
if (pagination.total > 0) done("ok");
else done("empty");
}
} }
readPosts(); readPosts();

View File

@ -0,0 +1,83 @@
<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>