Merge pull request #9 from IMPHNEN/feat/gacha-modal-login
feat: add modal login gacha page
This commit is contained in:
+54
-13
@@ -1,9 +1,9 @@
|
|||||||
import { ArrowDownOutlined } from '@ant-design/icons';
|
import { ArrowDownOutlined } from '@ant-design/icons';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { ModalsGacha } from '@imphnen-frontend-service/ui/organisms';
|
import { InputForm, Modal } from '@imphnen-frontend-service/ui/molecules';
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
import { FC, Fragment, ReactElement, useEffect, useState } from 'react';
|
import { FC, Fragment, ReactElement, useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
interface GachaItemProps {
|
interface GachaItemProps {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -14,7 +14,6 @@ interface GachaItemProps {
|
|||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
// Pinjem
|
// Pinjem
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
//
|
|
||||||
|
|
||||||
const scrollToRoulette = () => {
|
const scrollToRoulette = () => {
|
||||||
const rouletteSection = document.getElementById('roulette');
|
const rouletteSection = document.getElementById('roulette');
|
||||||
@@ -222,16 +221,58 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<div className="sticky bottom-0 h-[86px] md:h-[200px] bg-gradient-to-b from-primary-500/0 to-primary-500/50 to-80%"></div>
|
<div className="sticky bottom-0 h-[86px] md:h-[200px] bg-gradient-to-b from-primary-500/0 to-primary-500/50 to-80%"></div>
|
||||||
|
|
||||||
{/**Izin pake untuk debug modals gacha */}
|
{/**Izin pake untuk debug modals gacha */}
|
||||||
<div>
|
<button onClick={() => setShowModal(true)}>Open Modal</button>
|
||||||
<p>Debug : Test Modals Gacha</p>
|
<Modal
|
||||||
<Button onClick={
|
className="py-[45px] min-w-[400px] lg:min-w-[455px] px-7"
|
||||||
() => setShowModal(true)
|
isOpen={showModal}
|
||||||
}>Tekan</Button>
|
onClose={() => setShowModal(false)}
|
||||||
</div>
|
>
|
||||||
{showModal && createPortal(
|
<Modal.Header className="space-y-4">
|
||||||
<ModalsGacha></ModalsGacha>,
|
<img src="/logos/logo.svg" alt="" className="h-[70px] w-auto" />
|
||||||
document.body
|
<h1 className="text-primary-500 text-p1 text-center font-semibold">
|
||||||
)}
|
Login
|
||||||
|
</h1>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="space-y-4">
|
||||||
|
<InputForm
|
||||||
|
label="Email"
|
||||||
|
placeholder="Masukkan Email"
|
||||||
|
type="email"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<InputForm
|
||||||
|
label="Password"
|
||||||
|
placeholder="Masukkan password"
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<Link to="/forgot-password">
|
||||||
|
<h1 className="text-end text-primary-500 text-xl font-medium">
|
||||||
|
Lupa Password?
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
</Modal.Content>
|
||||||
|
<Modal.Footer className="flex flex-col md:flex-col pt-4">
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => console.log('Login')}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
<div className="flex justify-center gap-2 pt-2.5 font-medium">
|
||||||
|
<p className="text-neutral-500">Belum punya akun?</p>
|
||||||
|
<Link
|
||||||
|
className="text-primary-500 hover:text-primary-600"
|
||||||
|
to="/register"
|
||||||
|
>
|
||||||
|
Daftar disini
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
{/**Izin pake untuk debug modals gacha */}
|
{/**Izin pake untuk debug modals gacha */}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './input-form';
|
export * from './input-form';
|
||||||
export * from './pagination';
|
export * from './pagination';
|
||||||
|
export * from './modal';
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
import React, { useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
overlayClassName?: string;
|
||||||
|
closeButtonClassName?: string;
|
||||||
|
disableEscapeKeyDown?: boolean;
|
||||||
|
'aria-label'?: string;
|
||||||
|
'aria-labelledby'?: string;
|
||||||
|
'aria-describedby'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
overlayClassName,
|
||||||
|
closeButtonClassName,
|
||||||
|
disableEscapeKeyDown = false,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
'aria-labelledby': ariaLabelledBy,
|
||||||
|
'aria-describedby': ariaDescribedBy,
|
||||||
|
}: ModalProps) => {
|
||||||
|
const handleEscapeKey = useCallback(
|
||||||
|
(event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape' && isOpen && !disableEscapeKeyDown) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isOpen, onClose, disableEscapeKeyDown]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
window.addEventListener('keydown', handleEscapeKey);
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
window.removeEventListener('keydown', handleEscapeKey);
|
||||||
|
};
|
||||||
|
}, [isOpen, handleEscapeKey]);
|
||||||
|
|
||||||
|
const modalNode = useMemo(() => document.createElement('div'), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.appendChild(modalNode);
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(modalNode);
|
||||||
|
};
|
||||||
|
}, [modalNode]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="fixed inset-0 z-50">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-0 bg-gray-900/80 transition-opacity duration-200',
|
||||||
|
isOpen ? 'opacity-100' : 'opacity-0',
|
||||||
|
overlayClassName
|
||||||
|
)}
|
||||||
|
onClick={onClose}
|
||||||
|
role="presentation"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'fixed left-[50%] top-[50%] z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-[#F0F8FF] rounded-lg p-6 shadow-xl transition-all duration-200',
|
||||||
|
'sm:rounded-lg sm:max-w-md',
|
||||||
|
isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
|
aria-describedby={ariaDescribedBy}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className={cn(
|
||||||
|
'absolute right-4 top-4 rounded-sm p-1 text-gray-500 transition-colors hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2',
|
||||||
|
closeButtonClassName
|
||||||
|
)}
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
<CloseOutlined className="h-4 w-4 cursor-pointer" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
modalNode
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ModalHeaderProps {
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalHeader = ({ className, children }: ModalHeaderProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'mb-4 flex flex-col space-y-1.5 text-center sm:text-left',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ModalContentProps {
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalContent = ({ className, children }: ModalContentProps) => (
|
||||||
|
<div className={cn('mb-4', className)}>{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ModalFooterProps {
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalFooter = ({ className, children }: ModalFooterProps) => (
|
||||||
|
<div className={cn('flex gap-2 sm:flex-row sm:justify-end', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ModalTitleProps {
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalTitle = ({ className, children, id }: ModalTitleProps) => (
|
||||||
|
<h2
|
||||||
|
id={id}
|
||||||
|
className={cn(
|
||||||
|
'text-lg font-semibold leading-none tracking-tight',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ModalDescriptionProps {
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalDescription = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
id,
|
||||||
|
}: ModalDescriptionProps) => (
|
||||||
|
<p id={id} className={cn('text-sm text-gray-500', className)}>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
Modal.Header = ModalHeader;
|
||||||
|
Modal.Content = ModalContent;
|
||||||
|
Modal.Footer = ModalFooter;
|
||||||
|
Modal.Title = ModalTitle;
|
||||||
|
Modal.Description = ModalDescription;
|
||||||
|
|
||||||
|
export { Modal };
|
||||||
|
export type {
|
||||||
|
ModalProps,
|
||||||
|
ModalHeaderProps,
|
||||||
|
ModalContentProps,
|
||||||
|
ModalFooterProps,
|
||||||
|
ModalTitleProps,
|
||||||
|
ModalDescriptionProps,
|
||||||
|
};
|
||||||
@@ -40,6 +40,14 @@ export const Navbar: FC = (): ReactElement => {
|
|||||||
<Link to="#">Merch Gacha</Link>
|
<Link to="#">Merch Gacha</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
className="lg:text-[19px] lg:max-h-[44px] text-neutral-50 hover:text-neutral-200 transition-colors"
|
||||||
|
>
|
||||||
|
<Link to="/login">Login</Link>
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<button
|
<button
|
||||||
className={`md:hidden duration-200 ${
|
className={`md:hidden duration-200 ${
|
||||||
@@ -69,6 +77,14 @@ export const Navbar: FC = (): ReactElement => {
|
|||||||
Merch Gacha
|
Merch Gacha
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="#"
|
||||||
|
className="block text-gray-600 transition-colors px-4 py-2 text-center font-semibold"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user