将 HTML/SCSS 导航栏重写为 React 样式组件

问题描述 投票:0回答:1

我有我的个人网站,是我多年前使用 HTML + ejs 模板和 scss 编写的。我现在正在使用 nextjs 和 React styled-components 重写网站。

我在重现我之前的代码中的导航栏行为时遇到问题,请寻求帮助。这主要是一个 React/CSS 问题。

期望的行为

我希望我的导航栏是这样的:

  1. 屏幕上只有一个栏,所有链接/按钮都在线。
    1. 左侧不同页面的链接,右侧一些数据/其他链接。
  2. 左侧的一些按钮应该是悬停时显示的下拉菜单。
    1. 下拉内容应直接显示在按钮下方,作为一个小矩形,其中链接对齐堆叠。
  3. 导航栏应该具有响应能力。
    1. 当用户使窗口小于 x 量(此处为 968px)时,导航栏会折叠并仅显示博客页面(主页)的主链接和右侧的汉堡按钮。
    2. 当用户单击汉堡包按钮时,菜单会下拉并显示所有可用的链接。
    3. 当将鼠标悬停在移动设备上或单击移动设备时,下拉菜单仍应在此响应模式下运行。

我在简化代码时遇到了另一个问题。我对使用钩子确保组件在渲染之前安装的理解是否正确?我注意到,如果删除此挂钩,导航栏将在不应用样式的情况下呈现。我不确定这是否是正确的方法或者是否有更好的方法。

最小代码示例

好的,这是调试导航栏的最小示例:

npx create-next-app --ts .
# ✔ Would you like to use ESLint with this project? … No / Yes
# ✔ Would you like to use `src/` directory with this project? … No / Yes
# ✔ Would you like to use experimental `app/` directory with this project? … No / Yes
# ✔ What import alias would you like configured? … @components/*
# Creating a new Next.js app in /Users/work/Desktop/next-minimal-navbar.
npm install styled-components

简化了以下文件:

index.tsx

import Head from 'next/head'

export default function Home() {
    return (
        <>
            <Head>
                <title>Hello, world!</title>
            </Head>
            <main>
                <h1>Hello, world!</h1>
                <p>
                    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                </p>
            </main>
        </>
    )
}

_document.tsx

import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
    return (
        <Html lang="en">
            <Head />
            <body>
                <Main />
                <NextScript />
            </body>
        </Html>
    )
}

简化的导航栏代码(问题代码)

这是我们导入

Navbar
组件的地方:

_app.tsx

import type { AppProps } from 'next/app'
import NavBar from './NavBar';

export default function App({ Component, pageProps }: AppProps) {
    return (
        <>
            <NavBar />
            <Component {...pageProps} />
        </>
    )
}

最后是

NavBar
组件:

import Link from 'next/link';
import { useState, useEffect } from 'react';
import styled from 'styled-components';

const NavContainer = styled.nav`
    background-color: red;
    overflow: hidden;
    margin: 20px auto;
    width: 90%;

    @media screen and (max-width: 1024px) {
        width: 95%;
    }
`;

const CommonNavItem = styled.div<{ rightmost?: boolean }>`
    cursor: pointer;
    float: ${(props) => (props.rightmost ? 'right' : 'left')};
    display: block;
    color: inherit;
    text-align: center;
    padding: 14px 13px;
    text-decoration: none;
    font-size: 17px;
`;

// allows for argument to determine if align left or right
// inherit from Link component allows for linking to other pages
const NavItem = styled(CommonNavItem).attrs({ as: Link }) <{ rightmost?: boolean }>``;

const NavImage = styled.img`
    height: 16px;
    cursor: pointer;
`;

const DropDownContainer = styled.div`
    float: left;
    overflow: hidden;
`;

// the same as NavItem but no link
const DropDownLabel = styled(CommonNavItem)<{ rightmost?: boolean }>``;

const DropDownContent = styled.div`
    display: none;
    position: absolute;
    background-color: green;
    min-width: 160px;
    z-index: 1;

    ${DropDownContainer}:hover & {
        display: block;
    }

    ${NavItem} {
        float: none;
        padding: 12px 16px;
        text-align: left;
    }
`;

// since using conditionals in components we must ensure that the component is mounted before rendering
// either this or use dynamic from next/dynamic
function NavBar() {
    const [hasMounted, setHasMounted] = useState(false);

    useEffect(() => {
        setHasMounted(true);
    }, []);

    if (!hasMounted) {
        return null;
    }
    
    return (
        <NavContainer>
            <NavItem href='/'>Blog</NavItem>
            <NavItem href='/'>Link1</NavItem>
            <NavItem href='/'>Link2</NavItem>
            <NavItem href='/'>Link3</NavItem>
            <NavItem href='/'>Link4</NavItem>
            <DropDownContainer>
                <DropDownLabel>Drop Down Label</DropDownLabel>
                <DropDownContent>
                    <NavItem href='/some-link'>Drop Down Item 1</NavItem>
                    <NavItem href='/chemistry'>Drop Down Item 1</NavItem>
                </DropDownContent>
            </DropDownContainer>

            <NavItem rightmost href='https://www.linkedin.com/in/dereck/' target='_blank' title='LinkedIn'>
                <NavImage src='/next.svg' />
            </NavItem>
        </NavContainer>
    )
}

export default NavBar;

截图

这是我之前版本的一些截图;您可以在此处访问其实时版本;我希望导航栏的新 React 版本充当此实时版本:https://derecksnotes.com/

这里的导航栏看起来应该是全屏的。

当您将鼠标悬停在下拉菜单上时,它会覆盖标签。

当您缩小窗口时,我希望出现一个汉堡菜单并隐藏“Link1-4”链接和下拉菜单。这就是我对 React 缺乏经验的地方,我应该做这个 css 还是使用钩子和条件等;我对如何实现这个感到困惑。

此外,我在让任何汉堡菜单代码与我的代码中当前的组件层次结构一起工作时遇到了很多麻烦。我嵌套元素的方式对我来说很有意义,我见过其他示例,其中有更多的元素让这变得令人困惑。

想要的结果。下面是我当前的实时版本的屏幕截图。

这是我希望将鼠标悬停在下拉菜单上时的样子:

css reactjs styled-components
1个回答
0
投票

我回到这个并解决了它。这是我的代码:

import Link from 'next/link';
import { useState, useEffect } from 'react';
import styled from 'styled-components';
import { FaBars, FaFilter, FaUser } from 'react-icons/fa';
import { theme } from '@styles/theme';

import { useDispatch } from 'react-redux';
import { toggleTagsFilter } from '@store/tagsFilterVisibilitySlice';

import Auth from '../modals/auth/Auth';

const HamburgerIcon = styled.div`
    display: none;
    float: right;
    cursor: pointer;
    padding: 14px 13px;

    @media screen and (max-width: ${theme.container.widths.min_width_mobile}) {
        display: block;
    }
`;

const ResponsiveMenu = styled.div<{ open: boolean }>`
    display: ${props => (props.open ? 'block' : 'none')};

    @media screen and (max-width: ${theme.container.widths.min_width_mobile}) {
        display: ${props => (props.open ? 'block' : 'none')};
    }

    @media screen and (min-width: ${theme.container.widths.min_width_mobile}) {
        display: block;
    }
`;

const NavContainer = styled.nav`
    background-color: ${theme.container.background.colour.primary()};
    overflow: hidden;
    margin: 20px auto;
    width: 90%;
    color: ${theme.theme_colours[5]()};

    border: 1px solid #ccc;
    border-radius: 5px;
    box-shadow: 1px 1px 20px rgba(153, 153, 153, 0.5), 0 0 20px rgba(100, 100, 40, 0.2) inset;

    &:hover {
        box-shadow: 1px 1px 20px rgba(153, 153, 153, 0.5);
    }

    @media screen and (max-width: ${theme.container.widths.min_width_snap_up}) {
        width: 95%;
    }
`;

const CommonNavItem = styled.div`
    cursor: pointer;
    display: block;
    color: inherit;
    text-align: center;
    padding: 14px 13px;
    text-decoration: none;
    font-size: 17px;

    @media screen and (max-width: ${theme.container.widths.min_width_mobile}) {
        width: 100%;
        float: none;
        text-align: left;
    }
`;

// allows for argument to determine if align left or right
// inherit from Link component allows for linking to other pages
const NavLeftItem = styled(CommonNavItem).attrs({ as: Link }) <{ rightmost?: boolean }>`
    float: left;
    &:hover {
        color: ${theme.text.colour.white()};
        background-color: ${theme.theme_colours[5]()};
    }

    @media screen and (max-width: ${theme.container.widths.min_width_mobile}) {
        float: none;
    }
`;

const NavRightItemLink = styled(CommonNavItem).attrs({ as: Link }) <{ rightmost?: boolean }>`
    float: right;
`;

const NavRightItem = styled(CommonNavItem) <{ rightmost?: boolean }>`
    float: right;
    &:hover {
        color: ${theme.text.colour.white()};
        background-color: ${theme.theme_colours[5]()};
    }

    @media screen and (max-width: ${theme.container.widths.min_width_mobile}) {
        float: none;
    }
`;

const DropDownContainer = styled.div`
    float: left;
    overflow: hidden;

    @media screen and (max-width: ${theme.container.widths.min_width_mobile}) {
        width: 100%;
        float: none;
        text-align: left;
    }
`;

// the same as NavItem but no link
const DropDownLabel = styled(CommonNavItem) <{ rightmost?: boolean }>`
    &:hover {
        color: ${theme.text.colour.white()};
        background-color: ${theme.theme_colours[5]()};
    }
`;

const DropDownContent = styled.div`
    display: none;
    position: absolute;
    min-width: 160px;
    z-index: 1;
    border: 1px solid #ccc;
    box-shadow: 1px 1px 10px #ccc;
    background-color: ${theme.container.background.colour.primary()};

    ${DropDownContainer}:hover & {
        display: block;
    }

    ${NavLeftItem} {
        float: none;
        padding: 12px 16px;
        text-align: left;
    }

    /* TODO: still not working as intended */
    ${NavLeftItem}:first-child {
        border-top-left-radius: 0;
        border-top-right-radius: 0;
    }

    ${NavLeftItem}:last-child {
        border-bottom-left-radius: 5px;
        border-bottom-right-radius: 5px;
    }

    @media screen and (max-width: ${theme.container.widths.min_width_mobile}) {
        border: none;
        width: 100%;
        position: relative;
        float: none;
        text-align: left;
        box-shadow: none;

        ${NavLeftItem} {
            padding-left: 30px;
        }
    }
`;

const DateTimeDisplay = styled.div`
    cursor: pointer;
    float: right;
    display: block;
    color: inherit;
    text-align: center;
    padding: 14px 13px;
    text-decoration: none;
    font-size: 17px;
    @media screen and (max-width: ${theme.container.widths.min_width_mobile}) {
        width: 100%;
        position: relative;
        float: none;
        text-align: left;
    }
`;

// since using conditionals in components we must ensure that the component is mounted before rendering
// either this or use dynamic from next/dynamic
function NavBar() {
    const [hasMounted, setHasMounted] = useState(false);
    const [dateTime, setDateTime] = useState<string | null>(null);
    const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
    const [isMenuOpen, setIsMenuOpen] = useState(false);

    const closeMenu = () => {
        setIsMenuOpen(false);
    };    

    // redux control of tag filter
    // Error: `useDispatch` was conditionally called inside an `if` statement. Fixed: Moved `useDispatch` to the top-level, ensuring consistent hook order across renders.
    const dispatch = useDispatch();

    const handleToggleFilterClick = () => {
        dispatch(toggleTagsFilter());
    };

    useEffect(() => {
        setHasMounted(true);

        const updateDateTime = () => {
            const currentDate = new Date();
            const displayDate = currentDate.toLocaleDateString('en-US', {
                day: '2-digit',
                month: 'short'
            });
            const displayTime = currentDate.toLocaleTimeString('en-US', {
                hour12: false,
                hour: '2-digit',
                minute: '2-digit',
                second: '2-digit'
            });
            setDateTime(`${displayDate} ${displayTime}`);
        };

        updateDateTime();
        const interval = setInterval(updateDateTime, 1000);
        return () => {
            clearInterval(interval);
        };
    }, []);

    if (!hasMounted) {
        return null;
    }

    return (
        <NavContainer>
            <HamburgerIcon onClick={() => setIsMenuOpen(!isMenuOpen)}>
                <FaBars />
            </HamburgerIcon>

            <NavLeftItem onClick={closeMenu} href='/'>Blog</NavLeftItem>
            <ResponsiveMenu open={isMenuOpen}>
                <NavLeftItem onClick={closeMenu} href='/courses'>Courses</NavLeftItem>
                <NavLeftItem onClick={closeMenu} href='/references'>References</NavLeftItem>
                <DropDownContainer>
                    <DropDownLabel>Dictionaries</DropDownLabel>
                    <DropDownContent>
                        <NavLeftItem onClick={closeMenu} href='/dictionaries/biology'>Biology Dictionary</NavLeftItem>
                        <NavLeftItem onClick={closeMenu} href='/dictionaries/chemistry'>Chemistry Dictionary</NavLeftItem>
                    </DropDownContent>
                </DropDownContainer>
                {/* <NavRightItemLink href='https://www.linkedin.com/in/dereck/' target='_blank' title='LinkedIn'>
                <FaLinkedin />
            </NavRightItemLink> */}
                <DateTimeDisplay>{dateTime || "00 Jan 00:00:00"}</DateTimeDisplay>
                <NavRightItem onClick={handleToggleFilterClick}>
                    <FaFilter />
                </NavRightItem>
                <NavRightItem onClick={() => setIsAuthModalOpen(true)}>
                    <FaUser />
                </NavRightItem>
                {isAuthModalOpen && <Auth onClose={() => setIsAuthModalOpen(false)} />}
            </ResponsiveMenu>
        </NavContainer>
    )
}

export default NavBar;
© www.soinside.com 2019 - 2024. All rights reserved.