♻️ 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:
2025-07-26 10:01:27 +08:00
parent e659de23fb
commit 99e291654d
29 changed files with 2214 additions and 355 deletions

6
web/.eslintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
}

View File

@ -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;

View File

@ -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)

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -1,9 +1,8 @@
export default function ArchivesPage() {
return (
<div>
<h1></h1>
<p></p>
</div>
)
return (
<div>
<h1></h1>
<p></p>
</div>
)
}

View File

@ -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>
</>
);
)
}

View File

@ -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} />
}

View File

@ -1,8 +1,8 @@
export default function LabelsPage() {
return (
<div>
<h1></h1>
<p></p>
</div>
)
return (
<div>
<h1></h1>
<p></p>
</div>
)
}

View File

@ -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>
)
}

View File

@ -1,11 +1,11 @@
export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode
}>) {
return (
<>
{children}
</>
);
}
return (
<>
{children}
</>
)
}

View File

@ -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",

View File

@ -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>
)
}
}

View File

@ -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>
);
}
)
}

View File

@ -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,

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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,
}

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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,
}

View File

@ -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,
}

View File

@ -3,7 +3,6 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
type Mode = "light" | "dark";
type Lang = string;
interface DeviceContextProps {
isMobile: boolean;

View File

@ -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 {};
}
})