Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(app): add modal to add question #390

Merged
merged 19 commits into from
Dec 8, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
@@ -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",
@@ -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",
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";

@@ -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}
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
@@ -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,
)}
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;
@@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Te zaokrąglone brzegi wyglądają średnio:
Screenshot 2022-12-08 at 14 42 52

"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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brakuje chyba appearance-none

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`;
9 changes: 9 additions & 0 deletions apps/app/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -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: [],
Loading