mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-02-23 15:34:21 +01:00
feat: make project modal full page on mobile
- created a new store slice (theme) to hold properties needed to style the app - made nav fixed - refactored resizing throttled funcs to a custom hook - changed the modal to full size page on mobile for certain modal types ( currently just Project )
This commit is contained in:
16
src/App.tsx
16
src/App.tsx
@@ -1,20 +1,21 @@
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import Navbar from "./Components/Shared/Navbar/Navbar";
|
||||
import ExplorePage from "./Components/ExplorePage/ExplorePage";
|
||||
import ModalsContainer from "./Components/Shared/ModalsContainer/ModalsContainer";
|
||||
import { useAppDispatch, useAppSelector } from './utils/hooks';
|
||||
import { useAppDispatch, useAppSelector, useResizeListener } from './utils/hooks';
|
||||
import { connectWallet } from './redux/features/wallet.slice';
|
||||
import { setIsMobileScreen } from "./redux/features/theme.slice";
|
||||
|
||||
function App() {
|
||||
const { isWalletConnected, webln } = useAppSelector(state => ({
|
||||
isWalletConnected: state.wallet.isConnected,
|
||||
webln: state.wallet.provider,
|
||||
isWalletConnected: state.wallet.isConnected,
|
||||
webln: state.wallet.provider,
|
||||
}));
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if(typeof window.webln != "undefined") {
|
||||
if (typeof window.webln != "undefined") {
|
||||
window.webln.enable().then((res: any) => {
|
||||
dispatch(connectWallet(window.webln));
|
||||
console.log("called:webln.enable()", res);
|
||||
@@ -24,6 +25,11 @@ function App() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useResizeListener(() => {
|
||||
dispatch(setIsMobileScreen(document.body.clientWidth < 768));
|
||||
}, [dispatch])
|
||||
|
||||
|
||||
return <div id="app" className='w-screen overflow-hidden'>
|
||||
<Navbar />
|
||||
<ExplorePage />
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ReactElement, useEffect, useRef, useState } from "react";
|
||||
import { ReactElement, useCallback, useRef, useState } from "react";
|
||||
import { ProjectCard } from "../../../utils/interfaces";
|
||||
import Carousel from 'react-multi-carousel';
|
||||
import { MdDoubleArrow, } from 'react-icons/md';
|
||||
import { useAppDispatch } from "../../../utils/hooks";
|
||||
import { ModalId, openModal } from "../../../redux/features/modals.slice";
|
||||
import _throttle from 'lodash.throttle'
|
||||
import ProjectCardMini from "../ProjectCardMini/ProjectCardMini";
|
||||
import { useResizeListener } from 'src/utils/hooks'
|
||||
|
||||
const responsive = {
|
||||
all: {
|
||||
@@ -38,16 +38,10 @@ export default function ProjectsRow({ title, categoryId, projects }: Props) {
|
||||
dispatch(openModal({ modalId: ModalId.Project, propsToPass: { projectId } }))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const listener = _throttle(() => {
|
||||
setCarouselItmsCnt(calcNumItems());
|
||||
}, 250);
|
||||
useResizeListener(() => {
|
||||
setCarouselItmsCnt(calcNumItems());
|
||||
}, [setCarouselItmsCnt])
|
||||
|
||||
window.addEventListener('resize', listener)
|
||||
return () => {
|
||||
window.removeEventListener('resize', listener)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='mb-48'>
|
||||
|
||||
@@ -27,10 +27,11 @@ export default function ProjectCard({ onClose, direction, ...props }: ModalCard)
|
||||
}
|
||||
);
|
||||
|
||||
const { isWalletConnected, webln, project } = useAppSelector(state => ({
|
||||
const { isWalletConnected, webln, project, isMobileScreen } = useAppSelector(state => ({
|
||||
isWalletConnected: state.wallet.isConnected,
|
||||
webln: state.wallet.provider,
|
||||
project: state.project.project,
|
||||
isMobileScreen: state.theme.isMobileScreen
|
||||
}));
|
||||
|
||||
|
||||
@@ -87,7 +88,7 @@ export default function ProjectCard({ onClose, direction, ...props }: ModalCard)
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[710px]"
|
||||
className={`modal-card max-w-[768px] ${props.isPageModal && isMobileScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
|
||||
>
|
||||
<div className="relative h-[80px] lg:h-[152px]">
|
||||
|
||||
@@ -16,15 +16,15 @@ export default function Modal({ onClose, children, ...props }: Props) {
|
||||
initial={false}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className='fixed w-screen h-screen items-center overflow-x-hidden no-scrollbar'
|
||||
className='fixed w-full h-full items-center overflow-x-hidden no-scrollbar'
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className='w-screen min-h-screen relative flex flex-col justify-center items-center py-32 px-16 md:py-64 overflow-x-hidden no-scrollbar'
|
||||
className='w-screen min-h-screen relative flex flex-col justify-center items-center md:py-64 md:px-16 overflow-x-hidden no-scrollbar'
|
||||
>
|
||||
|
||||
<div
|
||||
className="absolute w-full h-full top-0 left-0 bg-gray-300 bg-opacity-50 "
|
||||
className={`absolute w-full h-full top-0 left-0 bg-gray-300 bg-opacity-50 ${props.isPageModal && "hidden md:block"}`}
|
||||
onClick={onClose}
|
||||
></div>
|
||||
{children}
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function ModalsContainer() {
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{isOpen &&
|
||||
<motion.div
|
||||
className="w-screen fixed inset-0 overflow-x-hidden"
|
||||
className="w-screen fixed inset-0 overflow-x-hidden z-[2020]"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{
|
||||
@@ -113,8 +113,8 @@ export default function ModalsContainer() {
|
||||
{openModals.map(modal => {
|
||||
const Child = ModalsMap(modal.modalId);
|
||||
return (
|
||||
<Modal key={modal.modalId} onClose={onClose} direction={direction}>
|
||||
<Child onClose={onClose} direction={direction} {...modal.propsToPass} />
|
||||
<Modal key={modal.modalId} onClose={onClose} direction={direction} isPageModal={modal.isPageModal}>
|
||||
<Child onClose={onClose} direction={direction} isPageModal={modal.isPageModal} {...modal.propsToPass} />
|
||||
</Modal>)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function NavMobile({ onSearch }: Props) {
|
||||
|
||||
|
||||
return (
|
||||
<nav className='block lg:hidden overflow-hidden z-[2010]'>
|
||||
<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="" />
|
||||
|
||||
@@ -4,13 +4,16 @@ 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 { FormEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { GrClose } from 'react-icons/gr';
|
||||
import { useAppDispatch, useAppSelector } from "../../../utils/hooks";
|
||||
import { ModalId, openModal } from "../../../redux/features/modals.slice";
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../Button/Button";
|
||||
import { setNavHeight } from "src/redux/features/theme.slice";
|
||||
import _throttle from 'lodash.throttle'
|
||||
import { useResizeListener } from 'src/utils/hooks'
|
||||
|
||||
export const navLinks = [
|
||||
{ text: "Explore", url: "/", icon: FaHome, color: 'text-primary-600' },
|
||||
@@ -63,12 +66,26 @@ export default function Navbar() {
|
||||
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 lg:flex py-36 px-32 items-center">
|
||||
{/* 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">
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
@apply rounded-[40px] bg-gray-50 overflow-hidden w-full max-w-[600px] shadow-2xl z-10;
|
||||
@apply rounded-[40px] bg-gray-50 overflow-hidden w-full shadow-2xl z-10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ svg {
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
|
||||
@@ -23,6 +23,7 @@ export enum ModalId {
|
||||
|
||||
interface OpenModal {
|
||||
modalId: ModalId;
|
||||
isPageModal?: boolean;
|
||||
propsToPass?: any;
|
||||
}
|
||||
|
||||
@@ -33,6 +34,7 @@ interface StoreState {
|
||||
flows: ModalId[];
|
||||
toOpenLater: OpenModal | null;
|
||||
openModals: OpenModal[];
|
||||
isMobileScreen?: boolean;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
@@ -75,9 +77,11 @@ export const modalSlice = createSlice({
|
||||
) {
|
||||
state.direction = Direction.START;
|
||||
state.isOpen = true;
|
||||
const isPageModal = action.payload.modalId === ModalId.Project;
|
||||
state.openModals.push({
|
||||
modalId: action.payload.modalId,
|
||||
propsToPass: action.payload.propsToPass,
|
||||
isPageModal,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -91,9 +95,11 @@ export const modalSlice = createSlice({
|
||||
) {
|
||||
state.direction = action.payload.direction;
|
||||
state.openModals.pop();
|
||||
const isPageModal = action.payload.modalId === ModalId.Project;
|
||||
state.openModals.push({
|
||||
modalId: action.payload.modalId,
|
||||
propsToPass: action.payload.propsToPass || {},
|
||||
isPageModal,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Project } from "../../utils/interfaces";
|
||||
|
||||
import mockData from "../../api/mockData.json";
|
||||
|
||||
interface StoreState {
|
||||
project: Project | null;
|
||||
projectSet: boolean;
|
||||
|
||||
28
src/redux/features/theme.slice.ts
Normal file
28
src/redux/features/theme.slice.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
interface StoreState {
|
||||
navHeight: number;
|
||||
isMobileScreen: boolean;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
navHeight: 88,
|
||||
isMobileScreen: false,
|
||||
} as StoreState;
|
||||
|
||||
export const themeSlice = createSlice({
|
||||
name: "theme",
|
||||
initialState,
|
||||
reducers: {
|
||||
setNavHeight(state, action: PayloadAction<number>) {
|
||||
state.navHeight = action.payload;
|
||||
},
|
||||
setIsMobileScreen(state, action: PayloadAction<boolean>) {
|
||||
state.isMobileScreen = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setNavHeight, setIsMobileScreen } = themeSlice.actions;
|
||||
|
||||
export default themeSlice.reducer;
|
||||
@@ -2,12 +2,14 @@ import { configureStore } from "@reduxjs/toolkit";
|
||||
import modalsSlice from "./features/modals.slice";
|
||||
import projectSlice from "./features/project.slice";
|
||||
import walletSlice from "./features/wallet.slice";
|
||||
import themeSlice from "./features/theme.slice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
modals: modalsSlice,
|
||||
project: projectSlice,
|
||||
wallet: walletSlice,
|
||||
theme: themeSlice,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
2
src/utils/hooks/index.ts
Normal file
2
src/utils/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./storeHooks";
|
||||
export * from "./useResizeListener";
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch, RootState } from "../redux/store";
|
||||
import { AppDispatch, RootState } from "../../redux/store";
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
20
src/utils/hooks/useResizeListener.ts
Normal file
20
src/utils/hooks/useResizeListener.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import _throttle from "lodash.throttle";
|
||||
|
||||
export const useResizeListener = (
|
||||
listener: () => void,
|
||||
depsArray: any[] = [],
|
||||
options: { throttleValue?: number } = {}
|
||||
) => {
|
||||
options.throttleValue = options.throttleValue ?? 250;
|
||||
const cb = useCallback(listener, depsArray);
|
||||
useEffect(() => {
|
||||
const func = _throttle(cb, 250);
|
||||
func();
|
||||
|
||||
window.addEventListener("resize", func);
|
||||
return () => {
|
||||
window.removeEventListener("resize", func);
|
||||
};
|
||||
}, [cb]);
|
||||
};
|
||||
Reference in New Issue
Block a user