Merge pull request #3 from Sonny93/responsive

Add responsive
This commit is contained in:
Sonny
2023-06-08 23:42:03 +02:00
committed by GitHub
21 changed files with 319 additions and 172 deletions

View File

@@ -0,0 +1,28 @@
import Link from "next/link";
import { CSSProperties, ReactNode } from "react";
export default function ButtonLink({
href = "#",
onClick,
children,
style = {},
className = "",
}: {
href?: string;
onClick?: (...args: any) => any;
children: ReactNode;
style?: CSSProperties;
className?: string;
}) {
const handleClick = (event) => {
if (!href || href === "#") {
event.preventDefault();
}
onClick && onClick();
};
return (
<Link href={href} onClick={handleClick} style={style} className={className}>
{children}
</Link>
);
}

View File

@@ -3,8 +3,6 @@ import Link from "next/link";
import MessageManager from "components/MessageManager/MessageManager";
import styles from "styles/create.module.scss";
interface FormProps {
title: string;
@@ -39,29 +37,23 @@ export default function Form({
return (
<>
<NextSeo title={title} />
<div className={`App ${styles["create-app"]}`}>
<h2>{title}</h2>
<form onSubmit={handleSubmit}>
{children}
<button
type="submit"
className={classBtnConfirm}
disabled={!canSubmit}
>
{textBtnConfirm}
</button>
</form>
{!disableHomeLink && (
<Link href={categoryId ? `/?categoryId=${categoryId}` : "/"}>
Revenir à l'accueil
</Link>
)}
<MessageManager
info={infoMessage}
error={errorMessage}
success={successMessage}
/>
</div>
<h2>{title}</h2>
<form onSubmit={handleSubmit}>
{children}
<button type="submit" className={classBtnConfirm} disabled={!canSubmit}>
{textBtnConfirm}
</button>
</form>
{!disableHomeLink && (
<Link href={categoryId ? `/?categoryId=${categoryId}` : "/"}>
Revenir à l'accueil
</Link>
)}
<MessageManager
info={infoMessage}
error={errorMessage}
success={successMessage}
/>
</>
);
}

View File

@@ -1,3 +1,4 @@
import { motion } from "framer-motion";
import LinkTag from "next/link";
import { Category, Link } from "types";
@@ -6,15 +7,24 @@ import EditItem from "components/QuickActions/EditItem";
import RemoveItem from "components/QuickActions/RemoveItem";
import LinkItem from "./LinkItem";
import { motion } from "framer-motion";
import ButtonLink from "components/ButtonLink";
import CreateItem from "components/QuickActions/CreateItem";
import QuickActionSearch from "components/QuickActions/Search";
import { RxHamburgerMenu } from "react-icons/rx";
import styles from "./links.module.scss";
export default function Links({
category,
toggleFavorite,
isMobile,
openMobileModal,
openSearchModal,
}: {
category: Category;
toggleFavorite: (linkId: Link["id"]) => void;
isMobile: boolean;
openMobileModal: () => void;
openSearchModal: () => void;
}) {
if (category === null) {
return (
@@ -29,13 +39,28 @@ export default function Links({
return (
<div className={styles["links-wrapper"]}>
<h2 className={styles["category-header"]}>
<span className={styles["category-name"]}>
<span
className={styles["category-name"]}
style={{ display: "flex", alignItems: "center", gap: ".25em" }}
>
{isMobile && (
<ButtonLink
style={{
display: "flex",
}}
onClick={openMobileModal}
>
<RxHamburgerMenu size={"1.5em"} style={{ marginRight: ".5em" }} />
</ButtonLink>
)}
{name}
{links.length > 0 && (
<span className={styles["links-count"]}> {links.length}</span>
)}
</span>
<span className={styles["category-controls"]}>
<QuickActionSearch openSearchModal={openSearchModal} />
<CreateItem type="link" categoryId={category.id} />
<EditItem type="category" id={id} />
<RemoveItem type="category" id={id} />
</span>

View File

@@ -23,7 +23,7 @@
}
.links-wrapper {
height: 100%;
height: calc(100% - 54px); // FIXME: eurk
min-width: 0;
padding: 10px;
display: flex;

View File

@@ -48,4 +48,14 @@
flex: 1;
align-items: center;
flex-direction: column;
overflow: auto;
}
@media (max-width: 768px) {
.modal-container {
max-height: calc(100% - 2em);
width: calc(100% - 2em);
min-width: unset;
margin-top: 1em;
}
}

View File

@@ -1,5 +1,5 @@
import LinkTag from "next/link";
import { GrAdd } from "react-icons/gr";
import { IoAddOutline } from "react-icons/io5";
import { Category } from "types";
@@ -11,7 +11,7 @@ export default function CreateItem({
onClick,
}: {
type: "category" | "link";
categoryId: Category["id"];
categoryId?: Category["id"];
onClick?: (event: any) => void; // FIXME: find good event type
}) {
return (
@@ -21,7 +21,7 @@ export default function CreateItem({
className={styles["action"]}
onClick={onClick && onClick}
>
<GrAdd />
<IoAddOutline />
</LinkTag>
);
}

View File

@@ -0,0 +1,16 @@
import ButtonLink from "components/ButtonLink";
import { BsSearch } from "react-icons/bs";
import styles from "./quickactions.module.scss";
export default function QuickActionSearch({
openSearchModal,
}: {
openSearchModal: () => void;
}) {
return (
<ButtonLink className={styles["action"]} onClick={openSearchModal}>
<BsSearch />
</ButtonLink>
);
}

View File

@@ -19,11 +19,13 @@ export default function SearchModal({
handleSelectCategory,
categories,
items,
noHeader = true,
}: {
close: () => void;
handleSelectCategory: (category: Category) => void;
categories: Category[];
items: SearchItem[];
noHeader?: boolean;
}) {
const autoFocusRef = useAutoFocus();
@@ -98,7 +100,7 @@ export default function SearchModal({
);
return (
<Modal title="Rechercher" close={close} noHeader padding={"0"}>
<Modal close={close} noHeader={noHeader} padding={"0"}>
<form onSubmit={handleSubmit} className={styles["search-form"]}>
<div className={styles["search-input-wrapper"]}>
<label htmlFor="search">

View File

@@ -0,0 +1,38 @@
import PATHS from "constants/paths";
import { SideMenuProps } from "./SideMenu";
import ButtonLink from "components/ButtonLink";
import styles from "./sidemenu.module.scss";
export default function NavigationLinks({
categoryActive,
openSearchModal,
}: {
categoryActive: SideMenuProps["categoryActive"];
openSearchModal: SideMenuProps["openSearchModal"];
}) {
const handleOpenSearchModal = (event) => {
event.preventDefault();
openSearchModal();
};
return (
<div className={styles["menu-controls"]}>
<div className={styles["action"]}>
<ButtonLink onClick={openSearchModal}>Rechercher</ButtonLink>
<kbd>S</kbd>
</div>
<div className={styles["action"]}>
<ButtonLink href={PATHS.CATEGORY.CREATE}>Créer categorie</ButtonLink>
<kbd>C</kbd>
</div>
<div className={styles["action"]}>
<ButtonLink
href={`${PATHS.LINK.CREATE}?categoryId=${categoryActive.id}`}
>
Créer lien
</ButtonLink>
<kbd>L</kbd>
</div>
</div>
);
}

View File

@@ -1,29 +1,63 @@
import LinkTag from "next/link";
import { useHotkeys } from "react-hotkeys-hook";
import BlockWrapper from "components/BlockWrapper/BlockWrapper";
import Categories from "./Categories/Categories";
import NavigationLinks from "./NavigationLinks";
import Favorites from "./Favorites/Favorites";
import UserCard from "./UserCard/UserCard";
import * as Keys from "constants/keys";
import { Category, Link } from "types";
import PATHS from "constants/paths";
import styles from "./sidemenu.module.scss";
interface SideMenuProps {
export interface SideMenuProps {
categories: Category[];
favorites: Link[];
handleSelectCategory: (category: Category) => void;
categoryActive: Category;
openSearchModal: () => void;
isModalShowing: boolean;
}
export default function SideMenu({
categories,
favorites,
handleSelectCategory,
categoryActive,
openSearchModal,
isModalShowing = false,
}: SideMenuProps) {
useHotkeys(
Keys.ARROW_UP,
() => {
const currentCategoryIndex = categories.findIndex(
({ id }) => id === categoryActive.id
);
if (currentCategoryIndex === -1 || currentCategoryIndex === 0) return;
handleSelectCategory(categories[currentCategoryIndex - 1]);
},
{ enabled: !isModalShowing }
);
useHotkeys(
Keys.ARROW_DOWN,
() => {
const currentCategoryIndex = categories.findIndex(
({ id }) => id === categoryActive.id
);
if (
currentCategoryIndex === -1 ||
currentCategoryIndex === categories.length - 1
)
return;
handleSelectCategory(categories[currentCategoryIndex + 1]);
},
{ enabled: !isModalShowing }
);
return (
<div className={styles["side-menu"]}>
<BlockWrapper>
@@ -37,7 +71,7 @@ export default function SideMenu({
/>
</BlockWrapper>
<BlockWrapper>
<MenuControls
<NavigationLinks
categoryActive={categoryActive}
openSearchModal={openSearchModal}
/>
@@ -48,36 +82,3 @@ export default function SideMenu({
</div>
);
}
function MenuControls({
categoryActive,
openSearchModal,
}: {
categoryActive: SideMenuProps["categoryActive"];
openSearchModal: SideMenuProps["openSearchModal"];
}) {
const handleOpenSearchModal = (event) => {
event.preventDefault();
openSearchModal();
};
return (
<div className={styles["menu-controls"]}>
<div className={styles["action"]}>
<LinkTag href={"/#"} onClick={handleOpenSearchModal}>
Rechercher
</LinkTag>
<kbd>S</kbd>
</div>
<div className={styles["action"]}>
<LinkTag href={PATHS.CATEGORY.CREATE}>Créer categorie</LinkTag>
<kbd>C</kbd>
</div>
<div className={styles["action"]}>
<LinkTag href={`${PATHS.LINK.CREATE}?categoryId=${categoryActive.id}`}>
Créer lien
</LinkTag>
<kbd>L</kbd>
</div>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { useEffect, useState } from "react";
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState<boolean>(getMediaMatches(query));
const handleMediaChange = () => setMatches(getMediaMatches(query));
useEffect(() => {
const matchMedia = window.matchMedia(query);
handleMediaChange();
matchMedia.addEventListener("change", handleMediaChange);
return () => matchMedia.removeEventListener("change", handleMediaChange);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
return matches;
}
function getMediaMatches(query: string): boolean {
if (typeof window !== "undefined") {
return window.matchMedia(query).matches;
}
return false;
}

View File

@@ -9,14 +9,14 @@ import TextBox from "components/TextBox";
import PATHS from "constants/paths";
import useAutoFocus from "hooks/useAutoFocus";
import { redirectWithoutClientCache } from "utils/client";
import { HandleAxiosError } from "utils/front";
import getUserCategoriesCount from "lib/category/getUserCategoriesCount";
import getUser from "lib/user/getUser";
import styles from "styles/create.module.scss";
import { redirectWithoutClientCache } from "utils/client";
import { HandleAxiosError } from "utils/front";
import { getSession } from "utils/session";
import styles from "styles/form.module.scss";
function CreateCategory({ categoriesCount }: { categoriesCount: number }) {
const autoFocusRef = useAutoFocus();
const router = useRouter();
@@ -52,7 +52,7 @@ function CreateCategory({ categoriesCount }: { categoriesCount: number }) {
};
return (
<PageTransition className="page-category-create">
<PageTransition className={styles["form-container"]}>
<FormLayout
title="Créer une catégorie"
errorMessage={error}

View File

@@ -15,7 +15,7 @@ import { Category } from "types";
import { HandleAxiosError } from "utils/front";
import { getSession } from "utils/session";
import styles from "styles/create.module.scss";
import styles from "styles/form.module.scss";
function EditCategory({ category }: { category: Category }) {
const autoFocusRef = useAutoFocus();
@@ -52,7 +52,7 @@ function EditCategory({ category }: { category: Category }) {
};
return (
<PageTransition className="page-category-edit">
<PageTransition className={styles["form-container"]}>
<FormLayout
title="Modifier une catégorie"
errorMessage={error}

View File

@@ -15,7 +15,7 @@ import { Category } from "types";
import { HandleAxiosError } from "utils/front";
import { getSession } from "utils/session";
import styles from "styles/create.module.scss";
import styles from "styles/form.module.scss";
function RemoveCategory({ category }: { category: Category }) {
const router = useRouter();
@@ -51,7 +51,7 @@ function RemoveCategory({ category }: { category: Category }) {
};
return (
<PageTransition className="page-category-remove">
<PageTransition className={styles["form-container"]}>
<FormLayout
title="Supprimer une catégorie"
categoryId={category.id.toString()}

View File

@@ -3,18 +3,23 @@ import { useRouter } from "next/router";
import { useCallback, useMemo, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import BlockWrapper from "components/BlockWrapper/BlockWrapper";
import ButtonLink from "components/ButtonLink";
import Links from "components/Links/Links";
import Modal from "components/Modal/Modal";
import PageTransition from "components/PageTransition";
import SearchModal from "components/SearchModal/SearchModal";
import Categories from "components/SideMenu/Categories/Categories";
import SideMenu from "components/SideMenu/SideMenu";
import UserCard from "components/SideMenu/UserCard/UserCard";
import * as Keys from "constants/keys";
import PATHS from "constants/paths";
import { useMediaQuery } from "hooks/useMediaQuery";
import useModal from "hooks/useModal";
import { Category, Link, SearchItem } from "types";
import getUserCategories from "lib/category/getUserCategories";
import getUser from "lib/user/getUser";
import { Category, Link, SearchItem } from "types";
import { pushStateVanilla } from "utils/link";
import { getSession } from "utils/session";
@@ -25,7 +30,10 @@ interface HomePageProps {
function Home(props: HomePageProps) {
const router = useRouter();
const modal = useModal();
const searchModal = useModal();
const isMobile = useMediaQuery("(max-width: 768px)");
const mobileModal = useModal();
const [categories, setCategories] = useState<Category[]>(props.categories);
const [categoryActive, setCategoryActive] = useState<Category | null>(
@@ -99,18 +107,20 @@ function Home(props: HomePageProps) {
const handleSelectCategory = (category: Category) => {
setCategoryActive(category);
pushStateVanilla(`${PATHS.HOME}?categoryId=${category.id}`);
mobileModal.close();
};
const areHokeysEnabled = { enabled: !searchModal.isShowing };
useHotkeys(
Keys.OPEN_SEARCH_KEY,
(event) => {
event.preventDefault();
modal.open();
searchModal.open();
},
{ enabled: !modal.isShowing }
areHokeysEnabled
);
useHotkeys(Keys.CLOSE_SEARCH_KEY, modal.close, {
enabled: modal.isShowing,
useHotkeys(Keys.CLOSE_SEARCH_KEY, searchModal.close, {
enabled: searchModal.isShowing,
enableOnFormTags: ["INPUT"],
});
@@ -119,66 +129,63 @@ function Home(props: HomePageProps) {
() => {
router.push(`${PATHS.LINK.CREATE}?categoryId=${categoryActive.id}`);
},
{
enabled: !modal.isShowing,
}
areHokeysEnabled
);
useHotkeys(
Keys.OPEN_CREATE_CATEGORY_KEY,
() => {
router.push("/category/create");
},
{
enabled: !modal.isShowing,
}
);
useHotkeys(
Keys.ARROW_UP,
() => {
const currentCategoryIndex = categories.findIndex(
({ id }) => id === categoryActive.id
);
if (currentCategoryIndex === -1 || currentCategoryIndex === 0) return;
handleSelectCategory(categories[currentCategoryIndex - 1]);
},
{ enabled: !modal.isShowing }
);
useHotkeys(
Keys.ARROW_DOWN,
() => {
const currentCategoryIndex = categories.findIndex(
({ id }) => id === categoryActive.id
);
if (
currentCategoryIndex === -1 ||
currentCategoryIndex === categories.length - 1
)
return;
handleSelectCategory(categories[currentCategoryIndex + 1]);
},
{ enabled: !modal.isShowing }
areHokeysEnabled
);
return (
<PageTransition className="App">
<SideMenu
categories={categories}
favorites={favorites}
handleSelectCategory={handleSelectCategory}
categoryActive={categoryActive}
openSearchModal={modal.open}
{isMobile ? (
<>
<UserCard />
<AnimatePresence>
{mobileModal.isShowing && (
<Modal close={mobileModal.close}>
<BlockWrapper style={{ minHeight: "0", flex: "1" }}>
<ButtonLink href={PATHS.CATEGORY.CREATE}>
Créer categorie
</ButtonLink>
<Categories
categories={categories}
categoryActive={categoryActive}
handleSelectCategory={handleSelectCategory}
/>
</BlockWrapper>
</Modal>
)}
</AnimatePresence>
</>
) : (
<SideMenu
categories={categories}
favorites={favorites}
handleSelectCategory={handleSelectCategory}
categoryActive={categoryActive}
openSearchModal={searchModal.open}
isModalShowing={searchModal.isShowing}
/>
)}
<Links
category={categoryActive}
toggleFavorite={toggleFavorite}
isMobile={isMobile}
openMobileModal={mobileModal.open}
openSearchModal={searchModal.open}
/>
<Links category={categoryActive} toggleFavorite={toggleFavorite} />
<AnimatePresence>
{modal.isShowing && (
{searchModal.isShowing && (
<SearchModal
close={modal.close}
close={searchModal.close}
categories={categories}
items={itemsSearch}
handleSelectCategory={handleSelectCategory}
noHeader={!isMobile}
/>
)}
</AnimatePresence>

View File

@@ -17,7 +17,7 @@ import { Category, Link } from "types";
import { HandleAxiosError, IsValidURL } from "utils/front";
import { getSession } from "utils/session";
import styles from "styles/create.module.scss";
import styles from "styles/form.module.scss";
function CreateLink({ categories }: { categories: Category[] }) {
const autoFocusRef = useAutoFocus();
@@ -64,7 +64,7 @@ function CreateLink({ categories }: { categories: Category[] }) {
};
return (
<PageTransition className="page-link-create">
<PageTransition className={styles["form-container"]}>
<FormLayout
title="Créer un lien"
categoryId={categoryIdQuery}

View File

@@ -9,17 +9,16 @@ import PageTransition from "components/PageTransition";
import Selector from "components/Selector";
import TextBox from "components/TextBox";
import PATHS from "constants/paths";
import useAutoFocus from "hooks/useAutoFocus";
import getUserCategories from "lib/category/getUserCategories";
import getUserLink from "lib/link/getUserLink";
import getUser from "lib/user/getUser";
import { Category, Link } from "types";
import { HandleAxiosError, IsValidURL } from "utils/front";
import { getSession } from "utils/session";
import getUserCategories from "lib/category/getUserCategories";
import getUserLink from "lib/link/getUserLink";
import PATHS from "constants/paths";
import getUser from "lib/user/getUser";
import styles from "styles/create.module.scss";
import styles from "styles/form.module.scss";
function EditLink({
link,
@@ -85,7 +84,7 @@ function EditLink({
};
return (
<PageTransition className="page-link-edit">
<PageTransition className={styles["form-container"]}>
<FormLayout
title="Modifier un lien"
errorMessage={error}

View File

@@ -15,7 +15,7 @@ import { Link } from "types";
import { HandleAxiosError } from "utils/front";
import { getSession } from "utils/session";
import styles from "styles/create.module.scss";
import styles from "styles/form.module.scss";
function RemoveLink({ link }: { link: Link }) {
const router = useRouter();
@@ -46,7 +46,7 @@ function RemoveLink({ link }: { link: Link }) {
};
return (
<PageTransition className="page-link-remove">
<PageTransition className={styles["form-container"]}>
<FormLayout
title="Supprimer un lien"
categoryId={link.category.id.toString()}

View File

@@ -1,40 +1,36 @@
import { Provider } from "next-auth/providers";
import { getProviders, signIn } from "next-auth/react";
import { NextSeo } from "next-seo";
import Link from "next/link";
import MessageManager from "components/MessageManager/MessageManager";
import PageTransition from "components/PageTransition";
import PATHS from "constants/paths";
import getUser from "lib/user/getUser";
import { getSession } from "utils/session";
import getUser from "lib/user/getUser";
import styles from "styles/login.module.scss";
import styles from "styles/form.module.scss";
interface SignInProps {
providers: Provider[];
}
export default function SignIn({ providers }: SignInProps) {
return (
<>
<PageTransition className={styles["form-container"]}>
<NextSeo title="Authentification" />
<div className="App">
<div className={styles["wrapper"]}>
<h2>Se connecter</h2>
<MessageManager />
<div className={styles["providers"]}>
{Object.values(providers).map(({ name, id }) => (
<button
onClick={() => signIn(id, { callbackUrl: PATHS.HOME })}
key={id}
>
Continuer avec {name}
</button>
))}
</div>
<Link href={PATHS.HOME}> Revenir à l'accueil</Link>
</div>
<h2>Se connecter</h2>
<MessageManager info="Authentification requise pour utiliser ce service" />
<div className={styles["providers"]}>
{Object.values(providers).map(({ name, id }) => (
<button
onClick={() => signIn(id, { callbackUrl: PATHS.HOME })}
key={id}
>
Continuer avec {name}
</button>
))}
</div>
</>
</PageTransition>
);
}

View File

@@ -1,34 +1,36 @@
@import "keyframes.scss";
@import "colors.scss";
.create-app {
.form-container {
height: fit-content;
width: 680px;
margin-top: 150px;
width: 768px;
margin-top: 10em;
display: flex;
flex-direction: column;
gap: 15px;
gap: 0.75em;
& h2 {
color: $blue;
}
& form {
height: 100%;
width: 100%;
display: flex;
gap: 5px;
gap: 0.5em;
flex-direction: column;
}
& .input-field {
display: flex;
gap: 5px;
gap: 0.25em;
flex-direction: column;
}
}
@media (max-width: 680px) {
.create-app {
@media (max-width: 768px) {
.form-container {
width: 100%;
margin-top: 5em;
padding: 0 1em;
}
}

View File

@@ -10,8 +10,8 @@
html,
body {
height: 100%;
width: 100%;
height: 100dvh;
width: 100dvw;
color: $black;
background-color: $light-grey;
font-family: "Poppins", sans-serif;
@@ -198,3 +198,9 @@ kbd {
width: 100%;
}
}
@media (max-width: 768px) {
.App {
flex-direction: column;
}
}