mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-06 16:04:23 +01:00
improved Modals Container store structure
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
|
||||
import Navbar from "./Components/Shared/Navbar/Navbar";
|
||||
import ExplorePage from "./Components/ExplorePage/ExplorePage";
|
||||
import Modal from "./Components/Shared/Modal/Modal";
|
||||
import ModalsContainer from "./Components/Shared/ModalsContainer/ModalsContainer";
|
||||
|
||||
function App() {
|
||||
return <div id="app" className='w-screen overflow-hidden'>
|
||||
<Navbar />
|
||||
<ExplorePage />
|
||||
<Modal />
|
||||
<ModalsContainer />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { ModalCard, modalCardVariants } from "../Shared/Modal/Modal";
|
||||
import { ModalCard, modalCardVariants } from "../Shared/ModalsContainer/ModalsContainer";
|
||||
|
||||
interface Props extends ModalCard {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ModalCard, modalCardVariants } from '../Shared/Modal/Modal'
|
||||
import { ModalCard, modalCardVariants } from '../Shared/ModalsContainer/ModalsContainer'
|
||||
|
||||
export default function LoginCard_1({ onClose, direction, ...props }: ModalCard) {
|
||||
return (
|
||||
|
||||
@@ -2,11 +2,11 @@ import { motion } from 'framer-motion'
|
||||
import { BiArrowBack } from 'react-icons/bi'
|
||||
import { BsJoystick } from 'react-icons/bs'
|
||||
import { MdLocalFireDepartment } from 'react-icons/md';
|
||||
import { ModalCard, modalCardVariants } from '../Shared/Modal/Modal';
|
||||
import { ModalCard, modalCardVariants } from '../Shared/ModalsContainer/ModalsContainer';
|
||||
import { useQuery } from 'react-query';
|
||||
import { getProjectById } from '../../api';
|
||||
import { useAppDispatch } from '../../utils/hooks';
|
||||
import { Direction, ModalId, openModal, setDirection } from '../../redux/features/modals.slice';
|
||||
import { ModalId, openModal } from '../../redux/features/modals.slice';
|
||||
|
||||
|
||||
export default function ProjectCard({ onClose, direction, ...props }: ModalCard) {
|
||||
@@ -19,9 +19,16 @@ export default function ProjectCard({ onClose, direction, ...props }: ModalCard)
|
||||
if (isLoading || !project) return <></>;
|
||||
|
||||
|
||||
const onVote = () => {
|
||||
|
||||
dispatch(openModal({ modalId: ModalId.Vote, initialModalProps: { projectId: props.projectId } }))
|
||||
}
|
||||
|
||||
const onClaim = () => {
|
||||
dispatch(setDirection(Direction.NEXT));
|
||||
dispatch(openModal({ modalId: ModalId.Login1, initialModalProps: { projectId: props.projectId } }))
|
||||
dispatch(openModal({
|
||||
modalId: ModalId.Login1,
|
||||
initialModalProps: { projectId: props.projectId },
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -31,7 +38,7 @@ export default function ProjectCard({ onClose, direction, ...props }: ModalCard)
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card"
|
||||
className="modal-card max-w-[710px]"
|
||||
|
||||
>
|
||||
<div className="relative h-[152px]">
|
||||
@@ -47,9 +54,9 @@ export default function ProjectCard({ onClose, direction, ...props }: ModalCard)
|
||||
<h3 className="text-h3 font-regular">{project.title}</h3>
|
||||
<a className="text-blue-400 font-regular text-body4" target='_blank' rel="noreferrer" href={project.website}>{project.website}</a>
|
||||
</div>
|
||||
<div className="flex ml-auto gap-16">
|
||||
<div className="flex-shrink-0 hidden md:flex ml-auto gap-16">
|
||||
<button className="btn btn-primary py-12 px-24 rounded-lg my-16">Play <BsJoystick /></button>
|
||||
<button className="btn bg-yellow-100 hover:bg-yellow-200 py-12 px-24 rounded-lg my-16">Vote <MdLocalFireDepartment className='text-fire' /></button>
|
||||
<button onClick={onVote} className="btn bg-yellow-100 hover:bg-yellow-50 py-12 px-24 rounded-lg my-16">Vote <MdLocalFireDepartment className='text-fire' /></button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-40 text-body4 leading-normal">{project.description}</p>
|
||||
@@ -57,20 +64,23 @@ export default function ProjectCard({ onClose, direction, ...props }: ModalCard)
|
||||
<span className="chip-small bg-red-100 text-red-800 font-regular"> payments </span>
|
||||
<span className="chip-small bg-primary-100 text-primary-800 font-regular"> lightining </span>
|
||||
</div>
|
||||
|
||||
<div className="md:hidden">
|
||||
<button className="btn btn-primary w-full py-12 px-24 rounded-lg mt-24 mb-16">Play <BsJoystick /></button>
|
||||
<button onClick={onVote} className="btn w-full bg-yellow-100 hover:bg-yellow-50 py-12 px-24 rounded-lg mb-24">Vote <MdLocalFireDepartment className='text-fire' /></button>
|
||||
</div>
|
||||
<div className="mt-40">
|
||||
<h3 className="text-h5 font-bold">Screen Shots</h3>
|
||||
<h3 className="text-h5 font-bold">Screenshots</h3>
|
||||
<div className="grid grid-cols-1 justify-items-center md:grid-cols-2 gap-x-24 gap-y-20">
|
||||
<div className="w-full max-w-[260px] self-center h-[130px] bg-gray-300 rounded-xl"></div>
|
||||
<div className="w-full max-w-[260px] self-center h-[130px] bg-gray-300 rounded-xl"></div>
|
||||
<div className="w-full max-w-[260px] self-center h-[130px] bg-gray-300 rounded-xl"></div>
|
||||
<div className="w-full max-w-[260px] self-center h-[130px] bg-gray-300 rounded-xl"></div>
|
||||
<div className="w-full max-w-[310px] self-center h-[130px] bg-gray-300 rounded-xl"></div>
|
||||
<div className="w-full max-w-[310px] self-center h-[130px] bg-gray-300 rounded-xl"></div>
|
||||
<div className="w-full max-w-[310px] self-center h-[130px] bg-gray-300 rounded-xl"></div>
|
||||
<div className="w-full max-w-[310px] self-center h-[130px] bg-gray-300 rounded-xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-40" />
|
||||
<div className="text-center">
|
||||
<h3 className="text-body4 font-regular">Are you the creator of this project?</h3>
|
||||
<button className="btn py-12 px-24 rounded-lg my-16" onClick={onClaim}>Claim 🖐</button>
|
||||
<button className="btn btn-gray py-12 px-24 rounded-lg my-16" onClick={onClaim}>Claim 🖐</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ReactElement, useEffect, useRef } from "react";
|
||||
import { Direction } from "../../../redux/features/modals.slice";
|
||||
import { Portal } from "../Portal/Portal";
|
||||
|
||||
export interface ModalCard {
|
||||
onClose?: () => void;
|
||||
direction: number;
|
||||
modalCard: ReactElement
|
||||
}
|
||||
|
||||
export const modalCardVariants = {
|
||||
initial: (direction: any) => {
|
||||
if (direction === Direction.START) return { opacity: 0, y: 300 };
|
||||
else if (direction === Direction.NEXT) return { opacity: 0, x: 300 };
|
||||
else if (direction === Direction.PREVIOUS) return { opacity: 0, x: -300 };
|
||||
return {}
|
||||
},
|
||||
animate: {
|
||||
x: 0, y: 0, opacity: 1, transition: { type: "spring" }
|
||||
},
|
||||
exit: (direction: Direction) => {
|
||||
const transition = { ease: "easeIn" }
|
||||
if (direction === Direction.EXIT) return { transition, opacity: 0, y: 300 };
|
||||
else if (direction === Direction.NEXT) return { transition, opacity: 0, x: -300 };
|
||||
else if (direction === Direction.PREVIOUS) return { transition, opacity: 0, x: 300 };
|
||||
return {}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
interface Props {
|
||||
isOpen?: boolean;
|
||||
onClose?: () => void,
|
||||
maxWidth?: string;
|
||||
children: ReactElement
|
||||
}
|
||||
|
||||
export default function Modal({ children, isOpen = true, onClose = () => { }, maxWidth = "max-w-[600px]" }: Props) {
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const handleOutsideClick = (e: any) => {
|
||||
if (e.target === ref.current) onClose();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) document.body.style.overflowY = "hidden";
|
||||
else document.body.style.overflowY = "initial";
|
||||
}, [isOpen])
|
||||
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{isOpen && <motion.div
|
||||
ref={ref}
|
||||
onClick={handleOutsideClick}
|
||||
className={`fixed overscroll-y-none inset-0 bg-gray-300 bg-opacity-70 overflow-y-auto h-full w-full`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { ease: "easeInOut" },
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`w-full ${maxWidth} mx-auto py-64`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>}
|
||||
</AnimatePresence>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
@@ -1,100 +1,20 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { closeModal, Direction, ModalId } from "../../../redux/features/modals.slice";
|
||||
import { useAppDispatch, useAppSelector } from "../../../utils/hooks";
|
||||
import LoginCard_1 from "../../Login/LoginCard-1";
|
||||
import ProjectCard from "../../Project/ProjectCard";
|
||||
import { Portal } from "../Portal/Portal";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
export interface ModalCard {
|
||||
onClose?: () => void;
|
||||
direction: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const modalCardVariants = {
|
||||
initial: (direction: any) => {
|
||||
if (direction === Direction.START) return { opacity: 0, y: 300 };
|
||||
else if (direction === Direction.NEXT) return { opacity: 0, x: 300 };
|
||||
else if (direction === Direction.PREVIOUS) return { opacity: 0, x: -300 };
|
||||
return {}
|
||||
},
|
||||
animate: {
|
||||
x: 0, y: 0, opacity: 1, transition: { type: "spring" }
|
||||
},
|
||||
exit: (direction: Direction) => {
|
||||
const transition = { ease: "easeIn" }
|
||||
if (direction === Direction.EXIT) return { transition, opacity: 0, y: 300 };
|
||||
else if (direction === Direction.NEXT) return { transition, opacity: 0, x: -300 };
|
||||
else if (direction === Direction.PREVIOUS) return { transition, opacity: 0, x: 300 };
|
||||
return {}
|
||||
},
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
children: ReactElement
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ModalsMap = (modalId: ModalId) => {
|
||||
switch (modalId) {
|
||||
case ModalId.Project:
|
||||
return ProjectCard;
|
||||
case ModalId.Login1:
|
||||
return LoginCard_1;
|
||||
default:
|
||||
return () => <></>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default function Modal() {
|
||||
|
||||
const { isOpen, modalId, direction, initialModalProps } = useAppSelector(state => ({ isOpen: state.modals.isOpen, modalId: state.modals.openModalId, direction: state.modals.direction, initialModalProps: state.modals.initialModalProps }))
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const onClose = () => dispatch(closeModal());
|
||||
|
||||
const handleOutsideClick = (e: any) => {
|
||||
console.log(e.target);
|
||||
|
||||
if (e.target === ref.current) onClose();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) document.body.style.overflowY = "hidden";
|
||||
else document.body.style.overflowY = "initial";
|
||||
}, [isOpen]);
|
||||
|
||||
|
||||
const Child = ModalsMap(modalId);
|
||||
|
||||
|
||||
export default function Modal(props: Props) {
|
||||
return (
|
||||
<Portal>
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{isOpen && <motion.div
|
||||
|
||||
onClick={handleOutsideClick}
|
||||
className={`fixed overscroll-y-none overflow-x-hidden inset-0 bg-gray-300 bg-opacity-70 overflow-y-auto h-full w-full`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { ease: "easeInOut" },
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`w-screen min-h-screen flex justify-center items-center py-64`}
|
||||
>
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
<Child onClose={onClose} direction={direction} {...initialModalProps} />
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.div>}
|
||||
</AnimatePresence>
|
||||
</Portal>
|
||||
<div className='w-full h-full fixed inset-0 py-32 md:py-64 flex justify-center overflow-x-hidden overflow-y-scroll no-scrollbar'>
|
||||
<div
|
||||
className="w-full h-full bg-gray-300 bg-opacity-50 absolute inset-0"
|
||||
onClick={props.onClose}
|
||||
></div>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
92
src/Components/Shared/ModalsContainer/ModalsContainer.tsx
Normal file
92
src/Components/Shared/ModalsContainer/ModalsContainer.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { closeModal, Direction, ModalId } from "../../../redux/features/modals.slice";
|
||||
import { useAppDispatch, useAppSelector } from "../../../utils/hooks";
|
||||
import LoginCard_1 from "../../Login/LoginCard-1";
|
||||
import ProjectCard from "../../Project/ProjectCard";
|
||||
import VoteCard from "../../Vote/VoteCard";
|
||||
import Modal from "../Modal/Modal";
|
||||
import { Portal } from "../Portal/Portal";
|
||||
|
||||
export interface ModalCard {
|
||||
onClose?: () => void;
|
||||
direction: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const modalCardVariants = {
|
||||
initial: (direction: any) => {
|
||||
if (direction === Direction.START) return { opacity: 0, y: 300 };
|
||||
else if (direction === Direction.NEXT) return { opacity: 0, x: 300 };
|
||||
else if (direction === Direction.PREVIOUS) return { opacity: 0, x: -300 };
|
||||
return {}
|
||||
},
|
||||
animate: {
|
||||
x: 0, y: 0, opacity: 1, transition: { type: "spring" }
|
||||
},
|
||||
exit: (direction: Direction) => {
|
||||
const transition = { ease: "easeIn" }
|
||||
if (direction === Direction.EXIT) return { transition, opacity: 0, y: 300 };
|
||||
else if (direction === Direction.NEXT) return { transition, opacity: 0, x: -300 };
|
||||
else if (direction === Direction.PREVIOUS) return { transition, opacity: 0, x: 300 };
|
||||
return {}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ModalsMap = (modalId: ModalId) => {
|
||||
switch (modalId) {
|
||||
case ModalId.Project:
|
||||
return ProjectCard;
|
||||
case ModalId.Login1:
|
||||
return LoginCard_1;
|
||||
case ModalId.Vote:
|
||||
return VoteCard;
|
||||
default:
|
||||
return () => <></>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default function ModalsContainer() {
|
||||
|
||||
const { isOpen, openModals, direction } = useAppSelector(state => ({ isOpen: state.modals.isOpen, openModals: state.modals.openModals, direction: state.modals.direction }))
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const onClose = () => dispatch(closeModal());
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) document.body.style.overflowY = "hidden";
|
||||
else document.body.style.overflowY = "initial";
|
||||
}, [isOpen]);
|
||||
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<AnimatePresence exitBeforeEnter>
|
||||
{isOpen &&
|
||||
<motion.div
|
||||
className="w-screen h-scree fixed inset-0"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { ease: "easeInOut" },
|
||||
}}
|
||||
>
|
||||
|
||||
{openModals.map(modal => {
|
||||
const Child = ModalsMap(modal.modalId);
|
||||
return (
|
||||
<Modal onClose={onClose}>
|
||||
<Child onClose={onClose} direction={direction} {...modal.propsToPass} />
|
||||
</Modal>)
|
||||
})}
|
||||
</motion.div>}
|
||||
</AnimatePresence>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
21
src/Components/Vote/VoteCard.tsx
Normal file
21
src/Components/Vote/VoteCard.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ModalCard, modalCardVariants } from '../Shared/ModalsContainer/ModalsContainer'
|
||||
|
||||
export default function VoteCard({ onClose, direction, ...props }: ModalCard) {
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[400px] h-[400px]"
|
||||
|
||||
>
|
||||
<div className="p-24 h-full flex flex-col items-center justify-center">
|
||||
<h3 className="text-h2 font-bold">Vote Modal</h3>
|
||||
<p className="text-body2 mt-40">WIP</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,10 @@
|
||||
@apply bg-primary-600 hover:bg-primary-500 text-white;
|
||||
}
|
||||
|
||||
.btn-gray {
|
||||
@apply bg-gray-200 hover:bg-gray-100 text-gray-900;
|
||||
}
|
||||
|
||||
.chip {
|
||||
@apply bg-gray-100 text-body4 px-16 py-8 rounded-24 font-regular;
|
||||
}
|
||||
@@ -20,7 +24,7 @@
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
@apply rounded-[40px] bg-gray-50 overflow-hidden w-full max-w-[600px] shadow-xl;
|
||||
@apply rounded-[40px] bg-gray-50 overflow-y-scroll w-full max-w-[600px] shadow-xl z-10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,3 +56,12 @@ svg {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.no-scrollbar ::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
|
||||
@@ -12,21 +12,26 @@ export enum ModalId {
|
||||
Project,
|
||||
Login1,
|
||||
Login2,
|
||||
Vote,
|
||||
}
|
||||
|
||||
interface OpenModal {
|
||||
modalId: ModalId;
|
||||
propsToPass: any;
|
||||
}
|
||||
|
||||
interface StoreState {
|
||||
isOpen: boolean;
|
||||
isLoading: boolean;
|
||||
direction: Direction;
|
||||
openModalId: ModalId;
|
||||
initialModalProps: any;
|
||||
openModals: OpenModal[];
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
isOpen: false,
|
||||
isLoading: false,
|
||||
direction: Direction.START,
|
||||
openModalId: ModalId.None,
|
||||
openModals: [] as OpenModal[],
|
||||
} as StoreState;
|
||||
|
||||
export const modalSlice = createSlice({
|
||||
@@ -41,20 +46,43 @@ export const modalSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{ modalId: ModalId; initialModalProps: any }>
|
||||
) {
|
||||
if (!state.isOpen) state.direction = Direction.START;
|
||||
state.direction = Direction.START;
|
||||
state.isOpen = true;
|
||||
state.openModalId = action.payload.modalId;
|
||||
state.initialModalProps = action.payload.initialModalProps;
|
||||
state.openModals.push({
|
||||
modalId: action.payload.modalId,
|
||||
propsToPass: action.payload.initialModalProps,
|
||||
});
|
||||
},
|
||||
|
||||
replaceModal(
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
modalId: ModalId;
|
||||
initialModalProps: any;
|
||||
direction: Direction;
|
||||
}>
|
||||
) {
|
||||
state.direction = action.payload.direction;
|
||||
state.openModals.pop();
|
||||
state.openModals.push({
|
||||
modalId: action.payload.modalId,
|
||||
propsToPass: action.payload.initialModalProps || {},
|
||||
});
|
||||
},
|
||||
|
||||
closeModal(state) {
|
||||
state.direction = Direction.EXIT;
|
||||
state.isOpen = false;
|
||||
state.openModalId = ModalId.None;
|
||||
state.openModals.pop();
|
||||
state.isOpen = Boolean(state.openModals.length);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { closeModal, openModal, setDirection } = modalSlice.actions;
|
||||
export const {
|
||||
closeModal,
|
||||
openModal,
|
||||
replaceModal,
|
||||
setDirection,
|
||||
} = modalSlice.actions;
|
||||
|
||||
export default modalSlice.reducer;
|
||||
|
||||
Reference in New Issue
Block a user