diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index f220190ff..3aadaf088 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -77,9 +77,9 @@ export const Button = styled('button', { fontFamily: 'Inter', borderRadius: '8px', cursor: 'pointer', - color: '$grayTextContrast', + color: 'white', p: '10px 12px', - bg: 'rgb(125, 125, 125, 0.1)', + bg: 'rgb(125, 125, 125, 0.3)', '&:hover': { bg: 'rgb(47, 47, 47, 0.1)', '.ctaButtonIcon': { diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 039ed728f..75bb983dc 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -1,14 +1,7 @@ import { Action, createAction, useKBar, useRegisterActions } from 'kbar' import debounce from 'lodash/debounce' import { useRouter } from 'next/router' -import { - useCallback, - useEffect, - useMemo, - useReducer, - useRef, - useState, -} from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Toaster } from 'react-hot-toast' import TopBarProgress from 'react-topbar-progress-indicator' import { useFetchMore } from '../../../lib/hooks/useFetchMoreScroll' @@ -48,10 +41,13 @@ import { LibraryHeader, MultiSelectMode } from './LibraryHeader' import { UploadModal } from '../UploadModal' import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation' import { bulkActionMutation } from '../../../lib/networking/mutations/bulkActionMutation' -import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' +import { + showErrorToast, + showSuccessToast, + showSuccessToastWithUndo, +} from '../../../lib/toastHelpers' import { SetPageLabelsModalPresenter } from '../article/SetLabelsModalPresenter' import { NotebookPresenter } from '../article/NotebookPresenter' -import { Highlight } from '../../../lib/networking/fragments/highlightFragment' export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT' export type LibraryMode = 'reads' | 'highlights' @@ -94,7 +90,6 @@ export function HomeFeedContainer(): JSX.Element { const [showAddLinkModal, setShowAddLinkModal] = useState(false) const [showEditTitleModal, setShowEditTitleModal] = useState(false) - const [linkToRemove, setLinkToRemove] = useState() const [linkToEdit, setLinkToEdit] = useState() const [linkToUnsubscribe, setLinkToUnsubscribe] = useState() @@ -110,6 +105,19 @@ export function HomeFeedContainer(): JSX.Element { mutate, } = useGetLibraryItemsQuery(queryInputs) + useEffect(() => { + const handleRevalidate = () => { + ;(async () => { + console.log('revalidating library') + await mutate() + })() + } + document.addEventListener('revalidateLibrary', handleRevalidate) + return () => { + document.removeEventListener('revalidateLibrary', handleRevalidate) + } + }, [mutate]) + useEffect(() => { if (queryValue.startsWith('#')) { debouncedFetchSearchResults( @@ -367,8 +375,8 @@ export function HomeFeedContainer(): JSX.Element { } const modalTargetItem = useMemo(() => { - return labelsTarget || linkToEdit || linkToRemove || linkToUnsubscribe - }, [labelsTarget, linkToEdit, linkToRemove, linkToUnsubscribe]) + return labelsTarget || linkToEdit || linkToUnsubscribe + }, [labelsTarget, linkToEdit, linkToUnsubscribe]) const [checkedItems, setCheckedItems] = useState([]) const [multiSelectMode, setMultiSelectMode] = useState('off') @@ -758,8 +766,6 @@ export function HomeFeedContainer(): JSX.Element { setActiveItem={(item: LibraryItem) => { activateCard(item.node.id) }} - linkToRemove={linkToRemove} - setLinkToRemove={setLinkToRemove} linkToEdit={linkToEdit} setLinkToEdit={setLinkToEdit} linkToUnsubscribe={linkToUnsubscribe} @@ -796,8 +802,6 @@ type HomeFeedContentProps = { setShowEditTitleModal: (show: boolean) => void setActiveItem: (item: LibraryItem) => void - linkToRemove: LibraryItem | undefined - setLinkToRemove: (set: LibraryItem | undefined) => void linkToEdit: LibraryItem | undefined setLinkToEdit: (set: LibraryItem | undefined) => void linkToUnsubscribe: LibraryItem | undefined @@ -911,23 +915,11 @@ type LibraryItemsLayoutProps = { } & HomeFeedContentProps function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element { - const [showRemoveLinkConfirmation, setShowRemoveLinkConfirmation] = - useState(false) const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] = useState(false) const [showUploadModal, setShowUploadModal] = useState(false) const [, updateState] = useState({}) - const removeItem = () => { - if (!props.linkToRemove) { - return - } - - props.actionHandler('delete', props.linkToRemove) - props.setLinkToRemove(undefined) - setShowRemoveLinkConfirmation(false) - } - const unsubscribe = () => { if (!props.linkToUnsubscribe) { return @@ -977,9 +969,7 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element { setShowEditTitleModal={props.setShowEditTitleModal} setLinkToEdit={props.setLinkToEdit} setShowUnsubscribeConfirmation={setShowUnsubscribeConfirmation} - setLinkToRemove={props.setLinkToRemove} setLinkToUnsubscribe={props.setLinkToUnsubscribe} - setShowRemoveLinkConfirmation={setShowRemoveLinkConfirmation} actionHandler={props.actionHandler} multiSelectMode={props.multiSelectMode} /> @@ -1014,43 +1004,6 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element { item={props.linkToEdit as LibraryItem} /> )} - {showRemoveLinkConfirmation && ( - - - Are you sure you want to delete this item? All associated notes - and highlights will be deleted. - - {props.linkToRemove?.node && props.viewer && ( - - {}} - // eslint-disable-next-line @typescript-eslint/no-empty-function - handleAction={() => {}} - /> - - )} - - } - onAccept={removeItem} - acceptButtonLabel="Delete Item" - onOpenChange={() => setShowRemoveLinkConfirmation(false)} - /> - )} {showUnsubscribeConfirmation && ( void setLinkToEdit: (set: LibraryItem | undefined) => void setShowUnsubscribeConfirmation: (show: true) => void - setLinkToRemove: (set: LibraryItem | undefined) => void setLinkToUnsubscribe: (set: LibraryItem | undefined) => void - setShowRemoveLinkConfirmation: (show: true) => void isChecked: (itemId: string) => boolean setIsChecked: (itemId: string, set: boolean) => void @@ -1199,10 +1150,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element { setIsChecked={props.setIsChecked} multiSelectMode={props.multiSelectMode} handleAction={(action: LinkedItemCardAction) => { - if (action === 'delete') { - props.setShowRemoveLinkConfirmation(true) - props.setLinkToRemove(linkedItem) - } else if (action === 'editTitle') { + if (action === 'editTitle') { props.setShowEditTitleModal(true) props.setLinkToEdit(linkedItem) } else if (action == 'unsubscribe') { diff --git a/packages/web/lib/hooks/useReaderSettings.tsx b/packages/web/lib/hooks/useReaderSettings.tsx index d93b419a9..186a9043b 100644 --- a/packages/web/lib/hooks/useReaderSettings.tsx +++ b/packages/web/lib/hooks/useReaderSettings.tsx @@ -15,11 +15,9 @@ export type ReaderSettings = { setMarginWidth: (newMarginWidth: number) => void showSetLabelsModal: boolean - showDeleteConfirmation: boolean showEditDisplaySettingsModal: boolean setShowSetLabelsModal: (showSetLabelsModal: boolean) => void - setShowDeleteConfirmation: (showDeleteConfirmation: boolean) => void setShowEditDisplaySettingsModal: ( showEditDisplaySettingsModal: boolean ) => void @@ -70,7 +68,6 @@ export const useReaderSettings = (): ReaderSettings => { const [showSetLabelsModal, setShowSetLabelsModal] = useState(false) const [showEditDisplaySettingsModal, setShowEditDisplaySettingsModal] = useState(false) - const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false) const updateFontSize = useCallback( (newFontSize: number) => { @@ -209,12 +206,10 @@ export const useReaderSettings = (): ReaderSettings => { setFontSize, setLineHeight, setMarginWidth, - showDeleteConfirmation, showSetLabelsModal, showEditDisplaySettingsModal, setShowSetLabelsModal, setShowEditDisplaySettingsModal, - setShowDeleteConfirmation, actionHandler, setFontFamily, fontFamily, diff --git a/packages/web/lib/networking/mutations/updatePageMutation.ts b/packages/web/lib/networking/mutations/updatePageMutation.ts index ee15559c2..b7d9b3b67 100644 --- a/packages/web/lib/networking/mutations/updatePageMutation.ts +++ b/packages/web/lib/networking/mutations/updatePageMutation.ts @@ -1,13 +1,15 @@ import { gql } from 'graphql-request' import { gqlFetcher } from '../networkHelpers' +import { State } from '../fragments/articleFragment' export type UpdatePageInput = { pageId: string - title: string + title?: string byline?: string | undefined - description: string + description?: string savedAt?: string publishedAt?: string + state?: State } export async function updatePageMutation( diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index 9b175684d..4a0535c28 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -1,15 +1,20 @@ import { gql } from 'graphql-request' import useSWRInfinite from 'swr/infinite' import { gqlFetcher } from '../networkHelpers' -import type { PageType, State } from '../fragments/articleFragment' +import { PageType, State } from '../fragments/articleFragment' import { ContentReader } from '../fragments/articleFragment' import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation' import { deleteLinkMutation } from '../mutations/deleteLinkMutation' import { unsubscribeMutation } from '../mutations/unsubscribeMutation' import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation' import { Label } from './../fragments/labelFragment' -import { showErrorToast, showSuccessToast } from '../../toastHelpers' +import { + showErrorToast, + showSuccessToast, + showSuccessToastWithUndo, +} from '../../toastHelpers' import { Highlight, highlightFragment } from '../fragments/highlightFragment' +import { updatePageMutation } from '../mutations/updatePageMutation' export interface ReadableItem { id: string @@ -343,9 +348,26 @@ export function useGetLibraryItemsQuery({ break case 'delete': updateData(undefined) - deleteLinkMutation(item.node.id).then((res) => { + + const pageId = item.node.id + deleteLinkMutation(pageId).then((res) => { if (res) { - showSuccessToast('Link removed', { position: 'bottom-right' }) + showSuccessToastWithUndo('Page deleted', async () => { + const result = await updatePageMutation({ + pageId: pageId, + state: State.SUCCEEDED, + }) + + mutate() + + if (result) { + showSuccessToast('Page recovered') + } else { + showErrorToast( + 'Error recovering page, check your deleted items' + ) + } + }) } else { showErrorToast('Error removing link', { position: 'bottom-right' }) } diff --git a/packages/web/lib/toastHelpers.tsx b/packages/web/lib/toastHelpers.tsx index 98503bccf..867880a41 100644 --- a/packages/web/lib/toastHelpers.tsx +++ b/packages/web/lib/toastHelpers.tsx @@ -2,6 +2,7 @@ import { toast, ToastOptions } from 'react-hot-toast' import { CheckCircle, WarningCircle, X } from 'phosphor-react' import { Box, HStack } from '../components/elements/LayoutPrimitives' import { styled } from '@stitches/react' +import { Button } from '../components/elements/Button' const toastStyles = { minWidth: 265, @@ -67,10 +68,64 @@ const showToast = ( ) } +const showToastWithUndo = ( + message: string, + background: string, + undoAction: () => Promise, + options?: ToastOptions +) => { + return toast( + ({ id }) => ( + + + {message} + + + + + ), + { + style: { + ...toastStyles, + background: background, + }, + duration: 3500, + ...options, + } + ) +} + export const showSuccessToast = (message: string, options?: ToastOptions) => { - return showToast(message, '#55B938', 'success', options) + return showToast(message, '#55B938', 'success', { + position: 'bottom-right', + ...options, + }) } export const showErrorToast = (message: string, options?: ToastOptions) => { - return showToast(message, '#cc0000', 'error', options) + return showToast(message, '#cc0000', 'error', { + position: 'bottom-right', + ...options, + }) +} + +export const showSuccessToastWithUndo = ( + message: string, + undoAction: () => Promise +) => { + return showToastWithUndo(message, '#55B938', undoAction, { + position: 'bottom-right', + }) } diff --git a/packages/web/pages/[username]/[slug]/index.tsx b/packages/web/pages/[username]/[slug]/index.tsx index 6f7a3ae94..a5a729bc6 100644 --- a/packages/web/pages/[username]/[slug]/index.tsx +++ b/packages/web/pages/[username]/[slug]/index.tsx @@ -27,7 +27,11 @@ import { ArticleActionsMenu } from '../../../components/templates/article/Articl import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLinkArchivedMutation' import { Label } from '../../../lib/networking/fragments/labelFragment' import { useSWRConfig } from 'swr' -import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' +import { + showErrorToast, + showSuccessToast, + showSuccessToastWithUndo, +} from '../../../lib/toastHelpers' import { SetLabelsModal } from '../../../components/templates/article/SetLabelsModal' import { DisplaySettingsModal } from '../../../components/templates/article/DisplaySettingsModal' import { useReaderSettings } from '../../../lib/hooks/useReaderSettings' @@ -41,6 +45,8 @@ import { VerticalArticleActionsMenu } from '../../../components/templates/articl import { PdfHeaderSpacer } from '../../../components/templates/article/PdfHeaderSpacer' import { EpubContainerProps } from '../../../components/templates/article/EpubContainer' import { useSetPageLabels } from '../../../lib/hooks/useSetPageLabels' +import { updatePageMutation } from '../../../lib/networking/mutations/updatePageMutation' +import { State } from '../../../lib/networking/fragments/articleFragment' const PdfArticleContainerNoSSR = dynamic( () => import('./../../../components/templates/article/PdfArticleContainer'), @@ -138,7 +144,7 @@ export default function Home(): JSX.Element { } break case 'delete': - readerSettings.setShowDeleteConfirmation(true) + await deleteCurrentItem() break case 'openOriginalArticle': const url = article?.url @@ -206,10 +212,23 @@ export default function Home(): JSX.Element { const deleteCurrentItem = useCallback(async () => { if (article) { - removeItemFromCache(cache, mutate, article.id) - await deleteLinkMutation(article.id).then((res) => { + const pageId = article.id + + removeItemFromCache(cache, mutate, pageId) + await deleteLinkMutation(pageId).then((res) => { if (res) { - showSuccessToast('Page deleted', { position: 'bottom-right' }) + showSuccessToastWithUndo('Page deleted', async () => { + const result = await updatePageMutation({ + pageId: pageId, + state: State.SUCCEEDED, + }) + document.dispatchEvent(new Event('revalidateLibrary')) + if (result) { + showSuccessToast('Page recovered') + } else { + showErrorToast('Error recovering page, check your deleted items') + } + }) } else { // todo: revalidate or put back in cache? showErrorToast('Error deleting page', { position: 'bottom-right' }) @@ -253,8 +272,6 @@ export default function Home(): JSX.Element { perform: () => { if ( readerSettings.showSetLabelsModal || - readerSettings.showDeleteConfirmation || - readerSettings.showDeleteConfirmation || readerSettings.showEditDisplaySettingsModal ) { return @@ -550,13 +567,6 @@ export default function Home(): JSX.Element { }} /> )} - {readerSettings.showDeleteConfirmation && ( - readerSettings.setShowDeleteConfirmation(false)} - /> - )} {article && showEditModal && (