Skip to content

Instantly share code, notes, and snippets.

@jjhiggz
Created August 15, 2022 23:26
Show Gist options
  • Save jjhiggz/f82f22854a40dd66b0737600893f6943 to your computer and use it in GitHub Desktop.
Save jjhiggz/f82f22854a40dd66b0737600893f6943 to your computer and use it in GitHub Desktop.
Sidebar Stuff
import type { Entity } from "~/types/EntityType";
import { getChildEntityType } from "~/types/EntityType";
import useClickOutside from "~/hooks/useClickOutside";
import { useEffect, useRef } from "react";
import ActionsMenuItem from "./ActionsMenuItem";
import useCustomNavigation from "~/hooks/useCustomNavigation";
import { RemenuIcons } from "~/icons";
import { useFetcher, useNavigate } from "@remix-run/react";
import { objectToFormData } from "~/utils/form-data/object-to-form-data";
const ActionsMenu = ({
entity,
parentEntity: _parentEntity,
onClickOut,
}: {
entity: Entity;
parentEntity?: Entity;
onClickOut: () => void;
}) => {
const actionsMenuRef = useRef<HTMLDivElement>(null);
const { getDeleteEntityLocation } = useCustomNavigation();
const fetcher = useFetcher();
const navigate = useNavigate();
useEffect(() => {
if (fetcher.type === "done") {
onClickOut();
}
}, [fetcher, onClickOut]);
useClickOutside(actionsMenuRef, onClickOut, {
propogates: false,
});
return (
<div className="actions-menu" ref={actionsMenuRef}>
<ul>
{entity.entityType !== "ITEM" && (
// create child
<>
<ActionsMenuItem
icon={RemenuIcons.mainMenuIcon}
label={`Add ${getChildEntityType(entity.entityType)}`}
onClick={() => {
fetcher.submit(
objectToFormData({
createEntityType: getChildEntityType(entity.entityType),
offOfEntityId: entity.id,
}),
{ method: "post", action: "/api/create-entity" }
);
}}
/>
</>
)}
<ActionsMenuItem
icon={RemenuIcons.deleteIcon}
id={`actions-menu-delete-${entity.entityType.toLowerCase()}-button`}
label={`Delete`}
onClick={
() => {
navigate(getDeleteEntityLocation(entity));
onClickOut();
} /* */
}
/>
</ul>
</div>
);
};
export default ActionsMenu;
const ActionsMenuItem = ({
id,
icon,
label,
onClick,
}: {
id?: string;
icon: string;
label: string;
onClick: () => void;
}) => (
<button
id={id}
onClick={(e) => {
onClick();
e.stopPropagation();
}}
>
<img src={icon} alt="icon" />
{label}
</button>
);
export default ActionsMenuItem;
const DragoverBar = ({
disabled = false,
onDragEnter,
onDragLeave,
isFirst = false,
}: {
onDragEnter: () => void;
onDragLeave: () => void;
isFirst?: boolean;
disabled?: boolean;
}) => {
return (
<div
className={`drag-to-box dragover ${isFirst ? "first" : ""}`}
onDragEnter={(e) => {
e.preventDefault();
onDragEnter();
}}
onDragLeave={(e) => {
e.preventDefault();
onDragLeave();
}}
onDrop={(_e) => {}}
onDragOver={(e) => {
e.preventDefault();
}}
>
{!disabled && (
<div className="drag-to-line">
<div className="circle"></div>
<div className="line"></div>
</div>
)}
</div>
);
};
export default DragoverBar;
import { RemenuIcons } from "~/icons";
import type { Entity } from "~/types/EntityType";
import ActionsMenu from "./ActionsMenu";
import { useSidebarStore } from "./SidebarStore";
const OpenActionsMenuButton = ({ entity }: { entity: Entity }) => {
const { actionsMenuOpenFor, setActionsMenuOpenFor } = useSidebarStore();
const showMenu = actionsMenuOpenFor === entity?.id;
return (
<div>
<div className="actions-menu-icon-container">
<img
src={RemenuIcons.dots}
alt=""
className="actions-menu-icon"
onClick={(e) => {
e.stopPropagation();
setActionsMenuOpenFor(
actionsMenuOpenFor === entity?.id ? null : entity?.id
);
}}
/>
{showMenu && (
<ActionsMenu
entity={entity}
onClickOut={() => {
setActionsMenuOpenFor(null);
}}
/>
)}
</div>
</div>
);
};
export default OpenActionsMenuButton;
generator client {
provider = "prisma-client-js"
previewFeatures = ["interactiveTransactions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
role Role
items Item[]
menus Menu[]
notes Note[]
password Password?
restaurants Restaurant[]
sections Section[]
}
model Restaurant {
id String @id @default(cuid())
name String
description String @default("")
address String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
order Int
showMenus Boolean @default(false)
entityType RestaurantType @default(RESTAURANT)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
items Item[]
menus Menu[]
sections Section[]
@@unique([userId, name])
@@unique([userId, order])
}
model Menu {
id String @id @default(cuid())
name String @default("")
description String @default("")
timeOfDay String @default("")
footer String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
restaurantId String
userId String
showSections Boolean @default(false)
order Int
entityType MenuType @default(MENU)
restaurant Restaurant @relation(fields: [restaurantId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
Item Item[]
sections Section[]
@@unique([restaurantId, order])
}
model Section {
id String @id @default(cuid())
name String @default("")
description String @default("")
timeOfDay String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
menuId String
userId String
restaurantId String
showItems Boolean @default(false)
order Int
entityType SectionType @default(SECTION)
menu Menu @relation(fields: [menuId], references: [id], onDelete: Cascade)
Restaurant Restaurant @relation(fields: [restaurantId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
items Item[]
@@unique([menuId, order])
}
model Item {
id String @id @default(cuid())
name String @default("")
description String @default("")
timeOfDay String @default("")
price String @default("0.00")
photoUrl String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sectionId String
userId String
restaurantId String
menuId String
order Int
isVegetarian Boolean @default(false)
isVegan Boolean @default(false)
isKosher Boolean @default(false)
isHalal Boolean @default(false)
isOrganic Boolean @default(false)
isGlutenFree Boolean @default(false)
entityType ItemType @default(ITEM)
menu Menu @relation(fields: [menuId], references: [id], onDelete: Cascade)
restaurant Restaurant @relation(fields: [restaurantId], references: [id])
section Section @relation(fields: [sectionId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([sectionId, order])
}
enum Role {
DEV
CLIENT
ADMIN
}
enum RestaurantType {
RESTAURANT
}
enum MenuType {
MENU
}
enum SectionType {
SECTION
}
enum ItemType {
ITEM
}
import type { getSidebarData } from "~/models/sidebar-data.server";
export type SidebarData = Awaited<ReturnType<typeof getSidebarData>>;
export type RestaurantOnSidebar = SidebarData[number];
export type MenuOnSidebar = RestaurantOnSidebar["menus"][number];
export type SectionOnSidebar = MenuOnSidebar["sections"][number];
export type ItemOnSidebar = SectionOnSidebar["items"][number];
// This is a zustand store, you could super easily do this in a react provider or
// in redux
import type { User } from "@prisma/client";
import type { useFetcher } from "@remix-run/react";
import create from "zustand";
import type { ToggleChildren } from "~/routes/api/toggle-children";
import type {
DraggingItem,
DragoverIndexDataPoint,
} from "~/types/SidebarTypes";
type FetcherType = ReturnType<typeof useFetcher>;
type SidebarStore = {
draggingItem: DraggingItem;
dragoverIndex: DragoverIndexDataPoint;
setDraggingItem: (newDraggingItem: DraggingItem) => void;
setDragoverIndex: (newDragoverIndex: DragoverIndexDataPoint) => void;
onDropRestaurant: (fetcher: FetcherType, user: User) => void;
onDropMenu: (fetcher: FetcherType, user: User) => void;
onDropSection: (fetcher: FetcherType, user: User) => void;
onDropItem: (fetcher: FetcherType, user: User) => void;
isLoading: boolean;
toggleChildren: (fetcher: FetcherType, input: ToggleChildren) => void;
actionsMenuOpenFor: string | null;
setActionsMenuOpenFor: (arg0: string | null) => void;
};
export const useSidebarStore = create<SidebarStore>((set, get) => ({
draggingItem: null,
dragoverIndex: null,
setDraggingItem: (newDraggingItem: DraggingItem) =>
set(() => ({
draggingItem: newDraggingItem,
})),
setDragoverIndex: (newDragoverIndex: DragoverIndexDataPoint) =>
set(() => ({
dragoverIndex: newDragoverIndex,
})),
onDropRestaurant: (fetcher, user) => {
set(() => ({ isLoading: true }));
if (
!get().draggingItem ||
!get().dragoverIndex ||
// onDropRestaurant may get called silmultaeneously with onDropMenu
get().draggingItem?.entityType !== "RESTAURANT"
) {
// do nothing
return;
}
fetcher.submit(
{
JSON: JSON.stringify({
type: "reorder-restaurant",
userId: user.id,
draggingItem: get().draggingItem,
dragoverIndex: get().dragoverIndex,
}),
},
{
action: "/api/reorder-sidebar",
method: "post",
}
);
set(() => ({ draggingItem: null, dragoverIndex: null }));
},
onDropMenu: (fetcher, user) => {
set(() => ({ isLoading: true }));
if (
!get().draggingItem ||
!get().dragoverIndex ||
get().draggingItem?.entityType !== "MENU"
) {
// do nothing
return;
}
fetcher.submit(
{
JSON: JSON.stringify({
type: "reorder-menu",
userId: user.id,
draggingItem: get().draggingItem,
dragoverIndex: get().dragoverIndex,
}),
},
{
action: "/api/reorder-sidebar",
method: "post",
}
);
set(() => ({ draggingItem: null, dragoverIndex: null }));
},
onDropSection: (fetcher, user) => {
set(() => ({ isLoading: true }));
if (
!get().draggingItem ||
!get().dragoverIndex ||
get().draggingItem?.entityType !== "SECTION"
) {
// do nothing
return;
}
fetcher.submit(
{
JSON: JSON.stringify({
type: "reorder-section",
userId: user.id,
draggingItem: get().draggingItem,
dragoverIndex: get().dragoverIndex,
}),
},
{
action: "/api/reorder-sidebar",
method: "post",
}
);
set(() => ({ draggingItem: null, dragoverIndex: null }));
},
onDropItem: (fetcher, user) => {
set(() => ({ isLoading: true }));
if (
!get().draggingItem ||
!get().dragoverIndex ||
get().draggingItem?.entityType !== "ITEM"
) {
// do nothing
return;
}
fetcher.submit(
{
JSON: JSON.stringify({
type: "reorder-item",
userId: user.id,
draggingItem: get().draggingItem,
dragoverIndex: get().dragoverIndex,
}),
},
{
action: "/api/reorder-sidebar",
method: "post",
}
);
set(() => ({ draggingItem: null, dragoverIndex: null }));
},
isLoading: false,
toggleChildren: (fetcher, { type, id, currentStatus }: ToggleChildren) => {
fetcher.submit(
{
JSON: JSON.stringify({
currentStatus,
id,
type,
}),
},
{ method: "post", action: "/api/toggle-children" }
);
},
actionsMenuOpenFor: null,
setActionsMenuOpenFor: (arg0) => {
set(() => ({
actionsMenuOpenFor: arg0,
}));
},
}));
import { Link, useFetchers } from "@remix-run/react";
import { RemenuIcons } from "~/icons";
import type { SidebarData } from "~/utils/types/sidebar-data.types";
import SideBarRestaurant from "./SideBarRestaurant";
const MapRestaurants = ({ sidebarData }: { sidebarData: SidebarData }) => (
<>
{sidebarData.map((restaurant) => (
<SideBarRestaurant restaurant={restaurant} key={restaurant.id} />
))}
</>
);
const SideBar = ({ sidebarData }: { sidebarData: SidebarData }) => {
const [fetcher1] = useFetchers();
return (
<div className="sidebar-row">
<div className="sidebar-header-container">
<div className="client-name">
{/* Note - both the client symbol and restaurant name should be dynamic */}
<div className="client-symbol">P</div>
<div>Ponte Restaurants</div>
</div>
<div className="sidebar-search">
<form action="" method="get">
<label htmlFor="searchbar"></label>
<input type="text" id="searchbar" placeholder="Search" />
</form>
</div>
</div>
<div
className={`sidebar-nav ${
fetcher1?.state === "submitting" ? "disabled" : ""
}`}
>
<ul>
<MapRestaurants sidebarData={sidebarData} />
</ul>
<Link to="restaurants/new" className="create-restaurant">
<h4>Add a restaurant</h4>
<div>
<img src={RemenuIcons.plusMath} alt="" />
</div>
</Link>
</div>
</div>
);
};
export default SideBar;
import type {
ItemOnSidebar,
MenuOnSidebar,
RestaurantOnSidebar,
SectionOnSidebar,
} from "~/utils/types/sidebar-data.types";
import { Link, useFetcher } from "@remix-run/react";
import OpenActionsMenuButton from "./OpenActionsMenuButton";
import { RemenuIcons } from "~/icons";
import RouteCalc from "~/utils/route-calc/route-calc";
import type { DragEventHandler } from "react";
import { useCallback } from "react";
import DragoverBar from "./DragoverBar";
import { useSidebarStore } from "./SidebarStore";
import { useUser } from "~/utils";
const SideBarItem: React.FC<{
restaurant: RestaurantOnSidebar;
menu: MenuOnSidebar;
section: SectionOnSidebar;
item: ItemOnSidebar;
}> = ({ restaurant: _restaurant, menu: _menu, section, item }) => {
const {
dragoverIndex,
setDraggingItem,
setDragoverIndex,
draggingItem,
onDropItem,
} = useSidebarStore();
const fetcher = useFetcher();
const user = useUser();
const onDragEnterInbetween = useCallback(() => {
if (draggingItem?.entityType === "ITEM") {
setDragoverIndex({
type: "reorder-item",
sectionId: section.id,
itemIndex: item.order,
});
}
}, [draggingItem?.entityType, item.order, section.id, setDragoverIndex]);
const onDragLeaveInbetween = useCallback(() => {
setDragoverIndex(null);
}, [setDragoverIndex]);
const onDragEnterInbetweenFirst = useCallback(() => {
if (draggingItem?.entityType === "ITEM") {
setDragoverIndex({
type: "reorder-item",
itemIndex: 0,
sectionId: section.id,
});
}
}, [draggingItem?.entityType, section.id, setDragoverIndex]);
const onDragLeaveInbetweenFirst = useCallback(() => {
setDragoverIndex(null);
}, [setDragoverIndex]);
const onDragEnd: DragEventHandler<HTMLDivElement> = (_e) => {
onDropItem(fetcher, user);
};
const onDragStart: DragEventHandler = (_e) => {
// In the future we can use this code ot custom style the ghost image
// const img = document.createElement("img");
// e.dataTransfer.setDragImage(img, 0, 0);
setDraggingItem(item);
};
return (
<li className="nav-list">
{item.order === 1 && (
<DragoverBar
onDragEnter={onDragEnterInbetweenFirst}
onDragLeave={onDragLeaveInbetweenFirst}
isFirst
disabled={
!(
dragoverIndex?.type === "reorder-item" &&
draggingItem?.entityType === "ITEM" &&
draggingItem.sectionId === section.id &&
dragoverIndex.itemIndex === 0
)
}
/>
)}
<div
className="li-item"
draggable
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
<div className="item-space"></div>
<img className="sidebar-icon" src={RemenuIcons.mainItemIcon} alt="" />
<div className="link-wrap">
<Link to={RouteCalc.item(item.id)}>{item.name}</Link>
</div>
<OpenActionsMenuButton entity={item} />
</div>
<DragoverBar
onDragEnter={onDragEnterInbetween}
onDragLeave={onDragLeaveInbetween}
disabled={
!(
dragoverIndex?.type === "reorder-item" &&
draggingItem?.entityType === "ITEM" &&
draggingItem.sectionId === section.id &&
dragoverIndex.itemIndex === item.order
)
}
/>
</li>
);
};
export default SideBarItem;
import type {
MenuOnSidebar,
RestaurantOnSidebar,
} from "~/utils/types/sidebar-data.types";
import type { DragEventHandler } from "react";
import { useCallback } from "react";
import { useFetcher, useNavigate } from "@remix-run/react";
import SideBarSection from "./SideBarSection";
import OpenActionsMenuButton from "./OpenActionsMenuButton";
import { RemenuIcons } from "~/icons";
import RouteCalc from "~/utils/route-calc/route-calc";
import DragoverBar from "./DragoverBar";
import { useSidebarStore } from "./SidebarStore";
import { useUser } from "~/utils";
const SideBarMenu = ({
menu,
restaurant,
}: {
menu: MenuOnSidebar;
restaurant: RestaurantOnSidebar;
}) => {
const navigate = useNavigate();
const {
toggleChildren,
dragoverIndex,
setDraggingItem,
setDragoverIndex,
draggingItem,
onDropMenu,
} = useSidebarStore();
const user = useUser();
const fetcher = useFetcher();
const onDragEnterInbetween = useCallback(() => {
if (draggingItem?.entityType === "MENU") {
setDragoverIndex({
type: "reorder-menu",
menuIndex: menu.order,
restaurantId: restaurant.id,
});
}
}, [draggingItem?.entityType, menu.order, restaurant.id, setDragoverIndex]);
const onDragLeaveInbetween = useCallback(() => {
setDragoverIndex(null);
}, [setDragoverIndex]);
const onDragEnterInbetweenFirst = useCallback(() => {
if (draggingItem?.entityType === "MENU") {
setDragoverIndex({
menuIndex: 0,
type: "reorder-menu",
restaurantId: restaurant.id,
});
}
}, [draggingItem?.entityType, restaurant.id, setDragoverIndex]);
const onDragLeaveInbetweenFirst = useCallback(() => {
setDragoverIndex(null);
}, [setDragoverIndex]);
const onDragEnd: DragEventHandler<HTMLDivElement> = (_e) => {
onDropMenu(fetcher, user);
};
const onDragStart: DragEventHandler = (_e) => {
// In the future we can use this code ot custom style the ghost image
// const img = document.createElement("img");
// e.dataTransfer.setDragImage(img, 0, 0);
setDraggingItem(menu);
};
return (
<li className="nav-list" key={menu.id}>
{menu.order === 1 && (
<DragoverBar
onDragEnter={onDragEnterInbetweenFirst}
onDragLeave={onDragLeaveInbetweenFirst}
isFirst
disabled={
!(
draggingItem?.entityType === "MENU" &&
draggingItem.restaurantId === restaurant.id &&
dragoverIndex?.type === "reorder-menu" &&
dragoverIndex.menuIndex === 0
)
}
/>
)}
<div
className="li-menu"
onClick={() => {
toggleChildren(fetcher, {
currentStatus: menu.showSections,
id: menu.id,
type: "menu",
});
}}
draggable
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
<img
src={menu.showSections ? RemenuIcons.downIcon : RemenuIcons.rightIcon}
alt="icon"
className="down-icon"
/>
<img
className="sidebar-icon"
src={RemenuIcons.mainMenuIcon}
alt="menu icon"
onClick={() => {}}
/>
<div
className="link-wrap"
onClick={(e) => {
e.stopPropagation();
navigate(RouteCalc.menu(menu.id));
}}
>
{menu.name || "New Menu"}
</div>
<OpenActionsMenuButton entity={menu} />
</div>
{menu.showSections && (
<ul>
{menu.sections.map((section) => (
<SideBarSection
restaurant={restaurant}
menu={menu}
section={section}
key={section.id}
/>
))}
</ul>
)}
<DragoverBar
onDragEnter={onDragEnterInbetween}
onDragLeave={onDragLeaveInbetween}
disabled={
!(
draggingItem?.entityType === "MENU" &&
draggingItem.restaurantId === restaurant.id &&
dragoverIndex?.type === "reorder-menu" &&
dragoverIndex.menuIndex === menu.order
)
}
/>
</li>
);
};
export default SideBarMenu;
import SideBarMenu from "./SideBarMenu";
import { Link, useFetcher } from "@remix-run/react";
import DragoverBar from "./DragoverBar";
import type { DragEventHandler } from "react";
import { useCallback } from "react";
import OpenActionsMenuButton from "./OpenActionsMenuButton";
import type { RestaurantOnSidebar } from "~/utils/types/sidebar-data.types";
import { RemenuIcons } from "~/icons";
import RouteCalc from "~/utils/route-calc/route-calc";
import { useSidebarStore } from "./SidebarStore";
import { useUser } from "~/utils";
const SideBarRestaurant = ({
restaurant,
}: {
restaurant: RestaurantOnSidebar;
}) => {
const fetcher = useFetcher();
const user = useUser();
const {
toggleChildren,
dragoverIndex,
setDraggingItem,
setDragoverIndex,
onDropRestaurant,
draggingItem,
} = useSidebarStore();
const toggleShowMenus = () => {
toggleChildren(fetcher, {
currentStatus: restaurant.showMenus,
id: restaurant.id,
type: "restaurant",
});
};
const onDragEnterInbetween = useCallback(() => {
if (draggingItem?.entityType === "RESTAURANT") {
setDragoverIndex({
type: "reorder-restaurant",
restaurantIndex: restaurant.order,
});
}
}, [draggingItem?.entityType, restaurant.order, setDragoverIndex]);
const onDragLeaveInbetween = useCallback(() => {
setDragoverIndex(null);
}, [setDragoverIndex]);
const onDragEnterInbetweenFirst = useCallback(() => {
if (draggingItem?.entityType === "RESTAURANT") {
setDragoverIndex({
type: "reorder-restaurant",
restaurantIndex: 0,
});
}
}, [draggingItem?.entityType, setDragoverIndex]);
const onDragLeaveInbetweenFirst = useCallback(() => {
setDragoverIndex(null);
}, [setDragoverIndex]);
const onDragStart: DragEventHandler = (_e) => {
// In the future we can use this code ot custom style the ghost image
// const img = document.createElement("img");
// e.dataTransfer.setDragImage(img, 0, 0);
setDraggingItem(restaurant);
};
const onDragEnd: DragEventHandler<HTMLDivElement> = (_e) => {
onDropRestaurant(fetcher, user);
};
return (
<li className="nav-list" key={restaurant.id}>
{restaurant.order === 1 && (
<DragoverBar
onDragEnter={onDragEnterInbetweenFirst}
onDragLeave={onDragLeaveInbetweenFirst}
disabled={
!(
draggingItem?.entityType === "RESTAURANT" &&
dragoverIndex?.type === "reorder-restaurant" &&
dragoverIndex.restaurantIndex === 0
)
}
isFirst={true}
/>
)}
<div
className={`li-restaurant draggable`}
id={`li-restaurant-${restaurant.order}`}
onClick={() => {
toggleShowMenus();
}}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
draggable
>
<img
src={
restaurant.showMenus ? RemenuIcons.downIcon : RemenuIcons.rightIcon
}
className="down-icon"
alt=""
/>
<img
className="sidebar-icon"
src={RemenuIcons.mainRestaurantIcon}
alt=""
/>
<div className="link-wrap">
<Link
to={RouteCalc.restaurant.edit(restaurant.id)}
className="noselect"
onClick={(e) => {
e.stopPropagation();
}}
role={"link"}
>
{restaurant.name}
</Link>
</div>
<OpenActionsMenuButton entity={restaurant} />
</div>
{restaurant.showMenus && (
<ul>
{restaurant.menus.map((menu) => (
<SideBarMenu menu={menu} restaurant={restaurant} key={menu.id} />
))}
</ul>
)}
{draggingItem?.entityType === "RESTAURANT" && (
<DragoverBar
onDragEnter={onDragEnterInbetween}
onDragLeave={onDragLeaveInbetween}
disabled={
!(
draggingItem?.entityType === "RESTAURANT" &&
dragoverIndex?.type === "reorder-restaurant" &&
dragoverIndex.restaurantIndex === restaurant.order
)
}
/>
)}
</li>
);
};
export default SideBarRestaurant;
import type {
MenuOnSidebar,
RestaurantOnSidebar,
SectionOnSidebar,
} from "~/utils/types/sidebar-data.types";
import type { DragEventHandler } from "react";
import { useCallback } from "react";
import SideBarItem from "./SideBarItem";
import { useFetcher, useNavigate } from "@remix-run/react";
import OpenActionsMenuButton from "./OpenActionsMenuButton";
import { RemenuIcons } from "~/icons";
import RouteCalc from "~/utils/route-calc/route-calc";
import DragoverBar from "./DragoverBar";
import { useSidebarStore } from "./SidebarStore";
import { useUser } from "~/utils";
const SideBarSection: React.FC<{
restaurant: RestaurantOnSidebar;
menu: MenuOnSidebar;
section: SectionOnSidebar;
}> = ({ section, menu, restaurant }) => {
const {
toggleChildren,
dragoverIndex,
setDraggingItem,
setDragoverIndex,
draggingItem,
onDropSection,
} = useSidebarStore();
const fetcher = useFetcher();
const user = useUser();
const onClick = () => {
toggleChildren(fetcher, {
currentStatus: section.showItems,
id: section.id,
type: "section",
});
};
const onDragEnterInbetween = useCallback(() => {
if (draggingItem?.entityType === "SECTION") {
setDragoverIndex({
type: "reorder-section",
sectionIndex: section.order,
menuId: menu.id,
});
}
}, [draggingItem?.entityType, menu.id, section.order, setDragoverIndex]);
const onDragLeaveInbetween = useCallback(() => {
setDragoverIndex(null);
}, [setDragoverIndex]);
const onDragEnterInbetweenFirst = useCallback(() => {
if (draggingItem?.entityType === "SECTION") {
setDragoverIndex({
type: "reorder-section",
sectionIndex: 0,
menuId: menu.id,
});
}
}, [draggingItem?.entityType, menu.id, setDragoverIndex]);
const onDragLeaveInbetweenFirst = useCallback(() => {
setDragoverIndex(null);
}, [setDragoverIndex]);
const onDragEnd: DragEventHandler<HTMLDivElement> = (_e) => {
onDropSection(fetcher, user);
};
const onDragStart: DragEventHandler = (_e) => {
// In the future we can use this code ot custom style the ghost image
// const img = document.createElement("img");
// e.dataTransfer.setDragImage(img, 0, 0);
setDraggingItem(section);
};
const navigate = useNavigate();
return (
<li className="nav-list" key={section.id}>
{section.order === 1 && (
<DragoverBar
onDragEnter={onDragEnterInbetweenFirst}
onDragLeave={onDragLeaveInbetweenFirst}
isFirst
disabled={
!(
draggingItem?.entityType === "SECTION" &&
draggingItem.menuId === menu.id &&
dragoverIndex?.type === "reorder-section" &&
dragoverIndex.sectionIndex === 0
)
}
/>
)}
<div
className="li-section"
onClick={() => {
toggleChildren(fetcher, {
currentStatus: section.showItems,
id: section.id,
type: "section",
});
}}
draggable
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
{section.showItems && (
<img
src={RemenuIcons.downIcon}
alt=""
className="down-icon"
onClick={() => {
onClick();
}}
/>
)}
{!section.showItems && (
<img
src={RemenuIcons.rightIcon}
alt=""
className="down-icon"
onClick={() => {
onClick();
}}
/>
)}
<img
src={RemenuIcons.mainSectionIcon}
className="sidebar-icon"
alt=""
/>
<div
className="link-wrap"
onClick={(e) => {
e.stopPropagation();
navigate(RouteCalc.section(section.id));
}}
>
{section.name || "New Section"}
</div>
<OpenActionsMenuButton entity={section} />
</div>
{section.showItems && (
<ul>
{section.items.map((item) => (
<SideBarItem
restaurant={restaurant}
menu={menu}
section={section}
item={item}
key={item.id}
/>
))}
</ul>
)}
<DragoverBar
onDragEnter={onDragEnterInbetween}
onDragLeave={onDragLeaveInbetween}
disabled={
!(
draggingItem?.entityType === "SECTION" &&
draggingItem.menuId === menu.id &&
dragoverIndex?.type === "reorder-section" &&
dragoverIndex.sectionIndex === section.order
)
}
/>
</li>
);
};
export default SideBarSection;
import { z } from "zod";
import type { Item } from "~/models/item.server";
import type { Menu } from "~/models/menu.server";
import type { Restaurant } from "~/models/restaurant.server";
import type { Section } from "~/models/section.server";
import { validateEntitySchema } from "./EntityType";
export type DraggingItem = null | Restaurant | Menu | Section | Item;
export const draggingItemSchema = z.union([validateEntitySchema, z.null()]);
export const reorderRestaurantDragoverSchema = z.object({
type: z.literal("reorder-restaurant"),
restaurantIndex: z.number(),
});
export const reorderMenuDragoverSchema = z.object({
type: z.literal("reorder-menu"),
menuIndex: z.number(),
restaurantId: z.string(),
});
export const reorderSectionDragoverSchema = z.object({
type: z.literal("reorder-section"),
sectionIndex: z.number(),
menuId: z.string(),
});
export const reorderItemDragoverSchema = z.object({
type: z.literal("reorder-item"),
itemIndex: z.number(),
sectionId: z.string(),
});
export const dragoverIndexSchema = z.union([
reorderRestaurantDragoverSchema,
reorderMenuDragoverSchema,
reorderSectionDragoverSchema,
reorderItemDragoverSchema,
z.null(),
]);
export type DragoverIndexDataPoint = z.infer<typeof dragoverIndexSchema>;
// in routes/api/toggle-children.ts
import { Response } from "@remix-run/node";
import type { ActionFunction } from "@remix-run/server-runtime";
import { prisma } from "~/db.server";
import { requireUserId } from "~/session.server";
export type ToggleChildren = {
type: "restaurant" | "menu" | "section";
id: string;
currentStatus: boolean;
};
export const action: ActionFunction = async ({ request }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const userId = await requireUserId(request);
const formData: ToggleChildren = await request
.formData()
.then((formData) => formData.get("JSON") as string)
.then((jsonData) => JSON.parse(jsonData));
const { currentStatus, id, type } = formData;
if (type === "restaurant") {
await prisma.restaurant.update({
where: {
id,
},
data: {
showMenus: !currentStatus,
},
});
}
if (type === "menu") {
await prisma.menu.update({
where: {
id,
},
data: {
showSections: !currentStatus,
},
});
}
if (type === "section") {
await prisma.section.update({
where: {
id,
},
data: {
showItems: !currentStatus,
},
});
}
return new Response(JSON.stringify("reordered"), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment