💄 Optimize posts
This commit is contained in:
@@ -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>
|
||||||
|
@@ -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 }
|
||||||
)
|
)
|
||||||
|
@@ -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 || "❓"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
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",
|
"@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
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",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user