diff --git a/packages/web/components/templates/navMenu/LibraryMenu.tsx b/packages/web/components/templates/navMenu/LibraryMenu.tsx new file mode 100644 index 000000000..5b015216b --- /dev/null +++ b/packages/web/components/templates/navMenu/LibraryMenu.tsx @@ -0,0 +1,613 @@ +import { ReactNode, useEffect, useMemo, useRef } from 'react' +import { StyledText } from '../../elements/StyledText' +import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { Button } from '../../elements/Button' +import { Circle } from 'phosphor-react' +import { + Subscription, + SubscriptionType, + useGetSubscriptionsQuery, +} from '../../../lib/networking/queries/useGetSubscriptionsQuery' +import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' +import { Label } from '../../../lib/networking/fragments/labelFragment' +import { theme } from '../../tokens/stitches.config' +import { useRegisterActions } from 'kbar' +import { LogoBox } from '../../elements/LogoBox' +import { usePersistedState } from '../../../lib/hooks/usePersistedState' +import { useGetSavedSearchQuery } from '../../../lib/networking/queries/useGetSavedSearchQuery' +import { SavedSearch } from '../../../lib/networking/fragments/savedSearchFragment' +import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon' +import Link from 'next/link' +import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon' +import { SplitButton } from '../../elements/SplitButton' +import { AvatarDropdown } from '../../elements/AvatarDropdown' +import { PrimaryDropdown } from '../PrimaryDropdown' +import { NavMenuFooter } from './Footer' + +export const LIBRARY_LEFT_MENU_WIDTH = '275px' + +type LibraryFilterMenuProps = { + setShowAddLinkModal: (show: boolean) => void + + searchTerm: string | undefined + applySearchQuery: (searchTerm: string) => void + + showFilterMenu: boolean + setShowFilterMenu: (show: boolean) => void +} + +export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element { + const [labels, setLabels] = usePersistedState({ + key: 'menu-labels', + isSessionStorage: false, + initialValue: [], + }) + const [savedSearches, setSavedSearches] = usePersistedState({ + key: 'menu-searches', + isSessionStorage: false, + initialValue: [], + }) + const [subscriptions, setSubscriptions] = usePersistedState({ + key: 'menu-subscriptions', + isSessionStorage: false, + initialValue: [], + }) + const labelsResponse = useGetLabelsQuery() + const searchesResponse = useGetSavedSearchQuery() + const subscriptionsResponse = useGetSubscriptionsQuery() + + useEffect(() => { + if ( + !labelsResponse.error && + !labelsResponse.isLoading && + labelsResponse.labels + ) { + setLabels(labelsResponse.labels) + } + }, [setLabels, labelsResponse]) + + useEffect(() => { + if ( + !subscriptionsResponse.error && + !subscriptionsResponse.isLoading && + subscriptionsResponse.subscriptions + ) { + setSubscriptions(subscriptionsResponse.subscriptions) + } + }, [setSubscriptions, subscriptionsResponse]) + + useEffect(() => { + if ( + !searchesResponse.error && + !searchesResponse.isLoading && + searchesResponse.savedSearches + ) { + setSavedSearches(searchesResponse.savedSearches) + } + }, [setSavedSearches, searchesResponse]) + + return ( + <> + + + + + + + + + + + {/* This spacer pushes library content to the right of + the fixed left side menu. */} + + + ) +} + +function SavedSearches( + props: LibraryFilterMenuProps & { savedSearches: SavedSearch[] | undefined } +): JSX.Element { + const sortedSearches = useMemo(() => { + return props.savedSearches + ?.filter((it) => it.visible) + ?.sort( + (left: SavedSearch, right: SavedSearch) => + left.position - right.position + ) + }, [props.savedSearches]) + + useRegisterActions( + (sortedSearches ?? []).map((item, idx) => { + const key = String(idx + 1) + return { + id: `saved_search_${key}`, + name: item.name, + shortcut: [key], + section: 'Saved Searches', + keywords: '?' + item.name, + perform: () => { + props.applySearchQuery(item.filter) + }, + } + }), + [props.savedSearches] + ) + + const [collapsed, setCollapsed] = usePersistedState({ + key: `--saved-searches-collapsed`, + initialValue: false, + }) + + return ( + + {!collapsed && + sortedSearches && + sortedSearches?.map((item) => ( + + ))} + {!collapsed && sortedSearches !== undefined && ( + + )} + + + + ) +} + +function Subscriptions( + props: LibraryFilterMenuProps & { subscriptions: Subscription[] | undefined } +): JSX.Element { + const [collapsed, setCollapsed] = usePersistedState({ + key: `--subscriptions-collapsed`, + initialValue: false, + }) + + const sortedSubscriptions = useMemo(() => { + if (!props.subscriptions) { + return [] + } + return props.subscriptions + .filter((s) => s.status == 'ACTIVE') + .sort((a, b) => a.name.localeCompare(b.name)) + }, [props.subscriptions]) + + useRegisterActions( + (sortedSubscriptions ?? []).map((subscription, idx) => { + const key = String(idx + 1) + const name = subscription.name + return { + id: `subscription_${key}`, + section: 'Subscriptions', + name: name, + keywords: '*' + name, + perform: () => { + props.applySearchQuery(`subscription:\"${name}\"`) + }, + } + }), + [sortedSubscriptions] + ) + + return ( + + {!collapsed ? ( + <> + + + + {(sortedSubscriptions ?? []).map((item) => { + switch (item.type) { + case SubscriptionType.NEWSLETTER: + return ( + + ) + case SubscriptionType.RSS: + return ( + + ) + } + })} + + + ) : ( + + )} + + ) +} + +function Labels( + props: LibraryFilterMenuProps & { labels: Label[] } +): JSX.Element { + const [collapsed, setCollapsed] = usePersistedState({ + key: `--labels-collapsed`, + initialValue: false, + }) + + const sortedLabels = useMemo(() => { + return props.labels.sort((left: Label, right: Label) => + left.name.localeCompare(right.name) + ) + }, [props.labels]) + + return ( + + {!collapsed && ( + <> + {sortedLabels.map((item) => { + return + })} + + + )} + + ) +} + +type MenuPanelProps = { + title: string + children: ReactNode + editFunc?: () => void + editTitle?: string + hideBottomBorder?: boolean + collapsed: boolean + setCollapsed: (collapsed: boolean) => void +} + +function MenuPanel(props: MenuPanelProps): JSX.Element { + return ( + + + + {props.title} + + + + + + {props.children} + + ) +} + +type FilterButtonProps = { + text: string + + filterTerm: string + searchTerm: string | undefined + + applySearchQuery: (searchTerm: string) => void + + setShowFilterMenu: (show: boolean) => void +} + +function FilterButton(props: FilterButtonProps): JSX.Element { + const isInboxFilter = (filter: string) => { + return filter === '' || filter === 'in:inbox' + } + const selected = useMemo(() => { + if (isInboxFilter(props.filterTerm) && !props.searchTerm) { + return true + } + return props.searchTerm === props.filterTerm + }, [props.searchTerm, props.filterTerm]) + + return ( + { + props.applySearchQuery(props.filterTerm) + props.setShowFilterMenu(false) + e.preventDefault() + }} + > + {props.text} + + ) +} + +type LabelButtonProps = { + label: Label + searchTerm: string | undefined + applySearchQuery: (searchTerm: string) => void +} + +function LabelButton(props: LabelButtonProps): JSX.Element { + const labelId = `checkbox-label-${props.label.id}` + const checkboxRef = useRef(null) + const state = useMemo(() => { + const term = props.searchTerm ?? '' + if (term.indexOf(`label:\"${props.label.name}\"`) >= 0) { + return 'on' + } + return 'off' + }, [props.searchTerm, props.label]) + + return ( + + + + { + if (e.target.checked) { + props.applySearchQuery( + `${props.searchTerm ?? ''} label:\"${props.label.name}\"` + ) + } else { + const query = + props.searchTerm?.replace( + `label:\"${props.label.name}\"`, + '' + ) ?? '' + props.applySearchQuery(query) + } + }} + /> + + + ) +} + +type EditButtonProps = { + title: string + destination: string +} + +function EditButton(props: EditButtonProps): JSX.Element { + return ( + + + {props.title} + + + ) +} diff --git a/packages/web/components/templates/navMenu/SettingsMenu.tsx b/packages/web/components/templates/navMenu/SettingsMenu.tsx new file mode 100644 index 000000000..c674d7282 --- /dev/null +++ b/packages/web/components/templates/navMenu/SettingsMenu.tsx @@ -0,0 +1,257 @@ +import { useMemo } from 'react' +import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { LIBRARY_LEFT_MENU_WIDTH } from './LibraryMenu' +import { LogoBox } from '../../elements/LogoBox' +import Link from 'next/link' +import { styled, theme } from '../../tokens/stitches.config' +import { Button } from '../../elements/Button' +import { ArrowSquareUpRight } from 'phosphor-react' +import { useRouter } from 'next/router' +import { NavMenuFooter } from './Footer' + +const HorizontalDivider = styled(SpanBox, { + width: '100%', + height: '1px', + my: '25px', + background: `${theme.colors.grayLine.toString()}`, +}) + +const StyledLink = styled(SpanBox, { + pl: '25px', + ml: '10px', + mb: '10px', + display: 'flex', + alignItems: 'center', + gap: '2px', + '&:hover': { + textDecoration: 'underline', + }, + + width: 'calc(100% - 10px)', + maxWidth: '100%', + height: '32px', + + fontSize: '14px', + fontWeight: 'regular', + fontFamily: '$display', + color: '$thLibraryMenuUnselected', + verticalAlign: 'middle', + borderRadius: '3px', + cursor: 'pointer', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}) + +type ExternalLinkProps = { + title: string + destination: string +} + +function ExternalLink(props: ExternalLinkProps): JSX.Element { + return ( + a': { + backgroundColor: 'transparent', + textDecoration: 'none', + }, + }} + title={props.title} + > + + + {props.title} + + + + + ) +} + +export function SettingsMenu(): JSX.Element { + const section1 = [ + { name: 'Account', destination: '/settings/account' }, + { name: 'API Keys', destination: '/settings/api' }, + { name: 'Emails', destination: '/settings/emails' }, + { name: 'Feeds', destination: '/settings/feeds' }, + { name: 'Subscriptions', destination: '/settings/subscriptions' }, + { name: 'Labels', destination: '/settings/labels' }, + { name: 'Saved Searches', destination: '/settings/saved-searches' }, + { name: 'Pinned Searches', destination: '/settings/pinned-searches' }, + ] + + const section2 = [ + { name: 'Integrations', destination: '/settings/integrations' }, + { name: 'Install', destination: '/settings/installation' }, + ] + return ( + <> + + + + + + + {section1.map((item) => { + return + })} + + {section2.map((item) => { + return + })} + + + + + + + + + + {/* This spacer pushes library content to the right of + the fixed left side menu. */} + + + ) +} + +type SettingsButtonProps = { + name: string + destination: string +} + +function SettingsButton(props: SettingsButtonProps): JSX.Element { + const router = useRouter() + const selected = useMemo(() => { + if (router && router.isReady) { + return router.asPath.endsWith(props.destination) + } + return false + }, [props, router]) + + return ( + + + {props.name} + + + ) +}