diff --git a/components/system/components/GlobalCarousel/jumpers/EditChannels.js b/components/system/components/GlobalCarousel/jumpers/EditChannels.js new file mode 100644 index 00000000..b9881023 --- /dev/null +++ b/components/system/components/GlobalCarousel/jumpers/EditChannels.js @@ -0,0 +1,481 @@ +import * as React from "react"; +import * as Styles from "~/common/styles"; +import * as System from "~/components/system"; +import * as Jumper from "~/components/core/Jumper"; +import * as SVG from "~/common/svg"; +import * as Actions from "~/common/actions"; +import * as UserBehaviors from "~/common/user-behaviors"; +import * as Constants from "~/common/constants"; +import * as MobileJumper from "~/components/system/components/GlobalCarousel/jumpers/MobileLayout"; + +import { Show } from "~/components/utility/Show"; +import { css } from "@emotion/react"; +import { AnimateSharedLayout, motion } from "framer-motion"; +import { v4 as uuid } from "uuid"; +import { useEventListener } from "~/common/hooks"; + +const STYLES_CHANNEL_BUTTON = (theme) => css` + position: relative; + ${Styles.BUTTON_RESET}; + padding: 5px 12px 7px; + border: 1px solid ${theme.semantic.borderGrayLight4}; + border-radius: 12px; + color: ${theme.semantic.textBlack}; + background-color: transparent; + transition: background-color 0.3 ease-in-out; +`; + +const STYLES_CHANNEL_BUTTON_SELECTED = (theme) => css` + background-color: ${theme.semantic.bgGrayLight4}; +`; + +function ChannelButton({ children, isSelected, css, ...props }) { + return ( + + ); +} + +/* -----------------------------------------------------------------------------------------------*/ + +const STYLES_RETURN_KEY = (theme) => css` + padding: 0px 2px; + border-radius: 6px; + background-color: ${theme.semantic.bgGrayLight}; +`; + +function ChannelKeyboardShortcut({ searchResults, searchQuery, onAddFileToChannel }) { + const [isFileAdded, setIsFileAdded] = React.useState(false); + React.useLayoutEffect(() => { + if (isFileAdded) { + setIsFileAdded(false); + } + }, [searchQuery]); + + const { publicChannels, privateChannels } = searchResults; + const selectedChannel = [...publicChannels, ...privateChannels][0]; + + useEventListener({ + type: "keyup", + handler: (e) => { + if (e.key === "Enter") { + onAddFileToChannel(selectedChannel, selectedChannel.doesContainFile); + setIsFileAdded(true); + } + }, + }); + + if (isFileAdded) return null; + + return ( +
+ + Select {selectedChannel.isPublic ? "public" : "private"} tag "{selectedChannel.slatename}" + + + ⏎ + +
+ ); +} + +const STYLES_SEARCH_CHANNELS_INPUT = (theme) => css` + background-color: transparent; + ${theme.semantic.textGray}; + box-shadow: none; + height: 52px; + padding: 0px; + ::placeholder { + color: ${theme.semantic.textGray}; + } +`; + +function ChannelInput({ value, searchResults, onChange, onAddFileToChannel, ...props }) { + const { publicChannels, privateChannels } = searchResults; + const showShortcut = publicChannels.length + privateChannels.length === 1; + + return ( +
+ +
+ {showShortcut ? ( + + ) : null} +
+
+ ); +} + +/* -----------------------------------------------------------------------------------------------*/ +const STYLES_TAG = (theme) => css` + padding: 7px 12px 9px; + border-radius: 12px; + background-color: ${theme.semantic.bgGrayLight4}; +`; + +function ChannelsEmpty() { + return ( +
+ + You don’t have any tags yet.
Start typing above to create one. +
+
+ +
+
+ ); +} + +/* -----------------------------------------------------------------------------------------------*/ + +const STYLES_CHANNEL_BUTTONS_WRAPPER = css` + display: flex; + flex-wrap: wrap; + margin: calc(-8px + 6px) 0 0 -8px; + width: calc(100% + 8px); + + & > * { + margin: 8px 0 0 8px !important; + } +`; + +function Channels({ + header, + isPublic, + + searchQuery, + isSearching, + + channels, + isCreatingChannel, + + onAddFileToChannel, + onCreateChannel, +}) { + const showChannel = !isSearching && channels.length === 0; + + return !showChannel ? ( +
+ + {isCreatingChannel ? `Create ${header.toLowerCase()} tag` : header} + + + + + Objects with a public tag will show up on your public profile. + + + + +
+ {channels.map((channel) => ( + + onAddFileToChannel(channel, channel.doesContainFile)} + > + {channel.slatename} + + + ))} + + + + onCreateChannel(searchQuery)} + > + + {searchQuery} + + + +
+
+
+ ) : null; +} + +/* -----------------------------------------------------------------------------------------------*/ + +const useChannelHandlers = ({ viewer, file, onAction }) => { + const handleAddFileToChannel = async (slate, isSelected) => { + const prevSlates = [...viewer.slates]; + const resetViewerSlates = () => + onAction({ type: "UPDATE_VIEWER", viewer: { slates: prevSlates } }); + + if (isSelected) { + const newSlates = viewer.slates.map((item) => { + if (slate.id === item.id) { + return { ...item, objects: item.objects.filter((object) => object.id !== file.id) }; + } + return item; + }); + onAction({ type: "UPDATE_VIEWER", viewer: { slates: newSlates } }); + + const response = await UserBehaviors.removeFromSlate({ slate, ids: [file.id] }); + if (!response) resetViewerSlates(); + return; + } + + const newSlates = viewer.slates.map((item) => { + if (slate.id === item.id) return { ...item, objects: [...item.objects, file] }; + return item; + }); + onAction({ type: "UPDATE_VIEWER", viewer: { slates: newSlates } }); + + const response = await UserBehaviors.saveCopy({ slate, files: [file], showAlerts: false }); + if (!response) resetViewerSlates(); + }; + + const handleCreateSlate = (isPublic) => async (name) => { + //TODO(amine): find better solution to show the channel optimistically + onAction({ + type: "UPDATE_VIEWER", + viewer: { + slates: [...viewer.slates, { id: uuid(), slatename: name, isPublic, objects: [file] }], + }, + }); + + const response = await Actions.createSlate({ + name: name, + isPublic, + }); + + await handleAddFileToChannel(response?.slate); + }; + return { handleCreateSlate, handleAddFileToChannel }; +}; + +const useGetPrivateAndPublicChannels = ({ slates, file }) => + React.useMemo(() => { + const privateChannels = []; + const publicChannels = []; + + slates.forEach((slate) => { + const doesContainFile = slate.objects.some((item) => item.id === file.id); + + if (slate.isPublic) { + publicChannels.push({ ...slate, doesContainFile }); + return; + } + privateChannels.push({ ...slate, doesContainFile }); + }); + + privateChannels.sort((a, b) => a.createdAt - b.createdAt); + publicChannels.sort((a, b) => a.createdAt - b.createdAt); + + return { privateChannels, publicChannels }; + }, [slates, file.id]); + +const useChannelsSearch = ({ privateChannels, publicChannels }) => { + const [query, setQuery] = React.useState(""); + + const { results, canCreatePrivateChannel, canCreatePublicChannel } = React.useMemo(() => { + let canCreatePrivateChannel = true; + let canCreatePublicChannel = true; + + const results = { privateChannels: [], publicChannels: [] }; + const searchRegex = new RegExp(query, "gi"); + + results.privateChannels = privateChannels.filter((channel) => { + if (channel.slatename === query) canCreatePrivateChannel = false; + return searchRegex.test(channel.slatename); + }); + + results.publicChannels = publicChannels.filter((channel) => { + if (channel.slatename === query) canCreatePublicChannel = false; + return searchRegex.test(channel.slatename); + }); + + return { results, canCreatePrivateChannel, canCreatePublicChannel }; + }, [query, privateChannels, publicChannels]); + + const handleQueryChange = (e) => setQuery(e.target.value); + const clearQuery = () => setQuery(""); + + return [ + { searchQuery: query, searchResults: results, canCreatePrivateChannel, canCreatePublicChannel }, + { handleQueryChange, clearQuery }, + ]; +}; + +const STYLES_EDIT_CHANNELS_HEADER = (theme) => css` + ${Styles.HORIZONTAL_CONTAINER_CENTERED}; + color: ${theme.semantic.textGray}; +`; + +export function EditChannels({ file, viewer, isOpen, onClose, onAction }) { + const { privateChannels, publicChannels } = useGetPrivateAndPublicChannels({ + slates: viewer.slates, + file, + }); + + const [ + { searchQuery, searchResults, canCreatePrivateChannel, canCreatePublicChannel }, + { handleQueryChange, clearQuery }, + ] = useChannelsSearch({ + privateChannels: privateChannels, + publicChannels: publicChannels, + }); + + const { handleAddFileToChannel, handleCreateSlate } = useChannelHandlers({ + viewer, + file, + onAction, + }); + + const isSearching = searchQuery.length > 0; + + const showEmptyState = !isSearching && viewer.slates.length === 0; + + return isOpen ? ( + + + + + + + + + + + {showEmptyState ? ( + + + + ) : ( + + +
+ +
+
+ )} +
+ ) : null; +} + +export function EditChannelsMobile({ file, viewer, onAction, isOpen, onClose }) { + const { privateChannels, publicChannels } = useGetPrivateAndPublicChannels({ + slates: viewer.slates, + file, + }); + + const [ + { searchQuery, searchResults, canCreatePrivateChannel, canCreatePublicChannel }, + { handleQueryChange, clearQuery }, + ] = useChannelsSearch({ + privateChannels: privateChannels, + publicChannels: publicChannels, + }); + + const { handleAddFileToChannel, handleCreateSlate } = useChannelHandlers({ + viewer, + file, + onAction, + }); + + const isSearching = searchQuery.length > 0; + + return isOpen ? ( + + + + + + +
+ +
+ + + +
+ +
+
+ + + +
+ ) : null; +}