Brain management 1 (#752)

* feat: add brain management button

* feat: add brains list

* feat: add brain search bar

* feat: sort brain list by name

* refactor: update brains management page structure

* feat(BrainManagement): add new brain button

* feat: update import links
This commit is contained in:
Mamadou DICKO 2023-07-24 14:17:21 +02:00 committed by GitHub
parent 914689957d
commit cf376fb59f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 271 additions and 2 deletions

View File

@ -0,0 +1,44 @@
import Link from "next/link";
import { FaBrain } from "react-icons/fa";
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
import { cn } from "@/lib/utils";
import { useBrainListItem } from "./hooks/useBrainListItem";
interface BrainsListItemProps {
brain: MinimalBrainForUser;
}
export const BrainListItem = ({ brain }: BrainsListItemProps): JSX.Element => {
const { selected } = useBrainListItem(brain);
return (
<div
className={cn(
"w-full min-w-48 border-b border-black/10 dark:border-white/25 last:border-none relative group flex overflow-x-hidden hover:bg-gray-100 dark:hover:bg-gray-800",
selected
? "bg-gray-100 dark:bg-gray-800 text-primary dark:text-white"
: ""
)}
data-testid="brains
-list-item"
>
<Link
className="flex flex-col flex-1 min-w-0 p-4"
href={`/brains-management/${brain.id}`}
key={brain.id}
>
<div className="flex items-center gap-2">
<FaBrain className="text-xl" />
<p>{brain.name}</p>
</div>
</Link>
<div
aria-hidden
className="not-sr-only absolute left-1/2 top-0 bottom-0 right-0 bg-gradient-to-r from-transparent to-white dark:to-black pointer-events-none"
></div>
</div>
);
};

View File

@ -0,0 +1,13 @@
import { usePathname } from "next/navigation";
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainListItem = (brain: MinimalBrainForUser) => {
const pathname = usePathname()?.split("/").at(-1);
const selected = brain.id === pathname;
return {
selected,
};
};

View File

@ -0,0 +1 @@
export * from './BrainListItem'

View File

@ -0,0 +1,24 @@
import Field from "@/lib/components/ui/Field";
type BrainSearchBarProps = {
searchQuery: string;
setSearchQuery: (searchQuery: string) => void;
};
export const BrainSearchBar = ({
searchQuery,
setSearchQuery,
}: BrainSearchBarProps): JSX.Element => {
return (
<div className="m-2">
<Field
name="brainsearch"
placeholder="Search for a brain"
autoFocus
autoComplete="off"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
);
};

View File

@ -0,0 +1 @@
export * from './BrainSearchBar';

View File

@ -0,0 +1,77 @@
"use client";
import { motion, MotionConfig } from "framer-motion";
import { MdChevronRight } from "react-icons/md";
import { AddBrainModal } from "@/lib/components/AddBrainModal";
import { cn } from "@/lib/utils";
import { BrainListItem } from "./BrainListItem";
import { BrainSearchBar } from "./BrainSearchBar";
import { useBrainsList } from "../hooks/useBrainsList";
export const BrainsList = (): JSX.Element => {
const { open, setOpen, searchQuery, setSearchQuery, brains } =
useBrainsList();
return (
<MotionConfig transition={{ massq: 1, damping: 10 }}>
<motion.div
drag="x"
dragConstraints={{ right: 0, left: 0 }}
dragElastic={0.15}
onDragEnd={(event, info) => {
if (info.offset.x > 100 && !open) {
setOpen(true);
} else if (info.offset.x < -100 && open) {
setOpen(false);
}
}}
className="flex flex-col lg:sticky fixed top-16 left-0 bottom-0 lg:h-[90vh] overflow-visible z-30 border-r border-black/10 dark:border-white/25 bg-white dark:bg-black"
>
<motion.div
animate={{
width: open ? "fit-content" : "0px",
opacity: open ? 1 : 0.5,
boxShadow: open
? "10px 10px 16px rgba(0, 0, 0, 0)"
: "10px 10px 16px rgba(0, 0, 0, 0.5)",
}}
className={cn("overflow-hidden flex flex-col flex-1")}
data-testid="brains-list"
>
<div className="flex flex-col flex-1">
<BrainSearchBar
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
<div
data-testid="brains-list-items"
className="flex-1 overflow-auto scrollbar h-full"
>
{brains.map((brain) => (
<BrainListItem brain={brain} key={brain.id} />
))}
</div>
<div className="m-2 mb flex flex-col">
<AddBrainModal />
</div>
</div>
</motion.div>
<button
onClick={() => {
setOpen(!open);
}}
className="absolute left-full top-16 text-3xl bg-black dark:bg-white text-white dark:text-black rounded-r-full p-3 pl-1"
data-testid="brains-list-toggle"
>
<motion.div
whileTap={{ scale: 0.9 }}
animate={{ scaleX: open ? -1 : 1 }}
>
<MdChevronRight />
</motion.div>
</button>
</motion.div>
</MotionConfig>
);
};

View File

@ -0,0 +1,3 @@
export * from "./BrainListItem";
export * from "./BrainSearchBar";
export * from "./BrainsList";

View File

@ -0,0 +1,36 @@
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useDevice } from "@/lib/hooks/useDevice";
import { sortBrainsByName } from "../utils/sortByName";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainsList = () => {
const { isMobile } = useDevice();
const [open, setOpen] = useState(!isMobile);
const [searchQuery, setSearchQuery] = useState("");
const { allBrains } = useBrainContext();
const pathname = usePathname();
useEffect(() => {
setOpen(!isMobile);
}, [isMobile, pathname]);
const brains = allBrains.filter((brain) => {
const query = searchQuery.toLowerCase();
const name = brain.name.toLowerCase();
return name.includes(query);
});
return {
open,
setOpen,
searchQuery,
setSearchQuery,
brains: sortBrainsByName(brains),
};
};

View File

@ -0,0 +1,11 @@
"use client";
const BrainsManagement = (): JSX.Element => {
return (
<main className="flex flex-col w-full pt-10" data-testid="brain-page">
<p>Coming soon</p>
</main>
);
};
export default BrainsManagement;

View File

@ -0,0 +1,12 @@
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
export const sortBrainsByName = (
minimalBrains: MinimalBrainForUser[]
): MinimalBrainForUser[] => {
// Use the sort method to sort the array by the 'name' property
const sortedMinimalBrains = minimalBrains
.slice()
.sort((a, b) => a.name.localeCompare(b.name));
return sortedMinimalBrains;
};

View File

@ -0,0 +1,27 @@
"use client";
import { ReactNode } from "react";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { BrainsList } from "./[brainId]/components";
interface LayoutProps {
children?: ReactNode;
}
const Layout = ({ children }: LayoutProps): JSX.Element => {
const { session } = useSupabase();
if (session === null) {
redirectToLogin();
}
return (
<div className="relative h-full w-full flex justify-stretch items-stretch">
<BrainsList />
{children}
</div>
);
};
export default Layout;

View File

@ -0,0 +1,4 @@
"use client";
import BrainsManagementPage from "./[brainId]/page";
export default BrainsManagementPage;

View File

@ -0,0 +1,14 @@
import Link from "next/link";
import { FaBrain } from "react-icons/fa";
import { MdSettings } from "react-icons/md";
export const BrainManagementButton = (): JSX.Element => {
return (
<Link href={"/brains-management"}>
<button type="button" className="flex items-center focus:outline-none">
<MdSettings className="w-6 h-6" color="gray" />
<FaBrain className="w-3 h-3" color="gray" />
</button>
</Link>
);
};

View File

@ -6,8 +6,8 @@ import Field from "@/lib/components/ui/Field";
import Popover from "@/lib/components/ui/Popover";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { AddBrainModal } from "./components/AddBrainModal";
import { BrainActions } from "./components/BrainActions/BrainActions";
import { AddBrainModal } from "../../../../../AddBrainModal";
export const BrainsDropDown = (): JSX.Element => {
const [searchQuery, setSearchQuery] = useState("");

View File

@ -1,2 +1,2 @@
export * from "./AddBrainModal";
export * from "@/lib/components/AddBrainModal";
export * from "./BrainActions";

View File

@ -8,6 +8,7 @@ import { useSupabase } from "@/lib/context/SupabaseProvider";
import { cn } from "@/lib/utils";
import { AuthButtons } from "./components/AuthButtons";
import { BrainManagementButton } from "./components/BrainManagementButton";
import { BrainsDropDown } from "./components/BrainsDropDown";
import { DarkModeToggle } from "./components/DarkModeToggle";
import { NavLink } from "./components/NavLink";
@ -58,6 +59,7 @@ export const NavItems = ({
{isUserLoggedIn && (
<>
<BrainsDropDown />
<BrainManagementButton />
<Link aria-label="account" className="" href={"/user"}>
<MdPerson className="text-2xl" />
</Link>