feat: home page with header

This commit is contained in:
alikia2x (寒寒) 2025-05-24 03:08:32 +08:00
parent dd70543594
commit fe2fd4fe36
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
22 changed files with 517 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View 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>
);

View 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>
);

View 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>
);

View 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>
);

View 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>
);

View 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>
);

View 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>
);

View 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>
);

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

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

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

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

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

View File

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB