♻️ Brand new post list
This commit is contained in:
parent
178f80c707
commit
3ae72cd9e0
@ -5,10 +5,8 @@ import "time"
|
||||
type Post struct {
|
||||
BaseModel
|
||||
|
||||
Alias string `json:"alias" gorm:"uniqueIndex"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Tags []Tag `json:"tags" gorm:"many2many:post_tags"`
|
||||
Hashtags []Tag `json:"tags" gorm:"many2many:post_tags"`
|
||||
Categories []Category `json:"categories" gorm:"many2many:post_categories"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
LikedAccounts []PostLike `json:"liked_accounts"`
|
||||
|
@ -12,13 +12,13 @@ import (
|
||||
func getOwnPost(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
id := c.Params("postId")
|
||||
id, _ := c.ParamsInt("postId", 0)
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
tx := database.C.Where(&models.Post{
|
||||
Alias: id,
|
||||
AuthorID: user.ID,
|
||||
BaseModel: models.BaseModel{ID: uint(id)},
|
||||
AuthorID: user.ID,
|
||||
})
|
||||
|
||||
post, err := services.GetPost(tx)
|
||||
|
@ -13,12 +13,12 @@ import (
|
||||
)
|
||||
|
||||
func getPost(c *fiber.Ctx) error {
|
||||
id := c.Params("postId")
|
||||
id, _ := c.ParamsInt("postId", 0)
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
tx := database.C.Where(&models.Post{
|
||||
Alias: id,
|
||||
BaseModel: models.BaseModel{ID: uint(id)},
|
||||
}).Where("published_at <= ? OR published_at IS NULL", time.Now())
|
||||
|
||||
post, err := services.GetPost(tx)
|
||||
@ -162,8 +162,6 @@ func createPost(c *fiber.Ctx) error {
|
||||
post, err := services.NewPost(
|
||||
user,
|
||||
realm,
|
||||
data.Alias,
|
||||
data.Title,
|
||||
data.Content,
|
||||
data.Attachments,
|
||||
data.Categories,
|
||||
@ -207,8 +205,6 @@ func editPost(c *fiber.Ctx) error {
|
||||
|
||||
post, err := services.EditPost(
|
||||
post,
|
||||
data.Alias,
|
||||
data.Title,
|
||||
data.Content,
|
||||
data.PublishedAt,
|
||||
data.Categories,
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.smartsheep.studio/hydrogen/identity/pkg/views"
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/views"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
|
@ -20,7 +20,7 @@ func PreloadRelatedPost(tx *gorm.DB) *gorm.DB {
|
||||
Preload("Author").
|
||||
Preload("Attachments").
|
||||
Preload("Categories").
|
||||
Preload("Tags").
|
||||
Preload("Hashtags").
|
||||
Preload("RepostTo").
|
||||
Preload("ReplyTo").
|
||||
Preload("RepostTo.Author").
|
||||
@ -29,8 +29,8 @@ func PreloadRelatedPost(tx *gorm.DB) *gorm.DB {
|
||||
Preload("ReplyTo.Attachments").
|
||||
Preload("RepostTo.Categories").
|
||||
Preload("ReplyTo.Categories").
|
||||
Preload("RepostTo.Tags").
|
||||
Preload("ReplyTo.Tags")
|
||||
Preload("RepostTo.Hashtags").
|
||||
Preload("ReplyTo.Hashtags")
|
||||
}
|
||||
|
||||
func FilterPostWithCategory(tx *gorm.DB, alias string) *gorm.DB {
|
||||
@ -161,7 +161,7 @@ WHERE t.id IN ?`, prefix, prefix, prefix, prefix, prefix), postIds).Scan(&reactI
|
||||
func NewPost(
|
||||
user models.Account,
|
||||
realm *models.Realm,
|
||||
alias, title, content string,
|
||||
content string,
|
||||
attachments []models.Attachment,
|
||||
categories []models.Category,
|
||||
tags []models.Tag,
|
||||
@ -202,11 +202,9 @@ func NewPost(
|
||||
}
|
||||
|
||||
post = models.Post{
|
||||
Alias: alias,
|
||||
Title: title,
|
||||
Content: content,
|
||||
Attachments: attachments,
|
||||
Tags: tags,
|
||||
Hashtags: tags,
|
||||
Categories: categories,
|
||||
AuthorID: user.ID,
|
||||
RealmID: realmId,
|
||||
@ -225,7 +223,7 @@ func NewPost(
|
||||
BaseModel: models.BaseModel{ID: *post.ReplyID},
|
||||
}).Preload("Author").First(&op).Error; err == nil {
|
||||
if op.Author.ID != user.ID {
|
||||
postUrl := fmt.Sprintf("https://%s/posts/%s", viper.GetString("domain"), post.Alias)
|
||||
postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), post.ID)
|
||||
err := NotifyAccount(
|
||||
op.Author,
|
||||
fmt.Sprintf("%s replied you", user.Name),
|
||||
@ -252,7 +250,7 @@ func NewPost(
|
||||
})
|
||||
|
||||
for _, account := range accounts {
|
||||
postUrl := fmt.Sprintf("https://%s/posts/%s", viper.GetString("domain"), post.Alias)
|
||||
postUrl := fmt.Sprintf("https://%s/posts/%d", viper.GetString("domain"), post.ID)
|
||||
err := NotifyAccount(
|
||||
account,
|
||||
fmt.Sprintf("%s just posted a post", user.Name),
|
||||
@ -270,7 +268,7 @@ func NewPost(
|
||||
|
||||
func EditPost(
|
||||
post models.Post,
|
||||
alias, title, content string,
|
||||
content string,
|
||||
publishedAt *time.Time,
|
||||
categories []models.Category,
|
||||
tags []models.Tag,
|
||||
@ -294,11 +292,9 @@ func EditPost(
|
||||
publishedAt = lo.ToPtr(time.Now())
|
||||
}
|
||||
|
||||
post.Alias = alias
|
||||
post.Title = title
|
||||
post.Content = content
|
||||
post.PublishedAt = *publishedAt
|
||||
post.Tags = tags
|
||||
post.Hashtags = tags
|
||||
post.Categories = categories
|
||||
post.Attachments = attachments
|
||||
|
||||
|
Binary file not shown.
6
pkg/views/embed.go
Normal file
6
pkg/views/embed.go
Normal file
@ -0,0 +1,6 @@
|
||||
package views
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed all:dist
|
||||
var FS embed.FS
|
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Goatplaza</title>
|
||||
</head>
|
||||
|
@ -13,9 +13,11 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@unocss/reset": "^0.58.5",
|
||||
"pinia": "^2.1.7",
|
||||
"universal-cookie": "^7.1.0",
|
||||
"unocss": "^0.58.5",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
|
21
pkg/views/public/favicon.svg
Executable file
21
pkg/views/public/favicon.svg
Executable file
@ -0,0 +1,21 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
|
||||
<title>SmartSheep Logo</title>
|
||||
<defs>
|
||||
<image width="124" height="198" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHwAAADGCAMAAAAnkRSfAAAAAXNSR0IB2cksfwAAAq9QTFRFAAAA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////J9QBRAAAAOV0Uk5TAA0WEiFfjMLMr40/BRdmtuz//vbQYAQmpfLdQAGovyURj/iOD0r07loDGJfbPDTf/bhd94QLdPrrVQLWcPmtHU31egknzehRg9Ev1xnzbgYHhfzmTMorSOSbFJloKsniReXDKG/7kQ6m8WMw3ue7I4cKNdTZOFTqsR5/Hzra0zZY7al4Ir5SPWHvnpbG4MRrlKJs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"/>
|
||||
<image width="122" height="142" id="img2" href="data:image/png;base64,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"/>
|
||||
</defs>
|
||||
<style>
|
||||
.s0 { fill: #ffffff;stroke: #000000;stroke-miterlimit:100;stroke-width: 56 }
|
||||
.s1 { fill: #4750a3;stroke: #000000;stroke-miterlimit:100;stroke-width: 56 }
|
||||
</style>
|
||||
<path id="Wool" fill-rule="evenodd" class="s0" d="m128 608.4c0 95.9 77.4 173.6 172.8 173.6h441.6c84.8 0 153.6-69.1 153.6-154.3 0-74.6-52.8-136.9-122.9-151.1 4.9-12.9 7.7-27 7.7-41.7 0-63.9-51.6-115.8-115.2-115.8-23.6 0-45.7 7.3-64 19.6-33.2-57.9-95.2-96.7-166.4-96.7-106.1 0-192 86.3-192 192.9 0 3.2 0.1 6.5 0.2 9.7-67.2 23.8-115.4 88.1-115.4 163.8z"/>
|
||||
<g id="Crystal">
|
||||
<path id="Crystal" class="s1" d="m699 224l138.6 80v160l-138.6 80-138.6-80v-160z"/>
|
||||
<use id="Highlight" href="#img1" x="688" y="255"/>
|
||||
</g>
|
||||
<g id="Horn">
|
||||
</g>
|
||||
<g id="Face">
|
||||
<use id="Slime" href="#img2" x="233" y="538"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.1 KiB |
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>
|
@ -4,6 +4,8 @@
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"baseUrl": ".",
|
||||
|
@ -12,5 +12,11 @@ export default defineConfig({
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/.well-known": "http://localhost:8445",
|
||||
"/api": "http://localhost:8445"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user