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

View File

@@ -7,29 +7,7 @@
@keydown.meta.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 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">
Post
<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
import { keysToCamel, keysToSnake } from "~/utils/transformKeys"
export const useSolarNetwork = () => {
const apiBase = useSolarNetworkUrl()
export const useSolarNetwork = (withoutProxy = false) => {
const apiBase = useSolarNetworkUrl(withoutProxy)
return $fetch.create({
baseURL: apiBase,

View File

@@ -2,7 +2,13 @@
<v-app :theme="colorMode.preference">
<v-app-bar flat class="app-bar-blur">
<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-for="link in links"
@@ -15,13 +21,39 @@
<v-spacer />
<v-avatar
class="me-4"
color="grey-darken-1"
size="32"
icon="mdi-account"
:image="`${apiBase}/drive/files/${user?.profile.picture?.id}`"
/>
<v-menu>
<template v-slot:activator="{ props }">
<v-avatar
v-bind="props"
class="me-4"
color="grey-darken-1"
size="32"
icon="mdi-account-circle-outline"
: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-app-bar>
@@ -32,8 +64,8 @@
</template>
<script lang="ts" setup>
import IconLight from '~/assets/images/cloudy-lamb.png'
import IconDark from '~/assets/images/cloudy-lamb@dark.png'
import IconLight from "~/assets/images/cloudy-lamb.png"
import IconDark from "~/assets/images/cloudy-lamb@dark.png"
import type { NavLink } from "~/types/navlink"

View File

@@ -326,7 +326,7 @@ useHead({
})
defineOgImage({
component: 'WithAvatar',
component: 'ImageCard',
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.` : ''),
avatarUrl: computed(() => userPicture.value),

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,14 @@
<template>
<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" />
<p class="mt-4">Loading post...</p>
</div>
<div v-else-if="error" class="text-center py-12">
<v-alert type="error" class="mb-4" prominent>
<v-alert-title>Error Loading Post</v-alert-title>
{{ error }}
{{ error?.statusMessage || "Failed to load post" }}
</v-alert>
<v-btn color="primary" @click="fetchPost">Try Again</v-btn>
</div>
<div v-else-if="post" class="max-w-4xl mx-auto">
<!-- Article Type: Split Header and Content -->
@@ -206,7 +205,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from "vue"
import { computed } from "vue"
import { Marked } from "marked"
import type { SnPost } from "~/types/api"
@@ -217,14 +216,33 @@ import PostReactionList from "~/components/PostReactionList.vue"
const route = useRoute()
const id = route.params.id as string
const post = ref<SnPost | null>(null)
const loading = ref(true)
const error = ref("")
const htmlContent = ref("")
const marked = new Marked()
const apiServer = useSolarNetwork(true);
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({
title: computed(() => {
if (loading.value) return "Loading post..."
if (pending.value) return "Loading post..."
if (error.value) return "Error"
if (!post.value) return "Post not found"
return post.value.title || "Post"
@@ -239,12 +257,29 @@ useHead({
})
})
// defineOgImage({
// title: computed(() => post.value?.title || 'Post'),
// description: computed(() => post.value?.description || post.value?.content?.substring(0, 150) || ''),
// })
const apiBase = useSolarNetworkUrl()
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 {
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) {
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).reactionsMade = reactionsMade
}
onMounted(() => {
fetchPost()
})
</script>