Skip to content

Commit 8adc36b

Browse files
authored
feat(app): add modal to add question (#390)
* feat(app): add modal to add question * feat(app): add modal provider * feat(app): add AddQuestionModal * refactor(app): remove portal component * feat(app): add select options to story * feat(app): add close button * refactor(app): add if statement * refactor(app): change heading level * feat(app): add appearance none * feat(app): memoize provider value * feat(app): add form for add question * feat(app): add select labels * refactor(app): pass unlockScroll reference to afterLeave * refactor(app): change label to aria-label * refactor(app): move styles into css class * refactor(app): change font size * refactor(app): remove AddQuestionForm component * refactor(app): remove CloseAddQuestionModalButton component * feat(app): add textarea label
1 parent 1674fc4 commit 8adc36b

19 files changed

+293
-10
lines changed

apps/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"build-storybook": "storybook build"
1414
},
1515
"dependencies": {
16+
"@headlessui/react": "1.7.4",
1617
"@next/font": "13.0.6",
1718
"client-only": "0.0.1",
1819
"next": "13.0.4",
@@ -31,6 +32,7 @@
3132
"@svgr/webpack": "6.5.1",
3233
"@types/node": "18.11.10",
3334
"@types/react": "18.0.26",
35+
"@types/react-dom": "18.0.9",
3436
"@vercel/analytics": "0.1.5",
3537
"autoprefixer": "^10.4.13",
3638
"css-loader": "^6.7.2",

apps/app/public/select-purple.svg

Lines changed: 1 addition & 0 deletions
Loading

apps/app/src/app/layout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Fira_Sans, Fira_Code } from "@next/font/google";
22
import { AnalyticsWrapper } from "../components/analytics";
3-
import { CtaHeader } from "../components/CtaHeader";
3+
import { CtaHeader } from "../components/CtaHeader/CtaHeader";
44
import { Header } from "../components/Header/Header";
55
import { Footer } from "../components/Footer";
66
import { AppProviders } from "../providers/AppProviders";
7+
import { AppModals } from "../components/AppModals";
78

89
import "../styles/globals.css";
910

@@ -22,6 +23,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
2223
<html lang="pl" className={`${firaSans.variable} ${firaCode.variable}`}>
2324
<body>
2425
<AppProviders>
26+
<AppModals />
2527
<Header />
2628
<CtaHeader />
2729
{children}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use client";
2+
3+
import { ComponentProps } from "react";
4+
import type { FormEvent } from "react";
5+
import { useModalContext } from "../providers/ModalProvider";
6+
import { BaseModal } from "./BaseModal";
7+
import { Button } from "./Button/Button";
8+
import { Select } from "./Select/Select";
9+
10+
export const AddQuestionModal = (props: ComponentProps<typeof BaseModal>) => {
11+
const { closeModal } = useModalContext();
12+
13+
const handleFormSubmit = (event: FormEvent<HTMLFormElement>) => {
14+
event.preventDefault();
15+
};
16+
17+
return (
18+
<BaseModal {...props}>
19+
<h2 className="text-center text-xl font-bold uppercase text-primary">Nowe pytanie</h2>
20+
<form onSubmit={handleFormSubmit}>
21+
<div className="mt-10 flex flex-col gap-y-3 px-5 sm:flex-row sm:justify-evenly sm:gap-x-5">
22+
<Select className="w-full" aria-label="Wybierz technologię">
23+
<option>Wybierz Technologię</option>
24+
<option>HTML5</option>
25+
<option>JavaScript</option>
26+
</Select>
27+
<Select className="w-full" aria-label="Wybierz poziom">
28+
<option>Wybierz Poziom</option>
29+
<option value="junior">junior</option>
30+
<option value="mid">Mid</option>
31+
<option value="senior">Senior</option>
32+
</Select>
33+
</div>
34+
<textarea className="mt-4 h-40 w-full border" aria-label="Wpisz treść pytania"></textarea>
35+
<div className="mt-3 flex flex-col gap-2 sm:flex-row-reverse">
36+
<Button type="submit" variant="brandingInverse">
37+
Dodaj pytanie
38+
</Button>
39+
<Button variant="branding" onClick={closeModal}>
40+
Anuluj
41+
</Button>
42+
</div>
43+
</form>
44+
</BaseModal>
45+
);
46+
};

apps/app/src/components/AppModals.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"use client";
2+
3+
import type { ComponentProps, ComponentType } from "react";
4+
import { useModalContext } from "../providers/ModalProvider";
5+
import type { Modal } from "../providers/ModalProvider";
6+
import { AddQuestionModal } from "./AddQuestionModal";
7+
import { BaseModal } from "./BaseModal";
8+
9+
const modals: Record<Modal, ComponentType<ComponentProps<typeof BaseModal>>> = {
10+
AddQuestionModal,
11+
};
12+
13+
export const AppModals = () => {
14+
const { openedModal, closeModal } = useModalContext();
15+
16+
return (
17+
<>
18+
{Object.entries(modals).map(([type, Modal]) => (
19+
<Modal key={type} isOpen={type === openedModal} onClose={closeModal} />
20+
))}
21+
</>
22+
);
23+
};

apps/app/src/components/BaseModal.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"use client";
2+
3+
import { ReactNode, useEffect } from "react";
4+
import { Transition } from "@headlessui/react";
5+
import { lockScroll, unlockScroll } from "../utils/pageScroll";
6+
import { CloseButton } from "./CloseButton/CloseButton";
7+
8+
type BaseModalProps = Readonly<{
9+
isOpen: boolean;
10+
onClose: () => void;
11+
children?: ReactNode;
12+
}>;
13+
14+
export const BaseModal = ({ isOpen, onClose, children }: BaseModalProps) => {
15+
useEffect(() => {
16+
if (isOpen) {
17+
lockScroll();
18+
}
19+
}, [isOpen]);
20+
21+
return (
22+
<Transition
23+
className="fixed top-0 left-0 z-[99] flex h-full w-full items-center justify-center overflow-y-auto bg-black/50 sm:px-2"
24+
onClick={onClose}
25+
show={isOpen}
26+
enter="transition-opacity duration-200"
27+
enterFrom="opacity-0"
28+
enterTo="opacity-100"
29+
leave="transition-opacity duration-100"
30+
leaveFrom="opacity-100"
31+
leaveTo="opacity-0"
32+
afterLeave={unlockScroll}
33+
>
34+
<div
35+
className="relative h-full w-full max-w-3xl animate-show rounded-sm bg-white px-3.5 py-9 sm:h-fit sm:px-11 sm:py-20"
36+
onClick={(event) => {
37+
// stop propagation to avoid triggering `onClick` on the backdrop behind the modal
38+
event.stopPropagation();
39+
}}
40+
>
41+
<CloseButton
42+
type="button"
43+
aria-label="Zamknij modal"
44+
className="absolute right-4 top-4"
45+
onClick={onClose}
46+
/>
47+
{children}
48+
</div>
49+
</Transition>
50+
);
51+
};

apps/app/src/components/Button/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type ButtonProps = Readonly<{
1919
export const Button = ({ variant, className, ...props }: ButtonProps) => (
2020
<button
2121
className={twMerge(
22-
"min-w-[160px] rounded-md border border-transparent px-8 py-1 text-sm leading-8 transition-colors duration-100 sm:py-0",
22+
"min-w-[160px] appearance-none rounded-md border border-transparent px-8 py-1 text-sm leading-8 transition-colors duration-100 sm:py-0",
2323
variants[variant],
2424
className,
2525
)}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Meta, StoryObj } from "@storybook/react";
2+
import { CloseButton } from "./CloseButton";
3+
4+
const meta: Meta<typeof CloseButton> = {
5+
title: "CloseButton",
6+
component: CloseButton,
7+
};
8+
9+
export default meta;
10+
11+
type Story = StoryObj<typeof CloseButton>;
12+
13+
export const Default: Story = {};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { twMerge } from "tailwind-merge";
2+
import { ButtonHTMLAttributes } from "react";
3+
4+
export const CloseButton = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => (
5+
<button
6+
className={twMerge(
7+
"h-8 w-8 appearance-none rounded-full p-0 text-4xl leading-7 text-violet-200 transition-colors duration-100 hover:bg-primary focus:shadow-[0_0_10px] focus:shadow-primary focus:outline-none",
8+
className,
9+
)}
10+
{...props}
11+
>
12+
&times;
13+
</button>
14+
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use client";
2+
3+
import { useModalContext } from "../../providers/ModalProvider";
4+
import { Button } from "../Button/Button";
5+
6+
export const AddQuestionButton = () => {
7+
const { openModal } = useModalContext();
8+
9+
return (
10+
<>
11+
<Button
12+
variant="brandingInverse"
13+
className="hidden sm:inline-block"
14+
onClick={() => openModal("AddQuestionModal")}
15+
>
16+
Dodaj pytanie
17+
</Button>
18+
</>
19+
);
20+
};

0 commit comments

Comments
 (0)