chore(git): merge branch '137-168-alert-popups' into 116-categories
This commit is contained in:
commit
ecbe839b30
72
src/components/AlertPopup.tsx
Normal file
72
src/components/AlertPopup.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { AlertPopupProps } from 'types'
|
||||
|
||||
export const AlertPopup = ({
|
||||
header,
|
||||
label,
|
||||
handleConfirm,
|
||||
handleClose
|
||||
}: AlertPopupProps) => {
|
||||
return createPortal(
|
||||
<div className='popUpMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='popUpMainCardWrapper'>
|
||||
<div className='popUpMainCard popUpMainCardQR'>
|
||||
<div className='popUpMainCardTop'>
|
||||
<div className='popUpMainCardTopInfo'>
|
||||
<h3>{header}</h3>
|
||||
</div>
|
||||
<div className='popUpMainCardTopClose' onClick={handleClose}>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-96 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pUMCB_Zaps'>
|
||||
<div className='pUMCB_ZapsInside'>
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label
|
||||
className='form-label labelMain'
|
||||
style={{ fontWeight: 'bold' }}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
gap: '10px'
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className='btn btnMain btnMainPopup'
|
||||
type='button'
|
||||
onPointerDown={() => handleConfirm(true)}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
className='btn btnMain btnMainPopup'
|
||||
type='button'
|
||||
onPointerDown={() => handleConfirm(false)}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
}
|
@ -1,16 +1,11 @@
|
||||
import { useAppSelector, useLocalStorage } from 'hooks'
|
||||
import React from 'react'
|
||||
import {
|
||||
FilterOptions,
|
||||
ModeratedFilter,
|
||||
NSFWFilter,
|
||||
SortBy,
|
||||
WOTFilterOptions
|
||||
} from 'types'
|
||||
import { FilterOptions, ModeratedFilter, SortBy, WOTFilterOptions } from 'types'
|
||||
import { DEFAULT_FILTER_OPTIONS } from 'utils'
|
||||
import { Dropdown } from './Dropdown'
|
||||
import { Option } from './Option'
|
||||
import { Filter } from '.'
|
||||
import { NsfwFilterOptions } from './NsfwFilterOptions'
|
||||
|
||||
type Props = {
|
||||
author?: string | undefined
|
||||
@ -115,19 +110,7 @@ export const BlogsFilter = React.memo(
|
||||
|
||||
{/* nsfw filter options */}
|
||||
<Dropdown label={filterOptions.nsfw}>
|
||||
{Object.values(NSFWFilter).map((item, index) => (
|
||||
<Option
|
||||
key={`nsfwFilterItem-${index}`}
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
nsfw: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</Option>
|
||||
))}
|
||||
<NsfwFilterOptions filterKey={filterKey} />
|
||||
</Dropdown>
|
||||
|
||||
{/* source filter options */}
|
||||
|
@ -5,13 +5,13 @@ import {
|
||||
SortBy,
|
||||
ModeratedFilter,
|
||||
WOTFilterOptions,
|
||||
NSFWFilter,
|
||||
RepostFilter
|
||||
} from 'types'
|
||||
import { DEFAULT_FILTER_OPTIONS } from 'utils'
|
||||
import { Filter } from '.'
|
||||
import { Dropdown } from './Dropdown'
|
||||
import { Option } from './Option'
|
||||
import { NsfwFilterOptions } from './NsfwFilterOptions'
|
||||
|
||||
type Props = {
|
||||
author?: string | undefined
|
||||
@ -115,19 +115,7 @@ export const ModFilter = React.memo(
|
||||
|
||||
{/* nsfw filter options */}
|
||||
<Dropdown label={filterOptions.nsfw}>
|
||||
{Object.values(NSFWFilter).map((item, index) => (
|
||||
<Option
|
||||
key={`nsfwFilterItem-${index}`}
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
nsfw: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</Option>
|
||||
))}
|
||||
<NsfwFilterOptions filterKey={filterKey} />
|
||||
</Dropdown>
|
||||
|
||||
{/* repost filter options */}
|
||||
|
64
src/components/Filters/NsfwFilterOptions.tsx
Normal file
64
src/components/Filters/NsfwFilterOptions.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { FilterOptions, NSFWFilter } from 'types'
|
||||
import { Option } from './Option'
|
||||
import { NsfwAlertPopup } from 'components/NsfwAlertPopup'
|
||||
import { useState } from 'react'
|
||||
import { useLocalStorage } from 'hooks'
|
||||
import { DEFAULT_FILTER_OPTIONS } from 'utils'
|
||||
|
||||
interface NsfwFilterOptionsProps {
|
||||
filterKey: string
|
||||
}
|
||||
|
||||
export const NsfwFilterOptions = ({ filterKey }: NsfwFilterOptionsProps) => {
|
||||
const [, setFilterOptions] = useLocalStorage<FilterOptions>(
|
||||
filterKey,
|
||||
DEFAULT_FILTER_OPTIONS
|
||||
)
|
||||
const [showNsfwPopup, setShowNsfwPopup] = useState<boolean>(false)
|
||||
const [selectedNsfwOption, setSelectedNsfwOption] = useState<
|
||||
NSFWFilter | undefined
|
||||
>()
|
||||
const [confirmNsfw] = useLocalStorage<boolean>('confirm-nsfw', false)
|
||||
const handleConfirm = (confirm: boolean) => {
|
||||
if (confirm && selectedNsfwOption) {
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
nsfw: selectedNsfwOption
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.values(NSFWFilter).map((item, index) => (
|
||||
<Option
|
||||
key={`nsfwFilterItem-${index}`}
|
||||
onClick={() => {
|
||||
// Trigger NSFW popup
|
||||
if (
|
||||
(item === NSFWFilter.Only_NSFW ||
|
||||
item === NSFWFilter.Show_NSFW) &&
|
||||
!confirmNsfw
|
||||
) {
|
||||
setSelectedNsfwOption(item)
|
||||
setShowNsfwPopup(true)
|
||||
} else {
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
nsfw: item
|
||||
}))
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Option>
|
||||
))}
|
||||
{showNsfwPopup && (
|
||||
<NsfwAlertPopup
|
||||
handleConfirm={handleConfirm}
|
||||
handleClose={() => setShowNsfwPopup(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
36
src/components/NsfwAlertPopup.tsx
Normal file
36
src/components/NsfwAlertPopup.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { AlertPopupProps } from 'types'
|
||||
import { AlertPopup } from './AlertPopup'
|
||||
import { useLocalStorage } from 'hooks'
|
||||
|
||||
type NsfwAlertPopup = Omit<AlertPopupProps, 'header' | 'label'>
|
||||
|
||||
/**
|
||||
* Triggers when the user wants to switch the filter to see any of the NSFW options
|
||||
* (including preferences)
|
||||
*
|
||||
* Option will be remembered for the session only and will not show the popup again
|
||||
*/
|
||||
export const NsfwAlertPopup = ({
|
||||
handleConfirm,
|
||||
handleClose
|
||||
}: NsfwAlertPopup) => {
|
||||
const [confirmNsfw, setConfirmNsfw] = useLocalStorage<boolean>(
|
||||
'confirm-nsfw',
|
||||
false
|
||||
)
|
||||
|
||||
return (
|
||||
!confirmNsfw && (
|
||||
<AlertPopup
|
||||
header='Confirm'
|
||||
label='Are you above 18 years of age?'
|
||||
handleClose={handleClose}
|
||||
handleConfirm={(confirm: boolean) => {
|
||||
setConfirmNsfw(confirm)
|
||||
handleConfirm(confirm)
|
||||
handleClose()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
@ -3,12 +3,12 @@ import { CheckboxFieldUncontrolled } from 'components/Inputs'
|
||||
import { useEffect } from 'react'
|
||||
import { ReportReason } from 'types/report'
|
||||
import { LoadingSpinner } from './LoadingSpinner'
|
||||
import { PopupProps } from 'types'
|
||||
|
||||
type ReportPopupProps = {
|
||||
openedAt: number
|
||||
reasons: ReportReason[]
|
||||
handleClose: () => void
|
||||
}
|
||||
} & PopupProps
|
||||
|
||||
export const ReportPopup = ({
|
||||
openedAt,
|
||||
|
@ -8,3 +8,4 @@ export * from './useReactions'
|
||||
export * from './useNDKContext'
|
||||
export * from './useScrollDisable'
|
||||
export * from './useLocalStorage'
|
||||
export * from './useSessionStorage'
|
||||
|
71
src/hooks/useSessionStorage.tsx
Normal file
71
src/hooks/useSessionStorage.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
getSessionStorageItem,
|
||||
removeSessionStorageItem,
|
||||
setSessionStorageItem
|
||||
} from 'utils'
|
||||
|
||||
const useSessionStorageSubscribe = (callback: () => void) => {
|
||||
window.addEventListener('sessionStorage', callback)
|
||||
return () => window.removeEventListener('sessionStorage', callback)
|
||||
}
|
||||
|
||||
function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
|
||||
if (typeof storedValue === 'object' && storedValue !== null) {
|
||||
return { ...initialValue, ...storedValue }
|
||||
}
|
||||
return storedValue
|
||||
}
|
||||
|
||||
export function useSessionStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, React.Dispatch<React.SetStateAction<T>>] {
|
||||
const getSnapshot = () => {
|
||||
// Get the stored value
|
||||
const storedValue = getSessionStorageItem(key, initialValue)
|
||||
|
||||
// Parse the value
|
||||
const parsedStoredValue = JSON.parse(storedValue)
|
||||
|
||||
// Merge the default and the stored in case some of the required fields are missing
|
||||
return JSON.stringify(
|
||||
mergeWithInitialValue(parsedStoredValue, initialValue)
|
||||
)
|
||||
}
|
||||
|
||||
const data = React.useSyncExternalStore(
|
||||
useSessionStorageSubscribe,
|
||||
getSnapshot
|
||||
)
|
||||
|
||||
const setState: React.Dispatch<React.SetStateAction<T>> = React.useCallback(
|
||||
(v: React.SetStateAction<T>) => {
|
||||
try {
|
||||
const nextState =
|
||||
typeof v === 'function'
|
||||
? (v as (prevState: T) => T)(JSON.parse(data))
|
||||
: v
|
||||
|
||||
if (nextState === undefined || nextState === null) {
|
||||
removeSessionStorageItem(key)
|
||||
} else {
|
||||
setSessionStorageItem(key, JSON.stringify(nextState))
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
},
|
||||
[data, key]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
// Set session storage only when it's empty
|
||||
const data = window.sessionStorage.getItem(key)
|
||||
if (data === null) {
|
||||
setSessionStorageItem(key, JSON.stringify(initialValue))
|
||||
}
|
||||
}, [key, initialValue])
|
||||
|
||||
return [JSON.parse(data) as T, setState]
|
||||
}
|
@ -14,17 +14,16 @@ import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { Filter } from 'components/Filters'
|
||||
import { Dropdown } from 'components/Filters/Dropdown'
|
||||
import { Option } from 'components/Filters/Option'
|
||||
import { NsfwFilterOptions } from 'components/Filters/NsfwFilterOptions'
|
||||
|
||||
export const BlogsPage = () => {
|
||||
const navigation = useNavigation()
|
||||
const blogs = useLoaderData() as Partial<BlogCardDetails>[] | undefined
|
||||
const [filterOptions, setFilterOptions] = useLocalStorage(
|
||||
'filter-blog-curated',
|
||||
{
|
||||
sort: SortBy.Latest,
|
||||
nsfw: NSFWFilter.Hide_NSFW
|
||||
}
|
||||
)
|
||||
const filterKey = 'filter-blog-curated'
|
||||
const [filterOptions, setFilterOptions] = useLocalStorage(filterKey, {
|
||||
sort: SortBy.Latest,
|
||||
nsfw: NSFWFilter.Hide_NSFW
|
||||
})
|
||||
|
||||
// Search
|
||||
const searchTermRef = useRef<HTMLInputElement>(null)
|
||||
@ -147,19 +146,7 @@ export const BlogsPage = () => {
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown label={filterOptions.nsfw}>
|
||||
{Object.values(NSFWFilter).map((item, index) => (
|
||||
<Option
|
||||
key={`nsfwFilterItem-${index}`}
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
nsfw: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</Option>
|
||||
))}
|
||||
<NsfwFilterOptions filterKey={filterKey} />
|
||||
</Dropdown>
|
||||
</Filter>
|
||||
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { useAppDispatch, useAppSelector, useNDKContext } from 'hooks'
|
||||
import { NsfwAlertPopup } from 'components/NsfwAlertPopup'
|
||||
import {
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
useNDKContext,
|
||||
useLocalStorage
|
||||
} from 'hooks'
|
||||
import { kinds, UnsignedEvent, Event } from 'nostr-tools'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
@ -19,6 +25,13 @@ export const PreferencesSetting = () => {
|
||||
const [wotLevel, setWotLevel] = useState(userWotLevel)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const [nsfw, setNsfw] = useState(false)
|
||||
const [confirmNsfw] = useLocalStorage<boolean>('confirm-nsfw', false)
|
||||
const [showNsfwPopup, setShowNsfwPopup] = useState<boolean>(false)
|
||||
const handleNsfwConfirm = (confirm: boolean) => {
|
||||
setNsfw(confirm)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.pubkey) {
|
||||
const hexPubkey = user.pubkey as string
|
||||
@ -191,6 +204,14 @@ export const PreferencesSetting = () => {
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name='NSFWPreference'
|
||||
checked={nsfw}
|
||||
onChange={(e) => {
|
||||
if (e.currentTarget.checked && !confirmNsfw) {
|
||||
setShowNsfwPopup(true)
|
||||
} else {
|
||||
setNsfw(e.currentTarget.checked)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -238,6 +259,12 @@ export const PreferencesSetting = () => {
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
{showNsfwPopup && (
|
||||
<NsfwAlertPopup
|
||||
handleConfirm={handleNsfwConfirm}
|
||||
handleClose={() => setShowNsfwPopup(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,3 +5,4 @@ export * from './user'
|
||||
export * from './zap'
|
||||
export * from './blog'
|
||||
export * from './category'
|
||||
export * from './popup'
|
||||
|
9
src/types/popup.ts
Normal file
9
src/types/popup.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface PopupProps {
|
||||
handleClose: () => void
|
||||
}
|
||||
|
||||
export interface AlertPopupProps extends PopupProps {
|
||||
header: string
|
||||
label: string
|
||||
handleConfirm: (confirm: boolean) => void
|
||||
}
|
@ -4,6 +4,7 @@ export * from './url'
|
||||
export * from './utils'
|
||||
export * from './zap'
|
||||
export * from './localStorage'
|
||||
export * from './sessionStorage'
|
||||
export * from './consts'
|
||||
export * from './blog'
|
||||
export * from './curationSets'
|
||||
|
32
src/utils/sessionStorage.ts
Normal file
32
src/utils/sessionStorage.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export function getSessionStorageItem<T>(key: string, defaultValue: T): string {
|
||||
try {
|
||||
const data = window.sessionStorage.getItem(key)
|
||||
if (data === null) return JSON.stringify(defaultValue)
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error(`Error while fetching session storage value: `, err)
|
||||
return JSON.stringify(defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
export function setSessionStorageItem(key: string, value: string) {
|
||||
try {
|
||||
window.sessionStorage.setItem(key, value)
|
||||
dispatchSessionStorageEvent(key, value)
|
||||
} catch (err) {
|
||||
console.error(`Error while saving session storage value: `, err)
|
||||
}
|
||||
}
|
||||
|
||||
export function removeSessionStorageItem(key: string) {
|
||||
try {
|
||||
window.sessionStorage.removeItem(key)
|
||||
dispatchSessionStorageEvent(key, null)
|
||||
} catch (err) {
|
||||
console.error(`Error while deleting session storage value: `, err)
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchSessionStorageEvent(key: string, newValue: string | null) {
|
||||
window.dispatchEvent(new StorageEvent('sessionStorage', { key, newValue }))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user