mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-03 15:56:22 +00:00
♻️ refactor: standardize code formatting and improve readability across components
- Updated component files to use consistent single quotes for strings. - Removed unnecessary newlines and adjusted indentation for better readability. - Simplified conditional rendering and improved code structure in various components. - Added ESLint configuration for better code quality and adherence to standards. - Enhanced error handling in i18n request logic.
This commit is contained in:
6
web/.eslintrc.json
Normal file
6
web/.eslintrc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"next/typescript"
|
||||
]
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
@ -1,10 +1,9 @@
|
||||
|
||||
import { BACKEND_URL } from "@/api/client";
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
import type { NextConfig } from 'next'
|
||||
import createNextIntlPlugin from 'next-intl/plugin'
|
||||
import { BACKEND_URL } from '@/api/client'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
output: 'standalone',
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
@ -22,15 +21,14 @@ const nextConfig: NextConfig = {
|
||||
],
|
||||
},
|
||||
async rewrites() {
|
||||
const backendUrl = BACKEND_URL
|
||||
console.log("Using development API base URL:", backendUrl);
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: backendUrl + '/api/:path*',
|
||||
},
|
||||
]
|
||||
}
|
||||
};
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
export default withNextIntl(nextConfig);
|
||||
console.log('Using development API base URL:', BACKEND_URL)
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: `${BACKEND_URL}/api/:path*`,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
const withNextIntl = createNextIntlPlugin()
|
||||
export default withNextIntl(nextConfig)
|
||||
|
@ -28,6 +28,7 @@
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^5.0.0",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
|
1860
web/pnpm-lock.yaml
generated
1860
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,35 +1,35 @@
|
||||
import axios from "axios";
|
||||
import { camelToSnakeObj, snakeToCamelObj } from "field-conv";
|
||||
import axios from 'axios'
|
||||
import { camelToSnakeObj, snakeToCamelObj } from 'field-conv'
|
||||
|
||||
export const BACKEND_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://neo-blog-backend:8888";
|
||||
export const BACKEND_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://neo-blog-backend:8888'
|
||||
|
||||
const isServer = typeof window === "undefined";
|
||||
const isServer = typeof window === 'undefined'
|
||||
|
||||
const API_SUFFIX = "/api/v1";
|
||||
const API_SUFFIX = '/api/v1'
|
||||
|
||||
const axiosClient = axios.create({
|
||||
baseURL: isServer ? BACKEND_URL + API_SUFFIX : API_SUFFIX,
|
||||
timeout: 10000,
|
||||
});
|
||||
})
|
||||
|
||||
axiosClient.interceptors.request.use((config) => {
|
||||
if (config.data && typeof config.data === "object") {
|
||||
config.data = camelToSnakeObj(config.data);
|
||||
if (config.data && typeof config.data === 'object') {
|
||||
config.data = camelToSnakeObj(config.data)
|
||||
}
|
||||
if (config.params && typeof config.params === "object") {
|
||||
config.params = camelToSnakeObj(config.params);
|
||||
if (config.params && typeof config.params === 'object') {
|
||||
config.params = camelToSnakeObj(config.params)
|
||||
}
|
||||
return config;
|
||||
});
|
||||
return config
|
||||
})
|
||||
|
||||
axiosClient.interceptors.response.use(
|
||||
(response) => {
|
||||
if (response.data && typeof response.data === "object") {
|
||||
response.data = snakeToCamelObj(response.data);
|
||||
if (response.data && typeof response.data === 'object') {
|
||||
response.data = snakeToCamelObj(response.data)
|
||||
}
|
||||
return response;
|
||||
return response
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
error => Promise.reject(error),
|
||||
)
|
||||
|
||||
export default axiosClient;
|
||||
export default axiosClient
|
||||
|
@ -1,10 +1,9 @@
|
||||
import type { Label } from "@/models/label";
|
||||
import type { BaseResponse } from "@/models/resp";
|
||||
import axiosClient from "./client";
|
||||
|
||||
import type { Label } from '@/models/label'
|
||||
import type { BaseResponse } from '@/models/resp'
|
||||
import axiosClient from './client'
|
||||
|
||||
export async function listLabels(): Promise<BaseResponse<Label[] | null>> {
|
||||
const res = await axiosClient.get<BaseResponse<Label[] | null>>("/label/list", {
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
const res = await axiosClient.get<BaseResponse<Label[] | null>>('/label/list', {
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
@ -1,41 +1,42 @@
|
||||
import type { BaseResponse } from "@/models/resp";
|
||||
import type { Post } from "@/models/post";
|
||||
import axiosClient from "./client";
|
||||
import type { Post } from '@/models/post'
|
||||
import type { BaseResponse } from '@/models/resp'
|
||||
import axiosClient from './client'
|
||||
|
||||
interface ListPostsParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
orderBy?: string;
|
||||
desc?: boolean;
|
||||
keywords?: string;
|
||||
page?: number
|
||||
size?: number
|
||||
orderBy?: string
|
||||
desc?: boolean
|
||||
keywords?: string
|
||||
}
|
||||
|
||||
export async function getPostById(id: string): Promise<Post | null> {
|
||||
console.log("Fetching post by ID:", id);
|
||||
try {
|
||||
const res = await axiosClient.get<BaseResponse<Post>>(`/post/p/19`);
|
||||
return res.data.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching post by ID:", error);
|
||||
return null;
|
||||
}
|
||||
console.log('Fetching post by ID:', id)
|
||||
try {
|
||||
const res = await axiosClient.get<BaseResponse<Post>>(`/post/p/19`)
|
||||
return res.data.data
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching post by ID:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function listPosts({
|
||||
page = 1,
|
||||
size = 10,
|
||||
orderBy = 'updated_at',
|
||||
desc = false,
|
||||
keywords = ''
|
||||
page = 1,
|
||||
size = 10,
|
||||
orderBy = 'updated_at',
|
||||
desc = false,
|
||||
keywords = '',
|
||||
}: ListPostsParams = {}): Promise<BaseResponse<Post[]>> {
|
||||
const res = await axiosClient.get<BaseResponse<Post[]>>("/post/list", {
|
||||
params: {
|
||||
page,
|
||||
size,
|
||||
orderBy,
|
||||
desc,
|
||||
keywords
|
||||
}
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
const res = await axiosClient.get<BaseResponse<Post[]>>('/post/list', {
|
||||
params: {
|
||||
page,
|
||||
size,
|
||||
orderBy,
|
||||
desc,
|
||||
keywords,
|
||||
},
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
@ -1,60 +1,60 @@
|
||||
import axiosClient from "./client";
|
||||
import type { OidcConfig } from "@/models/oidc-config";
|
||||
import type { User } from "@/models/user";
|
||||
import type { BaseResponse } from "@/models/resp";
|
||||
import type { OidcConfig } from '@/models/oidc-config'
|
||||
import type { BaseResponse } from '@/models/resp'
|
||||
import type { User } from '@/models/user'
|
||||
import axiosClient from './client'
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
rememberMe?: boolean; // 可以轻松添加新字段
|
||||
captcha?: string;
|
||||
username: string
|
||||
password: string
|
||||
rememberMe?: boolean // 可以轻松添加新字段
|
||||
captcha?: string
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
verificationCode?: string;
|
||||
username: string
|
||||
password: string
|
||||
nickname: string
|
||||
email: string
|
||||
verificationCode?: string
|
||||
}
|
||||
|
||||
export async function userLogin(
|
||||
data: LoginRequest
|
||||
): Promise<BaseResponse<{ token: string; user: User }>> {
|
||||
const res = await axiosClient.post<BaseResponse<{ token: string; user: User }>>(
|
||||
"/user/login",
|
||||
data
|
||||
);
|
||||
return res.data;
|
||||
data: LoginRequest,
|
||||
): Promise<BaseResponse<{ token: string, user: User }>> {
|
||||
const res = await axiosClient.post<BaseResponse<{ token: string, user: User }>>(
|
||||
'/user/login',
|
||||
data,
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function userRegister(
|
||||
data: RegisterRequest
|
||||
): Promise<BaseResponse<{ token: string; user: User }>> {
|
||||
const res = await axiosClient.post<BaseResponse<{ token: string; user: User }>>(
|
||||
"/user/register",
|
||||
data
|
||||
);
|
||||
return res.data;
|
||||
data: RegisterRequest,
|
||||
): Promise<BaseResponse<{ token: string, user: User }>> {
|
||||
const res = await axiosClient.post<BaseResponse<{ token: string, user: User }>>(
|
||||
'/user/register',
|
||||
data,
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function ListOidcConfigs(): Promise<BaseResponse<OidcConfig[]>> {
|
||||
const res = await axiosClient.get<BaseResponse<OidcConfig[]>>(
|
||||
"/user/oidc/list"
|
||||
);
|
||||
return res.data;
|
||||
const res = await axiosClient.get<BaseResponse<OidcConfig[]>>(
|
||||
'/user/oidc/list',
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function getLoginUser(token: string = ""): Promise<BaseResponse<User>> {
|
||||
const res = await axiosClient.get<BaseResponse<User>>("/user/me", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
return res.data;
|
||||
export async function getLoginUser(token: string = ''): Promise<BaseResponse<User>> {
|
||||
const res = await axiosClient.get<BaseResponse<User>>('/user/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function getUserById(id: number): Promise<BaseResponse<User>> {
|
||||
const res = await axiosClient.get<BaseResponse<User>>(`/user/u/${id}`);
|
||||
return res.data;
|
||||
}
|
||||
const res = await axiosClient.get<BaseResponse<User>>(`/user/u/${id}`)
|
||||
return res.data
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
|
||||
export default function ArchivesPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>归档</h1>
|
||||
<p>这里是博客文章的归档页面。</p>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<h1>归档</h1>
|
||||
<p>这里是博客文章的归档页面。</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Navbar } from '@/components/navbar'
|
||||
|
||||
import { Navbar } from "@/components/navbar";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { usePathname } from "next/navigation";
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
const pathname = usePathname();
|
||||
const pathname = usePathname()
|
||||
return (
|
||||
<>
|
||||
<header className="fixed top-0 left-0 w-full z-50 bg-white/80 dark:bg-slate-900/80 backdrop-blur flex justify-center border-b border-slate-200 dark:border-slate-800">
|
||||
@ -21,9 +22,9 @@ export default function RootLayout({
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 16 }}
|
||||
transition={{
|
||||
type: "tween",
|
||||
ease: "easeOut",
|
||||
duration: 0.18
|
||||
type: 'tween',
|
||||
ease: 'easeOut',
|
||||
duration: 0.18,
|
||||
}}
|
||||
className="pt-16"
|
||||
>
|
||||
@ -31,5 +32,5 @@ export default function RootLayout({
|
||||
</motion.main>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { getPostById } from "@/api/post";
|
||||
import BlogPost from "@/components/blog-post";
|
||||
import { getPostById } from '@/api/post'
|
||||
import BlogPost from '@/components/blog-post'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: Props) {
|
||||
const { id } = await params;
|
||||
const post = await getPostById(id);
|
||||
if (!post) return <div>文章不存在</div>;
|
||||
return <BlogPost post={post} />;
|
||||
}
|
||||
const { id } = await params
|
||||
const post = await getPostById(id)
|
||||
if (!post)
|
||||
return <div>文章不存在</div>
|
||||
return <BlogPost post={post} />
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
export default function LabelsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>标签</h1>
|
||||
<p>这里是博客文章的标签页面。</p>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<h1>标签</h1>
|
||||
<p>这里是博客文章的标签页面。</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Page Title</h1>
|
||||
<p>This is the User content.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>Page Title</h1>
|
||||
<p>This is the User content.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { DeviceProvider } from "@/contexts/device-context";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getLocale } from 'next-intl/server';
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Image from "next/image"
|
||||
import { LoginForm } from "@/components/login-form"
|
||||
import config from "@/config"
|
||||
import { Suspense } from "react"
|
||||
import Image from 'next/image'
|
||||
import { Suspense } from 'react'
|
||||
import { LoginForm } from '@/components/login-form'
|
||||
import config from '@/config'
|
||||
|
||||
function LoginPageContent() {
|
||||
return (
|
||||
@ -27,7 +27,7 @@ function LoginPageContent() {
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<Suspense fallback={(
|
||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<div className="animate-pulse">
|
||||
@ -44,8 +44,9 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
)}
|
||||
>
|
||||
<LoginPageContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +1,36 @@
|
||||
import { Post } from "@/models/post";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Calendar, Eye, Heart, MessageCircle, Lock } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import config from "@/config";
|
||||
import type { Post } from '@/models/post'
|
||||
import { Calendar, Eye, Heart, Lock, MessageCircle } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import config from '@/config'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface BlogCardProps {
|
||||
post: Post;
|
||||
className?: string;
|
||||
post: Post
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function BlogCard({ post, className }: BlogCardProps) {
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
};
|
||||
day: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: 阅读时间估计
|
||||
|
||||
return (
|
||||
<Card className={cn(
|
||||
"group overflow-hidden hover:shadow-xl transition-all duration-300 h-full flex flex-col cursor-pointer pt-0 pb-4",
|
||||
className
|
||||
)}>
|
||||
'group overflow-hidden hover:shadow-xl transition-all duration-300 h-full flex flex-col cursor-pointer pt-0 pb-4',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* 封面图片区域 */}
|
||||
<div className="relative aspect-[16/9] overflow-hidden">
|
||||
{/* 自定义封面图片 */}
|
||||
@ -46,12 +47,16 @@ export function BlogCard({ post, className }: BlogCardProps) {
|
||||
// 默认渐变背景 - 基于热度生成颜色
|
||||
<div
|
||||
className={cn(
|
||||
"w-full h-full bg-gradient-to-br",
|
||||
post.heat > 80 ? "from-red-400 via-pink-500 to-orange-500" :
|
||||
post.heat > 60 ? "from-orange-400 via-yellow-500 to-red-500" :
|
||||
post.heat > 40 ? "from-blue-400 via-purple-500 to-pink-500" :
|
||||
post.heat > 20 ? "from-green-400 via-blue-500 to-purple-500" :
|
||||
"from-gray-400 via-slate-500 to-gray-600"
|
||||
'w-full h-full bg-gradient-to-br',
|
||||
post.heat > 80
|
||||
? 'from-red-400 via-pink-500 to-orange-500'
|
||||
: post.heat > 60
|
||||
? 'from-orange-400 via-yellow-500 to-red-500'
|
||||
: post.heat > 40
|
||||
? 'from-blue-400 via-purple-500 to-pink-500'
|
||||
: post.heat > 20
|
||||
? 'from-green-400 via-blue-500 to-purple-500'
|
||||
: 'from-gray-400 via-slate-500 to-gray-600',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@ -100,7 +105,9 @@ export function BlogCard({ post, className }: BlogCardProps) {
|
||||
{post.heat > 50 && (
|
||||
<div className="absolute bottom-2 right-2">
|
||||
<Badge className="bg-gradient-to-r from-orange-500 to-red-500 text-white border-0 text-xs">
|
||||
🔥 {post.heat}
|
||||
🔥
|
||||
{' '}
|
||||
{post.heat}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
@ -132,9 +139,8 @@ export function BlogCard({ post, className }: BlogCardProps) {
|
||||
</div>
|
||||
</CardFooter>
|
||||
|
||||
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// 骨架屏加载组件 - 使用 shadcn Card 结构
|
||||
@ -173,20 +179,20 @@ export function BlogCardSkeleton() {
|
||||
<div className="h-4 w-20 bg-muted rounded animate-pulse ml-auto" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// 网格布局的博客卡片列表
|
||||
export function BlogCardGrid({
|
||||
posts,
|
||||
isLoading,
|
||||
showPrivate = false
|
||||
showPrivate = false,
|
||||
}: {
|
||||
posts: Post[];
|
||||
isLoading?: boolean;
|
||||
showPrivate?: boolean;
|
||||
posts: Post[]
|
||||
isLoading?: boolean
|
||||
showPrivate?: boolean
|
||||
}) {
|
||||
const filteredPosts = showPrivate ? posts : posts.filter(post => !post.isPrivate);
|
||||
const filteredPosts = showPrivate ? posts : posts.filter(post => !post.isPrivate)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@ -195,7 +201,7 @@ export function BlogCardGrid({
|
||||
<BlogCardSkeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (filteredPosts.length === 0) {
|
||||
@ -208,16 +214,16 @@ export function BlogCardGrid({
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredPosts.map((post) => (
|
||||
{filteredPosts.map(post => (
|
||||
<Link key={post.id} href={`/p/${post.id}`} className="block h-full">
|
||||
<BlogCard post={post} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ export default function BlogHome() {
|
||||
const [posts, setPosts] = useState<Post[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sortType, setSortType, sortTypeLoaded] = useStoredState<SortType>(POST_SORT_TYPE, 'latest');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
|
||||
// 根据排序类型和防抖后的搜索关键词获取文章
|
||||
useEffect(() => {
|
||||
@ -46,7 +45,7 @@ export default function BlogHome() {
|
||||
desc = true;
|
||||
}
|
||||
// 处理关键词,空格分割转逗号
|
||||
const keywords = debouncedSearch.trim() ? debouncedSearch.trim().split(/\s+/).join(",") : undefined;
|
||||
const keywords = ""?.trim() ? ""?.trim().split(/\s+/).join(",") : undefined;
|
||||
const data = await listPosts({
|
||||
page: 1,
|
||||
size: 10,
|
||||
|
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import type { Post } from "@/models/post";
|
||||
|
||||
function WaveHeader({ title }: { title: string }) {
|
||||
@ -74,7 +75,7 @@ function BlogContent({ post }: { post: Post }) {
|
||||
return (
|
||||
<main className="relative z-10 max-w-3xl mx-auto bg-white rounded-xl shadow-lg p-8 -mt-32">
|
||||
{post.cover && (
|
||||
<img
|
||||
<Image
|
||||
src={post.cover}
|
||||
alt="cover"
|
||||
className="w-full h-64 object-cover rounded-lg mb-8"
|
||||
|
@ -1,28 +1,29 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function Badge({
|
||||
@ -30,9 +31,9 @@ function Badge({
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
}: React.ComponentProps<'span'>
|
||||
& VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'span'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
@ -1,38 +1,39 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
@ -41,11 +42,11 @@ function Button({
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
}: React.ComponentProps<'button'>
|
||||
& VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
@ -1,81 +1,81 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
className={cn('px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@ -83,10 +83,10 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Label({
|
||||
className,
|
||||
@ -13,8 +13,8 @@ function Label({
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { ChevronDownIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
@ -18,8 +18,8 @@ function NavigationMenu({
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -37,8 +37,8 @@ function NavigationMenuList({
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className
|
||||
'group flex flex-1 list-none items-center justify-center gap-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -52,14 +52,14 @@ function NavigationMenuItem({
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
className={cn('relative', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-lg font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-lg font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1',
|
||||
)
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
@ -70,10 +70,11 @@ function NavigationMenuTrigger({
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
className={cn(navigationMenuTriggerStyle(), 'group', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
{children}
|
||||
{' '}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
@ -90,9 +91,9 @@ function NavigationMenuContent({
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className
|
||||
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto',
|
||||
'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -106,14 +107,14 @@ function NavigationMenuViewport({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||
'absolute top-full left-0 isolate z-50 flex justify-center',
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
'origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -129,8 +130,8 @@ function NavigationMenuLink({
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
'data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*=\'text-\'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -145,8 +146,8 @@ function NavigationMenuIndicator({
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className
|
||||
'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -157,12 +158,12 @@ function NavigationMenuIndicator({
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||
import { XIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
@ -36,8 +36,8 @@ function SheetOverlay({
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@ -47,10 +47,10 @@ function SheetOverlay({
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
side = 'right',
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
@ -58,16 +58,16 @@ function SheetContent({
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
side === 'right'
|
||||
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
||||
side === 'left'
|
||||
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||
side === 'top'
|
||||
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
||||
side === 'bottom'
|
||||
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -81,21 +81,21 @@ function SheetContent({
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
className={cn('flex flex-col gap-1.5 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@ -108,7 +108,7 @@ function SheetTitle({
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
className={cn('text-foreground font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@ -121,7 +121,7 @@ function SheetDescription({
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@ -129,11 +129,11 @@ function SheetDescription({
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Mode = "light" | "dark";
|
||||
type Lang = string;
|
||||
|
||||
interface DeviceContextProps {
|
||||
isMobile: boolean;
|
||||
|
@ -9,7 +9,8 @@ export default getRequestConfig(async () => {
|
||||
locales.map(async (locale) => {
|
||||
try {
|
||||
return (await import(`@/locales/${locale}.json`)).default;
|
||||
} catch (error) {
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {};
|
||||
}
|
||||
})
|
||||
|
Reference in New Issue
Block a user