mirror of
https://github.com/hamster1963/nezha-dash.git
synced 2025-04-24 21:10:45 +08:00
Merge branch 'main' into cloudflare-dev
This commit is contained in:
commit
74dbba7881
10
.env.example
10
.env.example
@ -1,6 +1,6 @@
|
||||
NezhaBaseUrl=http://1.1.1.1:8008
|
||||
NezhaAuth=nezha-token
|
||||
ServerDisablePrefetch=false
|
||||
NEXT_PUBLIC_NezhaFetchInterval=2000
|
||||
NezhaBaseUrl=http://124.XX.XX.XX:8008
|
||||
NezhaAuth=your-nezha-api-token
|
||||
DefaultLocale=zh
|
||||
NEXT_PUBLIC_NezhaFetchInterval=5000
|
||||
NEXT_PUBLIC_ShowFlag=true
|
||||
NEXT_PUBLIC_DisableCartoon=false
|
||||
NEXT_PUBLIC_DisableCartoon=true
|
36
.github/workflows/Deploy.yml
vendored
36
.github/workflows/Deploy.yml
vendored
@ -5,8 +5,40 @@ on:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
REGISTRY_IMAGE: hamster1963/nezha-dash
|
||||
ALIYUN_REGISTRY_IMAGE: registry.cn-guangzhou.aliyuncs.com/hamster-home/nezha-dash
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
name: Generate Changelog
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_body: ${{ steps.git-cliff.outputs.content }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v4
|
||||
id: git-cliff
|
||||
with:
|
||||
config: git-cliff-config/cliff.toml
|
||||
args: -vv --latest --strip 'footer'
|
||||
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 }}
|
||||
|
||||
build-and-push:
|
||||
name: Build and push Docker image
|
||||
runs-on: ubuntu-latest
|
||||
environment: Production
|
||||
steps:
|
||||
@ -37,8 +69,8 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
hamster1963/nezha-dash
|
||||
registry.cn-guangzhou.aliyuncs.com/hamster-home/nezha-dash
|
||||
${{ env.REGISTRY_IMAGE }}
|
||||
${{ env.ALIYUN_REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=ref,event=tag
|
||||
|
@ -13,10 +13,10 @@
|
||||
#### 环境变量
|
||||
|
||||
| 变量名 | 含义 | 示例 |
|
||||
| ------------------------------ | -------------------- | -------------------------------- |
|
||||
| ------------------------------ | -------------------------------- | -------------------------------- |
|
||||
| NezhaBaseUrl | nezha 面板地址 | http://120.x.x.x:8008 |
|
||||
| NezhaAuth | nezha 面板 API Token | 5hAY3QX6Nl9B3Uxxxx26KMvOMyXS1Udi |
|
||||
| ServerDisablePrefetch | 是否禁用预加载 | **默认**:false |
|
||||
| DefaultLocale | 面板默认显示语言(代码参考下表) | **默认**:en |
|
||||
| NEXT_PUBLIC_NezhaFetchInterval | 获取数据间隔(毫秒) | **默认**:2000 |
|
||||
| NEXT_PUBLIC_ShowFlag | 是否显示旗帜 | **默认**:false |
|
||||
| NEXT_PUBLIC_DisableCartoon | 是否禁用卡通人物 | **默认**:false |
|
||||
|
@ -6,18 +6,29 @@ import { nezhaFetcher } from "../../../../lib/utils";
|
||||
import useSWR from "swr";
|
||||
import getEnv from "../../../../lib/env-entry";
|
||||
export default function ServerListClient() {
|
||||
const { data } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
|
||||
const { data, error } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
|
||||
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
|
||||
});
|
||||
if (error)
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p className="text-sm font-medium opacity-40">{error.message}</p>
|
||||
<p className="text-sm font-medium opacity-40">
|
||||
Please check your environment variables and review the server console
|
||||
logs for more details.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
if (!data) return null;
|
||||
const sortedServers = data.result.sort((a, b) => {
|
||||
if (a.display_index && b.display_index) {
|
||||
return b.display_index - a.display_index;
|
||||
}
|
||||
if (a.display_index) return -1;
|
||||
if (b.display_index) return 1;
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
{sortedServers.map((serverInfo) => (
|
||||
|
@ -1,9 +1,15 @@
|
||||
import { unstable_setRequestLocale } from "next-intl/server";
|
||||
import ServerList from "../../../components/ServerList";
|
||||
import ServerOverview from "../../../components/ServerOverview";
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export default function Home() {
|
||||
export default function Home({
|
||||
params: { locale },
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
unstable_setRequestLocale(locale);
|
||||
return (
|
||||
<div className="mx-auto grid w-full max-w-5xl gap-4 md:gap-6">
|
||||
<ServerOverview />
|
||||
|
@ -1,6 +1,8 @@
|
||||
// @auto-i18n-check. Please do not delete the line.
|
||||
|
||||
import "@/styles/globals.css";
|
||||
import "/node_modules/flag-icons/css/flag-icons.min.css";
|
||||
|
||||
import React from "react";
|
||||
import { NextIntlClientProvider, useMessages } from "next-intl";
|
||||
import { PublicEnvScript } from "next-runtime-env";
|
||||
@ -10,6 +12,7 @@ import { ThemeProvider } from "next-themes";
|
||||
import { Viewport } from "next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { locales } from "@/i18n-metadata";
|
||||
import { unstable_setRequestLocale } from "next-intl/server";
|
||||
|
||||
const fontSans = FontSans({
|
||||
subsets: ["latin"],
|
||||
@ -34,11 +37,12 @@ export const viewport: Viewport = {
|
||||
userScalable: false,
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
|
||||
export default function LocaleLayout({
|
||||
children,
|
||||
params: { locale },
|
||||
@ -46,6 +50,8 @@ export default function LocaleLayout({
|
||||
children: React.ReactNode;
|
||||
params: { locale: string };
|
||||
}) {
|
||||
unstable_setRequestLocale(locale);
|
||||
|
||||
const messages = useMessages();
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
|
@ -8,17 +8,18 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
interface NezhaDataResponse {
|
||||
error?: string;
|
||||
data?: ServerApi;
|
||||
}
|
||||
|
||||
export async function GET(_: Request) {
|
||||
try {
|
||||
const response = await GetNezhaData();
|
||||
return NextResponse.json(response, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json(
|
||||
{ error: "fetch nezha data failed" },
|
||||
{ status: 400 },
|
||||
);
|
||||
const response = (await GetNezhaData()) as NezhaDataResponse;
|
||||
if (response.error) {
|
||||
console.log(response.error);
|
||||
return NextResponse.json({ error: response.error }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json(response, { status: 200 });
|
||||
}
|
||||
|
||||
async function GetNezhaData() {
|
||||
|
@ -9,8 +9,9 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { cn, formatNezhaInfo } from "@/lib/utils";
|
||||
import ServerCardPopover from "./ServerCardPopover";
|
||||
import getUnicodeFlagIcon from "country-flag-icons/unicode";
|
||||
|
||||
import { env } from "next-runtime-env";
|
||||
import ServerFlag from "./ServerFlag";
|
||||
|
||||
export default function ServerCard({
|
||||
serverInfo,
|
||||
@ -32,15 +33,7 @@ export default function ServerCard({
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<section className={"flex items-center justify-start gap-2 lg:w-28"}>
|
||||
{showFlag ? (
|
||||
country_code ? (
|
||||
<span className="text-[12px] text-muted-foreground">
|
||||
{getUnicodeFlagIcon(country_code)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[12px] text-muted-foreground">🏁</span>
|
||||
)
|
||||
) : null}
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
<p
|
||||
className={cn(
|
||||
"break-all font-bold tracking-tight",
|
||||
@ -58,8 +51,6 @@ export default function ServerCard({
|
||||
</Popover>
|
||||
<section className={"grid grid-cols-5 items-center gap-3"}>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
{" "}
|
||||
{/* 设置固定宽度 */}
|
||||
<p className="text-xs text-muted-foreground">{t("CPU")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{cpu.toFixed(2)}%
|
||||
@ -67,8 +58,6 @@ export default function ServerCard({
|
||||
<ServerUsageBar value={cpu} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
{" "}
|
||||
{/* 设置固定宽度 */}
|
||||
<p className="text-xs text-muted-foreground">{t("Mem")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{mem.toFixed(2)}%
|
||||
@ -76,8 +65,6 @@ export default function ServerCard({
|
||||
<ServerUsageBar value={mem} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
{" "}
|
||||
{/* 设置固定宽度 */}
|
||||
<p className="text-xs text-muted-foreground">{t("STG")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{stg.toFixed(2)}%
|
||||
@ -85,8 +72,6 @@ export default function ServerCard({
|
||||
<ServerUsageBar value={stg} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
{" "}
|
||||
{/* 设置固定宽度 */}
|
||||
<p className="text-xs text-muted-foreground">{t("Upload")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{up.toFixed(2)}
|
||||
@ -94,8 +79,6 @@ export default function ServerCard({
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
{" "}
|
||||
{/* 设置固定宽度 */}
|
||||
<p className="text-xs text-muted-foreground">{t("Download")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{down.toFixed(2)}
|
||||
@ -113,15 +96,7 @@ export default function ServerCard({
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<section className={"flex items-center justify-start gap-2 lg:w-28"}>
|
||||
{showFlag ? (
|
||||
country_code ? (
|
||||
<span className="text-[12px] text-muted-foreground">
|
||||
{getUnicodeFlagIcon(country_code)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[12px] text-muted-foreground">🏁</span>
|
||||
)
|
||||
) : null}
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
<p
|
||||
className={cn(
|
||||
"break-all font-bold tracking-tight",
|
||||
|
36
components/ServerFlag.tsx
Normal file
36
components/ServerFlag.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import getUnicodeFlagIcon from "country-flag-icons/unicode";
|
||||
|
||||
export default function ServerFlag({ country_code }: { country_code: string }) {
|
||||
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkEmojiSupport = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试
|
||||
if (!ctx) return;
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = "32px Arial";
|
||||
ctx.fillText(emojiFlag, 0, 0);
|
||||
|
||||
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0;
|
||||
setSupportsEmojiFlags(support);
|
||||
};
|
||||
|
||||
checkEmojiSupport();
|
||||
}, []);
|
||||
|
||||
if (!country_code) return null;
|
||||
|
||||
return (
|
||||
<span className="text-[12px] text-muted-foreground">
|
||||
{!supportsEmojiFlags ? (
|
||||
<span className={`fi fi-${country_code}`}></span>
|
||||
) : (
|
||||
getUnicodeFlagIcon(country_code)
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
NezhaBaseUrl=http://0.0.0.0:8008
|
||||
NezhaAuth=5hAY3QX6Nl9B3UOQgB26KdsdS1dsdUdM
|
||||
ServerDisablePrefetch=false
|
||||
NezhaBaseUrl=http://124.XX.XX.XX:8008
|
||||
NezhaAuth=your-nezha-api-token
|
||||
DefaultLocale=zh
|
||||
NEXT_PUBLIC_NezhaFetchInterval=5000
|
||||
NEXT_PUBLIC_ShowFlag=true
|
||||
NEXT_PUBLIC_DisableCartoon=true
|
83
git-cliff-config/cliff.toml
Normal file
83
git-cliff-config/cliff.toml
Normal file
@ -0,0 +1,83 @@
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
#
|
||||
# Lines starting with "#" are comments.
|
||||
# Configuration options are organized into tables and keys.
|
||||
# See documentation for more information on available options.
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/
|
||||
body = """
|
||||
{% if version %}\
|
||||
## Release {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
* {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }})) by [@{{ commit.author.name }}](https://github.com/{{ commit.author.name }})\
|
||||
{% for footer in commit.footers -%}
|
||||
, {{ footer.token }}{{ footer.separator }}{{ footer.value }}\
|
||||
{% endfor %}\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
postprocessors = [
|
||||
{ pattern = '\$REPO', replace = "https://github.com/hamster1963/nezha-dash" }, # replace repository URL
|
||||
]
|
||||
# changelog footer
|
||||
footer = """
|
||||
"""
|
||||
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = true
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 0 -->⛰️ Features" },
|
||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||
{ message = "^update", group = "<!-- 9 -->🔼 Updates" },
|
||||
{ message = "^delete", group = "<!-- 10 -->🔞 Delete" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-20]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = ""
|
||||
# regex for ignoring tags
|
||||
ignore_tags = "v.*-beta.*"
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
@ -1,5 +1,7 @@
|
||||
// @auto-i18n-check. Please do not delete the line.
|
||||
|
||||
import getEnv from "./lib/env-entry";
|
||||
|
||||
export const localeItems = [
|
||||
{ code: "en", name: "English" },
|
||||
{ code: "ja", name: "日本語" },
|
||||
@ -21,4 +23,4 @@ export const localeItems = [
|
||||
];
|
||||
|
||||
export const locales = localeItems.map((item) => item.code);
|
||||
export const defaultLocale = "en";
|
||||
export const defaultLocale = getEnv("DefaultLocale") || "en";
|
||||
|
@ -10,8 +10,8 @@ export async function GetNezhaData() {
|
||||
|
||||
var nezhaBaseUrl = getEnv("NezhaBaseUrl");
|
||||
if (!nezhaBaseUrl) {
|
||||
console.error("NezhaBaseUrl is not set");
|
||||
throw new Error("NezhaBaseUrl is not set");
|
||||
console.log("NezhaBaseUrl is not set");
|
||||
return { error: "NezhaBaseUrl is not set" };
|
||||
}
|
||||
|
||||
// Remove trailing slash
|
||||
@ -27,7 +27,12 @@ export async function GetNezhaData() {
|
||||
revalidate: 0,
|
||||
},
|
||||
});
|
||||
const nezhaData = (await response.json()).result as NezhaAPI[];
|
||||
const resData = await response.json();
|
||||
const nezhaData = resData.result as NezhaAPI[];
|
||||
if (!nezhaData) {
|
||||
console.log(resData);
|
||||
return { error: "NezhaData fetch failed" };
|
||||
}
|
||||
const data: ServerApi = {
|
||||
live_servers: 0,
|
||||
offline_servers: 0,
|
||||
|
@ -1,14 +1,14 @@
|
||||
// @auto-i18n-check. Please do not delete the line.
|
||||
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
import { locales } from "./i18n-metadata";
|
||||
import { defaultLocale, locales } from "./i18n-metadata";
|
||||
|
||||
export default createMiddleware({
|
||||
// A list of all locales that are supported
|
||||
locales: locales,
|
||||
|
||||
// Used when no locale matches
|
||||
defaultLocale: "en",
|
||||
defaultLocale: defaultLocale,
|
||||
|
||||
// 'always': This is the default, The home page will also be redirected to the default language, such as www.abc.com to www.abc.com/en
|
||||
// 'as-needed': The default page is not redirected. For example, if you open www.abc.com, it is still www.abc.com
|
||||
|
@ -1,3 +1,9 @@
|
||||
import withBundleAnalyzer from "@next/bundle-analyzer";
|
||||
|
||||
const bundleAnalyzer = withBundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
import withPWAInit from "@ducanh2912/next-pwa";
|
||||
@ -16,5 +22,10 @@ const withPWA = withPWAInit({
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
logging: {
|
||||
fetches: {
|
||||
fullUrl: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
export default withPWA(withNextIntl(nextConfig));
|
||||
export default bundleAnalyzer(withPWA(withNextIntl(nextConfig)));
|
||||
|
@ -27,6 +27,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"flag-icons": "^7.2.3",
|
||||
"lucide-react": "^0.414.0",
|
||||
"luxon": "^3.5.0",
|
||||
"next": "^14.2.13",
|
||||
@ -47,8 +48,8 @@
|
||||
"eslint-plugin-turbo": "^2.1.2",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"@next/bundle-analyzer": "^14.2.13",
|
||||
"@types/node": "^22.7.2",
|
||||
"@types/react": "^18.3.9",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.11.1",
|
||||
|
Loading…
Reference in New Issue
Block a user