Compare commits

...

3 Commits

Author SHA1 Message Date
e4111dc06e 💄 Optimized header & landing page 2025-03-17 22:23:40 +08:00
3e7f259834 🗑️ Clean up layouts 2025-03-17 21:10:33 +08:00
97449bdc1e 🗑️ Clean up posts 2025-03-17 20:58:36 +08:00
13 changed files with 272 additions and 210 deletions

View File

@ -3,10 +3,12 @@
<v-card-text> <v-card-text>
<div class="mb-3 flex flex-row gap-4"> <div class="mb-3 flex flex-row gap-4">
<nuxt-link :to="`/users/${post.publisher?.name}`"> <nuxt-link :to="`/users/${post.publisher?.name}`">
<v-avatar :image="post.publisher?.avatar" /> <v-avatar :image="getAttachmentUrl(post.publisher?.avatar)" icon="mdi-account-circle" />
</nuxt-link> </nuxt-link>
<div class="flex flex-col"> <div class="flex flex-col">
<span>{{ post.publisher?.nick }} <span class="text-xs">@{{ post.publisher?.name }}</span></span> <span
>{{ post.publisher?.nick }} <span class="text-xs">@{{ post.publisher?.name }}</span></span
>
<span v-if="post.body?.title" class="text-md">{{ post.body?.title }}</span> <span v-if="post.body?.title" class="text-md">{{ post.body?.title }}</span>
<span v-if="post.body?.description" class="text-sm">{{ post.body?.description }}</span> <span v-if="post.body?.description" class="text-sm">{{ post.body?.description }}</span>
<span v-if="!post.body?.title && !post.body?.description" class="text-sm"> <span v-if="!post.body?.title && !post.body?.description" class="text-sm">
@ -29,7 +31,7 @@
/> />
</div> </div>
<article v-if="post.type == 'story' || props.forceShowContent" class="text-base prose max-w-none"> <article v-if="(post.type == 'story' || props.forceShowContent) && post.body?.content" class="text-base prose max-w-none">
<m-d-c :value="post.body?.content"></m-d-c> <m-d-c :value="post.body?.content"></m-d-c>
</article> </article>
@ -41,24 +43,19 @@
</v-card> </v-card>
<div class="text-sm flex flex-col"> <div class="text-sm flex flex-col">
<span class="flex flex-row gap-1"> <span class="flex flex-row gap-1">
<span> <span> {{ post.metric.reply_count }} {{ post.metric.reply_count > 1 ? "replies" : "reply" }}, </span>
{{ post.metric.reply_count }} {{ post.metric.reply_count > 1 ? "replies" : "reply" }}, <span>
</span> {{ post.metric.reaction_count }} {{ post.metric.reaction_count > 1 ? "reactions" : "reaction" }}
<span> </span>
{{ post.metric.reaction_count }} {{ post.metric.reaction_count > 1 ? "reactions" : "reaction" }} </span>
</span>
</span>
<span> <span>
{{ post.type.startsWith("a") ? "An" : "A" }} {{ post.type }} posted on {{ post.type.startsWith("a") ? "An" : "A" }} {{ post.type }} posted on
{{ new Date(post.published_at).toLocaleString() }} {{ new Date(post.published_at).toLocaleString() }}
</span> </span>
</div> </div>
<div <div v-if="post.tags?.length > 0" class="text-xs text-grey flex flex-row gap-1 mt-3">
v-if="post.tags?.length > 0"
class="text-xs text-grey flex flex-row gap-1 mt-3"
>
<nuxt-link <nuxt-link
v-for="tag in post.tags" v-for="tag in post.tags"
:to="`/posts/tags/${tag.alias}`" :to="`/posts/tags/${tag.alias}`"
@ -73,10 +70,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ post: any, forceShowContent?: boolean, noClickableAttachment?: boolean }>() const props = defineProps<{ post: any; forceShowContent?: boolean; noClickableAttachment?: boolean }>()
const config = useRuntimeConfig() const config = useRuntimeConfig()
const { t } = useI18n() const { t } = useI18n()
const url = computed(() => props.post.alias ? `/posts/${props.post.area_alias}/${props.post.alias}` : `/posts/${props.post.id}`) const url = computed(() =>
props.post.alias ? `/posts/${props.post.area_alias}/${props.post.alias}` : `/posts/${props.post.id}`,
)
</script> </script>

View File

@ -5,13 +5,11 @@
</v-alert> </v-alert>
</v-expand-transition> </v-expand-transition>
<v-data-table-server <v-data-table
density="compact" density="compact"
:headers="dataDefinitions.stickers" :headers="dataDefinitions.stickers"
:items="stickers" :items="stickers"
:items-length="pagination.stickers.total"
:loading="reverting.stickers" :loading="reverting.stickers"
v-model:items-per-page="pagination.stickers.pageSize"
@update:options="readStickers" @update:options="readStickers"
item-value="id" item-value="id"
> >
@ -74,23 +72,24 @@
<template v-slot:default="{ isActive }"> <template v-slot:default="{ isActive }">
<v-card :title="`Delete sticker #${item.id}?`"> <v-card :title="`Delete sticker #${item.id}?`">
<v-card-text> <v-card-text>
This action will delete this sticker, all content used it will no longer show your sticker. This action will delete this sticker, all content used it will no longer show your sticker. But the
But the attachment will still exists. attachment will still exists.
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn text="Cancel" color="grey" @click="isActive.value = false"></v-btn>
text="Cancel"
color="grey"
@click="isActive.value = false"
></v-btn>
<v-btn <v-btn
text="Delete" text="Delete"
color="error" color="error"
@click="() => { deleteSticker(item); isActive.value = false }" @click="
() => {
deleteSticker(item)
isActive.value = false
}
"
/> />
</v-card-actions> </v-card-actions>
</v-card> </v-card>
@ -99,7 +98,7 @@
</td> </td>
</tr> </tr>
</template> </template>
</v-data-table-server> </v-data-table>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -108,7 +107,7 @@ import { solarFetch } from "~/utils/request"
const config = useRuntimeConfig() const config = useRuntimeConfig()
const { t } = useI18n() const { t } = useI18n()
const props = defineProps<{ packId: number, packPrefix?: string }>() const props = defineProps<{ packId: number; packPrefix?: string }>()
const error = ref<null | string>(null) const error = ref<null | string>(null)
@ -125,34 +124,20 @@ const dataDefinitions: { [id: string]: any[] } = {
const stickers = ref<any>([]) const stickers = ref<any>([])
const reverting = reactive({ stickers: false }) const reverting = reactive({ stickers: false })
const pagination = reactive({
stickers: { page: 1, pageSize: 5, total: 0 },
})
async function readStickers({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
if (itemsPerPage) pagination.stickers.pageSize = itemsPerPage
if (page) pagination.stickers.page = page
async function readStickers() {
reverting.stickers = true reverting.stickers = true
const res = await solarFetch( const res = await solarFetch("/cgi/uc/stickers/packs/" + props.packId)
"/cgi/uc/stickers?" +
new URLSearchParams({
pack: props.packId.toString(),
take: pagination.stickers.pageSize.toString(),
offset: ((pagination.stickers.page - 1) * pagination.stickers.pageSize).toString(),
}),
)
if (res.status !== 200) { if (res.status !== 200) {
error.value = await res.text() error.value = await res.text()
} else { } else {
const data = await res.json() const data = await res.json()
stickers.value = data["data"] stickers.value = data["stickers"]
pagination.stickers.total = data["count"]
} }
reverting.stickers = false reverting.stickers = false
} }
onMounted(() => readStickers({})) onMounted(() => readStickers())
const submitting = ref(false) const submitting = ref(false)
@ -165,7 +150,7 @@ async function deleteSticker(item: any) {
if (res.status !== 200) { if (res.status !== 200) {
error.value = await res.text() error.value = await res.text()
} else { } else {
await readStickers({}) await readStickers()
} }
submitting.value = false submitting.value = false

View File

@ -1,17 +1,10 @@
<template> <template>
<v-app-bar flat color="primary" scroll-behavior="hide" scroll-threshold="800"> <v-app-bar flat color="primary" scroll-behavior="hide" scroll-threshold="800">
<v-container fluid class="mx-auto d-flex align-center justify-center px-8"> <v-container fluid class="mx-auto d-flex align-center justify-center px-8">
<v-tooltip> <v-app-bar-nav-icon @click="openDrawer = !openDrawer" />
<template #activator="{ props }">
<div @click="openDrawer = !openDrawer" v-bind="props" class="cursor-pointer">
<v-img class="me-4 ms-1" width="32" height="32" alt="Logo" :src="Logo" />
</div>
</template>
Open / close drawer
</v-tooltip>
<nuxt-link to="/dev" exact> <nuxt-link to="/creator" exact>
<h2 class="mt-1">Creator Hub</h2> <h2>Creator Hub</h2>
</nuxt-link> </nuxt-link>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@ -45,12 +38,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Logo from "../assets/logo-w-shadow.png"
const { t } = useI18n() const { t } = useI18n()
const openDrawer = ref(false) const openDrawer = ref(false)
useHead({ useHead({
titleTemplate: "%s | Solsynth Creator Hub" titleTemplate: "%s | Solsynth Creator Hub",
}) })
</script> </script>

View File

@ -1,41 +1,38 @@
<template> <template>
<v-app-bar flat color="primary"> <v-app-bar app flat color="surface" class="app-bar-blur">
<v-container fluid class="mx-auto d-flex align-center justify-center px-8"> <v-container fluid class="mx-auto d-flex align-center justify-center pr-8">
<v-tooltip> <v-app-bar-nav-icon @click="openDrawer = !openDrawer" />
<template #activator="{ props }">
<div @click="openDrawer = !openDrawer" v-bind="props" class="cursor-pointer">
<v-img class="me-4 ms-1" width="32" height="32" alt="Logo" :src="Logo" />
</div>
</template>
Open / close drawer
</v-tooltip>
<nuxt-link to="/" exact> <nuxt-link to="/" exact>
<h2 class="mt-1">Solsynth LLC</h2> <h2>Solsynth LLC</h2>
</nuxt-link> </nuxt-link>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<div class="flex gap-2">
<v-btn to="/products" exact prepend-icon="mdi-shape">{{ t("navProducts") }}</v-btn>
<v-btn to="/posts" exact prepend-icon="mdi-note-text">{{ t("navPosts") }}</v-btn>
<v-btn to="/gallery" exact prepend-icon="mdi-image-multiple">{{ t("navGallery") }}</v-btn>
</div>
<v-spacer></v-spacer>
<locale-select /> <locale-select />
<user-menu /> <user-menu />
</v-container> </v-container>
</v-app-bar> </v-app-bar>
<v-navigation-drawer v-model="openDrawer" location="left" width="300" floating> <v-navigation-drawer v-model="openDrawer" location="left" width="300" temporary order="-1">
<v-list density="compact" nav color="primary"> <v-list density="compact" nav color="primary">
<v-list-item :title="t('navProducts')" prepend-icon="mdi-shape" to="/products" exact /> <v-list-item title="Knowledge Base" prepend-icon="mdi-library" to="/docs" exact />
<v-list-item :title="t('navPosts')" prepend-icon="mdi-note-text" to="/posts" exact /> <v-list-item title="Developer Portal" prepend-icon="mdi-code-tags" to="/dev" exact />
<v-list-item :title="t('navActivity')" prepend-icon="mdi-newspaper-variant-multiple-outline" to="/activity" exact /> <v-list-item title="Creator Hub" prepend-icon="mdi-pencil" to="/creator" exact />
<v-list-item :title="t('navGallery')" prepend-icon="mdi-image-multiple" to="/gallery" exact />
</v-list> </v-list>
<v-divider class="border-opacity-50 my-1" /> <v-divider class="border-opacity-50 my-1" />
<v-list density="compact" nav color="primary"> <v-list density="compact" nav color="primary">
<v-list-item title="Knowledge Base" prepend-icon="mdi-library" to="/docs" exact /> <v-list-item title="Code Repository" prepend-icon="mdi-git" href="https://git.solsynth.dev" target="_blank" />
<v-list-item title="Developer Portal" prepend-icon="mdi-code-tags" to="/dev" exact />
<v-list-item title="Creator Hub" prepend-icon="mdi-pencil" to="/creator" exact />
</v-list> </v-list>
<v-divider class="border-opacity-50 mb-4 mt-0.5" /> <v-divider class="border-opacity-50 mb-4 mt-0.5" />
@ -51,9 +48,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Logo from "../assets/logo-w-shadow.png" const { t } = useI18n()
const { t } = useI18n()
const openDrawer = ref(false) const openDrawer = ref(false)
</script> </script>
<style lang="css" scoped>
.app-bar-blur {
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 40%, rgba(0, 0, 0, 0.5) 65%, rgba(0, 0, 0, 0) 100%);
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 40%, rgba(0, 0, 0, 0.5) 65%, rgba(0, 0, 0, 0) 100%);
mask-repeat: no-repeat;
mask-size: 100%;
}
</style>

View File

@ -1,17 +1,10 @@
<template> <template>
<v-app-bar flat color="primary" scroll-behavior="hide" scroll-threshold="800"> <v-app-bar flat color="primary" scroll-behavior="hide" scroll-threshold="800">
<v-container fluid class="mx-auto d-flex align-center justify-center px-8"> <v-container fluid class="mx-auto d-flex align-center justify-center pr-8">
<v-tooltip> <v-app-bar-nav-icon @click="openDrawer = !openDrawer" />
<template #activator="{ props }">
<div @click="openDrawer = !openDrawer" v-bind="props" class="cursor-pointer">
<v-img class="me-4 ms-1" width="32" height="32" alt="Logo" :src="Logo" />
</div>
</template>
Open / close drawer
</v-tooltip>
<nuxt-link to="/dev" exact> <nuxt-link to="/dev" exact>
<h2 class="mt-1">Developer Portal</h2> <h2>Developer Portal</h2>
</nuxt-link> </nuxt-link>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@ -51,6 +44,6 @@ const { t } = useI18n()
const openDrawer = ref(false) const openDrawer = ref(false)
useHead({ useHead({
titleTemplate: "%s | Solsynth Dev Portal" titleTemplate: "%s | Solsynth Dev Portal",
}) })
</script> </script>

View File

@ -151,7 +151,7 @@ export default defineNuxtConfig({
"@pinia/nuxt", "@pinia/nuxt",
"@nuxtjs/i18n", "@nuxtjs/i18n",
"nuxt-schema-org", "nuxt-schema-org",
"nuxt-gtag", "@vueuse/motion/nuxt",
(_options, nuxt) => { (_options, nuxt) => {
nuxt.hooks.hook("vite:extendConfig", (config) => { nuxt.hooks.hook("vite:extendConfig", (config) => {
// @ts-expect-error // @ts-expect-error

View File

@ -17,6 +17,7 @@
"@nuxtjs/i18n": "^8.5.6", "@nuxtjs/i18n": "^8.5.6",
"@nuxtjs/sitemap": "^6.1.5", "@nuxtjs/sitemap": "^6.1.5",
"@pinia/nuxt": "^0.5.5", "@pinia/nuxt": "^0.5.5",
"@vueuse/motion": "^3.0.3",
"feed": "^4.2.2", "feed": "^4.2.2",
"nuxt": "^3.16.0", "nuxt": "^3.16.0",
"nuxt-gtag": "^2.1.0", "nuxt-gtag": "^2.1.0",

View File

@ -1,37 +0,0 @@
<template>
<v-container class="content-container mx-auto">
<div class="my-3 mx-[1.5ch]">
<div class="flex gap-1">
<h1 class="text-2xl">{{ t("navActivity") }}</h1>
<v-btn size="x-small" variant="text" icon="mdi-rss" slim to="/activity/feed" />
</div>
<span>{{ t("navActivityCaption") }}</span>
</div>
<post-list class="mx-[-2.5ch]" :realm="config.public.solarRealm" />
</v-container>
</template>
<script setup lang="ts">
const { t } = useI18n()
useHead({
title: t("navActivity"),
})
useSeoMeta({
title: t("navActivity"),
ogTitle: t("navActivity"),
description: t("navActivityCaption"),
ogDescription: t("navActivityCaption"),
ogType: "website",
})
const config = useRuntimeConfig()
</script>
<style scoped>
.content-container {
max-width: 70ch !important;
}
</style>

View File

@ -6,13 +6,7 @@
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<v-btn <v-btn color="primary" text="New" append-icon="mdi-plus" variant="tonal" to="/creator/stickers/new" />
color="primary"
text="New"
append-icon="mdi-plus"
variant="tonal"
to="/creator/stickers/new"
/>
</div> </div>
</div> </div>
@ -24,10 +18,7 @@
<div class="mt-5"> <div class="mt-5">
<v-expansion-panels> <v-expansion-panels>
<v-expansion-panel <v-expansion-panel v-for="item in data" :key="'sticker-pack#' + item.id">
v-for="item in data"
:key="'sticker-pack#'+item.id"
>
<template #title> <template #title>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p>{{ item.name }}</p> <p>{{ item.name }}</p>
@ -87,16 +78,17 @@
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn text="Cancel" color="grey" @click="isActive.value = false"></v-btn>
text="Cancel"
color="grey"
@click="isActive.value = false"
></v-btn>
<v-btn <v-btn
text="Delete" text="Delete"
color="error" color="error"
@click="() => { deletePack(item); isActive.value = false }" @click="
() => {
deletePack(item)
isActive.value = false
}
"
/> />
</v-card-actions> </v-card-actions>
</v-card> </v-card>
@ -131,6 +123,7 @@ useHead({
}) })
const { t } = useI18n() const { t } = useI18n()
const ua = useUserinfo()
const loading = ref(false) const loading = ref(false)
const error = ref<null | string>(null) const error = ref<null | string>(null)
@ -140,7 +133,7 @@ const data = ref<any[]>([])
async function readPacks() { async function readPacks() {
loading.value = true loading.value = true
const res = await solarFetch(`/cgi/uc/stickers/packs?take=10&offset=${data.value.length}`) const res = await solarFetch(`/cgi/uc/stickers/packs?take=10&author=${ua.userinfo?.id}&offset=${data.value.length}`)
if (res.status != 200) { if (res.status != 200) {
error.value = await res.text() error.value = await res.text()
} else { } else {

View File

@ -1,39 +1,71 @@
<template> <template>
<v-container class="flex flex-col my-2 px-12 gap-[4rem]"> <v-container class="flex flex-col my-2 px-12 gap-[4rem]">
<v-row class="content-section"> <section class="content-section flex flex-col items-center justify-center text-center px-4">
<v-col cols="12" md="4" class="flex justify-start"> <img
<div class="flex flex-col items-start"> v-motion="{
<h1 class="text-4xl font-bold">{{ t("brandName") }}</h1> initial: {
<p class="text-lg mt-3 max-w-2/3"> y: 100,
{{ t("indexIntroduce") }} opacity: 0,
</p> },
<p class="text-grey mt-2"> enter: {
{{ t("indexProductListHint") }} y: 0,
<v-icon icon="mdi-arrow-right" size="16" class="mb-0.5" /> opacity: 1,
</p> },
</div> }"
</v-col> :src="Logo"
<v-col cols="12" md="8"> alt="Company Logo"
<v-card> class="w-32 h-32 mb-4"
/>
<h1 class="text-4xl font-bold">Welcome to {{ t("brandName") }}</h1>
<p class="mt-2 text-lg">Building cool, open-source, and elegant apps for human.</p>
<v-btn class="mt-4" color="primary" prepend-icon="mdi-arrow-down" href="#products">{{ t("learnMore") }}</v-btn>
</section>
<section class="content-section py-16" id="products">
<div class="container mx-auto text-center">
<h2 class="text-3xl font-bold">Our Projects</h2>
<p>Take a peek of our works.</p>
<v-card class="mt-12">
<product-carousel class="carousel-section" :products="products as any[]" /> <product-carousel class="carousel-section" :products="products as any[]" />
</v-card> </v-card>
</v-col> </div>
</v-row> </section>
<v-row class="content-section"> <v-row class="content-section">
<v-col cols="12" md="8"> <v-col cols="12" md="6">
<v-card class="h-[500px]"> <v-card>
<activity-list class="carousel-section" /> <v-list>
<v-list-item
title="GitHub"
subtitle="The place hosts most of our public projects' code"
prepend-icon="mdi-github"
href="https://github.com/Solsynth"
target="_blank"
/>
<v-list-item
lines="two"
title="Solsynth Code Repository"
subtitle="Our self-hosted git server, may contains some unpublished projects' code"
prepend-icon="mdi-git"
href="https://git.solsynth.dev/explore"
target="_blank"
/>
</v-list>
</v-card> </v-card>
</v-col> </v-col>
<v-col cols="12" md="4" class="flex justify-end" order="first" order-md="last"> <v-col cols="12" md="6" class="flex justify-end" order="first" order-md="last">
<div class="text-right flex flex-col items-end"> <div class="text-right flex flex-col items-end">
<h2 class="text-4xl font-bold">{{ t("indexActivities") }}</h2> <h2 class="text-4xl font-bold">
<p class="text-lg mt-3 max-w-2/3"> We<br />
{{ t("indexActivitiesCaption") }} Open-source
</h2>
<p class="text-md mt-3 max-w-2/3">
No software can run without the support of open source software, and our software is no exception.
Therefore, we feel it is important to contribute to open source as well.
</p> </p>
<p class="text-grey mt-2"> <p class="text-grey mt-2">
<v-icon icon="mdi-arrow-left" size="16" class="mb-0.5" /> <v-icon icon="mdi-arrow-left" size="16" class="mb-0.5" />
{{ t("indexActivitiesHint") }} Check out our GitHub
</p> </p>
</div> </div>
</v-col> </v-col>
@ -42,6 +74,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Logo from "../assets/logo-w-shadow.png"
import { getLocale } from "~/utils/locale" import { getLocale } from "~/utils/locale"
const { t } = useI18n() const { t } = useI18n()
@ -61,13 +95,16 @@ useSeoMeta({
}) })
const { data: products } = await useAsyncData("products", () => { const { data: products } = await useAsyncData("products", () => {
return queryContent("/products").where({ _locale: getLocale(), archived: { $ne: true } }).limit(5).find() return queryContent("/products")
.where({ _locale: getLocale(), archived: { $ne: true } })
.limit(5)
.find()
}) })
</script> </script>
<style scoped> <style scoped>
.carousel-section { .carousel-section {
height: 96rem; height: 120rem;
} }
.content-section { .content-section {
@ -76,3 +113,10 @@ const { data: products } = await useAsyncData("products", () => {
place-items: center; place-items: center;
} }
</style> </style>
<style>
body,
html {
scroll-behavior: smooth;
}
</style>

View File

@ -1,14 +1,19 @@
<template> <template>
<v-container class="content-container mx-auto"> <v-container class="content-container mx-auto">
<div class="my-3 flex flex-row gap-4"> <div class="my-3 flex flex-row gap-4">
<nuxt-link :to="`/users/${post.publisher?.name}`"> <nuxt-link :to="`/publishers/${post.publisher?.name}`">
<v-avatar :image="post.publisher?.avatar" /> <v-avatar :image="getAttachmentUrl(post.publisher?.avatar)" />
</nuxt-link> </nuxt-link>
<div class="flex flex-col"> <div class="flex flex-col">
<span>{{ post.publisher?.nick }} <span class="text-xs">@{{ post.publisher?.name }}</span></span> <span>
{{ post.publisher?.nick }}
<span class="text-xs">@{{ post.publisher?.name }}</span>
</span>
<span v-if="post.body?.title" class="text-md">{{ post.body?.title }}</span> <span v-if="post.body?.title" class="text-md">{{ post.body?.title }}</span>
<span v-if="post.body?.description" class="text-sm">{{ post.body?.description }}</span> <span v-if="post.body?.description" class="text-sm">{{ post.body?.description }}</span>
<span v-if="!post.body?.title && !post.body?.description" class="text-sm">{{ post.publisher?.description }}</span> <span v-if="!post.body?.title && !post.body?.description" class="text-sm">{{
post.publisher?.description
}}</span>
</div> </div>
</div> </div>
@ -20,22 +25,18 @@
/> />
</v-card> </v-card>
<article class="text-base prose xl:text-lg mx-auto"> <article v-if="post.body?.content" class="text-base prose xl:text-lg mx-auto">
<m-d-c :value="post.body?.content"></m-d-c> <m-d-c :value="post.body?.content"></m-d-c>
</article> </article>
<v-card v-if="post.body?.attachments?.length > 0" class="mb-5"> <v-card v-if="post.body?.attachments?.length > 0" class="mb-5">
<attachment-carousel :attachments="post.body?.attachments" @update:metadata="args => attachments = args" /> <attachment-carousel :attachments="post.body?.attachments" @update:metadata="(args) => (attachments = args)" />
</v-card> </v-card>
<div class="mb-3 text-sm flex flex-col"> <div class="mb-3 text-sm flex flex-col">
<span class="flex flex-row gap-1"> <span class="flex flex-row gap-1">
<span> <span> {{ post.metric.reply_count }} {{ post.metric.reply_count > 1 ? "replies" : "reply" }}, </span>
{{ post.metric.reply_count }} {{ post.metric.reply_count > 1 ? "replies" : "reply" }}, <span> {{ post.metric.reaction_count }} {{ post.metric.reaction_count > 1 ? "reactions" : "reaction" }} </span>
</span>
<span>
{{ post.metric.reaction_count }} {{ post.metric.reaction_count > 1 ? "reactions" : "reaction" }}
</span>
</span> </span>
<span> <span>
{{ post.type.startsWith("a") ? "An" : "A" }} {{ post.type }} posted on {{ post.type.startsWith("a") ? "An" : "A" }} {{ post.type }} posted on
@ -43,10 +44,7 @@
</span> </span>
</div> </div>
<div <div v-if="post.tags?.length > 0" class="text-xs text-grey flex flex-row gap-1 mb-3">
v-if="post.tags?.length > 0"
class="text-xs text-grey flex flex-row gap-1 mb-3"
>
<nuxt-link <nuxt-link
v-for="tag in post.tags" v-for="tag in post.tags"
:to="`/posts/tags/${tag.alias}`" :to="`/posts/tags/${tag.alias}`"
@ -108,21 +106,29 @@ if (!post.value) {
navigateTo(`/posts/${post.value.area_alias}/${post.value.alias}`) navigateTo(`/posts/${post.value.area_alias}/${post.value.alias}`)
} }
const title = computed(() => post.value.body?.title ? `${post.value.body?.title} by @${post.value.publisher.name}` : `Post by @${post.value.publisher.name}`) const title = computed(() =>
post.value.body?.title
? `${post.value.body?.title} by @${post.value.publisher.name}`
: `Post by @${post.value.publisher.name}`,
)
const description = computed(() => post.value.body?.description ?? post.value.body?.content.substring(0, 280).trim()) const description = computed(() => post.value.body?.description ?? post.value.body?.content.substring(0, 280).trim())
watch(attachments, (value) => { watch(
if (post.value.body?.thumbnail) { attachments,
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${post.value.body?.thumbnail}` (value) => {
} if (post.value.body?.thumbnail) {
if (value.length > 0 && value[0].mimetype.split("/")[0] == "image") { firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${post.value.body?.thumbnail}`
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachments.value[0].rid}` }
} if (value.length > 0 && value[0].mimetype.split("/")[0] == "image") {
firstImage.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachments.value[0].rid}`
}
if (value.length > 0 && value[0].mimetype.split("/")[0] == "video") { if (value.length > 0 && value[0].mimetype.split("/")[0] == "video") {
firstVideo.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachments.value[0].rid}` firstVideo.value = `${config.public.solarNetworkApi}/cgi/uc/attachments/${attachments.value[0].rid}`
} }
}, { immediate: true, deep: true }) },
{ immediate: true, deep: true },
)
useHead({ useHead({
title: title.value, title: title.value,

View File

@ -0,0 +1,73 @@
<template>
<v-container class="mx-auto">
<v-img v-if="urlOfBanner" :src="urlOfBanner" :aspect-ratio="16 / 5" class="rounded-md mb-3" cover />
<div class="mx-[2.5ch]">
<div class="my-5 mx-4 flex flex-row gap-4">
<v-avatar :image="urlOfAvatar" />
<div class="flex flex-col">
<span
>{{ account?.nick }} <span class="text-xs">@{{ account?.name }}</span></span
>
<span class="text-sm">{{ account?.description }}</span>
</div>
</div>
<div class="mb-7">
<v-card rounded="xl" class="mx-[-5px]">
<v-tabs v-model="tab" align-tabs="start" color="primary" hide-slider>
<v-tab :value="1">{{ t("userActivity") }}</v-tab>
</v-tabs>
</v-card>
</div>
<v-row>
<v-col row="12" lg="8">
<post-list class="mx-[-2.5ch] mt-[-16px]" v-if="account" :author="account.name" />
</v-col>
<v-col row="12" lg="4" order="first" order-lg="last">
<div class="sticky top-0 h-fit">
<v-card prepend-icon="mdi-identifier" title="About">
<v-card-text>
<p><b>Description</b></p>
<p>{{ account.description }}</p>
<p class="mt-3"><b>Joined At</b></p>
<p>{{ new Date(account.created_at).toLocaleString() }}</p>
</v-card-text>
</v-card>
</div>
</v-col>
</v-row>
</div>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
alias: ["/@:name(.*)*"],
})
const { t } = useI18n()
const route = useRoute()
const config = useRuntimeConfig()
const tab = ref(1)
const { data: account } = await useFetch<any>(`${config.public.solarNetworkApi}/cgi/co/publisher/${route.params.name}`)
if (account.value == null) {
throw createError({
statusCode: 404,
statusMessage: "User Not Found",
})
}
const urlOfAvatar = computed(() =>
account.value?.avatar ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${account.value.avatar}` : void 0,
)
const urlOfBanner = computed(() =>
account.value?.banner ? `${config.public.solarNetworkApi}/cgi/uc/attachments/${account.value.banner}` : void 0,
)
const externalOpenLink = computed(() => `${config.public.solianUrl}/accounts/view/${route.params.name}`)
</script>

View File

@ -14,3 +14,13 @@ export async function solarFetch(input: string, init?: RequestInit) {
}, },
}) })
} }
export function getAttachmentUrl(identifier: string | undefined): string | undefined {
if (identifier == null || identifier.length == 0) {
return undefined
}
if (identifier.startsWith("http")) {
return identifier
}
return `${useRuntimeConfig().public.solarNetworkApi}/cgi/uc/attachments/${identifier}`
}