mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-08 00:44:29 +01:00
feat: refactor project structure
Refactored the project structure so that each page has its own tree of components and a global "Components" folder for the components that is used by more than one page. - Added an "assets" directory that exports all static images/icons/fonts/...etc
This commit is contained in:
104
src/Components/Navbar/NavMobile.tsx
Normal file
104
src/Components/Navbar/NavMobile.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { FiMenu } from 'react-icons/fi';
|
||||
import { GrClose } from 'react-icons/gr';
|
||||
import { BsSearch } from 'react-icons/bs'
|
||||
import { navLinks } from "./Navbar";
|
||||
import { AiFillThunderbolt } from 'react-icons/ai';
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../Button/Button";
|
||||
|
||||
const navBtnVariant = {
|
||||
menuHide: { rotate: 90, opacity: 0 },
|
||||
menuShow: { rotate: 0, opacity: 1 },
|
||||
closeHide: { rotate: -90, opacity: 0 },
|
||||
closeShow: { rotate: 0, opacity: 1 },
|
||||
}
|
||||
const navListVariants = {
|
||||
init: { x: 0 },
|
||||
show: { x: "-100%" },
|
||||
hide: { x: 0 }
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onSearch: (search: string) => void;
|
||||
}
|
||||
|
||||
export default function NavMobile({ onSearch }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchInput, setSearchInput] = useState("")
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(open => !open);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) document.body.style.overflowY = "hidden";
|
||||
else document.body.style.overflowY = "initial";
|
||||
}, [open]);
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (searchInput)
|
||||
onSearch(searchInput)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<nav className='block bg-white fixed top-0 left-0 w-full lg:hidden overflow-hidden z-[2010]'>
|
||||
<div className="p-16 px-32 w-screen flex justify-center items-center">
|
||||
<div className="w-40 h-40 bg-gray-100 rounded-8 mr-auto overflow-hidden">
|
||||
<img className="w-full h-full object-cover" src="https://www.figma.com/file/OFowr5RJk9YZCW35KT7D5K/image/07b85d84145942255afd215b3da26dbbf1dd03bd?fuid=772401335362859303" alt="" />
|
||||
</div>
|
||||
<Link to='/'><h2 className="text-h5 font-bold mx-auto">makers.bolt.fun</h2></Link>
|
||||
<button className='rounded-full ml-auto text-2xl w-[50px] h-[50px] hover:bg-gray-200' onClick={handleClick}>
|
||||
|
||||
{!open ? (<motion.div key={open ? 1 : 0} variants={navBtnVariant} initial='menuHide' animate='menuShow'><FiMenu /></motion.div>)
|
||||
: (<motion.div key={open ? 1 : 0} variants={navBtnVariant} initial='closeHide' animate='closeShow'><GrClose /></motion.div>)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="fixed overflow-hidden left-0 pointer-events-none z-[2010] w-full min-h-[calc(100vh-76px)]">
|
||||
{open && <div onClick={handleClick} className='pointer-events-auto absolute left-0 w-full min-h-full bg-gray-400 opacity-20'>
|
||||
|
||||
</div>}
|
||||
<motion.div
|
||||
className="pointer-events-auto bg-white w-full sm:max-w-[400px] min-h-full absolute left-full border shadow-2xl pt-32 sm:p-32 flex flex-col"
|
||||
variants={navListVariants}
|
||||
animate={open ? "show" : "hide"}
|
||||
>
|
||||
<div className="px-16">
|
||||
<form className='relative' onSubmit={handleSubmit}>
|
||||
<BsSearch className='absolute top-1/2 left-20 transform -translate-x-1/2 -translate-y-1/2 text-gray-500' />
|
||||
<input
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
className="bg-gray-100 text-gray-600 focus:outline-primary w-full py-12 px-20 pl-40 rounded-24 placeholder-gray-500" placeholder="Search" />
|
||||
|
||||
{/* <input className="btn bg-gray-100 w-full rounded-24 mt-16 placeholder-gray-500" placeholder="Search" /> */}
|
||||
</form>
|
||||
<Button color='primary' fullWidth className="py-12 px-40 rounded-24 mt-40">Submit App️</Button>
|
||||
<Button color='gray' fullWidth className="py-12 px-40 rounded-24 my-16"> <AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet </Button>
|
||||
|
||||
</div>
|
||||
<ul className="py-16 gap-64 border-t">
|
||||
{navLinks.map((link, idx) => <li key={idx} className="text-body3 p-16 hover:bg-gray-200">
|
||||
<Link to={link.url}><link.icon className={`text-body2 inline-block mr-12 ${link.color}`} /> {link.text} </Link></li>
|
||||
)}
|
||||
</ul>
|
||||
<ul className="px-16 py-16 pb-32 flex flex-wrap gap-y-12 border-t mt-auto">
|
||||
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2"><a href="/">About Us</a></li>
|
||||
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2"><a href="/">Support</a></li>
|
||||
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2"><a href="/">Press</a></li>
|
||||
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2"><a href="/">Contacts</a></li>
|
||||
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2"><a href="/">Careers</a></li>
|
||||
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2"><a href="/">Sitemap</a></li>
|
||||
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2"><a href="/">Legal</a></li>
|
||||
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2"><a href="/">Cookies Settings</a></li>
|
||||
</ul>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
14
src/Components/Navbar/Navbar.stories.tsx
Normal file
14
src/Components/Navbar/Navbar.stories.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import Navbar from './Navbar';
|
||||
|
||||
export default {
|
||||
title: 'Shared/Navbar',
|
||||
component: Navbar,
|
||||
|
||||
} as ComponentMeta<typeof Navbar>;
|
||||
|
||||
const Template: ComponentStory<typeof Navbar> = (args) => <Navbar />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
123
src/Components/Navbar/Navbar.tsx
Normal file
123
src/Components/Navbar/Navbar.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import NavMobile from "./NavMobile";
|
||||
import { FaHome } from 'react-icons/fa';
|
||||
import { MdLocalFireDepartment } from 'react-icons/md';
|
||||
import { IoExtensionPuzzle } from 'react-icons/io5';
|
||||
import { AiFillThunderbolt } from 'react-icons/ai';
|
||||
import { BsSearch } from "react-icons/bs";
|
||||
import { FormEvent, useRef, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { GrClose } from 'react-icons/gr';
|
||||
import { useAppDispatch, useAppSelector } from "src/utils/hooks";
|
||||
import { ModalId, openModal } from "src/redux/features/modals.slice";
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../Button/Button";
|
||||
import { setNavHeight } from "src/redux/features/theme.slice";
|
||||
import { useResizeListener } from 'src/utils/hooks'
|
||||
|
||||
export const navLinks = [
|
||||
{ text: "Explore", url: "/", icon: FaHome, color: 'text-primary-600' },
|
||||
{ text: "Hottest", url: "/categories/hottest", icon: MdLocalFireDepartment, color: 'text-primary-600' },
|
||||
{ text: "Categories", url: "/categories", icon: IoExtensionPuzzle, color: 'text-primary-600' },
|
||||
|
||||
]
|
||||
|
||||
export default function Navbar() {
|
||||
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { isWalletConnected, webln } = useAppSelector(state => ({
|
||||
isWalletConnected: state.wallet.isConnected,
|
||||
webln: state.wallet.provider,
|
||||
}));
|
||||
|
||||
const toggleSearch = () => {
|
||||
if (!searchOpen) {
|
||||
console.log(inputRef.current);
|
||||
inputRef.current?.focus();
|
||||
|
||||
}
|
||||
setSearchOpen(!searchOpen);
|
||||
}
|
||||
|
||||
const onSearch = (search: string) => {
|
||||
// Make Search Request
|
||||
alert(`Your Searched for: ${search}`)
|
||||
}
|
||||
|
||||
const onConnectWallet = () => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalId.Login_ScanWallet
|
||||
}));
|
||||
}
|
||||
|
||||
const onWithdraw = () => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalId.Claim_FundWithdraw
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
// Make Search Request
|
||||
onSearch(searchInput)
|
||||
}
|
||||
|
||||
|
||||
useResizeListener(function calcNavHeight() {
|
||||
const navs = document.querySelectorAll('nav');
|
||||
navs.forEach(nav => {
|
||||
const navStyles = getComputedStyle(nav);
|
||||
if (navStyles.display !== 'none') {
|
||||
dispatch(setNavHeight(nav.clientHeight))
|
||||
document.body.style.paddingTop = `${nav.clientHeight}px`
|
||||
}
|
||||
});
|
||||
}, [])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile Nav */}
|
||||
<NavMobile onSearch={onSearch} />
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<nav className="hidden bg-white w-full lg:flex fixed top-0 left-0 py-36 px-32 items-center z-[2010]">
|
||||
<Link to='/'><h2 className="text-h5 font-bold mr-40 lg:mr-64">makers.bolt.fun</h2></Link>
|
||||
<ul className="flex gap-32 xl:gap-64">
|
||||
{navLinks.map((link, idx) => <li key={idx} className="text-body4 hover:text-primary-600">
|
||||
<Link to={link.url}><link.icon className={`text-body2 align-middle inline-block mr-8 ${link.color}`} /> {link.text}</Link></li>
|
||||
)}
|
||||
|
||||
</ul>
|
||||
<div className="ml-auto flex">
|
||||
<motion.div
|
||||
animate={searchOpen ? { opacity: 0 } : { opacity: 1 }}
|
||||
className="flex">
|
||||
<Button color='primary' size='md' className="lg:px-40">Submit App️</Button>
|
||||
{isWalletConnected ?
|
||||
<Button className="ml-16 py-12 px-16 lg:px-20">Connected <AiFillThunderbolt className='inline-block text-thunder transform scale-125' /></Button>
|
||||
: <Button className="ml-16 py-12 px-16 lg:px-20" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet </Button>
|
||||
}
|
||||
</motion.div>
|
||||
<form onBlur={toggleSearch} className='relative flex items-center' onSubmit={handleSubmit}>
|
||||
{searchOpen ? <GrClose onClick={toggleSearch} className='text-gray-500 w-24 h-24 mx-12 z-20 hover:cursor-pointer' /> : <BsSearch onClick={toggleSearch} className='text-gray-500 w-24 h-24 mx-12 z-20 hover:cursor-pointer' />}
|
||||
{searchOpen && <motion.input
|
||||
ref={inputRef}
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
initial={{ scaleX: .3, opacity: 0, originX: 'right' }}
|
||||
animate={searchOpen ? { scaleX: 1, opacity: 1, originX: 'right' } : { scaleX: .3, opacity: 0, originX: 'right' }}
|
||||
onAnimationComplete={() => {
|
||||
if (searchOpen) inputRef.current?.focus()
|
||||
}}
|
||||
className="absolute top-0 right-0 z-10 bg-gray-100 text-gray-600 focus:outline-primary w-[300px] py-12 px-20 pr-40 rounded-24 placeholder-gray-500" placeholder="Search" />
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user