Better detail post

This commit is contained in:
2025-09-21 00:01:47 +08:00
parent e9de02b084
commit fcfb57f4a5
10 changed files with 152 additions and 86 deletions

View File

@@ -47,6 +47,9 @@ const themeRgb = computed(() => {
?.map((v) => Number.parseInt(v, 16)) ?.map((v) => Number.parseInt(v, 16))
.join(", ") .join(", ")
}) })
const textShadow = computed(() => {
return '2px 2px 8px rgba(0,0,0,0.8)'
})
const siteConfig = useSiteConfig() const siteConfig = useSiteConfig()
const siteName = computed(() => { const siteName = computed(() => {
return props.siteName || siteConfig.name return props.siteName || siteConfig.name
@@ -88,11 +91,11 @@ function toAbsoluteUrl(url: string | undefined) {
<template> <template>
<div <div
class="w-full h-full flex justify-between relative" class="w-full h-full flex justify-between relative text-white"
:class="[ :class="[
...(colorMode === 'light' ...(colorMode === 'light'
? ['bg-white', 'text-gray-900'] ? ['bg-white']
: ['bg-gray-900', 'text-white']) : ['bg-gray-900'])
]" ]"
> >
<div <div
@@ -103,8 +106,8 @@ function toAbsoluteUrl(url: string | undefined) {
<img <img
v-if="backgroundImage" v-if="backgroundImage"
:src="toAbsoluteUrl(backgroundImage)" :src="toAbsoluteUrl(backgroundImage)"
class="absolute top-0 left-0 w-full h-full object-cover opacity-70" class="absolute top-0 left-0 w-full h-full object-cover"
style="min-width: 1200px; min-height: 600px;" style="min-width: 1200px; min-height: 600px; filter: blur(8px)"
/> />
<div class="h-full w-full justify-between relative p-[60px]"> <div class="h-full w-full justify-between relative p-[60px]">
<div class="flex flex-row justify-between items-start"> <div class="flex flex-row justify-between items-start">
@@ -112,17 +115,15 @@ function toAbsoluteUrl(url: string | undefined) {
<h1 <h1
class="m-0 font-bold mb-[30px] text-[75px]" class="m-0 font-bold mb-[30px] text-[75px]"
style="display: block; text-overflow: ellipsis" style="display: block; text-overflow: ellipsis"
:style="{ lineClamp: description ? 2 : 3 }" :style="{ lineClamp: description ? 2 : 3, textShadow }"
> >
{{ title }} {{ title }}
</h1> </h1>
<p <p
v-if="description" v-if="description"
class="text-[35px] leading-12" class="text-[35px] leading-12 text-white"
:class="[
colorMode === 'light' ? ['text-gray-700'] : ['text-gray-300']
]"
style="display: block; line-clamp: 3; text-overflow: ellipsis" style="display: block; line-clamp: 3; text-overflow: ellipsis"
:style="{ textShadow }"
> >
{{ description }} {{ description }}
</p> </p>
@@ -136,7 +137,7 @@ function toAbsoluteUrl(url: string | undefined) {
</div> </div>
</div> </div>
<div class="flex flex-row justify-end items-center text-right gap-3 w-full"> <div class="flex flex-row justify-end items-center text-right gap-3 w-full">
<p v-if="siteName" style="font-size: 25px" class="font-bold"> <p v-if="siteName" style="font-size: 25px" class="font-bold" :style="{ textShadow }">
{{ siteName }} {{ siteName }}
</p> </p>
<img <img

View File

@@ -7,29 +7,7 @@
@keydown.meta.enter.exact="submit" @keydown.meta.enter.exact="submit"
@keydown.ctrl.enter.exact="submit" @keydown.ctrl.enter.exact="submit"
/> />
<div v-if="fileList.length > 0" class="d-flex gap-2 flex-wrap">
<v-img
v-for="file in fileList"
:key="file.name"
:src="file.url"
width="100"
height="100"
class="rounded"
/>
</div>
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex gap-2">
<v-file-input
v-model="selectedFiles"
multiple
accept="image/*,video/*,audio/*"
label="Upload files"
prepend-icon="mdi-upload"
hide-details
density="compact"
@change="handleFileSelect"
/>
</div>
<v-btn type="primary" :loading="submitting" @click="submit"> <v-btn type="primary" :loading="submitting" @click="submit">
Post Post
<template #append> <template #append>

View File

@@ -0,0 +1,36 @@
<template>
<div class="flex flex-col text-xs opacity-80 mx-3 mt-1">
<div class="flex flex-wrap gap-1.5">
<span class="font-bold">The Solar Network</span>
<span class="font-bold">·</span>
<span>FloatingIsland</span>
</div>
<div class="flex flex-wrap gap-1.5">
<a class="link" target="_blank" href="https://solsynth.dev/terms">
Terms of Services
</a>
<span class="font-bold">·</span>
<a class="link" target="_blank" href="https://status.solsynth.dev">
Service Status
</a>
<span class="font-bold">·</span>
<a
class="link"
target="_blank"
href="https://solian.app/swagger/index.html"
>
API
</a>
</div>
<p class="mt-2 opacity-80">
The FloatingIsland do not provides all the features the Solar Network has,
for further usage, see <a href="https://web.solian.app" class="font-bold underline">Solian</a>
</p>
</div>
</template>
<style scoped>
.link:hover {
text-decoration: underline;
}
</style>

View File

@@ -1,8 +1,8 @@
// Solar Network aka the api client // Solar Network aka the api client
import { keysToCamel, keysToSnake } from "~/utils/transformKeys" import { keysToCamel, keysToSnake } from "~/utils/transformKeys"
export const useSolarNetwork = () => { export const useSolarNetwork = (withoutProxy = false) => {
const apiBase = useSolarNetworkUrl() const apiBase = useSolarNetworkUrl(withoutProxy)
return $fetch.create({ return $fetch.create({
baseURL: apiBase, baseURL: apiBase,

View File

@@ -2,7 +2,13 @@
<v-app :theme="colorMode.preference"> <v-app :theme="colorMode.preference">
<v-app-bar flat class="app-bar-blur"> <v-app-bar flat class="app-bar-blur">
<v-container class="mx-auto d-flex align-center justify-center"> <v-container class="mx-auto d-flex align-center justify-center">
<img :src="$vuetify.theme.current.dark ? IconDark : IconLight" width="32" height="32" class="me-4" alt="The Solar Network" /> <img
:src="colorMode.value == 'dark' ? IconDark : IconLight"
width="32"
height="32"
class="me-4"
alt="The Solar Network"
/>
<v-btn <v-btn
v-for="link in links" v-for="link in links"
@@ -15,13 +21,39 @@
<v-spacer /> <v-spacer />
<v-menu>
<template v-slot:activator="{ props }">
<v-avatar <v-avatar
v-bind="props"
class="me-4" class="me-4"
color="grey-darken-1" color="grey-darken-1"
size="32" size="32"
icon="mdi-account" icon="mdi-account-circle-outline"
:image="`${apiBase}/drive/files/${user?.profile.picture?.id}`" :image="
user?.profile.picture
? `${apiBase}/drive/files/${user?.profile.picture?.id}`
: undefined
"
/> />
</template>
<v-list density="compact">
<v-list-item v-if="!user" to="/auth/login" prepend-icon="mdi-login"
>Login</v-list-item
>
<v-list-item
v-if="!user"
to="/auth/create-account"
prepend-icon="mdi-account-plus"
>Create Account</v-list-item
>
<v-list-item
v-if="user"
to="/accounts/me"
prepend-icon="mdi-view-dashboard"
>Dashboard</v-list-item
>
</v-list>
</v-menu>
</v-container> </v-container>
</v-app-bar> </v-app-bar>
@@ -32,8 +64,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import IconLight from '~/assets/images/cloudy-lamb.png' import IconLight from "~/assets/images/cloudy-lamb.png"
import IconDark from '~/assets/images/cloudy-lamb@dark.png' import IconDark from "~/assets/images/cloudy-lamb@dark.png"
import type { NavLink } from "~/types/navlink" import type { NavLink } from "~/types/navlink"

View File

@@ -326,7 +326,7 @@ useHead({
}) })
defineOgImage({ defineOgImage({
component: 'WithAvatar', component: 'ImageCard',
title: computed(() => user.value ? user.value.nick || user.value.name : 'User Profile'), title: computed(() => user.value ? user.value.nick || user.value.name : 'User Profile'),
description: computed(() => user.value ? `View the profile of ${user.value.nick || user.value.name} on Solar Network.` : ''), description: computed(() => user.value ? `View the profile of ${user.value.nick || user.value.name} on Solar Network.` : ''),
avatarUrl: computed(() => userPicture.value), avatarUrl: computed(() => userPicture.value),

View File

@@ -7,7 +7,7 @@
<div class="pa-8"> <div class="pa-8">
<div class="mb-4"> <div class="mb-4">
<img <img
:src="$vuetify.theme.current.dark ? IconDark : IconLight" :src="colorMode.value == 'dark' ? IconDark : IconLight"
alt="CloudyLamb" alt="CloudyLamb"
height="60" height="60"
width="60" width="60"
@@ -153,6 +153,8 @@ import IconDark from "~/assets/images/cloudy-lamb@dark.png"
const router = useRouter() const router = useRouter()
const api = useSolarNetwork() const api = useSolarNetwork()
const colorMode = useColorMode()
useHead({ useHead({
title: "Create Account" title: "Create Account"
}) })

View File

@@ -246,6 +246,8 @@ function getFactorName(factorType: number) {
return "Unknown Factor" return "Unknown Factor"
} }
} }
const colorMode = useColorMode()
</script> </script>
<template> <template>
@@ -257,7 +259,7 @@ function getFactorName(factorType: number) {
<div class="pa-8"> <div class="pa-8">
<div class="mb-4"> <div class="mb-4">
<img <img
:src="$vuetify.theme.current.dark ? IconDark : IconLight" :src="colorMode.value == 'dark' ? IconDark : IconLight"
alt="CloudyLamb" alt="CloudyLamb"
height="60" height="60"
width="60" width="60"

View File

@@ -10,7 +10,7 @@
/> />
</div> </div>
</div> </div>
<div class="sidebar"> <div class="sidebar flex flex-col gap-3">
<v-card v-if="!userStore.isAuthenticated" class="w-full" title="About"> <v-card v-if="!userStore.isAuthenticated" class="w-full" title="About">
<v-card-text> <v-card-text>
<p>Welcome to the <b>Solar Network</b></p> <p>Welcome to the <b>Solar Network</b></p>
@@ -31,6 +31,7 @@
<post-editor @posted="refreshActivities" /> <post-editor @posted="refreshActivities" />
</v-card-text> </v-card-text>
</v-card> </v-card>
<sidebar-footer />
</div> </div>
</div> </div>
</v-container> </v-container>
@@ -51,14 +52,14 @@ import IconLight from '~/assets/images/cloudy-lamb.png'
const router = useRouter() const router = useRouter()
useHead({ useHead({
title: "Home", title: "Explore",
meta: [ meta: [
{ name: 'description', content: 'The open social network. Friendly to everyone.' }, { name: 'description', content: 'The open social network. Friendly to everyone.' },
] ]
}) })
defineOgImage({ defineOgImage({
title: 'Home', title: 'Explore',
description: 'The open social network. Friendly to everyone.', description: 'The open social network. Friendly to everyone.',
}) })

View File

@@ -1,15 +1,14 @@
<template> <template>
<v-container class="py-6"> <v-container class="py-6">
<div v-if="loading" class="text-center py-12"> <div v-if="pending" class="text-center py-12">
<v-progress-circular indeterminate size="64" color="primary" /> <v-progress-circular indeterminate size="64" color="primary" />
<p class="mt-4">Loading post...</p> <p class="mt-4">Loading post...</p>
</div> </div>
<div v-else-if="error" class="text-center py-12"> <div v-else-if="error" class="text-center py-12">
<v-alert type="error" class="mb-4" prominent> <v-alert type="error" class="mb-4" prominent>
<v-alert-title>Error Loading Post</v-alert-title> <v-alert-title>Error Loading Post</v-alert-title>
{{ error }} {{ error?.statusMessage || "Failed to load post" }}
</v-alert> </v-alert>
<v-btn color="primary" @click="fetchPost">Try Again</v-btn>
</div> </div>
<div v-else-if="post" class="max-w-4xl mx-auto"> <div v-else-if="post" class="max-w-4xl mx-auto">
<!-- Article Type: Split Header and Content --> <!-- Article Type: Split Header and Content -->
@@ -206,7 +205,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from "vue" import { computed } from "vue"
import { Marked } from "marked" import { Marked } from "marked"
import type { SnPost } from "~/types/api" import type { SnPost } from "~/types/api"
@@ -217,14 +216,33 @@ import PostReactionList from "~/components/PostReactionList.vue"
const route = useRoute() const route = useRoute()
const id = route.params.id as string const id = route.params.id as string
const post = ref<SnPost | null>(null) const marked = new Marked()
const loading = ref(true)
const error = ref("") const apiServer = useSolarNetwork(true);
const htmlContent = ref("")
const { data: postData, error, pending } = await useAsyncData(`post-${id}`, async () => {
try {
const resp = await apiServer(`/sphere/posts/${id}`)
const post = resp as SnPost
let html = ""
if (post.content) {
html = await marked.parse(post.content, { breaks: true })
}
return { post, html }
} catch (e) {
throw createError({
statusCode: 404,
statusMessage: e instanceof Error ? e.message : "Failed to load post"
})
}
})
const post = computed(() => postData.value?.post || null)
const htmlContent = computed(() => postData.value?.html || "")
useHead({ useHead({
title: computed(() => { title: computed(() => {
if (loading.value) return "Loading post..." if (pending.value) return "Loading post..."
if (error.value) return "Error" if (error.value) return "Error"
if (!post.value) return "Post not found" if (!post.value) return "Post not found"
return post.value.title || "Post" return post.value.title || "Post"
@@ -239,12 +257,29 @@ useHead({
}) })
}) })
// defineOgImage({ const apiBase = useSolarNetworkUrl()
// title: computed(() => post.value?.title || 'Post'),
// description: computed(() => post.value?.description || post.value?.content?.substring(0, 150) || ''),
// })
const marked = new Marked() const userPicture = computed(() => {
return post.value?.publisher.picture
? `${apiBase}/drive/files/${post.value.publisher.picture.id}`
: undefined
})
const userBackground = computed(() => {
const firstImageAttachment = post.value?.attachments?.find(att =>
att.mimeType?.startsWith('image/')
)
return firstImageAttachment
? `${apiBase}/drive/files/${firstImageAttachment.id}`
: undefined
})
defineOgImage({
component: 'ImageCard',
title: computed(() => post.value?.title || 'Post'),
description: computed(() => post.value?.description || post.value?.content?.substring(0, 150) || ''),
avatarUrl: computed(() => userPicture.value),
backgroundImage: computed(() => userBackground.value),
})
function formatDate(dateString: string): string { function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString("en-US", { return new Date(dateString).toLocaleDateString("en-US", {
@@ -254,23 +289,6 @@ function formatDate(dateString: string): string {
}) })
} }
async function fetchPost() {
try {
const api = useSolarNetwork()
const resp = await api(`/sphere/posts/${id}`)
post.value = resp as SnPost
if (post.value.content) {
htmlContent.value = await marked.parse(post.value.content, {
breaks: true
})
}
} catch (e) {
error.value = e instanceof Error ? e.message : "Failed to load post"
} finally {
loading.value = false
}
}
function handleReaction(symbol: string, attitude: number, delta: number) { function handleReaction(symbol: string, attitude: number, delta: number) {
if (!post.value) return if (!post.value) return
@@ -297,8 +315,4 @@ function handleReaction(symbol: string, attitude: number, delta: number) {
;(post.value as any).reactions = reactions ;(post.value as any).reactions = reactions
;(post.value as any).reactionsMade = reactionsMade ;(post.value as any).reactionsMade = reactionsMade
} }
onMounted(() => {
fetchPost()
})
</script> </script>