diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..c96dc20 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,33 @@ + + + logo + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx new file mode 100644 index 0000000..6771c80 --- /dev/null +++ b/src/components/Header/index.tsx @@ -0,0 +1,148 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { Box, AppBar, Stack, Link, Button } from '@mui/material'; + +import Search from './search'; + +const NAV_LINK = [ + { + title: '行业百科', + href: '/v3/wiki', + }, + { + title: '技术博客', + href: '/v3/blog', + }, + { + title: '在线工具', + href: '/tools', + target: '_blank', + }, + { + title: '漏洞情报', + href: '/v3/vuldb', + }, +]; + +const Header = () => { + const [user, setUser] = useState(null); + + useEffect(() => { + fetch('/api/v1/user/profile', { + credentials: 'include', + }).then((res) => { + res.json().then((data) => { + setUser(data); + }); + }); + }, []); + + return ( + + + + { + window.open('/', '_self'); + }} + sx={{ ml: 5, mr: 10, cursor: 'pointer' }} + /> + + + {NAV_LINK.map((item) => ( + { + e.preventDefault(); + window.open(item.href, '_self'); + }} + sx={{ + fontSize: '16px', + color: '#041B0F', + fontWeight: 700, + '&:hover': { + color: 'primary.main', + }, + }} + > + {item.title} + + ))} + + + + + + {user ? ( + + + + ) : ( + <> + + + + )} + + + + ); +}; + +export default Header; diff --git a/src/components/Header/search.tsx b/src/components/Header/search.tsx new file mode 100644 index 0000000..4231bfd --- /dev/null +++ b/src/components/Header/search.tsx @@ -0,0 +1,325 @@ +'use client'; + +import React, { useState } from 'react'; +import { + Box, + InputBase, + Button, + createTheme, + ThemeProvider, + Dialog, + styled, + InputBaseProps, + BoxProps, + ButtonProps, + SvgIconProps, + Stack, + List, + ListItem, + ListItemButton, + ListItemText, + Typography, + IconButton, + Popover, +} from '@mui/material'; +import SearchRoundedIcon from '@mui/icons-material/SearchRounded'; +import ClearRoundedIcon from '@mui/icons-material/ClearRounded'; +import { useLocalStorageState } from 'ahooks'; +import { useRouter } from 'next/navigation'; + +const innerTheme = createTheme({ + palette: { + primary: { + main: '#1A191C', + }, + }, +}); + +const SearchBaseInput = styled(InputBase)(({ theme }) => ({ + flex: 1, + padding: '0 30px', + height: '40px', + fontSize: 20, + border: '1px solid', + borderColor: 'transparent', + transition: 'all 0.3s', + borderTopLeftRadius: '4px', + borderBottomLeftRadius: '4px', + borderRight: 'none', + '&:hover': { + borderColor: theme.palette.primary.main, + }, + '&:focus-within': { + borderColor: theme.palette.primary.main, + }, +})); + +const SearchBaseWrapper = styled(Box)(({ theme }) => { + return { + display: 'flex', + alignItems: 'center', + width: 360, + height: 40, + borderRadius: '4px', + transition: 'all 0.3s', + boxShadow: '0px 0px 20px 0px rgba(0,28,85,0.1)', + backgroundColor: '#fff', + '&:hover': { + boxShadow: '0px 10px 40px 0px rgba(0,28,85,0.15)', + }, + }; +}); + +const SearchBaseButton = styled(Button)({ + width: 82, + height: 40, + borderRadius: '0px 4px 4px 0px', +}); + +export const SearchBase = React.forwardRef( + ( + props: { + type?: 'icon' | 'button' | 'icon-button'; + open?: boolean; + setOpen?(open: boolean): void; + SearchBaseWrapperProps?: BoxProps; + SearchBaseInputProps?: InputBaseProps; + SearchBaseButtonProps?: ButtonProps; + SearchRoundedIconProps?: SvgIconProps; + keywords?: string; + }, + ref + ) => { + const { + type = 'button', + setOpen, + SearchBaseWrapperProps, + SearchBaseInputProps, + SearchBaseButtonProps, + SearchRoundedIconProps, + keywords = '', + } = props; + return type === 'icon-button' ? ( + { + setOpen?.(true); + }} + /> + ) : ( + { + setOpen?.(true); + }} + {...SearchBaseWrapperProps} + > + } + {...SearchBaseInputProps} + /> + {type === 'button' && ( + + + 搜索 + + + )} + + ); + } +); +SearchBase.displayName = 'SearchBase'; + +export const DialogSearch = (props: { open: boolean; onClose(): void }) => { + const { open, onClose } = props; + const [keywords, setKeywords] = useState(''); + const router = useRouter(); + const [recentSearch, setRecentSearch] = useLocalStorageState( + 'recent-search', + { + defaultValue: [], + } + ); + + const onSearch = () => { + if (!keywords) { + return; + } + onClose(); + if (recentSearch) { + const index = recentSearch.indexOf(keywords); + if (index > -1) { + recentSearch.splice(index, 1); + recentSearch.unshift(keywords); + } else { + if (recentSearch.length >= 10) { + recentSearch.pop(); + } + recentSearch.unshift(keywords); + } + setRecentSearch([...recentSearch]); + } else { + setRecentSearch([keywords]); + } + router.push(`/v3/s?keywords=${keywords}`); + }; + + return ( + + { + setKeywords(e.target.value.trim()); + }, + onKeyDown: (e) => { + if (e.key === 'Enter') { + onSearch(); + } + }, + sx: { + height: 56, + borderTopLeftRadius: '4px', + borderBottomLeftRadius: '4px', + borderRight: 'none !important', + }, + }} + SearchBaseButtonProps={{ + sx: { + width: 102, + height: 56, + fontSize: 18, + }, + onClick: () => { + onSearch(); + }, + }} + /> + + {!!recentSearch?.length && ( + + + + 最近搜索 + + + + {recentSearch?.map((k, index) => ( + { + e.stopPropagation(); + recentSearch.splice(index, 1); + setRecentSearch([...recentSearch]); + }} + > + + + } + onClick={() => { + onClose(); + router.push(`/v3/s?keywords=${k}`); + }} + > + + + + + ))} + + + )} + + ); +}; + +export const PopoverContent = React.forwardRef(() => { + return ( + + The content of the Popover. + + ); +}); +PopoverContent.displayName = 'PopoverContent'; + +const Search = React.forwardRef( + ( + props: { + type?: 'icon' | 'button' | 'icon-button'; + SearchBaseWrapperProps?: BoxProps; + SearchBaseInputProps?: InputBaseProps; + SearchBaseButtonProps?: ButtonProps; + SearchRoundedIconProps?: SvgIconProps; + }, + ref + ) => { + const [open, setOpen] = useState(false); + const { + type = 'button', + SearchBaseWrapperProps, + SearchBaseInputProps, + SearchBaseButtonProps, + SearchRoundedIconProps, + } = props; + const onClose = () => { + setOpen(false); + }; + return ( + <> + + {/* */} + + + ); + } +); + +Search.displayName = 'Search'; +export default Search; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index b6b51a1..e6a0cff 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -20,7 +20,7 @@ import { usePath } from '@/hooks'; import { useMemo } from 'react'; import { allTools } from '@/utils/tools'; import Head from 'next/head'; -import { Header as RiverHeader } from '@chaitin_rivers/multi_river'; +import RiverHeader from '../components/Header'; import '@chaitin_rivers/excalidraw/index.css'; const clientSideEmotionCache = createEmotionCache(); @@ -64,8 +64,15 @@ export default function App({ - {isProduction ? : null} - + {isProduction ? : null} +