♻️ Moved the components to use naive ui + daisyui

This commit is contained in:
2025-11-27 00:02:21 +08:00
parent 0ed0dbcab0
commit 8af7037b24
18 changed files with 324 additions and 308 deletions

View File

@@ -1,8 +1,20 @@
<template>
<nuxt-loading-indicator :color="colorMode.value == 'dark' ? 'white' : '#3f51b5'" />
<nuxt-layout>
<nuxt-page />
</nuxt-layout>
<naive-config>
<n-config-provider>
<n-dialog-provider>
<n-notification-provider>
<n-message-provider>
<n-loading-bar-provider>
<nuxt-loading-indicator :color="colorMode.value == 'dark' ? 'white' : '#3f51b5'" />
<nuxt-layout>
<nuxt-page />
</nuxt-layout>
</n-loading-bar-provider>
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</n-config-provider>
</naive-config>
</template>
<script setup lang="ts">

View File

@@ -1,3 +0,0 @@
@forward "vuetify/settings" with (
$layers: true
);

View File

@@ -1,3 +0,0 @@
@use "vuetify" with (
$reset: false
);

View File

@@ -1,49 +1,30 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "@tailwindcss/typography";
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css" layer(utilities);
@layer base {
:root {
--font-family: "Nunito Variable", "Helvatica", sans-serif;
}
.prose p,
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6,
.prose ul,
.prose ol,
.prose blockquote {
margin: revert;
html,
body {
font-family: var(--font-family);
}
}
@plugin "@tailwindcss/typography";
@import "@fontsource-variable/nunito";
@import "@mdi/font/css/materialdesignicons.css";
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
html,
body {
font-family: var(--font-family);
background-color: rgba(var(--v-theme-background), 1);
}
.page-enter-active,
.page-leave-active {
transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
filter: blur(1rem);
}
.prose pre {
padding: 0;
}
}

13
app/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const useDialog: typeof import('naive-ui').useDialog
const useLoadingBar: typeof import('naive-ui').useLoadingBar
const useMessage: typeof import('naive-ui').useMessage
const useNotification: typeof import('naive-ui').useNotification
}

46
app/components.d.ts vendored Normal file
View File

@@ -0,0 +1,46 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import { GlobalComponents } from 'vue'
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDropdown: typeof import('naive-ui')['NDropdown']
NInput: typeof import('naive-ui')['NInput']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NSelect: typeof import('naive-ui')['NSelect']
NThemeEditor: typeof import('naive-ui')['NThemeEditor']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}
// For TSX support
declare global {
const NAvatar: typeof import('naive-ui')['NAvatar']
const NButton: typeof import('naive-ui')['NButton']
const NConfigProvider: typeof import('naive-ui')['NConfigProvider']
const NDialogProvider: typeof import('naive-ui')['NDialogProvider']
const NDropdown: typeof import('naive-ui')['NDropdown']
const NInput: typeof import('naive-ui')['NInput']
const NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
const NSelect: typeof import('naive-ui')['NSelect']
const NThemeEditor: typeof import('naive-ui')['NThemeEditor']
const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView']
}

View File

@@ -1,9 +1,9 @@
<template>
<div class="d-flex justify-center">
<div class="flex justify-center">
<div v-if="provider === 'cloudflare'">
<turnstile v-if="!!apiKey" :sitekey="apiKey" @callback="handleSuccess" />
<div v-else class="mx-auto">
<v-progress-circular indeterminate />
<n-spin />
</div>
</div>
<div v-else-if="provider === 'hcaptcha'">
@@ -13,7 +13,7 @@
@verify="(tk: string) => handleSuccess(tk)"
/>
<div v-else class="mx-auto">
<v-progress-circular indeterminate />
<n-spin />
</div>
</div>
<div
@@ -21,8 +21,8 @@
class="h-captcha"
:data-sitekey="apiKey"
/>
<div v-else class="d-flex flex-column align-center justify-center gap-1">
<v-icon size="32"> mdi-alert-circle-outline </v-icon>
<div v-else class="flex flex-col items-center justify-center gap-1">
<span class="mdi mdi-alert-circle-outline text-3xl"></span>
<span>Captcha provider not configured correctly.</span>
</div>
</div>

View File

@@ -1,30 +1,17 @@
<template>
<v-container class="footer">
<div class="d-flex justify-space-between align-center">
<v-select
:items="['English (United States)']"
model-value="English (United States)"
variant="plain"
density="compact"
hide-details
<div class="container absolute bottom-5 left-1/2 -translate-x-1/2">
<div class="flex justify-between items-center">
<n-select
:options="[{ label: 'English (United States)', value: 'en-us' }]"
model-value="en-us"
size="small"
class="flex-grow-0"
/>
<div class="d-flex">
<v-btn variant="text" size="small" class="text-capitalize">Help</v-btn>
<v-btn variant="text" size="small" class="text-capitalize"
>Privacy</v-btn
>
<v-btn variant="text" size="small" class="text-capitalize">Terms</v-btn>
<div class="flex">
<n-button text size="small">Help</n-button>
<n-button text size="small">Privacy</n-button>
<n-button text size="small">Terms</n-button>
</div>
</div>
</v-container>
</div>
</template>
<style scoped>
.footer {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
}
</style>

View File

@@ -1,19 +1,20 @@
<template>
<div class="flex flex-col gap-2">
<pub-select v-model:value="publisher" />
<v-textarea
v-model="content"
<n-input
v-model:value="content"
type="textarea"
placeholder="What's happended?!"
@keydown.meta.enter.exact="submit"
@keydown.ctrl.enter.exact="submit"
/>
<div class="flex justify-between">
<v-btn type="primary" :loading="submitting" @click="submit">
<n-button type="primary" :loading="submitting" @click="submit">
Post
<template #append>
<v-icon>mdi-send</v-icon>
<template #icon>
<span class="mdi mdi-send"></span>
</template>
</v-btn>
</n-button>
</div>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<v-card :flat="props.flat">
<v-card-text :style="props.slim ? 'padding: 0' : null">
<div :class="['card', { 'shadow-none': props.flat }]">
<div :class="['card-body', { 'p-0': props.slim }]">
<div :class="['flex flex-col', compact ? 'gap-1' : 'gap-3']">
<post-header :item="props.item" :compact="compact" />
@@ -21,50 +21,42 @@
</article>
<template v-if="showReferenced">
<div v-if="props.item.repliedPost || props.item.repliedGone">
<v-card
title="Replying to"
prepend-icon="mdi-reply"
density="compact"
<div v-if="props.item.repliedPost || props.item.repliedGone" class="border rounded-md">
<div class="p-2 flex items-center gap-2">
<span class="mdi mdi-reply"></span>
<span class="font-bold">Replying to</span>
</div>
<div v-if="props.item.repliedGone" class="px-4 pb-3 text-sm opacity-60">
Post unavailable
</div>
<post-item
v-else-if="props.item.repliedPost"
class="px-4 pb-3"
:item="props.item.repliedPost"
slim
compact
flat
border
>
<div v-if="props.item.repliedGone" class="px-4 pb-3 text-sm opacity-60">
Post unavailable
</div>
<post-item
v-else-if="props.item.repliedPost"
class="px-4 pb-3"
:item="props.item.repliedPost"
slim
compact
flat
@react="handleReaction"
/>
</v-card>
@react="handleReaction"
/>
</div>
<div v-if="props.item.forwardedPost || props.item.forwardedGone">
<v-card
title="Forwarded"
prepend-icon="mdi-forward"
density="compact"
<div v-if="props.item.forwardedPost || props.item.forwardedGone" class="border rounded-md">
<div class="p-2 flex items-center gap-2">
<span class="mdi mdi-forward"></span>
<span class="font-bold">Forwarded</span>
</div>
<div v-if="props.item.forwardedGone" class="px-4 pb-3 text-sm opacity-60">
Post unavailable
</div>
<post-item
v-else-if="props.item.forwardedPost"
class="px-4 pb-3"
:item="props.item.forwardedPost"
slim
compact
flat
border
>
<div v-if="props.item.forwardedGone" class="px-4 pb-3 text-sm opacity-60">
Post unavailable
</div>
<post-item
v-else-if="props.item.forwardedPost"
class="px-4 pb-3"
:item="props.item.forwardedPost"
slim
compact
flat
@react="handleReaction"
/>
</v-card>
@react="handleReaction"
/>
</div>
</template>
@@ -74,22 +66,19 @@
:max-height="640"
/>
<v-lazy
v-if="props.item.repliesCount && !compact"
:options="{ threshold: 0.5 }"
transition="fade-transition"
>
<div ref="repliesTarget">
<replies-compact-list
v-if="props.item.repliesCount && !compact && repliesVisible"
:params="{ postId: props.item.id }"
:hide-quick-reply="true"
@react="handleReplyReaction"
/>
</v-lazy>
</div>
<div
v-if="props.item.isTruncated"
class="flex gap-2 text-xs opacity-80"
class="flex gap-2 text-xs opacity-80 items-center"
>
<v-icon icon="mdi-dots-horizontal" size="small" />
<span class="mdi mdi-dots-horizontal"></span>
<p>Post truncated, tap to see details...</p>
</div>
@@ -104,14 +93,15 @@
/>
</div>
</div>
</v-card-text>
</v-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue"
import { useMarkdownProcessor } from "~/composables/useMarkdownProcessor"
import type { SnPost } from "~/types/api"
import { useIntersectionObserver } from '@vueuse/core'
const props = withDefaults(
defineProps<{
@@ -156,4 +146,19 @@ watch(
},
{ immediate: true, deep: true }
)
const repliesTarget = ref(null)
const repliesVisible = ref(false)
useIntersectionObserver(
repliesTarget,
([{ isIntersecting }]) => {
if (isIntersecting) {
repliesVisible.value = true
}
},
{
threshold: 0.5
}
)
</script>

View File

@@ -1,38 +1,19 @@
<template>
<v-select
:items="pubStore.publishers"
item-title="nick"
item-value="name"
:model-value="props.value"
@update:model-value="(v) => emits('update:value', v)"
>
<template #item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps">
<template #prepend>
<v-avatar
:image="item.raw.picture ? `${apiBase}/drive/files/${item.raw.picture.id}` : undefined"
size="small"
/>
</template>
<v-list-item-subtitle>@{{ item.raw?.name }}</v-list-item-subtitle>
</v-list-item>
</template>
<template #selection="{ item }">
<div class="d-flex align-center">
<v-avatar
:image="item.raw.picture ? `${apiBase}/drive/files/${item.raw.picture.id}` : undefined"
size="24"
class="me-2"
/>
{{ item.raw?.nick }}
</div>
</template>
</v-select>
<n-select
:options="pubStore.publishers"
label-field="nick"
value-field="name"
:value="props.value"
@update:value="(v) => emits('update:value', v)"
:render-label="renderLabel"
:render-tag="renderTag"
/>
</template>
<script setup lang="ts">
import { usePubStore } from '~/stores/pub'
import { watch } from 'vue'
import { watch, h } from 'vue'
import type { SelectRenderLabel, SelectRenderTag } from 'naive-ui'
const pubStore = usePubStore()
const apiBase = useSolarNetworkUrl()
@@ -40,6 +21,38 @@ const apiBase = useSolarNetworkUrl()
const props = defineProps<{ value: string | undefined }>()
const emits = defineEmits(['update:value'])
const renderLabel: SelectRenderLabel = (option) => {
return h('div', { class: 'flex items-center' }, [
h(NAvatar, {
src: option.picture ? `${apiBase.value}/drive/files/${option.picture.id}` : undefined,
size: 'small',
class: 'mr-2'
}),
h('div', null, [
h('div', null, option.nick as string),
h('div', { class: 'text-xs text-gray-500' }, `@${option.name as string}`)
])
])
}
const renderTag: SelectRenderTag = ({ option }) => {
return h(
'div',
{
class: 'flex items-center'
},
[
h(NAvatar, {
src: option.picture ? `${apiBase.value}/drive/files/${option.picture.id}` : undefined,
size: 'small',
class: 'mr-2'
}),
option.nick as string
]
)
}
watch(
pubStore,
(value) => {

View File

@@ -6,15 +6,15 @@
<span>FloatingIsland</span>
</div>
<div class="flex flex-wrap gap-1.5">
<a class="link" target="_blank" href="https://solsynth.dev/terms">
<a class="hover:underline" target="_blank" href="https://solsynth.dev/terms">
Terms of Services
</a>
<span class="font-bold">·</span>
<a class="link" target="_blank" href="https://status.solsynth.dev">
<a class="hover:underline" target="_blank" href="https://status.solsynth.dev">
Service Status
</a>
<span class="font-bold">·</span>
<nuxt-link class="link" target="_blank" to="/swagger"> API </nuxt-link>
<nuxt-link class="hover:underline" target="_blank" to="/swagger"> API </nuxt-link>
</div>
<p class="mt-2 opacity-80">
The FloatingIsland do not provides all the features the Solar Network has,
@@ -23,9 +23,3 @@
</p>
</div>
</template>
<style scoped>
.link:hover {
text-decoration: underline;
}
</style>

View File

@@ -1,66 +1,34 @@
<template>
<v-app :theme="colorMode.preference">
<v-app-bar elevation="2" color="surface-lighten-5">
<v-container class="mx-auto flex align-center justify-center">
<img
:src="colorMode.value == 'dark' ? IconDark : IconLight"
width="32"
height="32"
class="me-4"
alt="The Solar Network"
/>
<div class="flex flex-col min-h-screen" :data-theme="colorMode.preference">
<header class="navbar bg-base-100 shadow-lg">
<div class="container mx-auto flex items-center justify-center">
<img :src="colorMode.value == 'dark' ? IconDark : IconLight" width="32" height="32" class="mr-4"
alt="The Solar Network" />
<v-btn
v-for="link in links"
:key="link.title"
:text="link.title"
:to="link.href"
:prepend-icon="link.icon"
variant="text"
/>
<v-spacer />
<v-menu>
<template #activator="{ props }">
<v-avatar
v-bind="props"
class="me-4"
color="grey-darken-1"
size="32"
icon="mdi-account-circle"
:image="
user?.profile.picture
? `${apiBase}/drive/files/${user?.profile.picture?.id}`
: undefined
"
/>
<n-button v-for="link in links" :key="link.title" text @click="() => router.push(link.href)">
<template #icon>
<span :class="`mdi ${link.icon}`"></span>
</template>
<v-list density="compact">
<v-list-item v-if="!user" to="/auth/login" prepend-icon="mdi-login"
>Login</v-list-item
>
<v-list-item
v-if="!user"
to="/auth/create-account"
prepend-icon="mdi-account-plus"
>Create Account</v-list-item
>
<v-list-item
v-if="user"
to="/accounts/me"
prepend-icon="mdi-view-dashboard"
>Dashboard</v-list-item
>
</v-list>
</v-menu>
</v-container>
</v-app-bar>
{{ link.title }}
</n-button>
<v-main>
<div class="grow" />
<n-dropdown :options="dropdownOptions" @select="handleDropdownSelect">
<n-avatar round class="mr-4 cursor-pointer" :size="32" :src="user?.profile.picture
? `${apiBase}/drive/files/${user?.profile.picture?.id}`
: undefined
">
<span v-if="!user" class="mdi mdi-account-circle text-3xl"></span>
</n-avatar>
</n-dropdown>
</div>
</header>
<main class="grow container mx-auto py-4">
<slot />
</v-main>
</v-app>
</main>
</div>
</template>
<script lang="ts" setup>
@@ -68,9 +36,12 @@ import IconLight from "~/assets/images/cloudy-lamb.png"
import IconDark from "~/assets/images/cloudy-lamb@dark.png"
import type { NavLink } from "~/types/navlink"
import { computed, h } from "vue"
import { useRouter } from "vue-router"
const apiBase = useSolarNetworkUrl()
const colorMode = useColorMode()
const router = useRouter()
const { user } = useUserStore()
@@ -81,4 +52,33 @@ const links: NavLink[] = [
icon: "mdi-compass"
}
]
const dropdownOptions = computed(() => {
if (user) {
return [
{
label: "Dashboard",
key: "/accounts/me",
icon: () => h('span', { class: 'mdi mdi-view-dashboard' })
}
];
} else {
return [
{
label: "Login",
key: "/auth/login",
icon: () => h('span', { class: 'mdi mdi-login' })
},
{
label: "Create Account",
key: "/auth/create-account",
icon: () => h('span', { class: 'mdi mdi-account-plus' })
}
];
}
});
function handleDropdownSelect(key: string) {
router.push(key);
}
</script>

View File

@@ -1,12 +1,14 @@
<template>
<v-app :theme="colorMode.preference">
<v-main>
<div class="min-h-screen" :data-theme="colorMode.preference">
<main>
<slot />
</v-main>
</main>
<nuxt-link to="/">
<v-footer app fixed flat height="48">
<v-container class="mx-auto d-flex align-center justify-between">
<footer
class="footer items-center h-12 px-4 bg-neutral text-neutral-content sticky bottom-0"
>
<div class="container mx-auto flex items-center">
<img
:src="Icon"
alt="Cloudy Lamb"
@@ -15,10 +17,10 @@
class="mr-2"
/>
<p class="text-sm">Solar Network</p>
</v-container>
</v-footer>
</div>
</footer>
</nuxt-link>
</v-app>
</div>
</template>
<script lang="ts" setup>

View File

@@ -1,5 +1,5 @@
<template>
<v-container>
<div class="container mx-auto">
<div class="layout">
<div class="main">
<div v-for="activity in activites" :key="activity.id" class="mb-4">
@@ -11,8 +11,9 @@
</div>
</div>
<div class="sidebar flex flex-col gap-3">
<v-card v-if="!userStore.isAuthenticated" class="w-full" title="About">
<v-card-text>
<div v-if="!userStore.isAuthenticated" class="card w-full bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">About</h2>
<p>Welcome to the <b>Solar Network</b></p>
<p>The open social network. Friendly to everyone.</p>
@@ -24,17 +25,17 @@
{{ version.updatedAt }}
</span>
</p>
</v-card-text>
</v-card>
<v-card v-else class="w-full">
<v-card-text>
</div>
</div>
<div v-else class="card w-full bg-base-100 shadow-xl">
<div class="card-body">
<post-editor @posted="refreshActivities" />
</v-card-text>
</v-card>
</div>
</div>
<sidebar-footer class="max-lg:hidden" />
</div>
</div>
</v-container>
</div>
</template>
<script setup lang="ts">

View File

@@ -1,4 +1,7 @@
import tailwindcss from "@tailwindcss/vite"
import tailwindcss from '@tailwindcss/vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
@@ -8,15 +11,13 @@ export default defineNuxtConfig({
"@nuxt/image",
"@nuxt/eslint",
"@pinia/nuxt",
"vuetify-nuxt-module",
"@nuxtjs/i18n",
"@nuxtjs/color-mode",
"nuxt-og-image"
"nuxt-og-image",
"@bg-dev/nuxt-naiveui",
],
css: [
"~/assets/css/main.css",
"~/assets/css/globals.scss",
"katex/dist/katex.min.css"
],
app: {
pageTransition: { name: "page", mode: "out-in" },
@@ -67,15 +68,23 @@ export default defineNuxtConfig({
}
},
vite: {
plugins: [tailwindcss()]
plugins: [
tailwindcss(),
AutoImport({
imports: [
{
'naive-ui': [
'useDialog',
'useMessage',
'useNotification',
'useLoadingBar'
]
}
]
}),
Components({
resolvers: [NaiveUiResolver()],
})
]
},
vuetify: {
moduleOptions: {
disableVuetifyStyles: true,
styles: {
configFile: "assets/css/components.scss"
}
},
vuetifyOptions: "./vuetify.config.ts"
}
})

View File

@@ -19,7 +19,6 @@
"@nuxtjs/color-mode": "3.5.2",
"@nuxtjs/i18n": "10.1.0",
"@pinia/nuxt": "0.11.2",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17",
"@vueuse/core": "^13.9.0",
"blurhash": "^2.0.5",
@@ -33,20 +32,23 @@
"markdown-it-texmath": "^1.0.0",
"nuxt": "^4.2.1",
"nuxt-og-image": "^5.1.12",
"nuxtjs-naive-ui": "1.0.2",
"pinia": "^3.0.4",
"sharp": "^0.34.5",
"swagger-themes": "^1.4.3",
"swagger-ui-dist": "^5.30.2",
"tailwindcss": "^4.1.17",
"tus-js-client": "^4.3.1",
"vue": "^3.5.24",
"vue-router": "^4.6.3",
"vuetify-nuxt-module": "0.18.7"
"vue-router": "^4.6.3"
},
"devDependencies": {
"@bg-dev/nuxt-naiveui": "^2.0.0",
"@mdi/font": "^7.4.47",
"@tailwindcss/typography": "^0.5.19",
"@types/luxon": "^3.7.1",
"@types/node": "^24.10.0",
"sass-embedded": "^1.93.3"
"daisyui": "^5.5.5",
"naive-ui": "^2.43.2",
"tailwindcss": "^4.1.17"
}
}
}

View File

@@ -1,44 +0,0 @@
import { defineVuetifyConfiguration } from "vuetify-nuxt-module/custom-configuration"
import { md3 } from "vuetify/blueprints"
export default defineVuetifyConfiguration({
blueprint: md3,
icons: {
defaultSet: "mdi"
},
theme: {
defaultTheme: "system",
themes: {
light: {
colors: {
background: "#f0f4fa",
surface: "#ffffff",
primary: "#3f51b5",
secondary: "#2196f3",
accent: "#009688",
error: "#f44336",
warning: "#ffc107",
info: "#03a9f4",
success: "#4caf50"
}
},
dark: {
dark: true,
colors: {
background: "#1e1f20",
surface: "#0e0e0e",
primary: "#3f51b5",
secondary: "#2196f3",
accent: "#009688",
error: "#f44336",
warning: "#ffc107",
info: "#03a9f4",
success: "#4caf50"
}
}
}
},
date: {
adapter: "luxon"
}
})