♻️ Brand new post list

This commit is contained in:
2024-03-02 20:01:59 +08:00
parent 178f80c707
commit 3ae72cd9e0
23 changed files with 314 additions and 65 deletions

View File

@@ -0,0 +1,11 @@
html, body, #app, .v-application {
overflow: auto !important;
}
.no-scrollbar {
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
width: 0;
}

View File

@@ -0,0 +1,31 @@
<template>
<v-card>
<template #text>
<div class="flex gap-3">
<div>
<v-avatar
color="grey-lighten-2"
icon="mdi-account-circle"
class="rounded-card"
:src="props.item?.author.avatar"
/>
</div>
<div>
<div class="font-bold">{{ props.item?.author.nick }}</div>
{{ props.item?.content }}
</div>
</div>
</template>
</v-card>
</template>
<script setup lang="ts">
const props = defineProps<{ item: any }>();
</script>
<style scoped>
.rounded-card {
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<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">
<template v-for="(item, index) in props.posts" :key="item">
<div class="mb-3 px-1">
<post-item :item="item" />
</div>
</template>
</v-infinite-scroll>
</div>
</template>
<script setup lang="ts">
import PostItem from "@/components/posts/PostItem.vue";
const props = defineProps<{ loading: boolean, posts: any[], loader: (opts: any) => Promise<any> }>();
</script>

View File

@@ -1,13 +1,24 @@
<template>
<v-navigation-drawer v-model="drawerOpen" color="grey-lighten-5" floating>
<div class="d-flex text-center justify-center items-center h-[64px]">
<h1>Goatplaza</h1>
</div>
<v-list density="compact" nav>
</v-list>
</v-navigation-drawer>
<v-app-bar height="64" color="primary" scroll-behavior="elevate" flat>
<div class="container mx-auto px-5">
<v-app-bar-nav-icon variant="text" @click.stop="toggleDrawer"></v-app-bar-nav-icon>
<div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center">
<v-app-bar-nav-icon variant="text" @click.stop="toggleDrawer" />
<router-link :to="{ name: 'explore' }">
<h2 class="ml-2 text-lg font-500">Goatplaza</h2>
</router-link>
<v-spacer />
<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" />
</template>
</v-tooltip>
</div>
</v-app-bar>
@@ -16,12 +27,16 @@
</v-main>
</template>
<script setup>
import { ref } from "vue"
<script setup lang="ts">
import { ref } from "vue";
const drawerOpen = ref(true)
const navigationMenu = [
{ name: "Explore", icon: "mdi-compass", to: "explore" }
];
const drawerOpen = ref(true);
function toggleDrawer() {
drawerOpen.value = !drawerOpen.value
drawerOpen.value = !drawerOpen.value;
}
</script>

View File

@@ -1,41 +1,50 @@
import "virtual:uno.css"
import "virtual:uno.css";
import { createApp } from "vue"
import { createPinia } from "pinia"
import "./assets/utils.css";
import "vuetify/styles"
import { createVuetify } from "vuetify"
import * as components from "vuetify/components"
import * as directives from "vuetify/directives"
import { createApp } from "vue";
import { createPinia } from "pinia";
import "@mdi/font/css/materialdesignicons.min.css"
import "vuetify/styles";
import { createVuetify } from "vuetify";
import { md3 } from "vuetify/blueprints";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
import index from "./index.vue"
import router from "./router"
import "@mdi/font/css/materialdesignicons.min.css";
import "@fontsource/roboto/latin.css";
import "@unocss/reset/tailwind.css";
const app = createApp(index)
import index from "./index.vue";
import router from "./router";
const app = createApp(index);
app.use(
createVuetify({
components,
directives,
blueprint: md3,
theme: {
defaultTheme: "original",
themes: {
light: {
primary: "#4a5099",
secondary: "#2196f3",
accent: "#009688",
error: "#f44336",
warning: "#ff9800",
info: "#03a9f4",
success: "#4caf50"
original: {
colors: {
primary: "#4a5099",
secondary: "#2196f3",
accent: "#009688",
error: "#f44336",
warning: "#ff9800",
info: "#03a9f4",
success: "#4caf50"
}
}
}
}
})
)
);
app.use(createPinia())
app.use(router)
app.use(createPinia());
app.use(router);
app.mount("#app")
app.mount("#app");

View File

@@ -1,6 +1,5 @@
import { createRouter, createWebHistory } from "vue-router"
import MasterLayout from "@/layouts/master.vue"
import LandingPage from "@/views/landing.vue"
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -11,8 +10,8 @@ const router = createRouter({
children: [
{
path: "/",
name: "landing",
component: LandingPage
name: "explore",
component: () => import("@/views/explore.vue")
}
]
}

View File

@@ -0,0 +1,10 @@
declare global {
interface Window {
__LAUNCHPAD_TARGET__?: string
}
}
export async function request(input: string, init?: RequestInit) {
const prefix = window.__LAUNCHPAD_TARGET__ ?? ""
return await fetch(prefix + input, init)
}

View File

@@ -0,0 +1,56 @@
import Cookie from "universal-cookie"
import { defineStore } from "pinia"
import { ref } from "vue"
import { request } from "@/scripts/request"
export interface Userinfo {
isReady: boolean
isLoggedIn: boolean
displayName: string
data: any
}
const defaultUserinfo: Userinfo = {
isReady: false,
isLoggedIn: false,
displayName: "Citizen",
data: null
}
export function getAtk(): string {
return new Cookie().get("identity_auth_key")
}
export function checkLoggedIn(): boolean {
return new Cookie().get("identity_auth_key")
}
export const useUserinfo = defineStore("userinfo", () => {
const userinfo = ref(defaultUserinfo)
const isReady = ref(false)
async function readProfiles() {
if (!checkLoggedIn()) {
isReady.value = true;
}
const res = await request("/api/users/me", {
headers: { "Authorization": `Bearer ${getAtk()}` }
});
if (res.status !== 200) {
return;
}
const data = await res.json();
userinfo.value = {
isReady: true,
isLoggedIn: true,
displayName: data["nick"],
data: data
};
}
return { userinfo, isReady, readProfiles }
})

View File

@@ -0,0 +1,14 @@
import { request } from "@/scripts/request"
import { defineStore } from "pinia"
import { ref } from "vue"
export const useWellKnown = defineStore("well-known", () => {
const wellKnown = ref({})
async function readWellKnown() {
const res = await request("/.well-known")
wellKnown.value = await res.json()
}
return { wellKnown, readWellKnown }
})

View File

@@ -0,0 +1,59 @@
<template>
<v-container class="flex max-md:flex-col gap-3 overflow-auto max-h-[calc(100vh-72px)] no-scrollbar">
<div class="timeline flex-grow-1 mt-[-16px]">
<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]">
<v-card title="Categories">
<v-list density="compact">
</v-list>
</v-card>
</div>
</v-container>
</template>
<script setup lang="ts">
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 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({
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"]);
}
}
async function readMore({ done }: any) {
// Reach the end of data
if (pagination.total <= pagination.page * pagination.pageSize) {
done("empty");
return;
}
pagination.page++;
await readPosts();
done("ok");
}
readPosts();
</script>

View File

@@ -1,3 +0,0 @@
<template>
<div>Good morning!</div>
</template>