feat: enhance picture notes browsing experience on large screen

This commit is contained in:
codytseng
2025-01-15 16:44:33 +08:00
parent 8e567dd401
commit 4acc1203fe
5 changed files with 57 additions and 13 deletions

View File

@@ -1,5 +1,7 @@
import { Carousel, CarouselApi, CarouselContent, CarouselItem } from '@/components/ui/carousel' import { Carousel, CarouselApi, CarouselContent, CarouselItem } from '@/components/ui/carousel'
import { isTouchDevice } from '@/lib/common'
import { TImageInfo } from '@/types' import { TImageInfo } from '@/types'
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Lightbox from 'yet-another-react-lightbox' import Lightbox from 'yet-another-react-lightbox'
import Zoom from 'yet-another-react-lightbox/plugins/zoom' import Zoom from 'yet-another-react-lightbox/plugins/zoom'
@@ -40,16 +42,23 @@ export function ImageCarousel({
} }
return ( return (
<> <div className="relative space-y-2">
<Carousel className="w-full" setApi={setApi}> <Carousel className="w-full" setApi={setApi}>
<CarouselContent> <CarouselContent className="xl:px-4">
{images.map((image, index) => ( {images.map((image, index) => (
<CarouselItem key={index}> <CarouselItem key={index} className="xl:basis-2/3 cursor-zoom-in">
<Image image={image} onClick={(e) => handlePhotoClick(e, index)} /> <Image
className="xl:rounded-lg"
image={image}
onClick={(e) => handlePhotoClick(e, index)}
/>
</CarouselItem> </CarouselItem>
))} ))}
</CarouselContent> </CarouselContent>
</Carousel> </Carousel>
{!isTouchDevice() && (
<ArrowButton total={images.length} currentIndex={currentIndex} onClick={onDotClick} />
)}
{images.length > 1 && ( {images.length > 1 && (
<CarouselDot total={images.length} currentIndex={currentIndex} onClick={onDotClick} /> <CarouselDot total={images.length} currentIndex={currentIndex} onClick={onDotClick} />
)} )}
@@ -67,7 +76,7 @@ export function ImageCarousel({
styles={{ toolbar: { paddingTop: '2.25rem' } }} styles={{ toolbar: { paddingTop: '2.25rem' } }}
/> />
{isNsfw && <NsfwOverlay className="rounded-lg" />} {isNsfw && <NsfwOverlay className="rounded-lg" />}
</> </div>
) )
} }
@@ -85,10 +94,41 @@ function CarouselDot({
{Array.from({ length: total }).map((_, index) => ( {Array.from({ length: total }).map((_, index) => (
<div <div
key={index} key={index}
className={`w-2 h-2 rounded-full ${index === currentIndex ? 'bg-foreground/40' : 'bg-muted'}`} className={`w-2 h-2 rounded-full cursor-pointer ${index === currentIndex ? 'bg-foreground/40' : 'bg-muted'}`}
onClick={() => onClick(index)} onClick={() => onClick(index)}
/> />
))} ))}
</div> </div>
) )
} }
function ArrowButton({
total,
currentIndex,
onClick
}: {
total: number
currentIndex: number
onClick: (index: number) => void
}) {
return (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none transition-opacity">
<div className="w-full flex justify-between px-2 xl:px-4">
<button
onClick={() => onClick(currentIndex - 1)}
className="w-8 h-8 rounded-full bg-background/50 flex justify-center items-center pointer-events-auto disabled:pointer-events-none disabled:opacity-0"
disabled={currentIndex === 0}
>
<ChevronLeftIcon className="w-4 h-4" />
</button>
<button
onClick={() => onClick(currentIndex + 1)}
className="w-8 h-8 rounded-full bg-background/50 flex justify-center items-center pointer-events-auto disabled:pointer-events-none disabled:opacity-0"
disabled={currentIndex === total - 1}
>
<ChevronRightIcon className="w-4 h-4" />
</button>
</div>
</div>
)
}

View File

@@ -30,7 +30,7 @@ export default function NoteList({
className?: string className?: string
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isLargeScreen } = useScreenSize()
const { signEvent, checkLogin } = useNostr() const { signEvent, checkLogin } = useNostr()
const { areAlgoRelays } = useFetchRelayInfos([...relayUrls]) const { areAlgoRelays } = useFetchRelayInfos([...relayUrls])
const [refreshCount, setRefreshCount] = useState(0) const [refreshCount, setRefreshCount] = useState(0)
@@ -180,7 +180,7 @@ export default function NoteList({
{isPictures ? ( {isPictures ? (
<PictureNoteCardMasonry <PictureNoteCardMasonry
className="px-2 sm:px-4" className="px-2 sm:px-4"
columnCount={isSmallScreen ? 2 : 3} columnCount={isLargeScreen ? 3 : 2}
events={events} events={events}
/> />
) : ( ) : (
@@ -268,7 +268,7 @@ function PictureNoteCardMasonry({
) )
}) })
return newColumns return newColumns
}, [events]) }, [events, columnCount])
return ( return (
<div <div

3
src/lib/common.ts Normal file
View File

@@ -0,0 +1,3 @@
export function isTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0
}

View File

@@ -12,14 +12,12 @@ import { useFetchEvent } from '@/hooks'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { getParentEventId, getRootEventId, isPictureEvent } from '@/lib/event' import { getParentEventId, getRootEventId, isPictureEvent } from '@/lib/event'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import NotFoundPage from '../NotFoundPage' import NotFoundPage from '../NotFoundPage'
export default function NotePage({ id, index }: { id?: string; index?: number }) { export default function NotePage({ id, index }: { id?: string; index?: number }) {
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { event, isFetching } = useFetchEvent(id) const { event, isFetching } = useFetchEvent(id)
const parentEventId = useMemo(() => getParentEventId(event), [event]) const parentEventId = useMemo(() => getParentEventId(event), [event])
const rootEventId = useMemo(() => getRootEventId(event), [event]) const rootEventId = useMemo(() => getRootEventId(event), [event])
@@ -35,7 +33,7 @@ export default function NotePage({ id, index }: { id?: string; index?: number })
} }
if (!event) return <NotFoundPage /> if (!event) return <NotFoundPage />
if (isPictureEvent(event) && isSmallScreen) { if (isPictureEvent(event)) {
return ( return (
<SecondaryPageLayout index={index} title={t('Note')} displayScrollToTopButton> <SecondaryPageLayout index={index} title={t('Note')} displayScrollToTopButton>
<PictureNote key={`note-${event.id}`} event={event} fetchNoteStats /> <PictureNote key={`note-${event.id}`} event={event} fetchNoteStats />

View File

@@ -5,6 +5,7 @@ type TScreenSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl'
type TScreenSizeContext = { type TScreenSizeContext = {
screenSize: TScreenSize screenSize: TScreenSize
isSmallScreen: boolean isSmallScreen: boolean
isLargeScreen: boolean
} }
const ScreenSizeContext = createContext<TScreenSizeContext | undefined>(undefined) const ScreenSizeContext = createContext<TScreenSizeContext | undefined>(undefined)
@@ -20,6 +21,7 @@ export const useScreenSize = () => {
export function ScreenSizeProvider({ children }: { children: React.ReactNode }) { export function ScreenSizeProvider({ children }: { children: React.ReactNode }) {
const [screenSize, setScreenSize] = useState<TScreenSize>('sm') const [screenSize, setScreenSize] = useState<TScreenSize>('sm')
const isSmallScreen = useMemo(() => screenSize === 'sm', [screenSize]) const isSmallScreen = useMemo(() => screenSize === 'sm', [screenSize])
const isLargeScreen = useMemo(() => ['2xl'].includes(screenSize), [screenSize])
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
@@ -47,7 +49,8 @@ export function ScreenSizeProvider({ children }: { children: React.ReactNode })
<ScreenSizeContext.Provider <ScreenSizeContext.Provider
value={{ value={{
screenSize, screenSize,
isSmallScreen isSmallScreen,
isLargeScreen
}} }}
> >
{children} {children}