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..46b65d762 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' @@ -110,6 +106,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( @@ -1199,10 +1208,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/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..5ea6f01dd 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' })