✨ Better detail post
This commit is contained in:
@@ -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
|
@@ -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>
|
||||
|
36
app/components/SidebarFooter.vue
Normal file
36
app/components/SidebarFooter.vue
Normal 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>
|
@@ -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,
|
||||
|
@@ -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"
|
||||
|
||||
|
@@ -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),
|
||||
|
@@ -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"
|
||||
})
|
||||
|
@@ -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"
|
||||
|
@@ -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.',
|
||||
})
|
||||
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user