💄 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> <template>
<nuxt-loading-indicator /> <nuxt-loading-indicator :color="colorMode.value == 'dark' ? 'white' : '#3f51b5'" />
<nuxt-layout> <nuxt-layout>
<nuxt-page /> <nuxt-page />
</nuxt-layout> </nuxt-layout>
</template> </template>
<script setup lang="ts">
const colorMode = useColorMode()
</script>

View File

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

View File

@@ -178,8 +178,17 @@ const availableReactions: ReactionTemplate[] = [
{ symbol: "heart", emoji: "❤️", attitude: 0 } { symbol: "heart", emoji: "❤️", attitude: 0 }
] ]
function camelToSnake(str: string): string {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
}
function getReactionEmoji(symbol: string): string { 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 || "❓" return reaction?.emoji || "❓"
} }

View File

@@ -181,7 +181,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from "vue" 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() 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) const htmlBio = ref<string | undefined>(undefined)
watch( watch(
user, user,
async (value) => { (value) => {
htmlBio.value = value?.profile.bio htmlBio.value = value?.profile.bio
? await marked.parse(value.profile.bio, { breaks: true }) ? String(processor.processSync(value.profile.bio))
: undefined : undefined
}, },
{ immediate: true, deep: true } { immediate: true, deep: true }
@@ -316,21 +330,29 @@ useHead({
}), }),
meta: computed(() => { meta: computed(() => {
if (user.value) { if (user.value) {
const description = `View the profile of ${user.value.nick || user.value.name} on Solar Network.` const description = `View the profile of ${
return [ user.value.nick || user.value.name
{ name: 'description', content: description }, } on Solar Network.`
] return [{ name: "description", content: description }]
} }
return [] return []
}) })
}) })
defineOgImage({ defineOgImage({
component: 'ImageCard', component: "ImageCard",
title: computed(() => user.value ? user.value.nick || user.value.name : 'User Profile'), title: computed(() =>
description: computed(() => user.value ? `View the profile of ${user.value.nick || user.value.name} on Solar Network.` : ''), 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), avatarUrl: computed(() => userPicture.value),
backgroundImage: computed(() => userBackground.value), backgroundImage: computed(() => userBackground.value)
}) })
</script> </script>

View File

@@ -91,7 +91,7 @@
<!-- Other Types: Merged Header, Content, and Attachments --> <!-- Other Types: Merged Header, Content, and Attachments -->
<template v-else> <template v-else>
<!-- Merged Header, Content, and Attachments Section --> <!-- 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"> <v-card-text class="pa-6">
<post-header :item="post" class="mb-4" /> <post-header :item="post" class="mb-4" />
@@ -206,7 +206,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue" 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 type { SnPost } from "~/types/api"
import PostHeader from "~/components/PostHeader.vue" import PostHeader from "~/components/PostHeader.vue"
@@ -216,17 +223,28 @@ 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 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 { try {
const resp = await apiServer(`/sphere/posts/${id}`) const resp = await apiServer(`/sphere/posts/${id}`)
const post = resp as SnPost const post = resp as SnPost
let html = "" let html = ""
if (post.content) { if (post.content) {
html = await marked.parse(post.content, { breaks: true }) html = String(processor.processSync(post.content))
} }
return { post, html } return { post, html }
} catch (e) { } catch (e) {
@@ -245,7 +263,7 @@ useHead({
if (pending.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"} from ${post.value?.publisher.nick}`
}), }),
meta: computed(() => { meta: computed(() => {
if (post.value) { if (post.value) {
@@ -265,8 +283,8 @@ const userPicture = computed(() => {
: undefined : undefined
}) })
const userBackground = computed(() => { const userBackground = computed(() => {
const firstImageAttachment = post.value?.attachments?.find(att => const firstImageAttachment = post.value?.attachments?.find((att) =>
att.mimeType?.startsWith('image/') att.mimeType?.startsWith("image/")
) )
return firstImageAttachment return firstImageAttachment
? `${apiBase}/drive/files/${firstImageAttachment.id}` ? `${apiBase}/drive/files/${firstImageAttachment.id}`
@@ -274,11 +292,14 @@ const userBackground = computed(() => {
}) })
defineOgImage({ defineOgImage({
component: 'ImageCard', component: "ImageCard",
title: computed(() => post.value?.title || 'Post'), title: computed(() => post.value?.title || "Post"),
description: computed(() => post.value?.description || post.value?.content?.substring(0, 150) || ''), description: computed(
() =>
post.value?.description || post.value?.content?.substring(0, 150) || ""
),
avatarUrl: computed(() => userPicture.value), avatarUrl: computed(() => userPicture.value),
backgroundImage: computed(() => userBackground.value), backgroundImage: computed(() => userBackground.value)
}) })
function formatDate(dateString: string): string { function formatDate(dateString: string): string {

View File

@@ -60,7 +60,7 @@ export interface SnPost {
awardedScore: number; awardedScore: number;
reactionsCount: Record<string, number>; reactionsCount: Record<string, number>;
repliesCount: number; repliesCount: number;
reactionsMade: Record<string, unknown>; reactionsMade: Record<string, boolean>;
repliedGone: boolean; repliedGone: boolean;
forwardedGone: boolean; forwardedGone: boolean;
repliedPostId: string | null; 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", "@nuxtjs/color-mode",
"nuxt-og-image" "nuxt-og-image"
], ],
css: ["~/assets/css/main.css"], css: ["~/assets/css/main.css", "katex/dist/katex.min.css"],
app: { app: {
pageTransition: { name: "page", mode: "out-in" }, pageTransition: { name: "page", mode: "out-in" },
head: { 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", "blurhash": "^2.0.5",
"cfturnstile-vue3": "^2.0.0", "cfturnstile-vue3": "^2.0.0",
"eslint": "^9.36.0", "eslint": "^9.36.0",
"katex": "^0.16.22",
"luxon": "^3.7.2", "luxon": "^3.7.2",
"marked": "^16.3.0",
"nuxt": "^4.1.2", "nuxt": "^4.1.2",
"nuxt-og-image": "^5.1.11", "nuxt-og-image": "^5.1.11",
"pinia": "^3.0.3", "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", "sharp": "^0.34.4",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
"tus-js-client": "^4.3.1", "tus-js-client": "^4.3.1",
"unified": "^11.0.5",
"vue": "^3.5.21", "vue": "^3.5.21",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"vuetify-nuxt-module": "0.18.7" "vuetify-nuxt-module": "0.18.7"
@@ -41,6 +51,6 @@
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
"@types/luxon": "^3.7.1", "@types/luxon": "^3.7.1",
"@types/node": "^24.5.2", "@types/node": "^24.5.2",
"eslint-plugin-tailwindcss": "^4.0.0-beta.0" "eslint-plugin-tailwindcss": "^3.18.2"
} }
} }