Toast (#431)
* Prototype of toast system * More toast adoption * share * flag * bookmark * subscribe * delete * More toast usage: * forms * settings save * Log error during flag failure * Incorporate PR feedback: 1. return `toaster` from `useToast` hook, with simplified `success` and `danger` methods 2. remove toast header, move close button to body 3. change how toast ids are generated to use a global incrementing int 4. update toast messages * PR feedback: * reduce width of toast on narrow screens * dynamic delete success toast message based on deleted type * add toasts to auth methods deletion operations * Dismiss all toasts upon page navigation * refine style and use delay prop * more styling --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
parent
61c7bb28c2
commit
0ee056b2a1
|
@ -1,8 +1,10 @@
|
||||||
import { useMutation } from '@apollo/client'
|
import { useMutation } from '@apollo/client'
|
||||||
import { gql } from 'graphql-tag'
|
import { gql } from 'graphql-tag'
|
||||||
import Dropdown from 'react-bootstrap/Dropdown'
|
import Dropdown from 'react-bootstrap/Dropdown'
|
||||||
|
import { useToast } from './toast'
|
||||||
|
|
||||||
export default function BookmarkDropdownItem ({ item: { id, meBookmark } }) {
|
export default function BookmarkDropdownItem ({ item: { id, meBookmark } }) {
|
||||||
|
const toaster = useToast()
|
||||||
const [bookmarkItem] = useMutation(
|
const [bookmarkItem] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation bookmarkItem($id: ID!) {
|
mutation bookmarkItem($id: ID!) {
|
||||||
|
@ -22,7 +24,15 @@ export default function BookmarkDropdownItem ({ item: { id, meBookmark } }) {
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() => bookmarkItem({ variables: { id } })}
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await bookmarkItem({ variables: { id } })
|
||||||
|
toaster.success(meBookmark ? 'bookmark removed' : 'bookmark added')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toaster.danger(meBookmark ? 'failed to remove bookmark' : 'failed to bookmark')
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{meBookmark ? 'remove bookmark' : 'bookmark'}
|
{meBookmark ? 'remove bookmark' : 'bookmark'}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<div className='d-flex justify-content-between'>
|
<div className='d-flex justify-content-between'>
|
||||||
<Delete itemId={comment.id} onDelete={onSuccess}>
|
<Delete itemId={comment.id} onDelete={onSuccess} type='comment'>
|
||||||
<Button variant='grey-medium'>delete</Button>
|
<Button variant='grey-medium'>delete</Button>
|
||||||
</Delete>
|
</Delete>
|
||||||
<EditFeeButton
|
<EditFeeButton
|
||||||
|
|
|
@ -5,8 +5,9 @@ import Alert from 'react-bootstrap/Alert'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import Dropdown from 'react-bootstrap/Dropdown'
|
import Dropdown from 'react-bootstrap/Dropdown'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
|
import { useToast } from './toast'
|
||||||
|
|
||||||
export default function Delete ({ itemId, children, onDelete }) {
|
export default function Delete ({ itemId, children, onDelete, type = 'post' }) {
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
|
|
||||||
const [deleteItem] = useMutation(
|
const [deleteItem] = useMutation(
|
||||||
|
@ -40,6 +41,7 @@ export default function Delete ({ itemId, children, onDelete }) {
|
||||||
showModal(onClose => {
|
showModal(onClose => {
|
||||||
return (
|
return (
|
||||||
<DeleteConfirm
|
<DeleteConfirm
|
||||||
|
type={type}
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
const { error } = await deleteItem({ variables: { id: itemId } })
|
const { error } = await deleteItem({ variables: { id: itemId } })
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -59,8 +61,9 @@ export default function Delete ({ itemId, children, onDelete }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeleteConfirm ({ onConfirm }) {
|
function DeleteConfirm ({ onConfirm, type }) {
|
||||||
const [error, setError] = useState()
|
const [error, setError] = useState()
|
||||||
|
const toaster = useToast()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -71,6 +74,7 @@ function DeleteConfirm ({ onConfirm }) {
|
||||||
variant='danger' onClick={async () => {
|
variant='danger' onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await onConfirm()
|
await onConfirm()
|
||||||
|
toaster.success(`deleted ${type.toLowerCase()}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e.message || e)
|
setError(e.message || e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@ import { gql, useMutation } from '@apollo/client'
|
||||||
import Dropdown from 'react-bootstrap/Dropdown'
|
import Dropdown from 'react-bootstrap/Dropdown'
|
||||||
import FundError from './fund-error'
|
import FundError from './fund-error'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
|
import { useToast } from './toast'
|
||||||
|
|
||||||
export default function DontLikeThisDropdownItem ({ id }) {
|
export default function DontLikeThisDropdownItem ({ id }) {
|
||||||
|
const toaster = useToast()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
|
|
||||||
const [dontLikeThis] = useMutation(
|
const [dontLikeThis] = useMutation(
|
||||||
|
@ -32,11 +34,15 @@ export default function DontLikeThisDropdownItem ({ id }) {
|
||||||
variables: { id },
|
variables: { id },
|
||||||
optimisticResponse: { dontLikeThis: true }
|
optimisticResponse: { dontLikeThis: true }
|
||||||
})
|
})
|
||||||
|
toaster.success('item flagged')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
if (error.toString().includes('insufficient funds')) {
|
if (error.toString().includes('insufficient funds')) {
|
||||||
showModal(onClose => {
|
showModal(onClose => {
|
||||||
return <FundError onClose={onClose} />
|
return <FundError onClose={onClose} />
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
toaster.danger('failed to flag item')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import InputGroup from 'react-bootstrap/InputGroup'
|
import InputGroup from 'react-bootstrap/InputGroup'
|
||||||
import BootstrapForm from 'react-bootstrap/Form'
|
import BootstrapForm from 'react-bootstrap/Form'
|
||||||
import Alert from 'react-bootstrap/Alert'
|
|
||||||
import { Formik, Form as FormikForm, useFormikContext, useField, FieldArray } from 'formik'
|
import { Formik, Form as FormikForm, useFormikContext, useField, FieldArray } from 'formik'
|
||||||
import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
|
import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
|
||||||
import copy from 'clipboard-copy'
|
import copy from 'clipboard-copy'
|
||||||
|
@ -19,6 +18,7 @@ import CloseIcon from '../svgs/close-line.svg'
|
||||||
import { useLazyQuery } from '@apollo/client'
|
import { useLazyQuery } from '@apollo/client'
|
||||||
import { USER_SEARCH } from '../fragments/users'
|
import { USER_SEARCH } from '../fragments/users'
|
||||||
import TextareaAutosize from 'react-textarea-autosize'
|
import TextareaAutosize from 'react-textarea-autosize'
|
||||||
|
import { useToast } from './toast'
|
||||||
|
|
||||||
export function SubmitButton ({
|
export function SubmitButton ({
|
||||||
children, variant, value, onClick, disabled, ...props
|
children, variant, value, onClick, disabled, ...props
|
||||||
|
@ -472,7 +472,12 @@ const StorageKeyPrefixContext = createContext()
|
||||||
export function Form ({
|
export function Form ({
|
||||||
initial, schema, onSubmit, children, initialError, validateImmediately, storageKeyPrefix, validateOnChange = true, ...props
|
initial, schema, onSubmit, children, initialError, validateImmediately, storageKeyPrefix, validateOnChange = true, ...props
|
||||||
}) {
|
}) {
|
||||||
const [error, setError] = useState(initialError)
|
const toaster = useToast()
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialError) {
|
||||||
|
toaster.danger(initialError.message || initialError.toString?.())
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
|
@ -481,26 +486,31 @@ export function Form ({
|
||||||
validationSchema={schema}
|
validationSchema={schema}
|
||||||
initialTouched={validateImmediately && initial}
|
initialTouched={validateImmediately && initial}
|
||||||
validateOnBlur={false}
|
validateOnBlur={false}
|
||||||
onSubmit={async (values, ...args) =>
|
onSubmit={async (values, ...args) => {
|
||||||
onSubmit && onSubmit(values, ...args).then((options) => {
|
try {
|
||||||
if (!storageKeyPrefix || options?.keepLocalStorage) return
|
if (onSubmit) {
|
||||||
Object.keys(values).forEach(v => {
|
const options = await onSubmit(values, ...args)
|
||||||
window.localStorage.removeItem(storageKeyPrefix + '-' + v)
|
if (!storageKeyPrefix || options?.keepLocalStorage) return
|
||||||
if (Array.isArray(values[v])) {
|
Object.keys(values).forEach(v => {
|
||||||
values[v].forEach(
|
window.localStorage.removeItem(storageKeyPrefix + '-' + v)
|
||||||
(iv, i) => {
|
if (Array.isArray(values[v])) {
|
||||||
Object.keys(iv).forEach(k => {
|
values[v].forEach(
|
||||||
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}].${k}`)
|
(iv, i) => {
|
||||||
|
Object.keys(iv).forEach(k => {
|
||||||
|
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}].${k}`)
|
||||||
|
})
|
||||||
|
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}]`)
|
||||||
})
|
})
|
||||||
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}]`)
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
} catch (err) {
|
||||||
}).catch(e => setError(e.message || e))}
|
console.log(err)
|
||||||
|
toaster.danger(err.message || err.toString?.())
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<FormikForm {...props} noValidate>
|
<FormikForm {...props} noValidate>
|
||||||
{error && <Alert variant='danger' onClose={() => setError(undefined)} dismissible>{error}</Alert>}
|
|
||||||
<StorageKeyPrefixContext.Provider value={storageKeyPrefix}>
|
<StorageKeyPrefixContext.Provider value={storageKeyPrefix}>
|
||||||
{children}
|
{children}
|
||||||
</StorageKeyPrefixContext.Provider>
|
</StorageKeyPrefixContext.Provider>
|
||||||
|
|
|
@ -140,7 +140,7 @@ export default function ItemInfo ({
|
||||||
{me && !item.meSats && !item.position && !item.meDontLike &&
|
{me && !item.meSats && !item.position && !item.meDontLike &&
|
||||||
!item.mine && !item.deletedAt && <DontLikeThisDropdownItem id={item.id} />}
|
!item.mine && !item.deletedAt && <DontLikeThisDropdownItem id={item.id} />}
|
||||||
{item.mine && !item.position && !item.deletedAt &&
|
{item.mine && !item.position && !item.deletedAt &&
|
||||||
<DeleteDropdownItem itemId={item.id} />}
|
<DeleteDropdownItem itemId={item.id} type={item.title ? 'post' : 'comment'} />}
|
||||||
</ItemDropdown>
|
</ItemDropdown>
|
||||||
{extraInfo}
|
{extraInfo}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,9 +2,11 @@ import Dropdown from 'react-bootstrap/Dropdown'
|
||||||
import ShareIcon from '../svgs/share-fill.svg'
|
import ShareIcon from '../svgs/share-fill.svg'
|
||||||
import copy from 'clipboard-copy'
|
import copy from 'clipboard-copy'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
|
import { useToast } from './toast'
|
||||||
|
|
||||||
export default function Share ({ item }) {
|
export default function Share ({ item }) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
|
const toaster = useToast()
|
||||||
const url = `https://stacker.news/items/${item.id}${me ? `/r/${me.name}` : ''}`
|
const url = `https://stacker.news/items/${item.id}${me ? `/r/${me.name}` : ''}`
|
||||||
|
|
||||||
return typeof window !== 'undefined' && navigator?.share
|
return typeof window !== 'undefined' && navigator?.share
|
||||||
|
@ -13,16 +15,17 @@ export default function Share ({ item }) {
|
||||||
<ShareIcon
|
<ShareIcon
|
||||||
width={20} height={20}
|
width={20} height={20}
|
||||||
className='mx-2 fill-grey theme'
|
className='mx-2 fill-grey theme'
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (navigator.share) {
|
try {
|
||||||
navigator.share({
|
await navigator.share({
|
||||||
title: item.title || '',
|
title: item.title || '',
|
||||||
text: '',
|
text: '',
|
||||||
url
|
url
|
||||||
}).then(() => console.log('Successful share'))
|
})
|
||||||
.catch((error) => console.log('Error sharing', error))
|
toaster.success('link shared')
|
||||||
} else {
|
} catch (err) {
|
||||||
console.log('no navigator.share')
|
console.error(err)
|
||||||
|
toaster.danger('failed to share link')
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -36,7 +39,13 @@ export default function Share ({ item }) {
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
copy(url)
|
try {
|
||||||
|
await copy(url)
|
||||||
|
toaster.success('link copied')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toaster.danger('failed to copy link')
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
copy link
|
copy link
|
||||||
|
@ -47,19 +56,25 @@ export default function Share ({ item }) {
|
||||||
|
|
||||||
export function CopyLinkDropdownItem ({ item }) {
|
export function CopyLinkDropdownItem ({ item }) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
|
const toaster = useToast()
|
||||||
const url = `https://stacker.news/items/${item.id}${me ? `/r/${me.name}` : ''}`
|
const url = `https://stacker.news/items/${item.id}${me ? `/r/${me.name}` : ''}`
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (navigator.share) {
|
try {
|
||||||
navigator.share({
|
if (navigator.share) {
|
||||||
title: item.title || '',
|
await navigator.share({
|
||||||
text: '',
|
title: item.title || '',
|
||||||
url
|
text: '',
|
||||||
}).then(() => console.log('Successful share'))
|
url
|
||||||
.catch((error) => console.log('Error sharing', error))
|
})
|
||||||
} else {
|
} else {
|
||||||
copy(url)
|
await copy(url)
|
||||||
|
}
|
||||||
|
toaster.success('link copied')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toaster.danger('failed to copy link')
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { useMutation } from '@apollo/client'
|
import { useMutation } from '@apollo/client'
|
||||||
import { gql } from 'graphql-tag'
|
import { gql } from 'graphql-tag'
|
||||||
import Dropdown from 'react-bootstrap/Dropdown'
|
import Dropdown from 'react-bootstrap/Dropdown'
|
||||||
|
import { useToast } from './toast'
|
||||||
|
|
||||||
export default function SubscribeDropdownItem ({ item: { id, meSubscription } }) {
|
export default function SubscribeDropdownItem ({ item: { id, meSubscription } }) {
|
||||||
|
const toaster = useToast()
|
||||||
const [subscribeItem] = useMutation(
|
const [subscribeItem] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation subscribeItem($id: ID!) {
|
mutation subscribeItem($id: ID!) {
|
||||||
|
@ -22,7 +24,15 @@ export default function SubscribeDropdownItem ({ item: { id, meSubscription } })
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() => subscribeItem({ variables: { id } })}
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await subscribeItem({ variables: { id } })
|
||||||
|
toaster.success(meSubscription ? 'unsubscribed' : 'subscribed')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toaster.danger(meSubscription ? 'failed to unsubscribe' : 'failed to subscribe')
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{meSubscription ? 'remove subscription' : 'subscribe'}
|
{meSubscription ? 'remove subscription' : 'subscribe'}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import Button from 'react-bootstrap/Button'
|
||||||
|
import Toast from 'react-bootstrap/Toast'
|
||||||
|
import ToastBody from 'react-bootstrap/ToastBody'
|
||||||
|
import ToastContainer from 'react-bootstrap/ToastContainer'
|
||||||
|
import styles from './toast.module.css'
|
||||||
|
|
||||||
|
const ToastContext = createContext(() => {})
|
||||||
|
|
||||||
|
export const ToastProvider = ({ children }) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const [toasts, setToasts] = useState([])
|
||||||
|
const toastId = useRef(0)
|
||||||
|
const dispatchToast = useCallback((toastConfig) => {
|
||||||
|
toastConfig = {
|
||||||
|
...toastConfig,
|
||||||
|
id: toastId.current++
|
||||||
|
}
|
||||||
|
setToasts(toasts => [...toasts, toastConfig])
|
||||||
|
}, [])
|
||||||
|
const toaster = useMemo(() => ({
|
||||||
|
success: body => {
|
||||||
|
dispatchToast({
|
||||||
|
body,
|
||||||
|
variant: 'success',
|
||||||
|
autohide: true,
|
||||||
|
delay: 5000
|
||||||
|
})
|
||||||
|
},
|
||||||
|
danger: body => {
|
||||||
|
dispatchToast({
|
||||||
|
body,
|
||||||
|
variant: 'danger',
|
||||||
|
autohide: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}), [dispatchToast])
|
||||||
|
const removeToast = useCallback(id => {
|
||||||
|
setToasts(toasts => toasts.filter(toast => toast.id !== id))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Clear all toasts on page navigation
|
||||||
|
useEffect(() => {
|
||||||
|
const handleRouteChangeStart = () => setToasts([])
|
||||||
|
router.events.on('routeChangeStart', handleRouteChangeStart)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
router.events.off('routeChangeStart', handleRouteChangeStart)
|
||||||
|
}
|
||||||
|
}, [router])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={toaster}>
|
||||||
|
<ToastContainer className='pb-3 pe-3' position='bottom-end' containerPosition='fixed'>
|
||||||
|
{toasts.map(toast => (
|
||||||
|
<Toast
|
||||||
|
key={toast.id} bg={toast.variant} show autohide={toast.autohide}
|
||||||
|
delay={toast.delay} className={styles.toast} onClose={() => removeToast(toast.id)}
|
||||||
|
>
|
||||||
|
<ToastBody>
|
||||||
|
<div className='d-flex align-items-center'>
|
||||||
|
<div className='flex-grow-1'>{toast.body}</div>
|
||||||
|
<Button
|
||||||
|
variant={null}
|
||||||
|
className='p-0 ps-2'
|
||||||
|
aria-label='close'
|
||||||
|
onClick={() => removeToast(toast.id)}
|
||||||
|
><div className={styles.toastClose}>X</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ToastBody>
|
||||||
|
</Toast>
|
||||||
|
))}
|
||||||
|
</ToastContainer>
|
||||||
|
{children}
|
||||||
|
</ToastContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useToast = () => useContext(ToastContext)
|
|
@ -0,0 +1,26 @@
|
||||||
|
.toast {
|
||||||
|
width: auto;
|
||||||
|
border: 1px solid var(--bs-success-border-subtle);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastClose {
|
||||||
|
color: #fff;
|
||||||
|
font-family: "lightning";
|
||||||
|
font-size: 150%;
|
||||||
|
line-height: 1rem;
|
||||||
|
margin-bottom: -0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastClose:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 400px) {
|
||||||
|
.toast {
|
||||||
|
width: var(--bs-toast-max-width);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { useEffect } from 'react'
|
||||||
import { ShowModalProvider } from '../components/modal'
|
import { ShowModalProvider } from '../components/modal'
|
||||||
import ErrorBoundary from '../components/error-boundary'
|
import ErrorBoundary from '../components/error-boundary'
|
||||||
import { LightningProvider } from '../components/lightning'
|
import { LightningProvider } from '../components/lightning'
|
||||||
|
import { ToastProvider } from '../components/toast'
|
||||||
import { ServiceWorkerProvider } from '../components/serviceworker'
|
import { ServiceWorkerProvider } from '../components/serviceworker'
|
||||||
import { SSR } from '../lib/constants'
|
import { SSR } from '../lib/constants'
|
||||||
import NProgress from 'nprogress'
|
import NProgress from 'nprogress'
|
||||||
|
@ -90,11 +91,13 @@ function MyApp ({ Component, pageProps: { ...props } }) {
|
||||||
<ServiceWorkerProvider>
|
<ServiceWorkerProvider>
|
||||||
<PriceProvider price={price}>
|
<PriceProvider price={price}>
|
||||||
<LightningProvider>
|
<LightningProvider>
|
||||||
<PaymentTokenProvider>
|
<ToastProvider>
|
||||||
<ShowModalProvider>
|
<PaymentTokenProvider>
|
||||||
<Component ssrData={ssrData} {...otherProps} />
|
<ShowModalProvider>
|
||||||
</ShowModalProvider>
|
<Component ssrData={ssrData} {...otherProps} />
|
||||||
</PaymentTokenProvider>
|
</ShowModalProvider>
|
||||||
|
</PaymentTokenProvider>
|
||||||
|
</ToastProvider>
|
||||||
</LightningProvider>
|
</LightningProvider>
|
||||||
</PriceProvider>
|
</PriceProvider>
|
||||||
</ServiceWorkerProvider>
|
</ServiceWorkerProvider>
|
||||||
|
|
|
@ -22,6 +22,7 @@ import PageLoading from '../components/page-loading'
|
||||||
import { useShowModal } from '../components/modal'
|
import { useShowModal } from '../components/modal'
|
||||||
import { authErrorMessage } from '../components/login'
|
import { authErrorMessage } from '../components/login'
|
||||||
import { NostrAuth } from '../components/nostr-auth'
|
import { NostrAuth } from '../components/nostr-auth'
|
||||||
|
import { useToast } from '../components/toast'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ function bech32encode (hexString) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Settings ({ ssrData }) {
|
export default function Settings ({ ssrData }) {
|
||||||
const [success, setSuccess] = useState()
|
const toaster = useToast()
|
||||||
const [setSettings] = useMutation(SET_SETTINGS, {
|
const [setSettings] = useMutation(SET_SETTINGS, {
|
||||||
update (cache, { data: { setSettings } }) {
|
update (cache, { data: { setSettings } }) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
|
@ -89,18 +90,22 @@ export default function Settings ({ ssrData }) {
|
||||||
|
|
||||||
const nostrRelaysFiltered = nostrRelays?.filter(word => word.trim().length > 0)
|
const nostrRelaysFiltered = nostrRelays?.filter(word => word.trim().length > 0)
|
||||||
|
|
||||||
await setSettings({
|
try {
|
||||||
variables: {
|
await setSettings({
|
||||||
tipDefault: Number(tipDefault),
|
variables: {
|
||||||
nostrPubkey,
|
tipDefault: Number(tipDefault),
|
||||||
nostrRelays: nostrRelaysFiltered,
|
nostrPubkey,
|
||||||
...values
|
nostrRelays: nostrRelaysFiltered,
|
||||||
}
|
...values
|
||||||
})
|
}
|
||||||
setSuccess('settings saved')
|
})
|
||||||
|
toaster.success('saved settings')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toaster.danger('failed to save settings')
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{success && <Alert variant='info' onClose={() => setSuccess(undefined)} dismissible>{success}</Alert>}
|
|
||||||
<Input
|
<Input
|
||||||
label='zap default'
|
label='zap default'
|
||||||
name='tipDefault'
|
name='tipDefault'
|
||||||
|
@ -329,6 +334,7 @@ function NostrLinkButton ({ unlink, status }) {
|
||||||
|
|
||||||
function UnlinkObstacle ({ onClose, type, unlinkAuth }) {
|
function UnlinkObstacle ({ onClose, type, unlinkAuth }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const toaster = useToast()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -344,9 +350,15 @@ function UnlinkObstacle ({ onClose, type, unlinkAuth }) {
|
||||||
}}
|
}}
|
||||||
schema={lastAuthRemovalSchema}
|
schema={lastAuthRemovalSchema}
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
await unlinkAuth({ variables: { authType: type } })
|
try {
|
||||||
router.push('/settings')
|
await unlinkAuth({ variables: { authType: type } })
|
||||||
onClose()
|
router.push('/settings')
|
||||||
|
onClose()
|
||||||
|
toaster.success('unlinked auth method')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toaster.danger('failed to unlink auth method')
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
@ -362,6 +374,7 @@ function UnlinkObstacle ({ onClose, type, unlinkAuth }) {
|
||||||
function AuthMethods ({ methods }) {
|
function AuthMethods ({ methods }) {
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const toaster = useToast()
|
||||||
const [err, setErr] = useState(authErrorMessage(router.query.error))
|
const [err, setErr] = useState(authErrorMessage(router.query.error))
|
||||||
const [unlinkAuth] = useMutation(
|
const [unlinkAuth] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
|
@ -396,7 +409,13 @@ function AuthMethods ({ methods }) {
|
||||||
if (links === 1) {
|
if (links === 1) {
|
||||||
showModal(onClose => (<UnlinkObstacle onClose={onClose} type={type} unlinkAuth={unlinkAuth} />))
|
showModal(onClose => (<UnlinkObstacle onClose={onClose} type={type} unlinkAuth={unlinkAuth} />))
|
||||||
} else {
|
} else {
|
||||||
await unlinkAuth({ variables: { authType: type } })
|
try {
|
||||||
|
await unlinkAuth({ variables: { authType: type } })
|
||||||
|
toaster.success('unlinked auth method')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toaster.danger('failed to unlink auth method')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,7 @@ $popover-body-padding-y: .5rem;
|
||||||
$popover-max-width: 320px !default;
|
$popover-max-width: 320px !default;
|
||||||
$popover-border-color: var(--theme-borderColor);
|
$popover-border-color: var(--theme-borderColor);
|
||||||
$grid-gutter-width: 2rem;
|
$grid-gutter-width: 2rem;
|
||||||
|
$toast-spacing: .5rem;
|
||||||
|
|
||||||
:root, [data-bs-theme=light] {
|
:root, [data-bs-theme=light] {
|
||||||
--theme-navLink: rgba(0, 0, 0, 0.55);
|
--theme-navLink: rgba(0, 0, 0, 0.55);
|
||||||
|
@ -153,6 +154,10 @@ $grid-gutter-width: 2rem;
|
||||||
height: 2px !important;
|
height: 2px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-body {
|
||||||
|
padding: 0.75rem 1.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
#nprogress .peg {
|
#nprogress .peg {
|
||||||
box-shadow: 0 0 10px var(--bs-primary), 0 0 5px var(--bs-primary) !important;
|
box-shadow: 0 0 10px var(--bs-primary), 0 0 5px var(--bs-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue