💄 Optimize posts
This commit is contained in:
@@ -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>
|
||||
|
@@ -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 }
|
||||
)
|
||||
|
@@ -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 || "❓"
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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
18
app/types/marked-katex.d.ts
vendored
Normal 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
|
||||
}
|
@@ -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
13455
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user