Skip to content

Commit

Permalink
feat(app): add modal to add question (#390)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
AdiPol1359 authored Dec 8, 2022
1 parent 1674fc4 commit 8adc36b
Show file tree
Hide file tree
Showing 19 changed files with 293 additions and 10 deletions.
2 changes: 2 additions & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@headlessui/react": "1.7.4",
"@next/font": "13.0.6",
"client-only": "0.0.1",
"next": "13.0.4",
Expand All @@ -31,6 +32,7 @@
"@svgr/webpack": "6.5.1",
"@types/node": "18.11.10",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
"@vercel/analytics": "0.1.5",
"autoprefixer": "^10.4.13",
"css-loader": "^6.7.2",
Expand Down
1 change: 1 addition & 0 deletions apps/app/public/select-purple.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion apps/app/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Fira_Sans, Fira_Code } from "@next/font/google";
import { AnalyticsWrapper } from "../components/analytics";
import { CtaHeader } from "../components/CtaHeader";
import { CtaHeader } from "../components/CtaHeader/CtaHeader";
import { Header } from "../components/Header/Header";
import { Footer } from "../components/Footer";
import { AppProviders } from "../providers/AppProviders";
import { AppModals } from "../components/AppModals";

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

Expand All @@ -22,6 +23,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<html lang="pl" className={`${firaSans.variable} ${firaCode.variable}`}>
<body>
<AppProviders>
<AppModals />
<Header />
<CtaHeader />
{children}
Expand Down
46 changes: 46 additions & 0 deletions apps/app/src/components/AddQuestionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import { ComponentProps } from "react";
import type { FormEvent } from "react";
import { useModalContext } from "../providers/ModalProvider";
import { BaseModal } from "./BaseModal";
import { Button } from "./Button/Button";
import { Select } from "./Select/Select";

export const AddQuestionModal = (props: ComponentProps<typeof BaseModal>) => {
const { closeModal } = useModalContext();

const handleFormSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
};

return (
<BaseModal {...props}>
<h2 className="text-center text-xl font-bold uppercase text-primary">Nowe pytanie</h2>
<form onSubmit={handleFormSubmit}>
<div className="mt-10 flex flex-col gap-y-3 px-5 sm:flex-row sm:justify-evenly sm:gap-x-5">
<Select className="w-full" aria-label="Wybierz technologię">
<option>Wybierz Technologię</option>
<option>HTML5</option>
<option>JavaScript</option>
</Select>
<Select className="w-full" aria-label="Wybierz poziom">
<option>Wybierz Poziom</option>
<option value="junior">junior</option>
<option value="mid">Mid</option>
<option value="senior">Senior</option>
</Select>
</div>
<textarea className="mt-4 h-40 w-full border" aria-label="Wpisz treść pytania"></textarea>
<div className="mt-3 flex flex-col gap-2 sm:flex-row-reverse">
<Button type="submit" variant="brandingInverse">
Dodaj pytanie
</Button>
<Button variant="branding" onClick={closeModal}>
Anuluj
</Button>
</div>
</form>
</BaseModal>
);
};
23 changes: 23 additions & 0 deletions apps/app/src/components/AppModals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use client";

import type { ComponentProps, ComponentType } from "react";
import { useModalContext } from "../providers/ModalProvider";
import type { Modal } from "../providers/ModalProvider";
import { AddQuestionModal } from "./AddQuestionModal";
import { BaseModal } from "./BaseModal";

const modals: Record<Modal, ComponentType<ComponentProps<typeof BaseModal>>> = {
AddQuestionModal,
};

export const AppModals = () => {
const { openedModal, closeModal } = useModalContext();

return (
<>
{Object.entries(modals).map(([type, Modal]) => (
<Modal key={type} isOpen={type === openedModal} onClose={closeModal} />
))}
</>
);
};
51 changes: 51 additions & 0 deletions apps/app/src/components/BaseModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { ReactNode, useEffect } from "react";
import { Transition } from "@headlessui/react";
import { lockScroll, unlockScroll } from "../utils/pageScroll";
import { CloseButton } from "./CloseButton/CloseButton";

type BaseModalProps = Readonly<{
isOpen: boolean;
onClose: () => void;
children?: ReactNode;
}>;

export const BaseModal = ({ isOpen, onClose, children }: BaseModalProps) => {
useEffect(() => {
if (isOpen) {
lockScroll();
}
}, [isOpen]);

return (
<Transition
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"
onClick={onClose}
show={isOpen}
enter="transition-opacity duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
afterLeave={unlockScroll}
>
<div
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"
onClick={(event) => {
// stop propagation to avoid triggering `onClick` on the backdrop behind the modal
event.stopPropagation();
}}
>
<CloseButton
type="button"
aria-label="Zamknij modal"
className="absolute right-4 top-4"
onClick={onClose}
/>
{children}
</div>
</Transition>
);
};
2 changes: 1 addition & 1 deletion apps/app/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type ButtonProps = Readonly<{
export const Button = ({ variant, className, ...props }: ButtonProps) => (
<button
className={twMerge(
"min-w-[160px] rounded-md border border-transparent px-8 py-1 text-sm leading-8 transition-colors duration-100 sm:py-0",
"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",
variants[variant],
className,
)}
Expand Down
13 changes: 13 additions & 0 deletions apps/app/src/components/CloseButton/CloseButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Meta, StoryObj } from "@storybook/react";
import { CloseButton } from "./CloseButton";

const meta: Meta<typeof CloseButton> = {
title: "CloseButton",
component: CloseButton,
};

export default meta;

type Story = StoryObj<typeof CloseButton>;

export const Default: Story = {};
14 changes: 14 additions & 0 deletions apps/app/src/components/CloseButton/CloseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { twMerge } from "tailwind-merge";
import { ButtonHTMLAttributes } from "react";

export const CloseButton = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={twMerge(
"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",
className,
)}
{...props}
>
&times;
</button>
);
20 changes: 20 additions & 0 deletions apps/app/src/components/CtaHeader/AddQuestionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

import { useModalContext } from "../../providers/ModalProvider";
import { Button } from "../Button/Button";

export const AddQuestionButton = () => {
const { openModal } = useModalContext();

return (
<>
<Button
variant="brandingInverse"
className="hidden sm:inline-block"
onClick={() => openModal("AddQuestionModal")}
>
Dodaj pytanie
</Button>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactNode } from "react";
import { ActiveLink } from "./ActiveLink";
import { Button } from "./Button/Button";
import { Container } from "./Container";
import { ActiveLink } from "../ActiveLink";
import { Container } from "../Container";
import { AddQuestionButton } from "./AddQuestionButton";

type CtaHeaderActiveLinkProps = Readonly<{
href: string;
Expand All @@ -23,9 +23,7 @@ export const CtaHeader = () => (
<CtaHeaderActiveLink href="/">Lista pytań</CtaHeaderActiveLink>
<CtaHeaderActiveLink href="/foo">Wybrane pytania</CtaHeaderActiveLink>
</nav>
<Button variant="brandingInverse" className="hidden sm:inline-block">
Dodaj pytanie
</Button>
<AddQuestionButton />
</Container>
</div>
);
22 changes: 22 additions & 0 deletions apps/app/src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Meta, StoryObj } from "@storybook/react";
import { Select } from "./Select";

const meta: Meta<typeof Select> = {
title: "Select",
component: Select,
args: {
children: (
<>
<option>lorem</option>
<option>ipsum</option>
<option>dolor</option>
</>
),
},
};

export default meta;

type Story = StoryObj<typeof Select>;

export const Default: Story = {};
12 changes: 12 additions & 0 deletions apps/app/src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { twMerge } from "tailwind-merge";
import type { SelectHTMLAttributes } from "react";

export const Select = ({ className, ...props }: SelectHTMLAttributes<HTMLSelectElement>) => (
<select
className={twMerge(
"select-purple cursor-pointer appearance-none rounded-none border-b border-primary py-2 pr-6 pl-1 text-base capitalize text-primary transition-shadow duration-100 focus:shadow-[0_0_10px] focus:shadow-primary focus:outline-0",
className,
)}
{...props}
/>
);
5 changes: 4 additions & 1 deletion apps/app/src/providers/AppProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { ReactNode } from "react";
import { ModalProvider } from "./ModalProvider";
import { ThemeProvider } from "./ThemeProvider";

type AppProvidersProps = Readonly<{
children: ReactNode;
}>;

export const AppProviders = ({ children }: AppProvidersProps) => (
<ThemeProvider>{children}</ThemeProvider>
<ThemeProvider>
<ModalProvider>{children}</ModalProvider>
</ThemeProvider>
);
36 changes: 36 additions & 0 deletions apps/app/src/providers/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import { useCallback, useMemo, useState } from "react";
import type { ReactNode } from "react";
import { createSafeContext } from "../lib/createSafeContext";

export type Modal = "AddQuestionModal";

interface ModalContextValue {
openedModal: Modal | null;
openModal: (modal: Modal) => void;
closeModal: () => void;
}

const [useModalContext, ModalContextProvider] = createSafeContext<ModalContextValue>();

const ModalProvider = ({ children }: { readonly children: ReactNode }) => {
const [openedModal, setOpenedModal] = useState<Modal | null>(null);

const openModal = useCallback((modal: Modal) => {
setOpenedModal(modal);
}, []);

const closeModal = useCallback(() => {
setOpenedModal(null);
}, []);

const value = useMemo(
() => ({ openedModal, openModal, closeModal }),
[openedModal, openModal, closeModal],
);

return <ModalContextProvider value={value}>{children}</ModalContextProvider>;
};

export { useModalContext, ModalProvider };
9 changes: 9 additions & 0 deletions apps/app/src/styles/tailwind.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
.select-purple {
background-image: url("/select-purple.svg");
background-size: 25px;
background-position: 100% 50%;
background-repeat: no-repeat;
}
}
2 changes: 1 addition & 1 deletion apps/app/src/utils/pageScroll.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "client-only";

const classes = ["overflow-hidden", "sm:overflow-scroll"];
const classes = ["overflow-hidden"];

export const lockScroll = () => {
document.body.style.paddingRight = `${window.innerWidth - document.body.offsetWidth}px`;
Expand Down
9 changes: 9 additions & 0 deletions apps/app/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ module.exports = {
sans: ["var(--font-fira-sans)", ...defaultTheme.fontFamily.sans],
mono: ["var(--font-fira-code)", ...defaultTheme.fontFamily.mono],
},
keyframes: {
show: {
from: { transform: "scale(0.95)" },
to: { transform: "scale(1)" },
},
},
animation: {
show: "show 0.2s",
},
},
},
plugins: [],
Expand Down
Loading

0 comments on commit 8adc36b

Please # to comment.