mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-08-31 22:36:23 +00:00
✨ feat: enhance post retrieval with authorization token
- Updated `getPostById` function to accept an optional authorization token. - Modified `PostPage` to retrieve the token from cookies and pass it to the API call. - Added smooth transition effects for background and text colors in `globals.css`. - Cleaned up imports and formatting in `blog-home.tsx`. - Refactored `blog-post.tsx` to use `MDXRemote` for rendering markdown content. - Introduced `blog-comment.tsx` and `blog-post-header.client.tsx` components for better structure. - Added a switch component for dark/light mode toggle in the navbar. - Updated `Post` model to include a description field.
This commit is contained in:
@ -31,6 +31,7 @@ EMAIL_PORT=465
|
||||
EMAIL_SSL=true
|
||||
|
||||
# App settings 应用程序配置
|
||||
LOG_LEVEL=debug
|
||||
BASE_URL=https://blog.shenyu.moe
|
||||
MAX_REQUEST_BODY_SIZE=1000000
|
||||
MODE=prod
|
||||
|
@ -13,7 +13,7 @@ func SetTokenCookie(c *app.RequestContext, token string) {
|
||||
|
||||
func SetTokenAndRefreshTokenCookie(c *app.RequestContext, token, refreshToken string) {
|
||||
c.SetCookie("token", token, utils.Env.GetAsInt(constant.EnvKeyTokenDuration, constant.EnvKeyTokenDurationDefault), "/", "", protocol.CookieSameSiteLaxMode, true, true)
|
||||
c.SetCookie("refresh_token", refreshToken, -1, "/", "", protocol.CookieSameSiteLaxMode, true, true)
|
||||
c.SetCookie("refresh_token", refreshToken, utils.Env.GetAsInt(constant.EnvKeyRefreshTokenDuration, constant.EnvKeyRefreshTokenDurationDefault), "/", "", protocol.CookieSameSiteLaxMode, true, true)
|
||||
}
|
||||
|
||||
func ClearTokenAndRefreshTokenCookie(c *app.RequestContext) {
|
||||
|
@ -352,12 +352,12 @@ func (s *UserService) UpdateUser(req *dto.UpdateUserReq) (*dto.UpdateUserResp, e
|
||||
}
|
||||
|
||||
func (s *UserService) generate2Token(userID uint) (string, string, error) {
|
||||
token := utils.Jwt.NewClaims(userID, "", false, time.Duration(utils.Env.GetAsInt(constant.EnvKeyTokenDuration, 24)*int(time.Hour)))
|
||||
token := utils.Jwt.NewClaims(userID, "", false, time.Duration(utils.Env.GetAsInt(constant.EnvKeyTokenDuration, constant.EnvKeyTokenDurationDefault)*int(time.Second)))
|
||||
tokenString, err := token.ToString()
|
||||
if err != nil {
|
||||
return "", "", errs.ErrInternalServer
|
||||
}
|
||||
refreshToken := utils.Jwt.NewClaims(userID, utils.Strings.GenerateRandomString(64), true, time.Duration(utils.Env.GetAsInt(constant.EnvKeyRefreshTokenDuration, 30)*int(time.Hour)))
|
||||
refreshToken := utils.Jwt.NewClaims(userID, utils.Strings.GenerateRandomString(64), true, time.Duration(utils.Env.GetAsInt(constant.EnvKeyRefreshTokenDuration, constant.EnvKeyRefreshTokenDurationDefault)*int(time.Second)))
|
||||
refreshTokenString, err := refreshToken.ToString()
|
||||
if err != nil {
|
||||
return "", "", errs.ErrInternalServer
|
||||
|
@ -11,11 +11,13 @@ const (
|
||||
RoleUser = "user"
|
||||
RoleAdmin = "admin"
|
||||
EnvKeyBaseUrl = "BASE_URL" // 环境变量:基础URL
|
||||
EnvKeyLogLevel = "LOG_LEVEL" // 环境变量:日志级别
|
||||
EnvKeyMode = "MODE" // 环境变量:运行模式
|
||||
EnvKeyJwtSecrete = "JWT_SECRET" // 环境变量:JWT密钥
|
||||
EnvKeyPasswordSalt = "PASSWORD_SALT" // 环境变量:密码盐
|
||||
EnvKeyTokenDuration = "TOKEN_DURATION" // 环境变量:令牌有效期
|
||||
EnvKeyTokenDurationDefault = 300
|
||||
EnvKeyRefreshTokenDurationDefault = 604800
|
||||
EnvKeyRefreshTokenDuration = "REFRESH_TOKEN_DURATION" // 环境变量:刷新令牌有效期
|
||||
EnvKeyRefreshTokenDurationWithRemember = "REFRESH_TOKEN_DURATION_WITH_REMEMBER" // 环境变量:记住我刷新令牌有效期
|
||||
KVKeyEmailVerificationCode = "email_verification_code:" // KV存储:邮箱验证码
|
||||
@ -40,6 +42,6 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
OrderByEnumPost = []string{OrderByCreatedAt, OrderByUpdatedAt, OrderByLikeCount, OrderByCommentCount, OrderByViewCount, OrderByHeat} // 帖子可用的排序方式
|
||||
OrderByEnumComment = []string{OrderByCreatedAt, OrderByUpdatedAt, OrderByCommentCount} // 评论可用的排序方式
|
||||
OrderByEnumPost = []string{OrderByCreatedAt, OrderByUpdatedAt, OrderByLikeCount, OrderByCommentCount, OrderByViewCount, OrderByHeat} // 帖子可用的排序方式
|
||||
OrderByEnumComment = []string{OrderByCreatedAt, OrderByUpdatedAt, OrderByCommentCount} // 评论可用的排序方式
|
||||
)
|
||||
|
@ -1,7 +1,9 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"os"
|
||||
"strconv"
|
||||
@ -12,10 +14,26 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
_ = godotenv.Load()
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
logrus.Warnf("Error loading .env file: %v", err)
|
||||
}
|
||||
|
||||
logrus.Infof("env loaded")
|
||||
// Init env
|
||||
IsDevMode = Env.Get(constant.EnvKeyMode, constant.ModeProd) == constant.ModeDev
|
||||
// Set log level
|
||||
logrus.SetLevel(getLogLevel(Env.Get(constant.EnvKeyLogLevel, "info")))
|
||||
if logrus.GetLevel() == logrus.DebugLevel {
|
||||
logrus.Debug("Debug mode is enabled, printing environment variables:")
|
||||
for _, e := range os.Environ() {
|
||||
if len(e) > 0 && e[0] == '_' {
|
||||
// Skip environment variables that start with '_'
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s ", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type envUtils struct{}
|
||||
@ -56,3 +74,23 @@ func (e *envUtils) GetAsBool(key string, defaultValue ...bool) bool {
|
||||
}
|
||||
return boolValue
|
||||
}
|
||||
|
||||
func getLogLevel(levelString string) logrus.Level {
|
||||
switch levelString {
|
||||
case "debug":
|
||||
return logrus.DebugLevel
|
||||
case "info":
|
||||
return logrus.InfoLevel
|
||||
case "warn":
|
||||
return logrus.WarnLevel
|
||||
case "error":
|
||||
return logrus.ErrorLevel
|
||||
case "fatal":
|
||||
return logrus.FatalLevel
|
||||
case "panic":
|
||||
return logrus.PanicLevel
|
||||
default:
|
||||
logrus.Warnf("Unknown log level: %s, defaulting to InfoLevel", levelString)
|
||||
return logrus.InfoLevel
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -22,14 +23,13 @@
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "15.4.1",
|
||||
"next-intl": "^4.3.4",
|
||||
"next-mdx-remote-client": "^2.1.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^5.0.0",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
|
821
web/pnpm-lock.yaml
generated
821
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -10,9 +10,13 @@ interface ListPostsParams {
|
||||
keywords?: string
|
||||
}
|
||||
|
||||
export async function getPostById(id: string): Promise<Post | null> {
|
||||
export async function getPostById(id: string, token: string=""): Promise<Post | null> {
|
||||
try {
|
||||
const res = await axiosClient.get<BaseResponse<Post>>(`/post/p/${id}`)
|
||||
const res = await axiosClient.get<BaseResponse<Post>>(`/post/p/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
return res.data.data
|
||||
}
|
||||
catch (error) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { getPostById } from '@/api/post'
|
||||
import { cookies } from 'next/headers'
|
||||
import BlogPost from '@/components/blog-post'
|
||||
|
||||
interface Props {
|
||||
@ -6,8 +7,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: Props) {
|
||||
const cookieStore = await cookies();
|
||||
const { id } = await params
|
||||
const post = await getPostById(id)
|
||||
const post = await getPostById(id, cookieStore.get('token')?.value || '');
|
||||
if (!post)
|
||||
return <div>文章不存在</div>
|
||||
return <BlogPost post={post} />
|
||||
|
@ -120,3 +120,7 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
transition: background-color 0.3s, color 0.3s !important;
|
||||
}
|
0
web/src/components/blog-comment.tsx
Normal file
0
web/src/components/blog-comment.tsx
Normal file
@ -9,11 +9,11 @@ import type { Label } from "@/models/label";
|
||||
import type { Post } from "@/models/post";
|
||||
import { listPosts } from "@/api/post";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useStoredState } from '@/hooks/use-storage-state';
|
||||
import { listLabels } from "@/api/label";
|
||||
import { POST_SORT_TYPE } from "@/localstore";
|
||||
import Image from "next/image";
|
||||
|
||||
|
||||
// 定义排序类型
|
||||
type SortType = 'latest' | 'popular';
|
||||
@ -23,7 +23,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');
|
||||
|
||||
// 根据排序类型和防抖后的搜索关键词获取文章
|
||||
useEffect(() => {
|
||||
if (!sortTypeLoaded) return;
|
||||
|
94
web/src/components/blog-post-header.client.tsx
Normal file
94
web/src/components/blog-post-header.client.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
"use client"
|
||||
import { useBackground } from "@/contexts/background-context";
|
||||
import type { Post } from "@/models/post";
|
||||
import { useEffect, useRef,Suspense } from "react";
|
||||
import { Calendar, Clock, FileText, Flame, Heart, MessageCircle, PenLine, SquarePen } from "lucide-react";
|
||||
|
||||
function PostMeta({ post }: { post: Post }) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-4 mt-6 text-secondary">
|
||||
{/* 作者 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<PenLine className="w-4 h-4" />
|
||||
{post.user.nickname || "未知作者"}
|
||||
</span>
|
||||
{/* 字数 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<FileText className="w-4 h-4" />
|
||||
{post.content.length || 0}
|
||||
</span>
|
||||
{/* 阅读时间 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
{post.content.length / 100 || 1} 分钟
|
||||
</span>
|
||||
{/* 发布时间 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{post.createdAt ? new Date(post.createdAt).toLocaleDateString("zh-CN") : ""}
|
||||
</span>
|
||||
{/* 最后编辑时间,如果和发布时间不一样 */}
|
||||
{post.updatedAt && post.createdAt !== post.updatedAt && (
|
||||
<span className="flex items-center gap-1">
|
||||
<SquarePen className="w-4 h-4" />
|
||||
{new Date(post.updatedAt).toLocaleDateString("zh-CN")}
|
||||
</span>
|
||||
)}
|
||||
{/* 浏览数 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Flame className="w-4 h-4" />
|
||||
{post.viewCount || 0}
|
||||
</span>
|
||||
{/* 点赞数 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Heart className="w-4 h-4" />
|
||||
{post.likeCount || 0}
|
||||
</span>
|
||||
{/* 评论数 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
{post.commentCount || 0}
|
||||
</span>
|
||||
{/* 热度 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Flame className="w-4 h-4" />
|
||||
{post.heat || 0}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PostHeader({ post }: { post: Post }) {
|
||||
const { setBackground } = useBackground();
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (headerRef.current) {
|
||||
const { clientHeight } = headerRef.current;
|
||||
setBackground(<div className={`bg-gradient-to-br from-blue-700 to-purple-700 dark:from-blue-500 dark:to-purple-500`} style={{ height: clientHeight }} />);
|
||||
}
|
||||
}, [headerRef, setBackground]);
|
||||
return (
|
||||
<div className="py-32" ref={headerRef}>
|
||||
{(post.labels || post.isOriginal) && (
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{post.isOriginal && (
|
||||
<span className="bg-green-100 text-green-600 text-xs px-2 py-1 rounded">
|
||||
原创
|
||||
</span>
|
||||
)}
|
||||
{(post.labels || []).map(label => (
|
||||
<span key={label.id} className="bg-blue-100 text-blue-600 text-xs px-2 py-1 rounded">
|
||||
{label.key}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<h1 className="text-5xl font-bold mb-2 text-primary-foreground">{post.title}</h1>
|
||||
{/* 元数据区 */}
|
||||
<div>
|
||||
<PostMeta post={post} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,105 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { Suspense } from "react";
|
||||
import type { Post } from "@/models/post";
|
||||
import { Calendar, Clock, FileText, Flame, Heart, MessageCircle, PenLine, SquarePen } from "lucide-react";
|
||||
import { MDXRemote } from "next-mdx-remote-client/rsc";
|
||||
import PostHeader from "@/components/blog-post-header.client";
|
||||
|
||||
function PostMeta({ post }: { post: Post }) {
|
||||
// 假设 post 结构包含这些字段
|
||||
// post.author, post.wordCount, post.readMinutes, post.createdAt, post.isOriginal, post.viewCount, post.commentCount
|
||||
|
||||
async function PostContent({ post }: { post: Post }) {
|
||||
return (
|
||||
console.log(post),
|
||||
<div className="flex flex-wrap items-center gap-4 mt-6">
|
||||
{/* 作者 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<PenLine className="w-4 h-4" />
|
||||
{post.user.nickname || "未知作者"}
|
||||
</span>
|
||||
{/* 字数 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<FileText className="w-4 h-4" />
|
||||
{post.content.length || 0}
|
||||
</span>
|
||||
{/* 阅读时间 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
{post.content.length / 100 || 1} 分钟
|
||||
</span>
|
||||
{/* 发布时间 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{post.createdAt ? new Date(post.createdAt).toLocaleDateString("zh-CN") : ""}
|
||||
</span>
|
||||
{/* 最后编辑时间,如果和发布时间不一样 */}
|
||||
{post.updatedAt && post.createdAt !== post.updatedAt && (
|
||||
<span className="flex items-center gap-1">
|
||||
<SquarePen className="w-4 h-4" />
|
||||
{new Date(post.updatedAt).toLocaleDateString("zh-CN")}
|
||||
</span>
|
||||
<div className="py-12 px-6">
|
||||
{post.type === "html" && (
|
||||
<div
|
||||
className="prose prose-lg max-w-none dark:prose-invert [&_h1]:text-5xl [&_h2]:text-4xl [&_h3]:text-3xl [&_h4]:text-2xl [&_h5]:text-xl [&_h6]:text-lg [&_p]:text-xl [&_p]:my-6 [&_ul]:my-6 [&_ol]:my-6 [&_pre]:my-8 [&_blockquote]:my-8"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
)}
|
||||
{post.type === "markdown" && (
|
||||
<div className="prose prose-lg max-w-none dark:prose-invert [&_h1]:text-5xl [&_h2]:text-4xl [&_h3]:text-3xl [&_h4]:text-2xl [&_h5]:text-xl [&_h6]:text-lg [&_p]:text-xl [&_p]:my-6 [&_ul]:my-6 [&_ol]:my-6 [&_pre]:my-8 [&_blockquote]:my-8">
|
||||
<Suspense>
|
||||
<MDXRemote
|
||||
source={post.content}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
{post.type === "text" && (
|
||||
<div className="text-xl text-slate-700 dark:text-slate-300 my-6">
|
||||
{post.content}
|
||||
</div>
|
||||
)}
|
||||
{/* 浏览数 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Flame className="w-4 h-4" />
|
||||
{post.viewCount || 0}
|
||||
</span>
|
||||
{/* 点赞数 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Heart className="w-4 h-4" />
|
||||
{post.likeCount || 0}
|
||||
</span>
|
||||
{/* 评论数 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
{post.commentCount || 0}
|
||||
</span>
|
||||
{/* 热度 */}
|
||||
<span className="flex items-center gap-1">
|
||||
<Flame className="w-4 h-4" />
|
||||
{post.heat || 0}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PostHeader({ post }: { post: Post }) {
|
||||
// 三排 标签/标题/一些元数据
|
||||
return (
|
||||
<div className="py-32">
|
||||
{
|
||||
post.labels && post.labels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{post.labels.map(label => (
|
||||
<span key={label.id} className="bg-blue-100 text-blue-600 text-xs px-2 py-1 rounded">
|
||||
{label.key}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<h1 className="text-5xl font-bold mb-2">{post.title}</h1>
|
||||
{/* 元数据区 */}
|
||||
<div>
|
||||
<PostMeta post={post} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PostContent({ post }: { post: Post }) {
|
||||
return (
|
||||
<div className="">
|
||||
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function BlogPost({ post }: { post: Post }) {
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
|
||||
}
|
||||
}, []);
|
||||
async function BlogPost({ post }: { post: Post }) {
|
||||
return (
|
||||
<div className="">
|
||||
<PostHeader post={post} />
|
||||
|
@ -18,6 +18,7 @@ import config from "@/config"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
||||
import { Menu } from "lucide-react"
|
||||
import { Switch } from "./ui/switch"
|
||||
|
||||
const navbarMenuComponents = [
|
||||
{
|
||||
@ -44,7 +45,7 @@ const navbarMenuComponents = [
|
||||
]
|
||||
|
||||
export function Navbar() {
|
||||
const { navbarAdditionalClassName } = useDevice()
|
||||
const { navbarAdditionalClassName, setMode, mode } = useDevice()
|
||||
return (
|
||||
<nav className={`grid grid-cols-[1fr_auto_1fr] items-center gap-4 h-16 px-4 w-full ${navbarAdditionalClassName}`}>
|
||||
<div className="flex items-center justify-start">
|
||||
@ -54,6 +55,7 @@ export function Navbar() {
|
||||
<NavMenuCenter />
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Switch checked={mode === "dark"} onCheckedChange={(checked) => setMode(checked ? "dark" : "light")} />
|
||||
<GravatarAvatar email="snowykami@outlook.com" />
|
||||
<SidebarMenuClientOnly />
|
||||
</div>
|
||||
|
31
web/src/components/ui/switch.tsx
Normal file
31
web/src/components/ui/switch.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
@ -2,6 +2,7 @@ import type { Label } from "@/models/label";
|
||||
import type { User } from "./user";
|
||||
|
||||
export interface Post {
|
||||
description: string;
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
|
Reference in New Issue
Block a user