feat: home page with header
This commit is contained in:
parent
dd70543594
commit
fe2fd4fe36
@ -114,6 +114,12 @@ a {
|
||||
@apply bg-surface dark:bg-dark-surface text-on-surface dark:text-dark-on-surface;
|
||||
}
|
||||
|
||||
[type="search"]::-webkit-search-cancel-button,
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root {
|
||||
font-family: "InterVariable", "MiSans VF", sans-serif;
|
||||
|
@ -3,8 +3,7 @@ import "./globals.css";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app"
|
||||
title: "中V档案馆"
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@ -19,7 +18,10 @@ export default function RootLayout({
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>中V档案馆</title>
|
||||
</head>
|
||||
<body className="min-h-screen flex flex-col">{children}</body>
|
||||
<body className="min-h-screen flex flex-col">
|
||||
{children}
|
||||
<div id="portal-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { Header } from "@/components/shell/Header";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="flex flex-col items-center justify-center h-full flex-grow gap-8 px-4">
|
||||
<h1 className="text-4xl font-medium text-center">正在施工中……</h1>
|
||||
<p>在搜索栏输入BV号或AV号,可以查询目前数据库收集到的信息~</p>
|
||||
|
@ -4,6 +4,7 @@
|
||||
"": {
|
||||
"name": "next",
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.12.2",
|
||||
"next": "15.3.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@ -159,6 +160,8 @@
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
|
||||
|
||||
"framer-motion": ["framer-motion@12.12.2", "", { "dependencies": { "motion-dom": "^12.12.1", "motion-utils": "^12.12.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-qCszZCiGWkilL40E3VuhIJJC/CS3SIBl2IHyGK8FU30nOUhTmhBNWPrNFyozAWH/bXxwzi19vJHIGVdALF0LCg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||
@ -195,6 +198,10 @@
|
||||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"motion-dom": ["motion-dom@12.12.1", "", { "dependencies": { "motion-utils": "^12.12.1" } }, "sha512-GXq/uUbZBEiFFE+K1Z/sxdPdadMdfJ/jmBALDfIuHGi0NmtealLOfH9FqT+6aNPgVx8ilq0DtYmyQlo6Uj9LKQ=="],
|
||||
|
||||
"motion-utils": ["motion-utils@12.12.1", "", {}, "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"next": ["next@15.3.2", "", { "dependencies": { "@next/env": "15.3.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.2", "@next/swc-darwin-x64": "15.3.2", "@next/swc-linux-arm64-gnu": "15.3.2", "@next/swc-linux-arm64-musl": "15.3.2", "@next/swc-linux-x64-gnu": "15.3.2", "@next/swc-linux-x64-musl": "15.3.2", "@next/swc-win32-arm64-msvc": "15.3.2", "@next/swc-win32-x64-msvc": "15.3.2", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ=="],
|
||||
|
12
packages/next/components/icons/CloseIcon.tsx
Normal file
12
packages/next/components/icons/CloseIcon.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export const CloseIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
|
||||
<div {...props}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
12
packages/next/components/icons/HomeIcon.tsx
Normal file
12
packages/next/components/icons/HomeIcon.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export const HomeIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
|
||||
<div {...props}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
12
packages/next/components/icons/InfoIcon.tsx
Normal file
12
packages/next/components/icons/InfoIcon.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export const InfoIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
|
||||
<div {...props}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M11 17h2v-6h-2zm1-8q.425 0 .713-.288T13 8t-.288-.712T12 7t-.712.288T11 8t.288.713T12 9m0 13q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
13
packages/next/components/icons/LeftArrow.tsx
Normal file
13
packages/next/components/icons/LeftArrow.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
export const LeftArrow: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg {...props} width="1em" height="1em" viewBox="0 0 10.72 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
id="path"
|
||||
d="M10.64 4.34C10.69 4.23 10.72 4.11 10.72 3.97C10.72 3.79 10.67 3.65 10.59 3.55C10.55 3.51 10.5 3.46 10.46 3.44C10.39 3.4 10.31 3.38 10.22 3.38L2.16 3.38L4.57 0.97C4.61 0.9 4.7 0.73 4.7 0.63C4.7 0.6 4.7 0.57 4.7 0.52C4.67 0.4 4.6 0.28 4.5 0.16C4.39 0.06 4.29 0 4.17 -0.02C4.13 -0.04 4.09 -0.05 4.04 -0.05C3.95 -0.05 3.87 -0.02 3.81 0.01C3.76 0.04 3.73 0.06 3.7 0.09L0.22 3.58C0.07 3.72 0 3.85 0 3.97C0 4.09 0.07 4.23 0.22 4.38L3.7 7.87C3.82 7.95 3.93 8 4.04 8C4.18 7.98 4.37 7.91 4.5 7.79C4.6 7.68 4.67 7.56 4.7 7.44C4.7 7.4 4.7 7.36 4.7 7.32C4.7 7.26 4.7 7.21 4.67 7.16C4.66 7.1 4.62 7.04 4.57 7L2.16 4.59L10.22 4.59C10.3 4.59 10.37 4.57 10.43 4.54C10.49 4.51 10.59 4.4 10.64 4.34Z"
|
||||
fill="currentColor"
|
||||
fillOpacity="1"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
15
packages/next/components/icons/MenuIcon.tsx
Normal file
15
packages/next/components/icons/MenuIcon.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
|
||||
export const MenuIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
|
||||
<div {...props}>
|
||||
<svg width="1em" height="1em" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
id="path"
|
||||
d="M4.66 21C4.33 21 4.05 20.88 3.83 20.66C3.61 20.44 3.5 20.16 3.5 19.83C3.49 19.5 3.61 19.22 3.83 19C4.06 18.77 4.33 18.66 4.66 18.66L23.33 18.66C23.66 18.66 23.94 18.77 24.16 19C24.38 19.22 24.5 19.5 24.5 19.83C24.49 20.16 24.38 20.44 24.16 20.66C23.94 20.88 23.66 21 23.33 21L4.66 21ZM4.66 15.16C4.33 15.16 4.05 15.05 3.83 14.83C3.61 14.6 3.5 14.32 3.5 14C3.49 13.67 3.61 13.39 3.83 13.16C4.06 12.94 4.33 12.83 4.66 12.83L23.33 12.83C23.66 12.83 23.94 12.94 24.16 13.16C24.38 13.39 24.5 13.67 24.5 14C24.49 14.32 24.38 14.6 24.16 14.83C23.94 15.05 23.66 15.16 23.33 15.16L4.66 15.16ZM4.66 9.33C4.33 9.33 4.05 9.22 3.83 8.99C3.61 8.77 3.5 8.49 3.5 8.16C3.49 7.83 3.61 7.56 3.83 7.33C4.06 7.11 4.33 7 4.66 7L23.33 7C23.66 7 23.94 7.11 24.16 7.33C24.38 7.56 24.5 7.83 24.5 8.16C24.49 8.49 24.38 8.77 24.16 8.99C23.94 9.22 23.66 9.33 23.33 9.33L4.66 9.33Z"
|
||||
fill="currentColor"
|
||||
fillOpacity="1"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
12
packages/next/components/icons/RegisterIcon.tsx
Normal file
12
packages/next/components/icons/RegisterIcon.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export const RegisterIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
|
||||
<div {...props}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15 14c-2.67 0-8 1.33-8 4v2h16v-2c0-2.67-5.33-4-8-4m-9-4V7H4v3H1v2h3v3h2v-3h3v-2m6 2a4 4 0 0 0 4-4a4 4 0 0 0-4-4a4 4 0 0 0-4 4a4 4 0 0 0 4 4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
12
packages/next/components/icons/RightArrow.tsx
Normal file
12
packages/next/components/icons/RightArrow.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export const RightArrow: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg {...props} width="1em" height="1em" viewBox="0 0 10.72 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M0.08 3.66Q0 3.82 0 4.03Q0 4.29 0.13 4.45Q0.13 4.46 0.13 4.46Q0.19 4.52 0.26 4.56Q0.36 4.62 0.49 4.62L8.56 4.62L6.15 7.03Q6.1 7.07 6.08 7.12Q6.01 7.22 6.01 7.36Q6.01 7.41 6.02 7.47Q6.06 7.66 6.22 7.83Q6.37 7.98 6.54 8.02Q6.61 8.04 6.67 8.04Q6.81 8.04 6.91 7.98Q6.97 7.95 7.01 7.9L10.5 4.42Q10.72 4.2 10.72 4.03Q10.72 3.84 10.5 3.62L7.01 0.13Q6.84 0 6.67 0Q6.64 0 6.61 0Q6.41 0.02 6.22 0.21Q6.06 0.37 6.02 0.56Q6.01 0.62 6.01 0.68Q6.01 0.76 6.04 0.84Q6.07 0.93 6.15 1L8.56 3.41L0.49 3.41Q0.38 3.41 0.29 3.46Q0.2 3.5 0.13 3.59Q0.1 3.63 0.08 3.66Z"
|
||||
fill="currentColor"
|
||||
fillOpacity="1"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
12
packages/next/components/icons/SearchIcon.tsx
Normal file
12
packages/next/components/icons/SearchIcon.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export const SearchIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
|
||||
<div {...props}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
124
packages/next/components/shell/Header.tsx
Normal file
124
packages/next/components/shell/Header.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import TitleLight from "@/public/icons/标题-浅色.svg";
|
||||
import TitleDark from "@/public/icons/标题-深色.svg";
|
||||
import LogoMobileLight from "@/public/icons/TitleBar Mobile Light.svg";
|
||||
import LogoMobileDark from "@/public/icons/TitleBar Mobile Dark.svg";
|
||||
import DarkModeImage from "@/components/utils/DarkModeImage";
|
||||
import React, { useState } from "react";
|
||||
import { NavigationDrawer } from "@/components/shell/NavigatinDrawer";
|
||||
import { Portal } from "@/components/utils/Portal";
|
||||
import { RegisterIcon } from "@/components/icons/RegisterIcon";
|
||||
import { SearchBox } from "@/components/ui/SearchBox";
|
||||
import { MenuIcon } from "@/components/icons/MenuIcon";
|
||||
import { SearchIcon } from "@/components/icons/SearchIcon";
|
||||
import { InfoIcon } from "../icons/InfoIcon";
|
||||
import { HomeIcon } from "../icons/HomeIcon";
|
||||
|
||||
export const HeaderDestop = () => {
|
||||
return (
|
||||
<div className="hidden md:flex relative top-0 left-0 w-full h-28 z-20 justify-between">
|
||||
<div className="w-[305px] xl:ml-8 inline-flex items-center">
|
||||
<a href="/">
|
||||
<DarkModeImage
|
||||
lightSrc={TitleLight}
|
||||
darkSrc={TitleDark}
|
||||
alt="logo"
|
||||
className="w-[305px] h-24 inline-block max-w-[15rem] lg:max-w-[305px]"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<SearchBox />
|
||||
|
||||
<div
|
||||
className="inline-flex relative gap-6 h-full lg:right-12
|
||||
text-xl font-medium items-center w-[15rem] min-w-[8rem] mr-4 lg:mr-0 lg:w-[305px] justify-end"
|
||||
>
|
||||
<a href="/register">注册</a>
|
||||
<a href="/about">关于</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const HeaderMobile = () => {
|
||||
const [showDrawer, setShowDrawer] = useState(false);
|
||||
const [showsearchBox, setShowsearchBox] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Portal>
|
||||
<NavigationDrawer show={showDrawer} onClose={() => setShowDrawer(false)}>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="w-full h-14 flex items-center px-4 mt-1 pl-5">
|
||||
<DarkModeImage
|
||||
lightSrc={LogoMobileLight}
|
||||
darkSrc={LogoMobileDark}
|
||||
alt="Logo"
|
||||
className="w-24 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full h-14 flex items-center px-4">
|
||||
<a href="/" className="flex">
|
||||
<HomeIcon className="text-2xl pr-4" />
|
||||
<span>首页</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="w-full h-14 flex items-center px-4">
|
||||
<a href="/about" className="flex">
|
||||
<InfoIcon className="text-2xl pr-4" />
|
||||
<span>关于</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="w-full h-14 flex items-center px-4">
|
||||
<a href="/register" className="flex">
|
||||
<RegisterIcon className="text-2xl pr-4" />
|
||||
<span>注册</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</NavigationDrawer>
|
||||
</Portal>
|
||||
<div className="md:hidden relative top-0 left-0 w-full h-16 z-20">
|
||||
{!showsearchBox && (
|
||||
<button
|
||||
className="inline-flex absolute left-0 ml-4 h-full items-center dark:text-white text-2xl"
|
||||
onClick={() => setShowDrawer(true)}
|
||||
>
|
||||
<MenuIcon />
|
||||
</button>
|
||||
)}
|
||||
{!showsearchBox && (
|
||||
<div className="absolute left-1/2 -translate-x-1/2 -translate-y-0.5 inline-flex h-full items-center">
|
||||
<a href="/">
|
||||
<DarkModeImage
|
||||
lightSrc={LogoMobileLight}
|
||||
darkSrc={LogoMobileDark}
|
||||
alt="Logo"
|
||||
className="w-24 h-8 translate-y-[2px]"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{showsearchBox && <SearchBox close={() => setShowsearchBox(false)} />}
|
||||
{!showsearchBox && (
|
||||
<button
|
||||
className="inline-flex absolute right-0 h-full items-center mr-4"
|
||||
onClick={() => setShowsearchBox(!showsearchBox)}
|
||||
>
|
||||
<SearchIcon className="text-[1.625rem]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<>
|
||||
<HeaderDestop />
|
||||
<HeaderMobile />
|
||||
</>
|
||||
);
|
||||
};
|
61
packages/next/components/shell/NavigatinDrawer.tsx
Normal file
61
packages/next/components/shell/NavigatinDrawer.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
interface DrawerProps {
|
||||
show?: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const NavigationDrawer = ({ show = false, onClose, children }: DrawerProps) => {
|
||||
const coverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOutsideClick = (event: MouseEvent) => {
|
||||
if (show && coverRef.current && event.target === coverRef.current) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("click", handleOutsideClick);
|
||||
return () => {
|
||||
window.removeEventListener("click", handleOutsideClick);
|
||||
};
|
||||
}, [show, onClose]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{show && (
|
||||
<>
|
||||
{/* Backdrop - Fade in/out */}
|
||||
<motion.div
|
||||
ref={coverRef}
|
||||
className="fixed top-0 left-0 w-full h-full z-40 bg-black/10"
|
||||
aria-hidden="true"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Drawer - Slide from left */}
|
||||
<motion.div
|
||||
className="fixed top-0 left-0 h-full bg-[#fff0ee] dark:bg-[#231918] z-50"
|
||||
style={{ width: "min(22.5rem, 70vw)" }}
|
||||
initial={{ x: -500, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -500, opacity: 0 }}
|
||||
transition={{ type: "spring", stiffness: 438, damping: 46 }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationDrawer;
|
92
packages/next/components/ui/SearchBox.tsx
Normal file
92
packages/next/components/ui/SearchBox.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import React, { useState, useRef, useCallback } from "react";
|
||||
import { SearchIcon } from "@/components/icons/SearchIcon";
|
||||
import { CloseIcon } from "@/components/icons/CloseIcon";
|
||||
|
||||
interface SearchBoxProps {
|
||||
close?: () => void;
|
||||
}
|
||||
|
||||
export const SearchBox: React.FC<SearchBoxProps> = ({ close = () => {} }) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const inputElement = useRef<HTMLInputElement>(null);
|
||||
|
||||
const search = useCallback((query: string) => {
|
||||
if (query.trim()) {
|
||||
window.location.href = `/song/${query.trim()}/info`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
search(inputValue);
|
||||
}
|
||||
},
|
||||
[inputValue, search]
|
||||
);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
setInputValue("");
|
||||
close();
|
||||
}, [close]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute md:relative left-0 h-full mr-0 inline-flex items-center w-full px-4
|
||||
md:px-0 md:w-full xl:max-w-[50rem] md:mx-4"
|
||||
>
|
||||
<div
|
||||
className="w-full h-10 lg:h-12 px-4 rounded-full bg-surface-container-high
|
||||
dark:bg-zinc-800/70 backdrop-blur-lg flex justify-between md:px-5"
|
||||
>
|
||||
<button className="w-6" onClick={() => search(inputValue)}>
|
||||
<SearchIcon
|
||||
className="h-full inline-flex items-center text-[1.5rem]
|
||||
text-on-surface-variant dark:text-dark-on-surface-variant"
|
||||
/>
|
||||
</button>
|
||||
<div className="md:hidden flex-grow px-4 top-0 h-full">
|
||||
<input
|
||||
ref={inputElement}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
type="search"
|
||||
placeholder="搜索"
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
className="bg-transparent h-full w-full focus:outline-none"
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden md:block flex-grow px-4 top-0 h-full">
|
||||
<input
|
||||
ref={inputElement}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
type="search"
|
||||
placeholder="搜索"
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
className="bg-transparent h-full w-full focus:outline-none"
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`w-6 duration-100 ${inputValue ? "md:opacity-100" : "md:opacity-0"}`}
|
||||
onClick={handleClear}
|
||||
>
|
||||
<CloseIcon className="h-full w-6 inline-flex items-center text-[1.5rem] text-on-surface-variant dark:text-dark-on-surface-variant" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
55
packages/next/components/utils/DarkModeImage.tsx
Normal file
55
packages/next/components/utils/DarkModeImage.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
interface Props {
|
||||
lightSrc: string;
|
||||
darkSrc: string;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const DarkModeImage = ({ lightSrc, darkSrc, alt = "", className = "", width, height }: Props) => {
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const [currentSrc, setCurrentSrc] = useState(lightSrc);
|
||||
const [opacity, setOpacity] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleDarkModeChange = (event: MediaQueryListEvent) => {
|
||||
setIsDarkMode(event.matches);
|
||||
setCurrentSrc(event.matches ? darkSrc : lightSrc);
|
||||
setOpacity(1);
|
||||
};
|
||||
|
||||
const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
setIsDarkMode(darkModeMediaQuery.matches);
|
||||
setCurrentSrc(darkModeMediaQuery.matches ? darkSrc : lightSrc);
|
||||
setOpacity(1);
|
||||
|
||||
darkModeMediaQuery.addEventListener("change", handleDarkModeChange);
|
||||
|
||||
return () => {
|
||||
darkModeMediaQuery.removeEventListener("change", handleDarkModeChange);
|
||||
};
|
||||
}, [darkSrc, lightSrc]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentSrc(isDarkMode ? darkSrc : lightSrc);
|
||||
}, [isDarkMode, darkSrc, lightSrc]);
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={currentSrc}
|
||||
alt={alt}
|
||||
className={className}
|
||||
style={{ opacity: opacity }}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DarkModeImage;
|
14
packages/next/components/utils/Portal.tsx
Normal file
14
packages/next/components/utils/Portal.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
export const Portal = ({ children }: { children: React.ReactNode }) => {
|
||||
const documentNotUndefined = typeof document !== "undefined";
|
||||
// Ensure portal root exists in your HTML
|
||||
const portalRoot = documentNotUndefined ? document.getElementById("portal-root") : null;
|
||||
|
||||
if (!portalRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(children, portalRoot);
|
||||
};
|
@ -10,9 +10,10 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.12.2",
|
||||
"next": "15.3.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"next": "15.3.2"
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
|
14
packages/next/public/icons/TitleBar Mobile Dark.svg
Normal file
14
packages/next/public/icons/TitleBar Mobile Dark.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.3 KiB |
14
packages/next/public/icons/TitleBar Mobile Light.svg
Normal file
14
packages/next/public/icons/TitleBar Mobile Light.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.4 KiB |
15
packages/next/public/icons/标题-浅色.svg
Normal file
15
packages/next/public/icons/标题-浅色.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
4
packages/next/public/icons/标题-深色.svg
Normal file
4
packages/next/public/icons/标题-深色.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 21 KiB |
Loading…
Reference in New Issue
Block a user