mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-09 07:03:25 +00:00
feat: escape shortcut redirect to home page + page transition for all pages
This commit is contained in:
25
src/components/PageTransition.tsx
Normal file
25
src/components/PageTransition.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default function PageTransition({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -10,12 +10,12 @@ export default function SearchList({
|
||||
items,
|
||||
noItem,
|
||||
cursor,
|
||||
setCursor,
|
||||
closeModal,
|
||||
}: {
|
||||
items: SearchItem[];
|
||||
noItem?: ReactNode;
|
||||
cursor: number;
|
||||
setCursor: (cursor: number) => void;
|
||||
closeModal: () => void;
|
||||
}) {
|
||||
const searchItemsGrouped = useMemo(
|
||||
() => groupItemBy(items, "category.name"),
|
||||
@@ -35,9 +35,8 @@ export default function SearchList({
|
||||
{items.map((item) => (
|
||||
<SearchListItem
|
||||
item={item}
|
||||
setCursor={setCursor}
|
||||
selected={index === cursor}
|
||||
index={index}
|
||||
closeModal={closeModal}
|
||||
key={item.id}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,65 +1,48 @@
|
||||
import { motion } from "framer-motion";
|
||||
import LinkTag from "next/link";
|
||||
import { AiOutlineFolder } from "react-icons/ai";
|
||||
|
||||
import LinkFavicon from "components/Links/LinkFavicon";
|
||||
import { SearchItem } from "types";
|
||||
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import { useEffect, useId, useRef } from "react";
|
||||
import styles from "./search.module.scss";
|
||||
|
||||
export default function SearchListItem({
|
||||
item,
|
||||
index,
|
||||
selected,
|
||||
setCursor,
|
||||
closeModal,
|
||||
}: {
|
||||
item: SearchItem;
|
||||
index?: number;
|
||||
selected: boolean;
|
||||
setCursor: (cursor: number) => void;
|
||||
closeModal: () => void;
|
||||
}) {
|
||||
const id = useId();
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
|
||||
const [isHover, setHover] = useState<boolean>(false);
|
||||
|
||||
const { name, type, url } = item;
|
||||
|
||||
useEffect(() => {
|
||||
if (selected && !isHover) {
|
||||
if (selected) {
|
||||
ref.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
}, [isHover, selected]);
|
||||
}, [selected]);
|
||||
|
||||
return (
|
||||
<motion.li
|
||||
className={styles["search-item"]}
|
||||
initial={{ y: -15, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 20,
|
||||
delay: index * 0.025,
|
||||
}}
|
||||
ref={ref}
|
||||
onMouseEnter={() => {
|
||||
setCursor(index);
|
||||
setHover(true);
|
||||
}}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
key={id}
|
||||
>
|
||||
<LinkTag href={url} target="_blank" rel="no-referrer">
|
||||
<li className={styles["search-item"]} ref={ref} key={id}>
|
||||
<LinkTag
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="no-referrer"
|
||||
onClick={closeModal}
|
||||
>
|
||||
{type === "link" ? (
|
||||
<LinkFavicon url={item.url} noMargin size={24} />
|
||||
) : (
|
||||
<AiOutlineFolder size={24} />
|
||||
)}
|
||||
<span>{name}</span>
|
||||
{selected && "selected"}
|
||||
{selected && "[selected]"}
|
||||
</LinkTag>
|
||||
</motion.li>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import SearchList from "./SearchList";
|
||||
|
||||
import * as Keys from "constants/keys";
|
||||
import { GOOGLE_SEARCH_URL } from "constants/search-urls";
|
||||
import { Category, SearchItem } from "types";
|
||||
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||
import { Category, SearchItem } from "types";
|
||||
|
||||
import styles from "./search.module.scss";
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function SearchModal({
|
||||
items={itemsCompletion}
|
||||
noItem={<LabelSearchWithGoogle />}
|
||||
cursor={cursor}
|
||||
setCursor={setCursor}
|
||||
closeModal={close}
|
||||
/>
|
||||
)}
|
||||
<button type="submit" disabled={!canSubmit} style={{ display: "none" }}>
|
||||
|
||||
@@ -3,8 +3,10 @@ import { DefaultSeo } from "next-seo";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useEffect } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
import AuthRequired from "components/AuthRequired";
|
||||
import * as Keys from "constants/keys";
|
||||
|
||||
import "nprogress/nprogress.css";
|
||||
import "styles/globals.scss";
|
||||
@@ -12,6 +14,11 @@ import "styles/globals.scss";
|
||||
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
|
||||
const router = useRouter();
|
||||
|
||||
useHotkeys(Keys.CLOSE_SEARCH_KEY, () => router.push("/"), {
|
||||
enabled: router.pathname !== "/",
|
||||
enableOnFormTags: ["INPUT"],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Chargement pages
|
||||
router.events.on("routeChangeStart", nProgress.start);
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
import { redirectWithoutClientCache } from "utils/client";
|
||||
import { HandleAxiosError } from "utils/front";
|
||||
|
||||
@@ -48,7 +48,7 @@ function CreateCategory() {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTransition className="page-category-create">
|
||||
<FormLayout
|
||||
title="Créer une catégorie"
|
||||
errorMessage={error}
|
||||
@@ -66,7 +66,7 @@ function CreateCategory() {
|
||||
innerRef={autoFocusRef}
|
||||
/>
|
||||
</FormLayout>
|
||||
</>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
|
||||
import { Category } from "types";
|
||||
import { prisma } from "utils/back";
|
||||
import { BuildCategory, HandleAxiosError } from "utils/front";
|
||||
@@ -51,7 +51,7 @@ function EditCategory({ category }: { category: Category }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTransition className="page-category-edit">
|
||||
<FormLayout
|
||||
title="Modifier une catégorie"
|
||||
errorMessage={error}
|
||||
@@ -68,7 +68,7 @@ function EditCategory({ category }: { category: Category }) {
|
||||
innerRef={autoFocusRef}
|
||||
/>
|
||||
</FormLayout>
|
||||
</>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMemo, useState } from "react";
|
||||
|
||||
import Checkbox from "components/Checkbox";
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import { Category } from "types";
|
||||
@@ -47,7 +48,7 @@ function RemoveCategory({ category }: { category: Category }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTransition className="page-category-remove">
|
||||
<FormLayout
|
||||
title="Supprimer une catégorie"
|
||||
categoryId={category.id.toString()}
|
||||
@@ -72,7 +73,7 @@ function RemoveCategory({ category }: { category: Category }) {
|
||||
onChangeCallback={(checked) => setConfirmDelete(checked)}
|
||||
/>
|
||||
</FormLayout>
|
||||
</>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
import useModal from "hooks/useModal";
|
||||
|
||||
import Links from "components/Links/Links";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import SearchModal from "components/SearchModal/SearchModal";
|
||||
import SideMenu from "components/SideMenu/SideMenu";
|
||||
|
||||
import * as Keys from "constants/keys";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import useModal from "hooks/useModal";
|
||||
import { Category, Link, SearchItem } from "types";
|
||||
import { prisma } from "utils/back";
|
||||
import { BuildCategory } from "utils/front";
|
||||
@@ -157,16 +157,7 @@ function Home(props: HomeProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="App"
|
||||
initial={{ opacity: 0, scale: 0.85 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
<PageTransition className="App">
|
||||
<SideMenu
|
||||
categories={categories}
|
||||
favorites={favorites}
|
||||
@@ -185,7 +176,7 @@ function Home(props: HomeProps) {
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useMemo, useState } from "react";
|
||||
|
||||
import Checkbox from "components/Checkbox";
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import Selector from "components/Selector";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
|
||||
import { Category, Link } from "types";
|
||||
import { prisma } from "utils/back";
|
||||
import { BuildCategory, HandleAxiosError, IsValidURL } from "utils/front";
|
||||
@@ -61,7 +61,7 @@ function CreateLink({ categories }: { categories: Category[] }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTransition className="page-link-create">
|
||||
<FormLayout
|
||||
title="Créer un lien"
|
||||
categoryId={categoryIdQuery}
|
||||
@@ -103,7 +103,7 @@ function CreateLink({ categories }: { categories: Category[] }) {
|
||||
label="Favoris"
|
||||
/>
|
||||
</FormLayout>
|
||||
</>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useMemo, useState } from "react";
|
||||
|
||||
import Checkbox from "components/Checkbox";
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import Selector from "components/Selector";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
|
||||
import { Category, Link } from "types";
|
||||
import { prisma } from "utils/back";
|
||||
import {
|
||||
@@ -85,7 +85,7 @@ function EditLink({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTransition className="page-link-edit">
|
||||
<FormLayout
|
||||
title="Modifier un lien"
|
||||
errorMessage={error}
|
||||
@@ -126,7 +126,7 @@ function EditLink({
|
||||
label="Favoris"
|
||||
/>
|
||||
</FormLayout>
|
||||
</>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMemo, useState } from "react";
|
||||
|
||||
import Checkbox from "components/Checkbox";
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import { Link } from "types";
|
||||
@@ -42,7 +43,7 @@ function RemoveLink({ link }: { link: Link }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTransition className="page-link-remove">
|
||||
<FormLayout
|
||||
title="Supprimer un lien"
|
||||
categoryId={link.category.id.toString()}
|
||||
@@ -86,7 +87,7 @@ function RemoveLink({ link }: { link: Link }) {
|
||||
onChangeCallback={(checked) => setConfirmDelete(checked)}
|
||||
/>
|
||||
</FormLayout>
|
||||
</>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,4 +4,5 @@ export function redirectWithoutClientCache(router: NextRouter, url: string) {
|
||||
router.push(url, undefined, {
|
||||
unstable_skipClientCache: true,
|
||||
});
|
||||
// FIXME: invalidate catch instead of weird hack
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user