💄 Optimize posts

This commit is contained in:
2025-09-24 00:04:13 +08:00
parent 8ce154eef2
commit 42f1d42506
11 changed files with 15973 additions and 39 deletions

View File

@@ -1,6 +1,10 @@
<template>
<nuxt-loading-indicator />
<nuxt-loading-indicator :color="colorMode.value == 'dark' ? 'white' : '#3f51b5'" />
<nuxt-layout>
<nuxt-page />
</nuxt-layout>
</template>
<script setup lang="ts">
const colorMode = useColorMode()
</script>

View File

@@ -1,5 +1,5 @@
<template>
<v-card>
<v-card class="px-4 py-3">
<v-card-text>
<div class="flex flex-col gap-3">
<post-header :item="props.item" />
@@ -20,7 +20,11 @@
<div v-html="htmlContent" />
</article>
<div v-if="props.item.attachments.length > 0" class="d-flex gap-2 flex-wrap" @click.stop>
<div
v-if="props.item.attachments.length > 0"
class="d-flex gap-2 flex-wrap"
@click.stop
>
<attachment-item
v-for="attachment in props.item.attachments"
:key="attachment.id"
@@ -32,8 +36,8 @@
<div @click.stop>
<post-reaction-list
:parent-id="props.item.id"
:reactions="(props.item as any).reactions || {}"
:reactions-made="(props.item as any).reactionsMade || {}"
:reactions="props.item.reactionsCount"
:reactions-made="props.item.reactionsMade"
:can-react="true"
@react="handleReaction"
/>
@@ -45,7 +49,14 @@
<script lang="ts" setup>
import { ref, watch } from "vue"
import { Marked } from "marked"
import { unified } from "unified"
import remarkParse from "remark-parse"
import remarkMath from "remark-math"
import remarkRehype from "remark-rehype"
import remarkBreaks from "remark-breaks"
import remarkGfm from "remark-gfm"
import rehypeKatex from "rehype-katex"
import rehypeStringify from "rehype-stringify"
import type { SnPost } from "~/types/api"
import PostHeader from "./PostHeader.vue"
@@ -54,7 +65,14 @@ import PostReactionList from "./PostReactionList.vue"
const props = defineProps<{ item: SnPost }>()
const marked = new Marked()
const processor = unified()
.use(remarkParse)
.use(remarkMath)
.use(remarkBreaks)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeKatex)
.use(rehypeStringify)
const htmlContent = ref<string>("")
@@ -87,9 +105,9 @@ function handleReaction(symbol: string, attitude: number, delta: number) {
watch(
props.item,
async (value) => {
(value) => {
if (value.content)
htmlContent.value = await marked.parse(value.content, { breaks: true })
htmlContent.value = String(processor.processSync(value.content))
},
{ immediate: true, deep: true }
)

View File

@@ -178,8 +178,17 @@ const availableReactions: ReactionTemplate[] = [
{ symbol: "heart", emoji: "❤️", attitude: 0 }
]
function camelToSnake(str: string): string {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
}
function getReactionEmoji(symbol: string): string {
const reaction = availableReactions.find((r) => r.symbol === symbol)
let reaction = availableReactions.find((r) => r.symbol === symbol)
if (reaction) return reaction.emoji
// Try camelCase to snake_case conversion
const snakeSymbol = camelToSnake(symbol)
reaction = availableReactions.find((r) => r.symbol === snakeSymbol)
return reaction?.emoji || "❓"
}

View File

@@ -181,7 +181,14 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue"
import { Marked } from "marked"
import { unified } from "unified"
import remarkParse from "remark-parse"
import remarkMath from "remark-math"
import remarkBreaks from "remark-breaks"
import remarkRehype from "remark-rehype"
import rehypeKatex from "rehype-katex"
import rehypeStringify from "rehype-stringify"
import remarkGfm from "remark-gfm"
const route = useRoute()
@@ -239,15 +246,22 @@ const perkSubscriptionNames: Record<string, PerkSubscriptionInfo> = {
}
}
const marked = new Marked()
const processor = unified()
.use(remarkParse)
.use(remarkMath)
.use(remarkBreaks)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeKatex)
.use(rehypeStringify)
const htmlBio = ref<string | undefined>(undefined)
watch(
user,
async (value) => {
(value) => {
htmlBio.value = value?.profile.bio
? await marked.parse(value.profile.bio, { breaks: true })
? String(processor.processSync(value.profile.bio))
: undefined
},
{ immediate: true, deep: true }
@@ -316,21 +330,29 @@ useHead({
}),
meta: computed(() => {
if (user.value) {
const description = `View the profile of ${user.value.nick || user.value.name} on Solar Network.`
return [
{ name: 'description', content: description },
]
const description = `View the profile of ${
user.value.nick || user.value.name
} on Solar Network.`
return [{ name: "description", content: description }]
}
return []
})
})
defineOgImage({
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.` : ''),
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),
backgroundImage: computed(() => userBackground.value),
backgroundImage: computed(() => userBackground.value)
})
</script>

View File

@@ -91,7 +91,7 @@
<!-- Other Types: Merged Header, Content, and Attachments -->
<template v-else>
<!-- Merged Header, Content, and Attachments Section -->
<v-card class="mb-4 elevation-1" rounded="lg">
<v-card class="px-4 py-3 mb-4 elevation-1" rounded="lg">
<v-card-text class="pa-6">
<post-header :item="post" class="mb-4" />
@@ -206,7 +206,14 @@
<script setup lang="ts">
import { computed } from "vue"
import { Marked } from "marked"
import { unified } from "unified"
import remarkParse from "remark-parse"
import remarkMath from "remark-math"
import remarkRehype from "remark-rehype"
import rehypeKatex from "rehype-katex"
import rehypeStringify from "rehype-stringify"
import remarkBreaks from "remark-breaks"
import remarkGfm from "remark-gfm"
import type { SnPost } from "~/types/api"
import PostHeader from "~/components/PostHeader.vue"
@@ -216,17 +223,28 @@ import PostReactionList from "~/components/PostReactionList.vue"
const route = useRoute()
const id = route.params.id as string
const marked = new Marked()
const processor = unified()
.use(remarkParse)
.use(remarkMath)
.use(remarkBreaks)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeKatex)
.use(rehypeStringify)
const apiServer = useSolarNetwork(true);
const apiServer = useSolarNetwork(true)
const { data: postData, error, pending } = await useAsyncData(`post-${id}`, async () => {
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 })
html = String(processor.processSync(post.content))
}
return { post, html }
} catch (e) {
@@ -245,7 +263,7 @@ useHead({
if (pending.value) return "Loading post..."
if (error.value) return "Error"
if (!post.value) return "Post not found"
return post.value.title || "Post"
return `${post.value?.title || "Post"} from ${post.value?.publisher.nick}`
}),
meta: computed(() => {
if (post.value) {
@@ -265,8 +283,8 @@ const userPicture = computed(() => {
: undefined
})
const userBackground = computed(() => {
const firstImageAttachment = post.value?.attachments?.find(att =>
att.mimeType?.startsWith('image/')
const firstImageAttachment = post.value?.attachments?.find((att) =>
att.mimeType?.startsWith("image/")
)
return firstImageAttachment
? `${apiBase}/drive/files/${firstImageAttachment.id}`
@@ -274,11 +292,14 @@ const userBackground = computed(() => {
})
defineOgImage({
component: 'ImageCard',
title: computed(() => post.value?.title || 'Post'),
description: computed(() => post.value?.description || post.value?.content?.substring(0, 150) || ''),
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),
backgroundImage: computed(() => userBackground.value)
})
function formatDate(dateString: string): string {

View File

@@ -60,7 +60,7 @@ export interface SnPost {
awardedScore: number;
reactionsCount: Record<string, number>;
repliesCount: number;
reactionsMade: Record<string, unknown>;
reactionsMade: Record<string, boolean>;
repliedGone: boolean;
forwardedGone: boolean;
repliedPostId: string | null;

18
app/types/marked-katex.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
declare module 'marked-katex' {
interface Options {
throwOnError?: boolean
errorColor?: string
displayMode?: boolean
leqno?: boolean
fleqn?: boolean
macros?: Record<string, string>
colorIsTextColor?: boolean
strict?: boolean | 'ignore' | 'warn' | 'error'
trust?: boolean | ((context: { command: string; url: string; protocol: string }) => boolean)
output?: 'html' | 'mathml' | 'htmlAndMathml'
}
function markedKatex(options?: Options): any
export default markedKatex
}

2377
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ export default defineNuxtConfig({
"@nuxtjs/color-mode",
"nuxt-og-image"
],
css: ["~/assets/css/main.css"],
css: ["~/assets/css/main.css", "katex/dist/katex.min.css"],
app: {
pageTransition: { name: "page", mode: "out-in" },
head: {

13455
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -25,14 +25,24 @@
"blurhash": "^2.0.5",
"cfturnstile-vue3": "^2.0.0",
"eslint": "^9.36.0",
"katex": "^0.16.22",
"luxon": "^3.7.2",
"marked": "^16.3.0",
"nuxt": "^4.1.2",
"nuxt-og-image": "^5.1.11",
"pinia": "^3.0.3",
"rehype-katex": "^7.0.1",
"rehype-stringify": "^10.0.1",
"remark": "^15.0.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-html": "^16.0.1",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"sharp": "^0.34.4",
"tailwindcss": "^4.1.13",
"tus-js-client": "^4.3.1",
"unified": "^11.0.5",
"vue": "^3.5.21",
"vue-router": "^4.5.1",
"vuetify-nuxt-module": "0.18.7"
@@ -41,6 +51,6 @@
"@mdi/font": "^7.4.47",
"@types/luxon": "^3.7.1",
"@types/node": "^24.5.2",
"eslint-plugin-tailwindcss": "^4.0.0-beta.0"
"eslint-plugin-tailwindcss": "^3.18.2"
}
}