diff --git a/.github/workflows/Deploy.yml b/.github/workflows/Deploy.yml index 6440c3f..322c347 100644 --- a/.github/workflows/Deploy.yml +++ b/.github/workflows/Deploy.yml @@ -18,11 +18,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host - name: Login to Docker Hub uses: docker/login-action@v3 @@ -50,30 +49,19 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - changelog: - name: Generate Changelog + release: runs-on: ubuntu-latest - needs: build-and-push - outputs: - release_body: ${{ steps.git-cliff.outputs.content }} steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Generate a changelog - uses: orhun/git-cliff-action@v4 - id: git-cliff + + - name: Set node + uses: actions/setup-node@v4 with: - config: git-cliff-config/cliff.toml - args: -vv --latest --strip 'footer' + registry-url: https://registry.npmjs.org/ + node-version: lts/* + + - run: npx changelogithub env: - OUTPUT: CHANGES.md - - name: Release - uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/') - with: - body: ${{ steps.git-cliff.outputs.content }} - token: ${{ secrets.GITHUB_TOKEN }} - env: - GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/Dockerfile b/Dockerfile index 776669f..e739a9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:1 AS base +FROM --platform=$BUILDPLATFORM oven/bun:1 AS base # Stage 1: Install dependencies FROM base AS deps diff --git a/README.md b/README.md index 12dc995..f6cbeb1 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ -| 一键部署到 Vercel-推荐 | Docker部署 | Cloudflare部署 | 如何更新? | -| ----------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------- | -| [部署简易教程](https://buycoffee.top/blog/tech/nezha) | [Docker 部署教程](https://buycoffee.top/blog/tech/nezha-docker) | [Cloudflare 部署教程](https://buycoffee.top/blog/tech/nezha-cloudflare) | [更新教程](https://buycoffee.top/blog/tech/nezha-upgrade) | -| [Vercel-demo](https://nezha-vercel.buycoffee.top) | [Docker-demo](https://nezha-docker.buycoffee.tech) | [Cloudflare-demo](https://nezha-cloudflare.buycoffee.tech) | +| 一键部署到 Vercel-推荐 | Docker部署 | Cloudflare部署 | 如何更新? | +| ----------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------- | +| [部署简易教程](https://buycoffee.top/blog/tech/nezha) | [Docker 部署教程](https://buycoffee.top/blog/tech/nezha-docker) | [Cloudflare 部署教程](https://buycoffee.top/blog/tech/nezha-cloudflare) | [更新教程](https://buycoffee.top/blog/tech/nezha-upgrade) | +| [Vercel-demo](https://nezha-vercel.buycoffee.top) | [Docker-demo](https://nezha-docker.buycoffee.tech) | [Cloudflare-demo](https://nezha-cloudflare.buycoffee.tech) [密码: nezhadash] | #### 环境变量 @@ -16,7 +16,7 @@ | ------------------------------ | ------------------------ | ------------------------------------------------------------- | | NezhaBaseUrl | nezha 面板地址 | http://120.x.x.x:8008 | | NezhaAuth | nezha 面板 API Token | 5hAY3QX6Nl9B3Uxxxx26KMvOMyXS1Udi | -| SitePassword | 页面密码 | 123456 | +| SitePassword | 页面密码 | **默认**:无密码 | | DefaultLocale | 面板默认显示语言 | **默认**:en [简中:zh 繁中:zh-t 英语:en 日语:ja] | | ForceShowAllServers | 是否强制显示所有服务器 | **默认**:false | | NEXT_PUBLIC_NezhaFetchInterval | 获取数据间隔(毫秒) | **默认**:2000 | diff --git a/app/[locale]/(main)/ClientComponents/NetworkChart.tsx b/app/[locale]/(main)/ClientComponents/NetworkChart.tsx index 8a69ecc..bbbda4f 100644 --- a/app/[locale]/(main)/ClientComponents/NetworkChart.tsx +++ b/app/[locale]/(main)/ClientComponents/NetworkChart.tsx @@ -51,7 +51,7 @@ export function NetworkChartClient({ { refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 15000, - isPaused: () => !show, + isVisible: () => show, }, ); @@ -181,13 +181,7 @@ export const NetworkChart = React.memo(function NetworkChart({
- { - router.push(`/${locale}/`); - }} - className="flex flex-none cursor-pointer items-center gap-0.5 text-xl" - > - + {serverName} diff --git a/app/[locale]/(main)/ClientComponents/NetworkChartLoading.tsx b/app/[locale]/(main)/ClientComponents/NetworkChartLoading.tsx index 3a9c908..6c3d383 100644 --- a/app/[locale]/(main)/ClientComponents/NetworkChartLoading.tsx +++ b/app/[locale]/(main)/ClientComponents/NetworkChartLoading.tsx @@ -12,13 +12,7 @@ export default function NetworkChartLoading() {
- { - router.push(`/${locale}/`); - }} - className="flex items-center cursor-pointer gap-0.5 text-xl" - > - +
diff --git a/app/[locale]/(main)/ClientComponents/ServerDetailChartClient.tsx b/app/[locale]/(main)/ClientComponents/ServerDetailChartClient.tsx index 43e41cc..ba04a7e 100644 --- a/app/[locale]/(main)/ClientComponents/ServerDetailChartClient.tsx +++ b/app/[locale]/(main)/ClientComponents/ServerDetailChartClient.tsx @@ -1,7 +1,7 @@ "use client"; import { ServerDetailChartLoading } from "@/app/[locale]/(main)/ClientComponents/ServerDetailLoading"; -import { NezhaAPISafe } from "@/app/[locale]/types/nezha-api"; +import { NezhaAPISafe, ServerApi } from "@/app/[locale]/types/nezha-api"; import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar"; import { Card, CardContent } from "@/components/ui/card"; import { ChartConfig, ChartContainer } from "@/components/ui/chart"; @@ -19,6 +19,7 @@ import { YAxis, } from "recharts"; import useSWR from "swr"; +import useSWRImmutable from "swr/immutable"; type cpuChartData = { timeStamp: string; @@ -62,12 +63,23 @@ export default function ServerDetailChartClient({ }) { const t = useTranslations("ServerDetailChartClient"); + const { data: allFallbackData } = useSWRImmutable( + "/api/server", + nezhaFetcher, + ); + const fallbackData = allFallbackData?.result?.find( + (item) => item.id === server_id, + ); + const { data, error } = useSWR( `/api/detail?server_id=${server_id}`, nezhaFetcher, { refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000, - isPaused: () => !show, + isVisible: () => show, + fallbackData, + revalidateOnMount: false, + revalidateIfStale: false, }, ); @@ -128,7 +140,7 @@ function CpuChart({ data }: { data: NezhaAPISafe }) { } satisfies ChartConfig; return ( - +
@@ -231,7 +243,7 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) { } satisfies ChartConfig; return ( - +
@@ -324,7 +336,7 @@ function MemChart({ data }: { data: NezhaAPISafe }) { } satisfies ChartConfig; return ( - +
@@ -445,7 +457,7 @@ function DiskChart({ data }: { data: NezhaAPISafe }) { } satisfies ChartConfig; return ( - +
@@ -557,7 +569,7 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) { } satisfies ChartConfig; return ( - +
@@ -677,7 +689,7 @@ function ConnectChart({ data }: { data: NezhaAPISafe }) { } satisfies ChartConfig; return ( - +
diff --git a/app/[locale]/(main)/ClientComponents/ServerDetailClient.tsx b/app/[locale]/(main)/ClientComponents/ServerDetailClient.tsx index 1c05446..a3236d7 100644 --- a/app/[locale]/(main)/ClientComponents/ServerDetailClient.tsx +++ b/app/[locale]/(main)/ClientComponents/ServerDetailClient.tsx @@ -1,15 +1,18 @@ "use client"; import { ServerDetailLoading } from "@/app/[locale]/(main)/ClientComponents/ServerDetailLoading"; -import { NezhaAPISafe } from "@/app/[locale]/types/nezha-api"; +import { NezhaAPISafe, ServerApi } from "@/app/[locale]/types/nezha-api"; import { BackIcon } from "@/components/Icon"; +import ServerFlag from "@/components/ServerFlag"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import getEnv from "@/lib/env-entry"; import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"; import { useLocale, useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; import useSWR from "swr"; +import useSWRImmutable from "swr/immutable"; export default function ServerDetailClient({ server_id, @@ -19,11 +22,47 @@ export default function ServerDetailClient({ const t = useTranslations("ServerDetailClient"); const router = useRouter(); const locale = useLocale(); + + const [hasHistory, setHasHistory] = useState(false); + + useEffect(() => { + window.scrollTo(0, 0); + }, []); + + useEffect(() => { + const previousPath = sessionStorage.getItem("lastPath"); + if (previousPath) { + setHasHistory(true); + } else { + const currentPath = window.location.pathname; + sessionStorage.setItem("lastPath", currentPath); + } + }, []); + + const linkClick = () => { + if (hasHistory) { + router.back(); + } else { + router.push(`/${locale}/`); + } + }; + + const { data: allFallbackData } = useSWRImmutable( + "/api/server", + nezhaFetcher, + ); + const fallbackData = allFallbackData?.result?.find( + (item) => item.id === server_id, + ); + const { data, error } = useSWR( `/api/detail?server_id=${server_id}`, nezhaFetcher, { refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000, + fallbackData, + revalidateOnMount: false, + revalidateIfStale: false, }, ); @@ -45,9 +84,7 @@ export default function ServerDetailClient({ return (
{ - router.push(`/${locale}/`); - }} + onClick={linkClick} className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl" > @@ -60,7 +97,7 @@ export default function ServerDetailClient({

{t("status")}

+ + +
+

{t("Region")}

+
+
+ {data?.host.CountryCode.toUpperCase()} +
+ +
+
+
+
diff --git a/app/[locale]/(main)/ClientComponents/ServerListClient.tsx b/app/[locale]/(main)/ClientComponents/ServerListClient.tsx index 2f163ba..707047f 100644 --- a/app/[locale]/(main)/ClientComponents/ServerListClient.tsx +++ b/app/[locale]/(main)/ClientComponents/ServerListClient.tsx @@ -6,17 +6,56 @@ import Switch from "@/components/Switch"; import getEnv from "@/lib/env-entry"; import { nezhaFetcher } from "@/lib/utils"; import { useTranslations } from "next-intl"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import useSWR from "swr"; export default function ServerListClient() { const t = useTranslations("ServerListClient"); + const containerRef = useRef(null); + const defaultTag = t("defaultTag"); - const [tag, setTag] = useState(t("defaultTag")); + const [tag, setTag] = useState(defaultTag); + + const [isMounted, setIsMounted] = useState(false); + useEffect(() => { + const savedTag = sessionStorage.getItem("selectedTag") || defaultTag; + setTag(savedTag); + + restoreScrollPosition(); + setIsMounted(true); + }, []); + + const handleTagChange = (newTag: string) => { + setTag(newTag); + sessionStorage.setItem("selectedTag", newTag); + sessionStorage.setItem( + "scrollPosition", + String(containerRef.current?.scrollTop || 0), + ); + }; + + const restoreScrollPosition = () => { + const savedPosition = sessionStorage.getItem("scrollPosition"); + if (savedPosition && containerRef.current) { + containerRef.current.scrollTop = Number(savedPosition); + } + }; + + useEffect(() => { + const handleRouteChange = () => { + restoreScrollPosition(); + }; + + window.addEventListener("popstate", handleRouteChange); + return () => { + window.removeEventListener("popstate", handleRouteChange); + }; + }, []); const { data, error } = useSWR("/api/server", nezhaFetcher, { refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000, }); + if (error) return (
@@ -24,32 +63,38 @@ export default function ServerListClient() {

{t("error_message")}

); - if (!data?.result) return null; + + if (!data?.result || !isMounted) return null; const { result } = data; - const sortedServers = result.sort((a, b) => { const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0); if (displayIndexDiff !== 0) return displayIndexDiff; return a.id - b.id; }); - const allTag = sortedServers.map((server) => server.tag).filter((tag) => tag); + const allTag = sortedServers.map((server) => server.tag).filter(Boolean); const uniqueTags = [...new Set(allTag)]; - - uniqueTags.unshift(t("defaultTag")); + uniqueTags.unshift(defaultTag); const filteredServers = - tag === t("defaultTag") + tag === defaultTag ? sortedServers : sortedServers.filter((server) => server.tag === tag); return ( <> {getEnv("NEXT_PUBLIC_ShowTag") === "true" && uniqueTags.length > 1 && ( - + )} -
+
{filteredServers.map((serverInfo) => ( ))} diff --git a/app/[locale]/(main)/ClientComponents/ServerOverviewClient.tsx b/app/[locale]/(main)/ClientComponents/ServerOverviewClient.tsx index ebfa63c..764ca38 100644 --- a/app/[locale]/(main)/ClientComponents/ServerOverviewClient.tsx +++ b/app/[locale]/(main)/ClientComponents/ServerOverviewClient.tsx @@ -18,13 +18,16 @@ export default function ServerOverviewClient() { ); const disableCartoon = getEnv("NEXT_PUBLIC_DisableCartoon") === "true"; - if (error) + if (error) { return (
-

{error.message}

+

+ Error status:{error.status} {error.info?.cause ?? error.message} +

{t("error_message")}

); + } return ( <> diff --git a/app/[locale]/(main)/[id]/page.tsx b/app/[locale]/(main)/[id]/page.tsx index 9614c6a..33aad32 100644 --- a/app/[locale]/(main)/[id]/page.tsx +++ b/app/[locale]/(main)/[id]/page.tsx @@ -29,12 +29,18 @@ export default function Page({ params }: { params: { id: string } }) {
- {currentTab === tabs[0] && ( - - )} - {currentTab === tabs[1] && ( - - )} +
+ +
+
+ +
); } diff --git a/app/[locale]/(main)/footer.tsx b/app/[locale]/(main)/footer.tsx index 7d2fb36..ce8334e 100644 --- a/app/[locale]/(main)/footer.tsx +++ b/app/[locale]/(main)/footer.tsx @@ -1,7 +1,9 @@ +import pack from "@/package.json"; import { useTranslations } from "next-intl"; export default function Footer() { const t = useTranslations("Footer"); + const version = pack.version; return (
@@ -14,6 +16,13 @@ export default function Footer() { > {t("a_303-585_GitHub")} + + v{version} +

{t("section_607-869_2020")} diff --git a/app/[locale]/(main)/header.tsx b/app/[locale]/(main)/header.tsx index e09323f..f3a6e9e 100644 --- a/app/[locale]/(main)/header.tsx +++ b/app/[locale]/(main)/header.tsx @@ -26,6 +26,7 @@ function Header() {
{ + sessionStorage.removeItem("selectedTag"); router.push(`/${locale}/`); }} className="flex cursor-pointer items-center text-base font-medium" @@ -98,7 +99,9 @@ function Overview() {

{mouted ? (

{timeString}

- ) : } + ) : ( + + )}
); diff --git a/app/[locale]/(main)/layout.tsx b/app/[locale]/(main)/layout.tsx index f71cb0f..47d4b85 100644 --- a/app/[locale]/(main)/layout.tsx +++ b/app/[locale]/(main)/layout.tsx @@ -1,9 +1,8 @@ import Footer from "@/app/[locale]/(main)/footer"; import Header from "@/app/[locale]/(main)/header"; import { auth } from "@/auth"; -import { SignIn } from "@/components/sign-in"; +import { SignIn } from "@/components/SignIn"; import getEnv from "@/lib/env-entry"; -import { redirect } from "next/navigation"; import React from "react"; type DashboardProps = { @@ -12,19 +11,12 @@ type DashboardProps = { export default async function MainLayout({ children }: DashboardProps) { const session = await auth(); - if (!session && getEnv("SitePassword")) { - // if (getEnv("CF_PAGES")) { - // redirect("/api/auth/signin"); - // } else { - return ; - // } - } return (
- {children} + {!session && getEnv("SitePassword") ? : children}
diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index db96506..33d5a32 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -1,5 +1,4 @@ // @auto-i18n-check. Please do not delete the line. -import { auth } from "@/auth"; import { locales } from "@/i18n-metadata"; import getEnv from "@/lib/env-entry"; import { cn } from "@/lib/utils"; @@ -13,8 +12,6 @@ import { ThemeProvider } from "next-themes"; import { Inter as FontSans } from "next/font/google"; import React from "react"; -import "/node_modules/flag-icons/css/flag-icons.min.css"; - const fontSans = FontSans({ subsets: ["latin"], variable: "--font-sans", @@ -65,6 +62,10 @@ export default function LocaleLayout({ + ) => { - if (timestamp - element.last_active > 300) { - data.offline_servers += 1; - element.online_status = false; - } else { - data.live_servers += 1; - element.online_status = true; - } - data.total_in_bandwidth += element.status.NetInTransfer; - data.total_out_bandwidth += element.status.NetOutTransfer; - - delete element.ipv4; - delete element.ipv6; - delete element.valid_ip; - - return element; - }, - ); - - return data; + const data = await GetNezhaData(); + return NextResponse.json(data, { status: 200 }); } catch (error) { - return error; + console.error("Error in GET handler:", error); + // @ts-ignore + const statusCode = error.statusCode || 500; + // @ts-ignore + const message = error.message || "Internal Server Error"; + return NextResponse.json({ error: message }, { status: statusCode }); } -} \ No newline at end of file +}); +import { auth } from "@/auth"; +import getEnv from "@/lib/env-entry"; +import { GetNezhaData } from "@/lib/serverFetch"; +import { redirect } from "next/navigation"; +import { NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; + +export const runtime = "edge"; + + + +export const GET = auth(async function GET(req) { + if (!req.auth && getEnv("SitePassword")) { + redirect("/"); + } + + try { + const data = await GetNezhaData(); + return NextResponse.json(data, { status: 200 }); + } catch (error) { + console.error("Error in GET handler:", error); + // @ts-ignore + const statusCode = error.statusCode || 500; + // @ts-ignore + const message = error.message || "Internal Server Error"; + return NextResponse.json({ error: message }, { status: statusCode }); + } +}); diff --git a/auth.ts b/auth.ts index fb64660..2a33075 100644 --- a/auth.ts +++ b/auth.ts @@ -1,21 +1,32 @@ import NextAuth from "next-auth"; -import Credentials from "next-auth/providers/credentials"; +import CredentialsProvider from "next-auth/providers/credentials"; import getEnv from "./lib/env-entry"; export const { handlers, signIn, signOut, auth } = NextAuth({ - secret: "H7Fijn9veJRkbizIwUQEpBAzzhRwkv7/ZoB5sGF5cwm5", + secret: process.env.AUTH_SECRET ?? "this_is_nezha_dash_web_secret", + trustHost: (process.env.AUTH_TRUST_HOST as boolean | undefined) ?? true, providers: [ - Credentials({ - credentials: { - password: {}, - }, - authorize: async (credentials) => { - if (credentials.password === getEnv("SitePassword")) { - return { id: "0" }; + CredentialsProvider({ + type: "credentials", + credentials: { password: { label: "Password", type: "password" } }, + // authorization function + async authorize(credentials) { + const { password } = credentials; + if (password === getEnv("SitePassword")) { + return { id: "nezha-dash-auth" }; } - return null; + return { error: "Invalid password" }; }, }), ], + callbacks: { + async signIn({ user }) { + // @ts-ignore + if (user.error) { + return false; + } + return true; + }, + }, }); diff --git a/components/ServerFlag.tsx b/components/ServerFlag.tsx index 74bb09b..a1f507a 100644 --- a/components/ServerFlag.tsx +++ b/components/ServerFlag.tsx @@ -1,8 +1,15 @@ import getEnv from "@/lib/env-entry"; +import { cn } from "@/lib/utils"; import getUnicodeFlagIcon from "country-flag-icons/unicode"; import { useEffect, useState } from "react"; -export default function ServerFlag({ country_code }: { country_code: string }) { +export default function ServerFlag({ + country_code, + className, +}: { + country_code: string; + className?: string; +}) { const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false); const useSvgFlag = getEnv("NEXT_PUBLIC_ForceUseSvgFlag") === "true"; @@ -38,9 +45,9 @@ export default function ServerFlag({ country_code }: { country_code: string }) { } return ( - + {useSvgFlag || !supportsEmojiFlags ? ( - + ) : ( getUnicodeFlagIcon(country_code) )} diff --git a/components/SignIn.tsx b/components/SignIn.tsx new file mode 100644 index 0000000..6c7a83b --- /dev/null +++ b/components/SignIn.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { getCsrfToken, signIn } from "next-auth/react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +import { Loader } from "./loading/Loader"; + +export function SignIn() { + const t = useTranslations("SignIn"); + + const [csrfToken, setCsrfToken] = useState(""); + const [loading, setLoading] = useState(false); + const [errorState, setErrorState] = useState(false); + const [successState, setSuccessState] = useState(false); + + const router = useRouter(); + + useEffect(() => { + async function loadProviders() { + const csrf = await getCsrfToken(); + setCsrfToken(csrf); + } + loadProviders(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + const formData = new FormData(e.currentTarget); + const password = formData.get("password") as string; + const res = await signIn("credentials", { + password: password, + redirect: false, + }); + if (res?.error) { + console.log("login error"); + setErrorState(true); + setSuccessState(false); + } else { + console.log("login success"); + setErrorState(false); + setSuccessState(true); + router.push("/"); + router.refresh(); + } + setLoading(false); + }; + return ( +
+ +
+ + +
+
+ ); +} diff --git a/components/Switch.tsx b/components/Switch.tsx index 607889e..1a83a6c 100644 --- a/components/Switch.tsx +++ b/components/Switch.tsx @@ -2,24 +2,68 @@ import { cn } from "@/lib/utils"; import { motion } from "framer-motion"; -import React from "react"; +import React, { createRef, useEffect, useRef, useState } from "react"; export default function Switch({ allTag, nowTag, - setTag, + onTagChange, }: { allTag: string[]; nowTag: string; - setTag: (tag: string) => void; + onTagChange: (tag: string) => void; }) { + const scrollRef = useRef(null); + const tagRefs = useRef(allTag.map(() => createRef())); + + useEffect(() => { + const savedTag = sessionStorage.getItem("selectedTag"); + if (savedTag && allTag.includes(savedTag)) { + onTagChange(savedTag); + } + }, [allTag]); + + useEffect(() => { + const container = scrollRef.current; + if (!container) return; + + const isOverflowing = container.scrollWidth > container.clientWidth; + if (!isOverflowing) return; + + const onWheel = (e: WheelEvent) => { + e.preventDefault(); + container.scrollLeft += e.deltaY; + }; + + container.addEventListener("wheel", onWheel, { passive: false }); + + return () => { + container.removeEventListener("wheel", onWheel); + }; + }, []); + + useEffect(() => { + const currentTagRef = tagRefs.current[allTag.indexOf(nowTag)]; + if (currentTagRef && currentTagRef.current) { + currentTagRef.current.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + } + }, [nowTag]); + return ( -
+
- {allTag.map((tag) => ( + {allTag.map((tag, index) => (
setTag(tag)} + ref={tagRefs.current[index]} + onClick={() => onTagChange(tag)} className={cn( "relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500", nowTag === tag diff --git a/components/TabSwitch.tsx b/components/TabSwitch.tsx index d2913ad..7701815 100644 --- a/components/TabSwitch.tsx +++ b/components/TabSwitch.tsx @@ -14,7 +14,7 @@ export default function TabSwitch({ setCurrentTab: (tab: string) => void; }) { return ( -
+
{tabs.map((tab: string) => (