feat: enhance picture notes browsing experience on large screen
This commit is contained in:
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
3
src/lib/common.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function isTouchDevice() {
|
||||||
|
return 'ontouchstart' in window || navigator.maxTouchPoints > 0
|
||||||
|
}
|
||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user