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 { gql } from 'graphql-tag'
|
||||
import Dropdown from 'react-bootstrap/Dropdown'
|
||||
import { useToast } from './toast'
|
||||
|
||||
export default function BookmarkDropdownItem ({ item: { id, meBookmark } }) {
|
||||
const toaster = useToast()
|
||||
const [bookmarkItem] = useMutation(
|
||||
gql`
|
||||
mutation bookmarkItem($id: ID!) {
|
||||
|
@ -22,7 +24,15 @@ export default function BookmarkDropdownItem ({ item: { id, meBookmark } }) {
|
|||
)
|
||||
return (
|
||||
<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'}
|
||||
</Dropdown.Item>
|
||||
|
|
|
@ -51,7 +51,7 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
|
|||
required
|
||||
/>
|
||||
<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>
|
||||
</Delete>
|
||||
<EditFeeButton
|
||||
|
|
|
@ -5,8 +5,9 @@ import Alert from 'react-bootstrap/Alert'
|
|||
import Button from 'react-bootstrap/Button'
|
||||
import Dropdown from 'react-bootstrap/Dropdown'
|
||||
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 [deleteItem] = useMutation(
|
||||
|
@ -40,6 +41,7 @@ export default function Delete ({ itemId, children, onDelete }) {
|
|||
showModal(onClose => {
|
||||
return (
|
||||
<DeleteConfirm
|
||||
type={type}
|
||||
onConfirm={async () => {
|
||||
const { error } = await deleteItem({ variables: { id: itemId } })
|
||||
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 toaster = useToast()
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -71,6 +74,7 @@ function DeleteConfirm ({ onConfirm }) {
|
|||
variant='danger' onClick={async () => {
|
||||
try {
|
||||
await onConfirm()
|
||||
toaster.success(`deleted ${type.toLowerCase()}`)
|
||||
} catch (e) {
|
||||
setError(e.message || e)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@ import { gql, useMutation } from '@apollo/client'
|
|||
import Dropdown from 'react-bootstrap/Dropdown'
|
||||
import FundError from './fund-error'
|
||||
import { useShowModal } from './modal'
|
||||
import { useToast } from './toast'
|
||||
|
||||
export default function DontLikeThisDropdownItem ({ id }) {
|
||||
const toaster = useToast()
|
||||
const showModal = useShowModal()
|
||||
|
||||
const [dontLikeThis] = useMutation(
|
||||
|
@ -32,11 +34,15 @@ export default function DontLikeThisDropdownItem ({ id }) {
|
|||
variables: { id },
|
||||
optimisticResponse: { dontLikeThis: true }
|
||||
})
|
||||
toaster.success('item flagged')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
if (error.toString().includes('insufficient funds')) {
|
||||
showModal(onClose => {
|
||||
return <FundError onClose={onClose} />
|
||||
})
|
||||
} else {
|
||||
toaster.danger('failed to flag item')
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Button from 'react-bootstrap/Button'
|
||||
import InputGroup from 'react-bootstrap/InputGroup'
|
||||
import BootstrapForm from 'react-bootstrap/Form'
|
||||
import Alert from 'react-bootstrap/Alert'
|
||||
import { Formik, Form as FormikForm, useFormikContext, useField, FieldArray } from 'formik'
|
||||
import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
|
||||
import copy from 'clipboard-copy'
|
||||
|
@ -19,6 +18,7 @@ import CloseIcon from '../svgs/close-line.svg'
|
|||
import { useLazyQuery } from '@apollo/client'
|
||||
import { USER_SEARCH } from '../fragments/users'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import { useToast } from './toast'
|
||||
|
||||
export function SubmitButton ({
|
||||
children, variant, value, onClick, disabled, ...props
|
||||
|
@ -472,7 +472,12 @@ const StorageKeyPrefixContext = createContext()
|
|||
export function Form ({
|
||||
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 (
|
||||
<Formik
|
||||
|
@ -481,26 +486,31 @@ export function Form ({
|
|||
validationSchema={schema}
|
||||
initialTouched={validateImmediately && initial}
|
||||
validateOnBlur={false}
|
||||
onSubmit={async (values, ...args) =>
|
||||
onSubmit && onSubmit(values, ...args).then((options) => {
|
||||
if (!storageKeyPrefix || options?.keepLocalStorage) return
|
||||
Object.keys(values).forEach(v => {
|
||||
window.localStorage.removeItem(storageKeyPrefix + '-' + v)
|
||||
if (Array.isArray(values[v])) {
|
||||
values[v].forEach(
|
||||
(iv, i) => {
|
||||
Object.keys(iv).forEach(k => {
|
||||
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}].${k}`)
|
||||
onSubmit={async (values, ...args) => {
|
||||
try {
|
||||
if (onSubmit) {
|
||||
const options = await onSubmit(values, ...args)
|
||||
if (!storageKeyPrefix || options?.keepLocalStorage) return
|
||||
Object.keys(values).forEach(v => {
|
||||
window.localStorage.removeItem(storageKeyPrefix + '-' + v)
|
||||
if (Array.isArray(values[v])) {
|
||||
values[v].forEach(
|
||||
(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(e => setError(e.message || e))}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
toaster.danger(err.message || err.toString?.())
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormikForm {...props} noValidate>
|
||||
{error && <Alert variant='danger' onClose={() => setError(undefined)} dismissible>{error}</Alert>}
|
||||
<StorageKeyPrefixContext.Provider value={storageKeyPrefix}>
|
||||
{children}
|
||||
</StorageKeyPrefixContext.Provider>
|
||||
|
|
|
@ -140,7 +140,7 @@ export default function ItemInfo ({
|
|||
{me && !item.meSats && !item.position && !item.meDontLike &&
|
||||
!item.mine && !item.deletedAt && <DontLikeThisDropdownItem id={item.id} />}
|
||||
{item.mine && !item.position && !item.deletedAt &&
|
||||
<DeleteDropdownItem itemId={item.id} />}
|
||||
<DeleteDropdownItem itemId={item.id} type={item.title ? 'post' : 'comment'} />}
|
||||
</ItemDropdown>
|
||||
{extraInfo}
|
||||
</div>
|
||||
|
|
|
@ -2,9 +2,11 @@ import Dropdown from 'react-bootstrap/Dropdown'
|
|||
import ShareIcon from '../svgs/share-fill.svg'
|
||||
import copy from 'clipboard-copy'
|
||||
import { useMe } from './me'
|
||||
import { useToast } from './toast'
|
||||
|
||||
export default function Share ({ item }) {
|
||||
const me = useMe()
|
||||
const toaster = useToast()
|
||||
const url = `https://stacker.news/items/${item.id}${me ? `/r/${me.name}` : ''}`
|
||||
|
||||
return typeof window !== 'undefined' && navigator?.share
|
||||
|
@ -13,16 +15,17 @@ export default function Share ({ item }) {
|
|||
<ShareIcon
|
||||
width={20} height={20}
|
||||
className='mx-2 fill-grey theme'
|
||||
onClick={() => {
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: item.title || '',
|
||||
text: '',
|
||||
url
|
||||
}).then(() => console.log('Successful share'))
|
||||
.catch((error) => console.log('Error sharing', error))
|
||||
} else {
|
||||
console.log('no navigator.share')
|
||||
})
|
||||
toaster.success('link shared')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toaster.danger('failed to share link')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -36,7 +39,13 @@ export default function Share ({ item }) {
|
|||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
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
|
||||
|
@ -47,19 +56,25 @@ export default function Share ({ item }) {
|
|||
|
||||
export function CopyLinkDropdownItem ({ item }) {
|
||||
const me = useMe()
|
||||
const toaster = useToast()
|
||||
const url = `https://stacker.news/items/${item.id}${me ? `/r/${me.name}` : ''}`
|
||||
return (
|
||||
<Dropdown.Item
|
||||
onClick={async () => {
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: item.title || '',
|
||||
text: '',
|
||||
url
|
||||
}).then(() => console.log('Successful share'))
|
||||
.catch((error) => console.log('Error sharing', error))
|
||||
} else {
|
||||
copy(url)
|
||||
try {
|
||||
if (navigator.share) {
|
||||
await navigator.share({
|
||||
title: item.title || '',
|
||||
text: '',
|
||||
url
|
||||
})
|
||||
} else {
|
||||
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 { gql } from 'graphql-tag'
|
||||
import Dropdown from 'react-bootstrap/Dropdown'
|
||||
import { useToast } from './toast'
|
||||
|
||||
export default function SubscribeDropdownItem ({ item: { id, meSubscription } }) {
|
||||
const toaster = useToast()
|
||||
const [subscribeItem] = useMutation(
|
||||
gql`
|
||||
mutation subscribeItem($id: ID!) {
|
||||
|
@ -22,7 +24,15 @@ export default function SubscribeDropdownItem ({ item: { id, meSubscription } })
|
|||
)
|
||||
return (
|
||||
<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'}
|
||||
</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 ErrorBoundary from '../components/error-boundary'
|
||||
import { LightningProvider } from '../components/lightning'
|
||||
import { ToastProvider } from '../components/toast'
|
||||
import { ServiceWorkerProvider } from '../components/serviceworker'
|
||||
import { SSR } from '../lib/constants'
|
||||
import NProgress from 'nprogress'
|
||||
|
@ -90,11 +91,13 @@ function MyApp ({ Component, pageProps: { ...props } }) {
|
|||
<ServiceWorkerProvider>
|
||||
<PriceProvider price={price}>
|
||||
<LightningProvider>
|
||||
<PaymentTokenProvider>
|
||||
<ShowModalProvider>
|
||||
<Component ssrData={ssrData} {...otherProps} />
|
||||
</ShowModalProvider>
|
||||
</PaymentTokenProvider>
|
||||
<ToastProvider>
|
||||
<PaymentTokenProvider>
|
||||
<ShowModalProvider>
|
||||
<Component ssrData={ssrData} {...otherProps} />
|
||||
</ShowModalProvider>
|
||||
</PaymentTokenProvider>
|
||||
</ToastProvider>
|
||||
</LightningProvider>
|
||||
</PriceProvider>
|
||||
</ServiceWorkerProvider>
|
||||
|
|
|
@ -22,6 +22,7 @@ import PageLoading from '../components/page-loading'
|
|||
import { useShowModal } from '../components/modal'
|
||||
import { authErrorMessage } from '../components/login'
|
||||
import { NostrAuth } from '../components/nostr-auth'
|
||||
import { useToast } from '../components/toast'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
||||
|
||||
|
@ -30,7 +31,7 @@ function bech32encode (hexString) {
|
|||
}
|
||||
|
||||
export default function Settings ({ ssrData }) {
|
||||
const [success, setSuccess] = useState()
|
||||
const toaster = useToast()
|
||||
const [setSettings] = useMutation(SET_SETTINGS, {
|
||||
update (cache, { data: { setSettings } }) {
|
||||
cache.modify({
|
||||
|
@ -89,18 +90,22 @@ export default function Settings ({ ssrData }) {
|
|||
|
||||
const nostrRelaysFiltered = nostrRelays?.filter(word => word.trim().length > 0)
|
||||
|
||||
await setSettings({
|
||||
variables: {
|
||||
tipDefault: Number(tipDefault),
|
||||
nostrPubkey,
|
||||
nostrRelays: nostrRelaysFiltered,
|
||||
...values
|
||||
}
|
||||
})
|
||||
setSuccess('settings saved')
|
||||
try {
|
||||
await setSettings({
|
||||
variables: {
|
||||
tipDefault: Number(tipDefault),
|
||||
nostrPubkey,
|
||||
nostrRelays: nostrRelaysFiltered,
|
||||
...values
|
||||
}
|
||||
})
|
||||
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
|
||||
label='zap default'
|
||||
name='tipDefault'
|
||||
|
@ -329,6 +334,7 @@ function NostrLinkButton ({ unlink, status }) {
|
|||
|
||||
function UnlinkObstacle ({ onClose, type, unlinkAuth }) {
|
||||
const router = useRouter()
|
||||
const toaster = useToast()
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -344,9 +350,15 @@ function UnlinkObstacle ({ onClose, type, unlinkAuth }) {
|
|||
}}
|
||||
schema={lastAuthRemovalSchema}
|
||||
onSubmit={async () => {
|
||||
await unlinkAuth({ variables: { authType: type } })
|
||||
router.push('/settings')
|
||||
onClose()
|
||||
try {
|
||||
await unlinkAuth({ variables: { authType: type } })
|
||||
router.push('/settings')
|
||||
onClose()
|
||||
toaster.success('unlinked auth method')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toaster.danger('failed to unlink auth method')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
|
@ -362,6 +374,7 @@ function UnlinkObstacle ({ onClose, type, unlinkAuth }) {
|
|||
function AuthMethods ({ methods }) {
|
||||
const showModal = useShowModal()
|
||||
const router = useRouter()
|
||||
const toaster = useToast()
|
||||
const [err, setErr] = useState(authErrorMessage(router.query.error))
|
||||
const [unlinkAuth] = useMutation(
|
||||
gql`
|
||||
|
@ -396,7 +409,13 @@ function AuthMethods ({ methods }) {
|
|||
if (links === 1) {
|
||||
showModal(onClose => (<UnlinkObstacle onClose={onClose} type={type} unlinkAuth={unlinkAuth} />))
|
||||
} 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-border-color: var(--theme-borderColor);
|
||||
$grid-gutter-width: 2rem;
|
||||
$toast-spacing: .5rem;
|
||||
|
||||
:root, [data-bs-theme=light] {
|
||||
--theme-navLink: rgba(0, 0, 0, 0.55);
|
||||
|
@ -153,6 +154,10 @@ $grid-gutter-width: 2rem;
|
|||
height: 2px !important;
|
||||
}
|
||||
|
||||
.toast-body {
|
||||
padding: 0.75rem 1.25rem !important;
|
||||
}
|
||||
|
||||
#nprogress .peg {
|
||||
box-shadow: 0 0 10px var(--bs-primary), 0 0 5px var(--bs-primary) !important;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue