chore(git): merge branch '137-168-alert-popups' into 116-categories

This commit is contained in:
enes 2024-12-04 14:17:41 +01:00
commit ecbe839b30
14 changed files with 329 additions and 57 deletions

View 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
)
}

View File

@ -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 */}

View File

@ -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 */}

View 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)}
/>
)}
</>
)
}

View 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()
}}
/>
)
)
}

View File

@ -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,

View File

@ -8,3 +8,4 @@ export * from './useReactions'
export * from './useNDKContext'
export * from './useScrollDisable'
export * from './useLocalStorage'
export * from './useSessionStorage'

View 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]
}

View File

@ -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>

View File

@ -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>

View File

@ -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
View File

@ -0,0 +1,9 @@
export interface PopupProps {
handleClose: () => void
}
export interface AlertPopupProps extends PopupProps {
header: string
label: string
handleConfirm: (confirm: boolean) => void
}

View File

@ -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'

View 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 }))
}