♻️ Refactored some components to new UI

This commit is contained in:
2025-11-27 21:52:51 +08:00
parent 8af7037b24
commit 040e19025e
19 changed files with 404 additions and 522 deletions

View File

@@ -1,11 +1,10 @@
<template> <template>
<naive-config> <naive-config>
<n-config-provider>
<n-dialog-provider> <n-dialog-provider>
<n-notification-provider> <n-notification-provider>
<n-message-provider> <n-message-provider>
<n-loading-bar-provider> <n-loading-bar-provider>
<nuxt-loading-indicator :color="colorMode.value == 'dark' ? 'white' : '#3f51b5'" /> <nuxt-loading-indicator />
<nuxt-layout> <nuxt-layout>
<nuxt-page /> <nuxt-page />
</nuxt-layout> </nuxt-layout>
@@ -13,10 +12,27 @@
</n-message-provider> </n-message-provider>
</n-notification-provider> </n-notification-provider>
</n-dialog-provider> </n-dialog-provider>
</n-config-provider>
</naive-config> </naive-config>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const colorMode = useColorMode() import "@fontsource-variable/nunito"
import { usePreferredColorScheme } from "@vueuse/core"
const { colorModePreference } = useNaiveColorMode()
const colorScheme = usePreferredColorScheme()
colorModePreference.set("system")
onMounted(() => {
switch (colorScheme.value) {
case "dark":
colorModePreference.set("dark")
case "light":
colorModePreference.set("light")
default:
colorModePreference.set("system")
}
colorModePreference.sync()
})
</script> </script>

View File

@@ -4,7 +4,6 @@
@layer theme, base, components, utilities; @layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css" layer(utilities); @import "tailwindcss/utilities.css" layer(utilities);
@layer base { @layer base {

22
app/components.d.ts vendored
View File

@@ -12,16 +12,27 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
NAlert: typeof import('naive-ui')['NAlert']
NAvatar: typeof import('naive-ui')['NAvatar'] NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton'] NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCardSection: typeof import('naive-ui')['NCardSection']
NChip: typeof import('naive-ui')['NChip']
NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialog: typeof import('naive-ui')['NDialog']
NDialogProvider: typeof import('naive-ui')['NDialogProvider'] NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDropdown: typeof import('naive-ui')['NDropdown'] NDropdown: typeof import('naive-ui')['NDropdown']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput'] NInput: typeof import('naive-ui')['NInput']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider'] NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider'] NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NSelect: typeof import('naive-ui')['NSelect'] NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NTag: typeof import('naive-ui')['NTag']
NThemeEditor: typeof import('naive-ui')['NThemeEditor'] NThemeEditor: typeof import('naive-ui')['NThemeEditor']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
@@ -30,16 +41,27 @@ declare module 'vue' {
// For TSX support // For TSX support
declare global { declare global {
const NAlert: typeof import('naive-ui')['NAlert']
const NAvatar: typeof import('naive-ui')['NAvatar'] const NAvatar: typeof import('naive-ui')['NAvatar']
const NButton: typeof import('naive-ui')['NButton'] const NButton: typeof import('naive-ui')['NButton']
const NCard: typeof import('naive-ui')['NCard']
const NCardSection: typeof import('naive-ui')['NCardSection']
const NChip: typeof import('naive-ui')['NChip']
const NConfigProvider: typeof import('naive-ui')['NConfigProvider'] const NConfigProvider: typeof import('naive-ui')['NConfigProvider']
const NDialog: typeof import('naive-ui')['NDialog']
const NDialogProvider: typeof import('naive-ui')['NDialogProvider'] const NDialogProvider: typeof import('naive-ui')['NDialogProvider']
const NDropdown: typeof import('naive-ui')['NDropdown'] const NDropdown: typeof import('naive-ui')['NDropdown']
const NIcon: typeof import('naive-ui')['NIcon']
const NInput: typeof import('naive-ui')['NInput'] const NInput: typeof import('naive-ui')['NInput']
const NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider'] const NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
const NMenu: typeof import('naive-ui')['NMenu']
const NMessageProvider: typeof import('naive-ui')['NMessageProvider'] const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
const NModal: typeof import('naive-ui')['NModal']
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
const NSelect: typeof import('naive-ui')['NSelect'] const NSelect: typeof import('naive-ui')['NSelect']
const NSpace: typeof import('naive-ui')['NSpace']
const NSpin: typeof import('naive-ui')['NSpin']
const NTag: typeof import('naive-ui')['NTag']
const NThemeEditor: typeof import('naive-ui')['NThemeEditor'] const NThemeEditor: typeof import('naive-ui')['NThemeEditor']
const RouterLink: typeof import('vue-router')['RouterLink'] const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView'] const RouterView: typeof import('vue-router')['RouterView']

View File

@@ -1,8 +1,9 @@
<template> <template>
<div :class="['flex gap-3 items-center', { 'gap-2': compact }]"> <div :class="['flex gap-3 items-center', { 'gap-2': compact }]">
<v-avatar <n-avatar
:image="publisherAvatar" round
:size="compact ? 24 : 40" :src="publisherAvatar"
:size="compact ? 24 : 36"
:border="compact" :border="compact"
@click="router.push('/publishers/' + props.item.publisher.name)" @click="router.push('/publishers/' + props.item.publisher.name)"
/> />

View File

@@ -1,6 +1,5 @@
<template> <template>
<div :class="['card', { 'shadow-none': props.flat }]"> <n-card>
<div :class="['card-body', { 'p-0': props.slim }]">
<div :class="['flex flex-col', compact ? 'gap-1' : 'gap-3']"> <div :class="['flex flex-col', compact ? 'gap-1' : 'gap-3']">
<post-header :item="props.item" :compact="compact" /> <post-header :item="props.item" :compact="compact" />
@@ -21,12 +20,18 @@
</article> </article>
<template v-if="showReferenced"> <template v-if="showReferenced">
<div v-if="props.item.repliedPost || props.item.repliedGone" class="border rounded-md"> <div
v-if="props.item.repliedPost || props.item.repliedGone"
class="border rounded-md"
>
<div class="p-2 flex items-center gap-2"> <div class="p-2 flex items-center gap-2">
<span class="mdi mdi-reply"></span> <span class="mdi mdi-reply"></span>
<span class="font-bold">Replying to</span> <span class="font-bold">Replying to</span>
</div> </div>
<div v-if="props.item.repliedGone" class="px-4 pb-3 text-sm opacity-60"> <div
v-if="props.item.repliedGone"
class="px-4 pb-3 text-sm opacity-60"
>
Post unavailable Post unavailable
</div> </div>
<post-item <post-item
@@ -40,12 +45,18 @@
/> />
</div> </div>
<div v-if="props.item.forwardedPost || props.item.forwardedGone" class="border rounded-md"> <div
v-if="props.item.forwardedPost || props.item.forwardedGone"
class="border rounded-md"
>
<div class="p-2 flex items-center gap-2"> <div class="p-2 flex items-center gap-2">
<span class="mdi mdi-forward"></span> <span class="mdi mdi-forward"></span>
<span class="font-bold">Forwarded</span> <span class="font-bold">Forwarded</span>
</div> </div>
<div v-if="props.item.forwardedGone" class="px-4 pb-3 text-sm opacity-60"> <div
v-if="props.item.forwardedGone"
class="px-4 pb-3 text-sm opacity-60"
>
Post unavailable Post unavailable
</div> </div>
<post-item <post-item
@@ -93,15 +104,14 @@
/> />
</div> </div>
</div> </div>
</div> </n-card>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from "vue" import { ref, watch } from "vue"
import { useMarkdownProcessor } from "~/composables/useMarkdownProcessor" import { useMarkdownProcessor } from "~/composables/useMarkdownProcessor"
import type { SnPost } from "~/types/api" import type { SnPost } from "~/types/api"
import { useIntersectionObserver } from '@vueuse/core' import { useIntersectionObserver } from "@vueuse/core"
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -152,8 +162,9 @@ const repliesVisible = ref(false)
useIntersectionObserver( useIntersectionObserver(
repliesTarget, repliesTarget,
([{ isIntersecting }]) => { (entries) => {
if (isIntersecting) { const entry = entries[0]
if (entry?.isIntersecting) {
repliesVisible.value = true repliesVisible.value = true
} }
}, },

View File

@@ -1,140 +1,57 @@
<template> <template>
<div class="d-flex flex-wrap gap-2"> <div class="flex flex-wrap gap-3">
<!-- Add Reaction Button --> <!-- Add Reaction Button -->
<v-chip <n-tag
v-if="canReact" v-if="canReact"
rounded clickable
style="cursor: pointer"
type="primary"
:disabled="submitting" :disabled="submitting"
prepend-icon="mdi-plus"
@click="showReactionDialog" @click="showReactionDialog"
> >
<template #icon>
<n-icon :component="HeartPlus" />
</template>
React React
</v-chip> </n-tag>
<!-- Existing Reactions --> <!-- Existing Reactions -->
<v-chip <n-space>
<n-tag
v-for="(count, symbol) in reactions" v-for="(count, symbol) in reactions"
:key="symbol" :key="symbol"
rounded :type="getReactionColor(symbol)"
:color="getReactionColor(symbol)"
:disabled="submitting" :disabled="submitting"
@click="reactToPost(symbol)" @click="reactToPost(symbol)"
style="cursor: pointer"
class="reaction-tag"
> >
<span class="reaction-emoji">{{ getReactionEmoji(symbol) }}</span> <span class="reaction-emoji">{{ getReactionEmoji(symbol) }}</span>
<span class="reaction-symbol">{{ symbol }}</span> <span class="reaction-symbol ms-2">{{ symbol }}</span>
<v-chip size="x-small" variant="flat" class="reaction-count ms-1"> <code class="text-xs ms-1.5">x{{ count }}</code>
{{ count }} </n-tag>
</v-chip> </n-space>
</v-chip>
</div> </div>
<!-- Reaction Selection Dialog --> <!-- Reaction Selection Dialog -->
<v-dialog v-model="reactionDialog" max-width="500" height="600"> <n-modal v-model:show="reactionDialog">
<v-card prepend-icon="mdi-emoticon-outline" title="React Post"> <n-card class="max-w-[540px]">
<!-- Dialog Content --> <template #header>
<span class="font-bold">React Post</span>
</template>
<div class="dialog-content"> <div class="dialog-content">
<!-- Positive Reactions --> <n-alert type="info" title="Reaction not available">
<div class="reaction-section"> Due to various of reasons, we stop providing the react creation on the
<div class="section-header d-flex align-center px-6 py-3"> FloatingIsland. To react post, head to web.solian.app
<v-icon class="me-2">mdi-emoticon-happy</v-icon> </n-alert>
<span class="font-bold">Positive</span>
</div> </div>
<div class="reaction-grid"> </n-card>
<v-card </n-modal>
v-for="reaction in getReactionsByAttitude(0)"
:key="reaction.symbol"
class="reaction-card mx-2"
:class="{ selected: isReactionMade(reaction.symbol) }"
:disabled="submitting"
@click="selectReaction(reaction.symbol)"
>
<div class="d-flex flex-column align-center justify-center pa-3">
<span class="text-h4 mb-1">{{ reaction.emoji }}</span>
<span class="text-xs text-center mb-1">{{
reaction.symbol
}}</span>
<span
v-if="getReactionCount(reaction.symbol) > 0"
class="text-xs"
>
x{{ getReactionCount(reaction.symbol) }}
</span>
<div v-else class="spacer"></div>
</div>
</v-card>
</div>
</div>
<!-- Neutral Reactions -->
<div class="reaction-section">
<div class="section-header d-flex align-center px-6 py-3">
<v-icon class="me-2">mdi-emoticon-neutral</v-icon>
<span class="font-bold">Neutral</span>
</div>
<div class="reaction-grid">
<v-card
v-for="reaction in getReactionsByAttitude(1)"
:key="reaction.symbol"
class="reaction-card mx-2"
:class="{ selected: isReactionMade(reaction.symbol) }"
:disabled="submitting"
@click="selectReaction(reaction.symbol)"
>
<div class="d-flex flex-column align-center justify-center pa-3">
<span class="text-h4 mb-1">{{ reaction.emoji }}</span>
<span class="text-xs text-center mb-1">{{
reaction.symbol
}}</span>
<span
v-if="getReactionCount(reaction.symbol) > 0"
class="text-xs"
>
x{{ getReactionCount(reaction.symbol) }}
</span>
<div v-else class="spacer"></div>
</div>
</v-card>
</div>
</div>
<!-- Negative Reactions -->
<div class="reaction-section">
<div class="section-header d-flex align-center px-6 py-3">
<v-icon class="me-2">mdi-emoticon-sad</v-icon>
<span class="font-bold">Negative</span>
</div>
<div class="reaction-grid">
<v-card
v-for="reaction in getReactionsByAttitude(2)"
:key="reaction.symbol"
class="reaction-card mx-2"
:class="{ selected: isReactionMade(reaction.symbol) }"
:disabled="submitting"
@click="selectReaction(reaction.symbol)"
>
<div class="d-flex flex-column align-center justify-center pa-3">
<span class="text-h4 mb-1">{{ reaction.emoji }}</span>
<span class="text-xs text-center mb-1">{{
reaction.symbol
}}</span>
<span
v-if="getReactionCount(reaction.symbol) > 0"
class="text-xs"
>
x{{ getReactionCount(reaction.symbol) }}
</span>
<div v-else class="spacer"></div>
</div>
</v-card>
</div>
</div>
</div>
</v-card>
</v-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue"
import { Smile, Meh, Frown, HeartPlus } from "lucide-vue-next"
interface Props { interface Props {
parentId: string parentId: string
@@ -191,12 +108,21 @@ function getReactionEmoji(symbol: string): string {
return reaction?.emoji || "❓" return reaction?.emoji || "❓"
} }
function getReactionColor(symbol: string): string { function getReactionColor(
const attitude = symbol: string
availableReactions.find((r) => r.symbol === symbol)?.attitude || 1 ):
| "success"
| "error"
| "primary"
| "default"
| "info"
| "warning"
| undefined {
const attitude = availableReactions.find((r) => r.symbol === symbol)?.attitude
if (attitude === 0) return "success" if (attitude === 0) return "success"
if (attitude === 2) return "error" if (attitude === 2) return "error"
return "primary" // neutral or unspecified attitudes use default
return "default"
} }
async function reactToPost(symbol: string) { async function reactToPost(symbol: string) {
@@ -255,64 +181,3 @@ function getReactionCount(symbol: string): number {
return (props.reactions || {})[symbol] || 0 return (props.reactions || {})[symbol] || 0
} }
</script> </script>
<style scoped>
.reaction-emoji {
font-size: 16px;
margin-right: 4px;
}
.reaction-symbol {
font-size: 12px;
font-weight: 500;
}
.reaction-count {
font-size: 10px;
padding: 0 4px;
}
.reaction-section {
margin-bottom: 16px;
}
.section-header {
background-color: rgba(var(--v-theme-surface-variant), 0.5);
border-bottom: 1px solid rgb(var(--v-theme-outline-variant));
}
.reaction-grid {
display: flex;
overflow-x: auto;
gap: 8px;
padding: 16px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.reaction-grid::-webkit-scrollbar {
display: none;
}
.reaction-card {
min-width: 80px;
height: 100px;
cursor: pointer;
transition: all 0.2s ease;
border: 2px solid transparent;
}
.reaction-card:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.reaction-card.selected {
border-color: rgb(var(--v-theme-primary));
background-color: rgb(var(--v-theme-primary-container));
}
.spacer {
height: 16px;
}
</style>

View File

@@ -1,29 +1,22 @@
<template> <template>
<v-card <n-card
class="replies-compact-list" class="replies-compact-list"
flat embedded
border
title="Replies" title="Replies"
prepend-icon="mdi-comment-text-multiple" prepend-icon="mdi-comment-text-multiple"
density="compact" size="small"
> >
<!-- Error State --> <!-- Error State -->
<v-alert <n-alert v-if="hasError" type="error" closable @click:close="refresh">
v-if="hasError"
type="error"
class="mb-4"
closable
@click:close="refresh"
>
{{ error }} {{ error }}
</v-alert> </n-alert>
<!-- Replies List --> <!-- Replies List -->
<div class="flex flex-col gap-2 pb-2.5"> <div class="flex flex-col gap-2">
<template v-for="item in replies" :key="item.id"> <template v-for="item in replies" :key="item.id">
<v-sheet class="px-4" @click="router.push('/posts/' + item.id)"> <div @click="router.push('/posts/' + item.id)">
<div class="flex gap-3"> <div class="flex gap-3">
<v-avatar :image="getPublisherAvatar(item)" size="24" border /> <n-avatar :src="getPublisherAvatar(item)" :size="24" round />
<article <article
v-if="getHtmlContent(item)" v-if="getHtmlContent(item)"
class="prose prose-sm dark:prose-invert prose-slate prose-p:m-0 max-w-none flex-1" class="prose prose-sm dark:prose-invert prose-slate prose-p:m-0 max-w-none flex-1"
@@ -31,24 +24,22 @@
<div v-html="getHtmlContent(item)" /> <div v-html="getHtmlContent(item)" />
</article> </article>
</div> </div>
</v-sheet> </div>
</template> </template>
<!-- Empty State --> <!-- Empty State -->
<div v-if="!replies || replies.length === 0" class="text-center py-8 text-muted-foreground"> <div
<v-icon v-if="!replies || replies.length === 0"
icon="mdi-comment-outline" class="text-center py-8 text-muted-foreground"
size="48" >
class="mb-2 opacity-50" <v-icon icon="mdi-comment-outline" size="48" class="mb-2 opacity-50" />
/>
<p>No replies yet</p> <p>No replies yet</p>
</div> </div>
</div> </div>
</v-card> </n-card>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRepliesList } from "~/composables/useRepliesList" import { useRepliesList } from "~/composables/useRepliesList"
import { useMarkdownProcessor } from "~/composables/useMarkdownProcessor" import { useMarkdownProcessor } from "~/composables/useMarkdownProcessor"
import type { RepliesListParams } from "~/composables/useRepliesList" import type { RepliesListParams } from "~/composables/useRepliesList"

View File

@@ -3,15 +3,15 @@
<post-quick-reply v-if="!props.hideQuickReply" class="mb-4" /> <post-quick-reply v-if="!props.hideQuickReply" class="mb-4" />
<!-- Error State --> <!-- Error State -->
<v-alert <n-alert
v-if="hasError" v-if="hasError"
type="error" type="error"
class="mb-4" class="mb-4"
closable :closable="true"
@click:close="refresh" @close="refresh"
> >
{{ error }} {{ error }}
</v-alert> </n-alert>
<!-- Replies List --> <!-- Replies List -->
<v-infinite-scroll <v-infinite-scroll
@@ -36,18 +36,16 @@
<!-- Loading State --> <!-- Loading State -->
<template #loading> <template #loading>
<div class="flex justify-center py-4"> <div class="flex justify-center py-4">
<v-progress-circular indeterminate size="32" /> <n-spin size="large" />
</div> </div>
</template> </template>
<!-- Empty State --> <!-- Empty State -->
<template #empty> <template #empty>
<div v-if="!replies" class="text-center py-8 text-muted-foreground"> <div v-if="!replies" class="text-center py-8 text-muted-foreground">
<v-icon <n-icon size="48" class="mb-2 opacity-50">
icon="mdi-comment-outline" <i class="mdi mdi-comment-outline"></i>
size="48" </n-icon>
class="mb-2 opacity-50"
/>
<p>No replies yet</p> <p>No replies yet</p>
</div> </div>
</template> </template>

View File

@@ -1,31 +1,43 @@
<template> <template>
<div class="flex flex-col min-h-screen" :data-theme="colorMode.preference"> <div class="flex flex-col min-h-screen">
<header class="navbar bg-base-100 shadow-lg"> <header
<div class="container mx-auto flex items-center justify-center"> class="navbar bg-transparent shadow-lg fixed top-0 left-0 right-0 backdrop-blur-2xl z-1000 h-[64px]"
<img :src="colorMode.value == 'dark' ? IconDark : IconLight" width="32" height="32" class="mr-4" >
alt="The Solar Network" /> <div class="container mx-auto flex items-center">
<img
:src="IconLight"
width="32"
height="32"
class="mr-4"
alt="The Solar Network"
/>
<n-button v-for="link in links" :key="link.title" text @click="() => router.push(link.href)"> <n-menu
<template #icon> v-model:value="activeKey"
<span :class="`mdi ${link.icon}`"></span> mode="horizontal"
</template> :options="menuOptions"
{{ link.title }} />
</n-button>
<div class="grow" /> <div class="grow" />
<n-dropdown :options="dropdownOptions" @select="handleDropdownSelect"> <n-dropdown :options="dropdownOptions" @select="handleDropdownSelect">
<n-avatar round class="mr-4 cursor-pointer" :size="32" :src="user?.profile.picture <n-avatar
round
class="mr-4 cursor-pointer"
:size="32"
:src="
user?.profile.picture
? `${apiBase}/drive/files/${user?.profile.picture?.id}` ? `${apiBase}/drive/files/${user?.profile.picture?.id}`
: undefined : undefined
"> "
>
<span v-if="!user" class="mdi mdi-account-circle text-3xl"></span> <span v-if="!user" class="mdi mdi-account-circle text-3xl"></span>
</n-avatar> </n-avatar>
</n-dropdown> </n-dropdown>
</div> </div>
</header> </header>
<main class="grow container mx-auto py-4"> <main class="grow container mx-auto py-4 mt-[64px]">
<slot /> <slot />
</main> </main>
</div> </div>
@@ -33,23 +45,36 @@
<script lang="ts" setup> <script lang="ts" setup>
import IconLight from "~/assets/images/cloudy-lamb.png" import IconLight from "~/assets/images/cloudy-lamb.png"
import IconDark from "~/assets/images/cloudy-lamb@dark.png"
import type { NavLink } from "~/types/navlink" import type { MenuOption } from "naive-ui"
import { computed, h } from "vue" import { computed, h } from "vue"
import { useRouter } from "vue-router" import { useRouter, useRoute } from "vue-router"
import { CompassIcon } from "lucide-vue-next"
const apiBase = useSolarNetworkUrl() const apiBase = useSolarNetworkUrl()
const colorMode = useColorMode()
const router = useRouter() const router = useRouter()
const route = useRoute()
const { user } = useUserStore() const { user } = useUserStore()
const links: NavLink[] = [ const activeKey = computed(() => {
// Map route paths to menu keys
if (route.path === "/") return "explore"
return null
})
function renderIcon(icon: any) {
return () => h(NIcon, null, { default: () => icon })
}
const menuOptions: MenuOption[] = [
{ {
title: "Explore", label: "Explore",
href: "/", key: "explore",
icon: "mdi-compass" icon: renderIcon(h(CompassIcon)),
props: {
onClick: () => router.push("/")
}
} }
] ]
@@ -59,26 +84,26 @@ const dropdownOptions = computed(() => {
{ {
label: "Dashboard", label: "Dashboard",
key: "/accounts/me", key: "/accounts/me",
icon: () => h('span', { class: 'mdi mdi-view-dashboard' }) icon: () => h("span", { class: "mdi mdi-view-dashboard" })
} }
]; ]
} else { } else {
return [ return [
{ {
label: "Login", label: "Login",
key: "/auth/login", key: "/auth/login",
icon: () => h('span', { class: 'mdi mdi-login' }) icon: () => h("span", { class: "mdi mdi-login" })
}, },
{ {
label: "Create Account", label: "Create Account",
key: "/auth/create-account", key: "/auth/create-account",
icon: () => h('span', { class: 'mdi mdi-account-plus' }) icon: () => h("span", { class: "mdi mdi-account-plus" })
} }
]; ]
} }
}); })
function handleDropdownSelect(key: string) { function handleDropdownSelect(key: string) {
router.push(key); router.push(key)
} }
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="min-h-screen" :data-theme="colorMode.preference"> <div class="min-h-screen">
<main> <main>
<slot /> <slot />
</main> </main>
@@ -25,6 +25,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import Icon from "~/assets/images/cloudy-lamb.png" import Icon from "~/assets/images/cloudy-lamb.png"
const colorMode = useColorMode()
</script> </script>

View File

@@ -6,12 +6,7 @@
</div> </div>
<div class="pa-8"> <div class="pa-8">
<div class="mb-4"> <div class="mb-4">
<img <img :src="IconLight" alt="CloudyLamb" height="60" width="60" />
:src="colorMode.value == 'dark' ? IconDark : IconLight"
alt="CloudyLamb"
height="60"
width="60"
/>
</div> </div>
<v-row> <v-row>
<v-col cols="12" lg="6" class="d-flex align-start justify-start"> <v-col cols="12" lg="6" class="d-flex align-start justify-start">
@@ -74,7 +69,7 @@
<v-btn <v-btn
color="primary" color="primary"
:loading="isAuthorizing" :loading="isAuthorizing"
class="flex-grow-1" class="grow"
size="large" size="large"
@click="handleAuthorize" @click="handleAuthorize"
> >
@@ -83,7 +78,7 @@
<v-btn <v-btn
variant="outlined" variant="outlined"
:disabled="isAuthorizing" :disabled="isAuthorizing"
class="flex-grow-1" class="grow"
size="large" size="large"
@click="handleDeny" @click="handleDeny"
> >
@@ -106,9 +101,6 @@ import { useRoute } from "vue-router"
import { useSolarNetwork } from "~/composables/useSolarNetwork" import { useSolarNetwork } from "~/composables/useSolarNetwork"
import IconLight from "~/assets/images/cloudy-lamb.png" import IconLight from "~/assets/images/cloudy-lamb.png"
import IconDark from "~/assets/images/cloudy-lamb@dark.png"
const colorMode = useColorMode()
const route = useRoute() const route = useRoute()
const api = useSolarNetwork() const api = useSolarNetwork()

View File

@@ -6,12 +6,7 @@
</div> </div>
<div class="pa-8"> <div class="pa-8">
<div class="mb-4"> <div class="mb-4">
<img <img :src="IconLight" alt="CloudyLamb" height="60" width="60" />
:src="colorMode.value == 'dark' ? IconDark : IconLight"
alt="CloudyLamb"
height="60"
width="60"
/>
</div> </div>
<v-row> <v-row>
<v-col cols="12" lg="6" class="d-flex align-start justify-start"> <v-col cols="12" lg="6" class="d-flex align-start justify-start">
@@ -148,13 +143,10 @@ import { useSolarNetwork } from "~/composables/useSolarNetwork"
import CaptchaWidget from "~/components/CaptchaWidget.vue" import CaptchaWidget from "~/components/CaptchaWidget.vue"
import IconLight from "~/assets/images/cloudy-lamb.png" import IconLight from "~/assets/images/cloudy-lamb.png"
import IconDark from "~/assets/images/cloudy-lamb@dark.png"
const router = useRouter() const router = useRouter()
const api = useSolarNetwork() const api = useSolarNetwork()
const colorMode = useColorMode()
useHead({ useHead({
title: "Create Account" title: "Create Account"
}) })

View File

@@ -242,8 +242,6 @@ function getFactorName(factorType: number) {
return "Unknown Factor" return "Unknown Factor"
} }
} }
const colorMode = useColorMode()
</script> </script>
<template> <template>
@@ -254,12 +252,7 @@ const colorMode = useColorMode()
</div> </div>
<div class="pa-8"> <div class="pa-8">
<div class="mb-4"> <div class="mb-4">
<img <img :src="IconLight" alt="CloudyLamb" height="60" width="60" />
:src="colorMode.value == 'dark' ? IconDark : IconLight"
alt="CloudyLamb"
height="60"
width="60"
/>
</div> </div>
<v-row> <v-row>
<v-col cols="12" lg="6" class="d-flex align-start justify-start"> <v-col cols="12" lg="6" class="d-flex align-start justify-start">

View File

@@ -11,8 +11,11 @@
</div> </div>
</div> </div>
<div class="sidebar flex flex-col gap-3"> <div class="sidebar flex flex-col gap-3">
<div v-if="!userStore.isAuthenticated" class="card w-full bg-base-100 shadow-xl"> <div
<div class="card-body"> v-if="!userStore.isAuthenticated"
class="card w-full bg-base-100 shadow-xl"
>
<n-card>
<h2 class="card-title">About</h2> <h2 class="card-title">About</h2>
<p>Welcome to the <b>Solar Network</b></p> <p>Welcome to the <b>Solar Network</b></p>
<p>The open social network. Friendly to everyone.</p> <p>The open social network. Friendly to everyone.</p>
@@ -25,7 +28,7 @@
{{ version.updatedAt }} {{ version.updatedAt }}
</span> </span>
</p> </p>
</div> </n-card>
</div> </div>
<div v-else class="card w-full bg-base-100 shadow-xl"> <div v-else class="card w-full bg-base-100 shadow-xl">
<div class="card-body"> <div class="card-body">
@@ -70,7 +73,7 @@ const userStore = useUserStore()
const version = ref<SnVersion | null>(null) const version = ref<SnVersion | null>(null)
async function fetchVersion() { async function fetchVersion() {
const api = useSolarNetwork() const api = useSolarNetwork()
const resp = await api("/sphere/version") const resp = await api("/version")
version.value = resp as SnVersion version.value = resp as SnVersion
} }
onMounted(() => fetchVersion()) onMounted(() => fetchVersion())
@@ -118,6 +121,7 @@ async function refreshActivities() {
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
} }
.hide-scrollbar::-webkit-scrollbar { .hide-scrollbar::-webkit-scrollbar {
display: none; display: none;
} }

View File

@@ -1,22 +1,26 @@
<template> <template>
<v-container class="py-6"> <div class="py-6">
<div v-if="pending" class="text-center py-12"> <div v-if="pending" class="text-center py-12">
<v-progress-circular indeterminate size="64" color="primary" /> <n-spin size="large" />
<p class="mt-4">Loading post...</p> <p class="mt-4">Loading post...</p>
</div> </div>
<div v-else-if="error" class="text-center py-12"> <div v-else-if="error" class="text-center py-12">
<v-alert type="error" class="mb-4" prominent> <n-alert
<v-alert-title>Error Loading Post</v-alert-title> type="error"
title="Error Loading Post"
class="mb-4"
:closable="false"
>
{{ error?.statusMessage || "Failed to load post" }} {{ error?.statusMessage || "Failed to load post" }}
</v-alert> </n-alert>
</div> </div>
<div v-else-if="post" class="max-w-7xl mx-auto"> <div v-else-if="post" class="max-w-7xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4"> <div class="grid grid-cols-1 lg:grid-cols-12 gap-4">
<!-- Main Content Column --> <!-- Main Content Column -->
<div class="lg:col-span-8 flex flex-col gap-4"> <div class="lg:col-span-8 flex flex-col gap-4">
<v-card class="pa-6"> <n-card class="pa-6">
<post-header :item="post" class="mb-4" /> <post-header :item="post" class="mb-4" />
<!-- Post Title and Description --> <!-- Post Title and Description -->
@@ -38,27 +42,27 @@
<!-- Post Metadata --> <!-- Post Metadata -->
<div class="flex items-center gap-4 text-sm text-medium-emphasis"> <div class="flex items-center gap-4 text-sm text-medium-emphasis">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<v-icon size="16">mdi-calendar</v-icon> <n-icon size="16" name="mdi-calendar" />
<span>{{ formatDate(post.createdAt) }}</span> <span>{{ formatDate(post.createdAt) }}</span>
</div> </div>
<div <div
v-if="post.updatedAt && post.updatedAt !== post.createdAt" v-if="post.updatedAt && post.updatedAt !== post.createdAt"
class="flex items-center gap-1" class="flex items-center gap-1"
> >
<v-icon size="16">mdi-pencil</v-icon> <n-icon size="16" name="mdi-pencil" />
<span>Updated {{ formatDate(post.updatedAt) }}</span> <span>Updated {{ formatDate(post.updatedAt) }}</span>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<v-icon size="16">mdi-eye</v-icon> <n-icon size="16" name="mdi-eye" />
<span> <span>
{{ post.viewsTotal }} / {{ post.viewsUnique }} {{ post.viewsTotal }} / {{ post.viewsUnique }}
views views
</span> </span>
</div> </div>
</div> </div>
</v-card> </n-card>
<v-card class="pa-6"> <n-card class="pa-6">
<article <article
v-if="htmlContent" v-if="htmlContent"
class="prose dark:prose-invert prose-slate max-w-none mb-8" class="prose dark:prose-invert prose-slate max-w-none mb-8"
@@ -72,57 +76,42 @@
v-if="post.type != 1" v-if="post.type != 1"
:attachments="post.attachments || []" :attachments="post.attachments || []"
/> />
</v-card> </n-card>
<v-card <n-card bordered>
title="Replies" <template #header> Replies </template>
prepend-icon="mdi-comment-text-multiple"
color="transparent"
flat
>
<replies-list :params="{ postId: post.id }" /> <replies-list :params="{ postId: post.id }" />
</v-card> </n-card>
</div> </div>
<!-- Sidebar Column --> <!-- Sidebar Column -->
<div class="lg:col-span-4 flex flex-col gap-4"> <div class="lg:col-span-4 flex flex-col gap-4">
<!-- Tags Section --> <!-- Tags Section -->
<v-card <n-card v-if="post.tags && post.tags.length > 0" bordered>
v-if="post.tags && post.tags.length > 0" <template #header> Tags & Categories </template>
rounded="lg"
prepend-icon="mdi-tag-multiple"
title="Tags & Categories"
>
<v-card-text>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<v-chip <n-tag
v-for="category in post.categories" v-for="category in post.categories"
:key="category.id" :key="category.id"
prepend-icon="mdi-shape" type="info"
rounded round
> >
{{ category.slug }} {{ category.slug }}
</v-chip> </n-tag>
<v-chip <n-tag
v-for="tag in post.tags" v-for="tag in post.tags"
:key="tag.id" :key="tag.id"
prepend-icon="mdi-tag" type="primary"
rounded round
> >
{{ tag.slug }} {{ tag.slug }}
</v-chip> </n-tag>
</div> </div>
</v-card-text> </n-card>
</v-card>
<!-- Post Reactions --> <!-- Post Reactions -->
<v-card <n-card class="elevation-1" bordered>
class="elevation-1" <template #header> Reactions </template>
rounded="lg"
title="Reactions"
prepend-icon="mdi-thumb-up"
>
<v-card-text>
<post-reaction-list <post-reaction-list
can-react can-react
:parent-id="id" :parent-id="id"
@@ -130,12 +119,11 @@
:reactions-made="(post as any).reactionsMade || {}" :reactions-made="(post as any).reactionsMade || {}"
@react="handleReaction" @react="handleReaction"
/> />
</v-card-text> </n-card>
</v-card> </div>
</div> </div>
</div> </div>
</div> </div>
</v-container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -239,16 +227,7 @@ const userBackground = computed(() => {
: undefined : undefined
}) })
defineOgImage({ // defineOgImage block removed due to type incompatibility
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)
})
function formatDate(dateString: string): string { function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString("en-US", { return new Date(dateString).toLocaleDateString("en-US", {

View File

@@ -6,26 +6,7 @@
// @ts-ignore // @ts-ignore
import { SwaggerUIBundle, SwaggerUIStandalonePreset } from "swagger-ui-dist" import { SwaggerUIBundle, SwaggerUIStandalonePreset } from "swagger-ui-dist"
import "swagger-ui-dist/swagger-ui.css" import "swagger-ui-dist/swagger-ui.css"
import "swagger-themes/themes/one-dark.css"
const colorMode = useColorMode()
onMounted(() => {
// Load theme once on page load
loadTheme(colorMode.value)
// Reactively switch if user toggles mode
watch(colorMode, (newVal) => {
loadTheme(newVal.value)
})
})
function loadTheme(mode: string) {
if (mode === "dark") {
import("swagger-themes/themes/one-dark.css")
} else {
import("swagger-themes/themes/material.css")
}
}
const apiBase = useSolarNetworkUrl() const apiBase = useSolarNetworkUrl()

View File

@@ -1,5 +1,7 @@
import type { VNode } from "vue"
export interface NavLink { export interface NavLink {
title: string title: string
href: string href: string
icon: string icon: VNode
} }

View File

@@ -1,7 +1,8 @@
import tailwindcss from '@tailwindcss/vite' import tailwindcss from "@tailwindcss/vite"
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from "unplugin-auto-import/vite"
import Components from 'unplugin-vue-components/vite' import Components from "unplugin-vue-components/vite"
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' import { NaiveUiResolver } from "unplugin-vue-components/resolvers"
import { generateTailwindColorThemes } from "@bg-dev/nuxt-naiveui/utils"
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
@@ -12,13 +13,10 @@ export default defineNuxtConfig({
"@nuxt/eslint", "@nuxt/eslint",
"@pinia/nuxt", "@pinia/nuxt",
"@nuxtjs/i18n", "@nuxtjs/i18n",
"@nuxtjs/color-mode",
"nuxt-og-image", "nuxt-og-image",
"@bg-dev/nuxt-naiveui", "@bg-dev/nuxt-naiveui"
],
css: [
"~/assets/css/main.css",
], ],
css: ["~/assets/css/main.css"],
app: { app: {
pageTransition: { name: "page", mode: "out-in" }, pageTransition: { name: "page", mode: "out-in" },
head: { head: {
@@ -44,10 +42,6 @@ export default defineNuxtConfig({
"Nunito:400" "Nunito:400"
] ]
}, },
colorMode: {
preference: "system",
fallback: "light"
},
features: { features: {
inlineStyles: false inlineStyles: false
}, },
@@ -73,18 +67,37 @@ export default defineNuxtConfig({
AutoImport({ AutoImport({
imports: [ imports: [
{ {
'naive-ui': [ "naive-ui": [
'useDialog', "useDialog",
'useMessage', "useMessage",
'useNotification', "useNotification",
'useLoadingBar' "useLoadingBar"
] ]
} }
] ]
}), }),
Components({ Components({
resolvers: [NaiveUiResolver()], resolvers: [NaiveUiResolver()]
}) })
] ]
}, },
naiveui: {
themeConfig: {
...generateTailwindColorThemes(),
shared: {
common: {
fontFamily:
"Nunito Variable, v-sans, ui-system, -apple-system, sans-serif",
primaryColor: "#3F51B5FF",
primaryColorHover: "#5767C1FF",
primaryColorPressed: "#3546A4FF",
primaryColorSuppl: "#4C5EC5FF",
borderRadius: "16px",
borderRadiusSmall: "8px"
}
},
light: {},
dark: {}
}
}
}) })

View File

@@ -16,7 +16,6 @@
"@hcaptcha/vue3-hcaptcha": "^1.3.0", "@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@nuxt/eslint": "1.9.0", "@nuxt/eslint": "1.9.0",
"@nuxt/image": "1.11.0", "@nuxt/image": "1.11.0",
"@nuxtjs/color-mode": "3.5.2",
"@nuxtjs/i18n": "10.1.0", "@nuxtjs/i18n": "10.1.0",
"@pinia/nuxt": "0.11.2", "@pinia/nuxt": "0.11.2",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
@@ -26,6 +25,7 @@
"eslint": "^9.39.1", "eslint": "^9.39.1",
"highlightjs": "^9.16.2", "highlightjs": "^9.16.2",
"katex": "^0.16.25", "katex": "^0.16.25",
"lucide-vue-next": "^0.555.0",
"luxon": "^3.7.2", "luxon": "^3.7.2",
"markdown-exit": "^1.0.0-beta.6", "markdown-exit": "^1.0.0-beta.6",
"markdown-it-highlightjs": "^4.2.0", "markdown-it-highlightjs": "^4.2.0",
@@ -36,9 +36,9 @@
"pinia": "^3.0.4", "pinia": "^3.0.4",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"swagger-themes": "^1.4.3", "swagger-themes": "^1.4.3",
"swagger-ui-dist": "^5.30.2", "swagger-ui-dist": "^5.30.3",
"tus-js-client": "^4.3.1", "tus-js-client": "^4.3.1",
"vue": "^3.5.24", "vue": "^3.5.25",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
@@ -46,7 +46,7 @@
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@types/luxon": "^3.7.1", "@types/luxon": "^3.7.1",
"@types/node": "^24.10.0", "@types/node": "^24.10.1",
"daisyui": "^5.5.5", "daisyui": "^5.5.5",
"naive-ui": "^2.43.2", "naive-ui": "^2.43.2",
"tailwindcss": "^4.1.17" "tailwindcss": "^4.1.17"