♻️ Brand new post list
This commit is contained in:
11
pkg/views/src/assets/utils.css
Normal file
11
pkg/views/src/assets/utils.css
Normal file
@@ -0,0 +1,11 @@
|
||||
html, body, #app, .v-application {
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
31
pkg/views/src/components/posts/PostItem.vue
Normal file
31
pkg/views/src/components/posts/PostItem.vue
Normal 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>
|
21
pkg/views/src/components/posts/PostList.vue
Normal file
21
pkg/views/src/components/posts/PostList.vue
Normal 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>
|
@@ -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>
|
||||
|
@@ -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");
|
||||
|
@@ -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")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
10
pkg/views/src/scripts/request.ts
Normal file
10
pkg/views/src/scripts/request.ts
Normal 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)
|
||||
}
|
56
pkg/views/src/stores/userinfo.ts
Normal file
56
pkg/views/src/stores/userinfo.ts
Normal 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 }
|
||||
})
|
14
pkg/views/src/stores/wellKnown.ts
Normal file
14
pkg/views/src/stores/wellKnown.ts
Normal 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 }
|
||||
})
|
59
pkg/views/src/views/explore.vue
Normal file
59
pkg/views/src/views/explore.vue
Normal 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>
|
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div>Good morning!</div>
|
||||
</template>
|
Reference in New Issue
Block a user