Compare commits

...

3 Commits

Author SHA1 Message Date
58ad9996cd Post tag & category filtered page 2024-08-17 00:51:09 +08:00
2b9601640b ♻️ Refactor post list 2024-08-17 00:43:08 +08:00
4a51a85d9c 🐛 Bug fixes 2024-08-17 00:04:21 +08:00
14 changed files with 259 additions and 38 deletions

View File

@ -27,7 +27,10 @@
</article> </article>
<v-card v-if="post.body?.attachments?.length > 0" class="mb-5"> <v-card v-if="post.body?.attachments?.length > 0" class="mb-5">
<attachment-carousel :attachments="post.body?.attachments" /> <attachment-carousel
:no-clickable-attachment="props.noClickableAttachment"
:attachments="post.body?.attachments"
/>
</v-card> </v-card>
<div class="text-sm flex flex-col"> <div class="text-sm flex flex-col">
@ -45,14 +48,24 @@
</span> </span>
</div> </div>
<div v-if="post.tags?.length > 0" class="text-xs text-grey flex flex-row gap-1 mt-3"> <div
<span v-for="tag in post.tags">#{{ tag.alias }}</span> v-if="post.tags?.length > 0"
class="text-xs text-grey flex flex-row gap-1 mt-3"
>
<nuxt-link
v-for="tag in post.tags"
:to="`/posts/tags/${tag.alias}`"
class="hover:underline hover:underline-dotted"
@click.stop
>
#{{ tag.alias }}
</nuxt-link>
</div> </div>
</v-card-text> </v-card-text>
</v-card> </v-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ post: any, forceShowContent?: boolean }>() const props = defineProps<{ post: any, forceShowContent?: boolean, noClickableAttachment?: boolean }>()
const config = useRuntimeConfig() const config = useRuntimeConfig()
</script> </script>

47
components/PostList.vue Normal file
View File

@ -0,0 +1,47 @@
<template>
<v-infinite-scroll :items="posts" :onLoad="loadPost">
<template v-for="item in posts" :key="item">
<post-item :post="item" no-clickable-attachment />
</template>
</v-infinite-scroll>
</template>
<script setup lang="ts">
const props = defineProps<{ author?: string, tag?: string, category?: string, realmId?: number }>()
const config = useRuntimeConfig()
const posts = ref<any[]>([])
async function loadPost({ done }: any) {
const searchQueries = new URLSearchParams({
take: (10).toString(),
offset: posts.value.length.toString(),
})
if (props.author) {
searchQueries.set("author", props.author)
}
if (props.realmId) {
searchQueries.set("realmId", props.realmId.toString())
}
if (props.tag) {
searchQueries.set("tag", props.tag)
}
if (props.category) {
searchQueries.set("category", props.category)
}
const res = await fetch(`${config.public.solarNetworkApi}/cgi/interactive/posts?` + searchQueries)
const result = await res.json()
if (result.data.length > 0) {
posts.value.push(...result.data)
done("ok")
} else {
done("empty")
}
}
</script>

View File

@ -0,0 +1,32 @@
<template>
<v-infinite-scroll :items="posts" :onLoad="loadPost">
<template v-for="item in posts" :key="item">
<post-item :post="item" no-clickable-attachment />
</template>
</v-infinite-scroll>
</template>
<script setup lang="ts">
const props = defineProps<{ postId: number }>()
const config = useRuntimeConfig()
const posts = ref<any[]>([])
async function loadPost({ done }: any) {
const searchQueries = new URLSearchParams({
take: (10).toString(),
offset: posts.value.length.toString(),
})
const res = await fetch(`${config.public.solarNetworkApi}/cgi/interactive/posts/${props.postId}/replies?` + searchQueries)
const result = await res.json()
if (result.data.length > 0) {
posts.value.push(...result.data)
done("ok")
} else {
done("empty")
}
}
</script>

View File

@ -7,13 +7,19 @@
height="auto" height="auto"
> >
<v-carousel-item v-for="item in metadata" class="fill-height"> <v-carousel-item v-for="item in metadata" class="fill-height">
<attachment-renderer :item="item" /> <nuxt-link v-if="item.mimetype.split('/')[0] == 'image' && !props.noClickableAttachment"
:to="`/gallery/${item.id}`">
<attachment-renderer :item="item" />
</nuxt-link>
<div v-else>
<attachment-renderer :item="item" />
</div>
</v-carousel-item> </v-carousel-item>
</v-carousel> </v-carousel>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ attachments: number[] }>() const props = defineProps<{ attachments: number[], noClickableAttachment?: boolean }>()
const emits = defineEmits(["update:metadata"]) const emits = defineEmits(["update:metadata"])
const config = useRuntimeConfig() const config = useRuntimeConfig()

View File

@ -20,7 +20,7 @@
<v-img v-else-if="item.mimetype.split('/')[0] == 'image'" :src="getAttachmentUrl(item.id)" :alt="item.alt" <v-img v-else-if="item.mimetype.split('/')[0] == 'image'" :src="getAttachmentUrl(item.id)" :alt="item.alt"
class="w-full h-full" cover /> class="w-full h-full" cover />
<video v-else-if="item.mimetype.split('/')[0] == 'video'" :src="getAttachmentUrl(item.id)" class="w-full h-full" <video v-else-if="item.mimetype.split('/')[0] == 'video'" :src="getAttachmentUrl(item.id)" class="w-full h-full"
controls /> controls @click.stop />
<v-sheet v-else color="rgba(0, 0, 0, .4)" height="calc(100% + 24px)" class="p-5"> <v-sheet v-else color="rgba(0, 0, 0, .4)" height="calc(100% + 24px)" class="p-5">
<v-row class="fill-height" align="center" justify="center"> <v-row class="fill-height" align="center" justify="center">
<v-col class="text-center"> <v-col class="text-center">

View File

@ -6,6 +6,10 @@
"navActivityCaption": "Explore our official recent activities.", "navActivityCaption": "Explore our official recent activities.",
"navGallery": "Gallery", "navGallery": "Gallery",
"navGalleryCaption": "Explore files that uploaded by Solar Network users.", "navGalleryCaption": "Explore files that uploaded by Solar Network users.",
"navPosts": "Posts",
"navPostsCaption": "Explore posts that posted by Solar Network users.",
"navPostsCaptionWithTag": "Explore posts with tag {0}.",
"navPostsCaptionWithCategory": "Explore posts in category {0}.",
"indexIntroduce": "An energetic software company that create wonderful software which everyone love.", "indexIntroduce": "An energetic software company that create wonderful software which everyone love.",
"indexProductListHint": "See some of our products just there", "indexProductListHint": "See some of our products just there",
"indexActivities": "Activities", "indexActivities": "Activities",
@ -54,5 +58,7 @@
"callbackHint": "You need to sign in before access that page. After you signed in, you will be redirected to:", "callbackHint": "You need to sign in before access that page. After you signed in, you will be redirected to:",
"lastUpdatedAt": "Last updated at {0}", "lastUpdatedAt": "Last updated at {0}",
"learnMore": "Learn more", "learnMore": "Learn more",
"open": "Open" "open": "Open",
"postReplies": "Replies",
"postRepliesCaption": "All replies of this post"
} }

View File

@ -6,6 +6,10 @@
"navActivityCaption": "了解我们官方近期的活动。", "navActivityCaption": "了解我们官方近期的活动。",
"navGallery": "相簿", "navGallery": "相簿",
"navGalleryCaption": "探索整个 Solar Network 上的文件。", "navGalleryCaption": "探索整个 Solar Network 上的文件。",
"navPosts": "帖子",
"navPostsCaption": "探索整个 Solar Network 上的帖子。",
"navPostsCaptionWithTag": "探索带有 {0} 标签的帖子",
"navPostsCaptionWithCategory": "探索被分类为 {0} 的帖子",
"indexIntroduce": "一家充满活力的软件公司,创造了人人喜爱的精彩软件。", "indexIntroduce": "一家充满活力的软件公司,创造了人人喜爱的精彩软件。",
"indexProductListHint": "在这里看看我们的一些产品", "indexProductListHint": "在这里看看我们的一些产品",
"indexActivities": "动态", "indexActivities": "动态",
@ -54,5 +58,7 @@
"approve": "批准", "approve": "批准",
"lastUpdatedAt": "最后更新于 {0}", "lastUpdatedAt": "最后更新于 {0}",
"learnMore": "了解更多", "learnMore": "了解更多",
"open": "打开" "open": "打开",
"postReplies": "回复",
"postRepliesCaption": "该帖子的全部回复"
} }

View File

@ -16,7 +16,7 @@ export default defineNuxtConfig({
"/api/sitemap/posts", "/api/sitemap/posts",
], ],
}, },
} },
}, },
i18n: { i18n: {
@ -71,6 +71,11 @@ export default defineNuxtConfig({
}, },
highlight: { highlight: {
theme: "github-dark", theme: "github-dark",
langs: ["json", "yaml", "toml", "java", "javascript", "astro", "css", "scss", "dart", "go", "typescript", "c", "csharp",
"cpp", "bat", "bash", "sh", "dockerfile", "dotenv", "erlang", "fsharp", "markdown", "log",
"lua", "objc", "swift", "regex", "ruby", "rust", "postcss", "blade", "asciidoc", "cmake", "cobol", "pascal",
"nginx", "angular-html", "angular-ts", "gdscript", "gdshader", "gdresource", "groovy", "gql", "python",
"crystal", "sql", "plsql", "kotlin", "html", "vue", "gleam", "julia", "lisp", "xml", "csv"],
}, },
locales: ["en", "zh-CN"], locales: ["en", "zh-CN"],
defaultLocale: "en", defaultLocale: "en",

View File

@ -5,11 +5,7 @@
<span>{{ t("navActivityCaption") }}</span> <span>{{ t("navActivityCaption") }}</span>
</div> </div>
<v-infinite-scroll :items="items" :onLoad="load"> <post-list :realm-id="config.public.solarRealmId" />
<template v-for="item in items" :key="item">
<post-item :post="item" />
</template>
</v-infinite-scroll>
</v-container> </v-container>
</template> </template>
@ -29,17 +25,6 @@ useSeoMeta({
}) })
const config = useRuntimeConfig() const config = useRuntimeConfig()
const items = ref<any[]>([])
async function load({ done }: any) {
const res = await fetch(`${config.public.solarNetworkApi}/cgi/interactive/posts?take=10&offset=${items.value.length}&realmId=${config.public.solarRealmId}`)
const result = await res.json()
items.value.push(...result.data)
done("ok")
}
</script> </script>
<style scoped> <style scoped>

View File

@ -41,14 +41,30 @@
</span> </span>
</div> </div>
<div v-if="post.tags?.length > 0" class="text-xs text-grey flex flex-row gap-1 mb-3"> <div
<span v-for="tag in post.tags">#{{ tag.alias }}</span> v-if="post.tags?.length > 0"
class="text-xs text-grey flex flex-row gap-1 mb-3"
>
<nuxt-link
v-for="tag in post.tags"
:to="`/posts/tags/${tag.alias}`"
class="hover:underline hover:underline-dotted"
@click.stop
>
#{{ tag.alias }}
</nuxt-link>
</div> </div>
<div class="text-xs text-grey flex flex-col"> <div class="text-xs text-grey flex flex-col mb-5">
<span>Solar Network Post Web Preview</span> <span>Solar Network Post Web Preview</span>
<span>To get full view of this post, open it on <a class="underline" :href="externalOpenLink">Solian</a></span> <span>To get full view of this post, open it on <a class="underline" :href="externalOpenLink">Solian</a></span>
</div> </div>
<div v-if="post.metric.reply_count" class="mb-5">
<v-card variant="outlined" :title="t('postReplies')" :subtitle="t('postRepliesCaption')">
<post-reply-list class="mt-[-20px] mx-[-1ch] mb-3" :post-id="post.id" />
</v-card>
</div>
</v-container> </v-container>
</template> </template>
@ -60,6 +76,8 @@ const attachments = ref<any[]>([])
const firstImage = ref<string | null>() const firstImage = ref<string | null>()
const firstVideo = ref<string | null>() const firstVideo = ref<string | null>()
const { t } = useI18n()
const { data: post } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/interactive/posts/${route.params.id}`) const { data: post } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/interactive/posts/${route.params.id}`)
if (!post.value) { if (!post.value) {

View File

@ -0,0 +1,34 @@
<template>
<v-container class="content-container mx-auto">
<div class="my-3 mx-[3.5ch]">
<h1 class="text-2xl">{{ t("navPosts") }}</h1>
<span>{{ t("navPostsCaptionWithCategory", [`#${route.params.slug}`]) }}</span>
</div>
<post-list :category="route.params.slug?.toString()" />
</v-container>
</template>
<script setup lang="ts">
const { t } = useI18n()
const route = useRoute()
useHead({
title: t("navActivity"),
})
useSeoMeta({
title: t("navActivity"),
ogTitle: t("navActivity"),
description: t("navActivityCaption"),
ogDescription: t("navActivityCaption"),
ogType: "website",
})
</script>
<style scoped>
.content-container {
max-width: 70ch !important;
}
</style>

32
pages/posts/index.vue Normal file
View File

@ -0,0 +1,32 @@
<template>
<v-container class="content-container mx-auto">
<div class="my-3 mx-[3.5ch]">
<h1 class="text-2xl">{{ t("navPosts") }}</h1>
<span>{{ t("navPostsCaption") }}</span>
</div>
<post-list />
</v-container>
</template>
<script setup lang="ts">
const { t } = useI18n()
useHead({
title: t("navActivity"),
})
useSeoMeta({
title: t("navActivity"),
ogTitle: t("navActivity"),
description: t("navActivityCaption"),
ogDescription: t("navActivityCaption"),
ogType: "website",
})
</script>
<style scoped>
.content-container {
max-width: 70ch !important;
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<v-container class="content-container mx-auto">
<div class="my-3 mx-[3.5ch]">
<h1 class="text-2xl">{{ t("navPosts") }}</h1>
<span>{{ t("navPostsCaptionWithTag", [`#${route.params.slug}`]) }}</span>
</div>
<post-list :tag="route.params.slug?.toString()" />
</v-container>
</template>
<script setup lang="ts">
const { t } = useI18n()
const route = useRoute()
useHead({
title: t("navActivity"),
})
useSeoMeta({
title: t("navActivity"),
ogTitle: t("navActivity"),
description: t("navActivityCaption"),
ogDescription: t("navActivityCaption"),
ogType: "website",
})
</script>
<style scoped>
.content-container {
max-width: 70ch !important;
}
</style>

View File

@ -6,8 +6,8 @@
<div class="my-5 flex flex-row gap-4"> <div class="my-5 flex flex-row gap-4">
<v-avatar :image="urlOfAvatar" /> <v-avatar :image="urlOfAvatar" />
<div class="flex flex-col"> <div class="flex flex-col">
<span>{{ account.nick }} <span class="text-xs">@{{ account.name }}</span></span> <span>{{ account?.nick }} <span class="text-xs">@{{ account?.name }}</span></span>
<span class="text-sm">{{ account.description }}</span> <span class="text-sm">{{ account?.description }}</span>
</div> </div>
</div> </div>
@ -24,11 +24,7 @@
</div> </div>
</div> </div>
<v-infinite-scroll :items="posts" :onLoad="loadPost"> <post-list v-if="account" :author="account.name" />
<template v-for="item in posts" :key="item">
<post-item :post="item" />
</template>
</v-infinite-scroll>
</v-container> </v-container>
</template> </template>
@ -45,8 +41,15 @@ const posts = ref<any[]>([])
const { data: account } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/auth/users/${route.params.name}`) const { data: account } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/auth/users/${route.params.name}`)
const urlOfAvatar = computed(() => account.value.avatar ? `${config.public.solarNetworkApi}/cgi/files/attachments/${account.value.avatar}` : void 0) if (account.value == null) {
const urlOfBanner = computed(() => account.value.banner ? `${config.public.solarNetworkApi}/cgi/files/attachments/${account.value.banner}` : void 0) throw createError({
statusCode: 404,
statusMessage: "User Not Found",
})
}
const urlOfAvatar = computed(() => account.value?.avatar ? `${config.public.solarNetworkApi}/cgi/files/attachments/${account.value.avatar}` : void 0)
const urlOfBanner = computed(() => account.value?.banner ? `${config.public.solarNetworkApi}/cgi/files/attachments/${account.value.banner}` : void 0)
const externalOpenLink = computed(() => `${config.public.solianUrl}/accounts/view/${route.params.name}`) const externalOpenLink = computed(() => `${config.public.solianUrl}/accounts/view/${route.params.name}`)