mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-22 14:09:30 -07:00
refactor: split monolithic state into context-based modules
Extract image, desktop, mobile, and config state into separate context providers to improve modularity and reduce unnecessary re-renders. Signed-off-by: Sped0n <hi@sped0n.com>
This commit is contained in:
@@ -1,209 +1,170 @@
|
||||
import { type gsap } from 'gsap'
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
on,
|
||||
onMount,
|
||||
Show,
|
||||
type Accessor,
|
||||
type JSX,
|
||||
type Setter
|
||||
untrack,
|
||||
type JSX
|
||||
} from 'solid-js'
|
||||
import { createStore } from 'solid-js/store'
|
||||
import { type Swiper } from 'swiper'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { type ImageJSON } from '../resources'
|
||||
import { useState } from '../state'
|
||||
import { loadGsap, type Vector } from '../utils'
|
||||
import { useImageState } from '../imageState'
|
||||
import { loadGsap, removeDuplicates, type Vector } from '../utils'
|
||||
|
||||
import GalleryImage from './galleryImage'
|
||||
import GalleryNav, { capitalizeFirstLetter } from './galleryNav'
|
||||
|
||||
function removeDuplicates<T>(arr: T[]): T[] {
|
||||
if (arr.length < 2) return arr // optimization
|
||||
return [...new Set(arr)]
|
||||
}
|
||||
|
||||
async function loadSwiper(): Promise<typeof Swiper> {
|
||||
const s = await import('swiper')
|
||||
return s.Swiper
|
||||
}
|
||||
import { closeGallery, openGallery } from './galleryTransitions'
|
||||
import { getActiveImageIndexes, loadSwiper } from './galleryUtils'
|
||||
import { useMobileState } from './state'
|
||||
|
||||
export default function Gallery(props: {
|
||||
children?: JSX.Element
|
||||
ijs: ImageJSON[]
|
||||
closeText: string
|
||||
loadingText: string
|
||||
isAnimating: Accessor<boolean>
|
||||
setIsAnimating: Setter<boolean>
|
||||
isOpen: Accessor<boolean>
|
||||
setIsOpen: Setter<boolean>
|
||||
setScrollable: Setter<boolean>
|
||||
}): JSX.Element {
|
||||
// variables
|
||||
let _gsap: typeof gsap
|
||||
let _swiper: Swiper
|
||||
let _swiper: Swiper | undefined
|
||||
let initPromise: Promise<void> | undefined
|
||||
|
||||
let curtain: HTMLDivElement | undefined
|
||||
let gallery: HTMLDivElement | undefined
|
||||
let galleryInner: HTMLDivElement | undefined
|
||||
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const _loadingText = capitalizeFirstLetter(props.loadingText)
|
||||
const imageState = useImageState()
|
||||
const [mobile, { setIndex, setIsAnimating, setIsScrollLocked }] = useMobileState()
|
||||
|
||||
const loadingText = createMemo(() => capitalizeFirstLetter(props.loadingText))
|
||||
|
||||
// states
|
||||
let lastIndex = -1
|
||||
let mounted = false
|
||||
let navigateVector: Vector = 'none'
|
||||
|
||||
const [state, { setIndex }] = useState()
|
||||
const [libLoaded, setLibLoaded] = createSignal(false)
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const [loads, setLoads] = createStore(Array<boolean>(props.ijs.length).fill(false))
|
||||
const [swiperReady, setSwiperReady] = createSignal(false)
|
||||
const [loads, setLoads] = createStore(Array<boolean>(imageState().length).fill(false))
|
||||
|
||||
// helper functions
|
||||
const slideUp: () => void = () => {
|
||||
// isAnimating is prechecked in isOpen effect
|
||||
if (!libLoaded() || !mounted) return
|
||||
props.setIsAnimating(true)
|
||||
|
||||
invariant(curtain, 'curtain is not defined')
|
||||
invariant(gallery, 'gallery is not defined')
|
||||
|
||||
_gsap.to(curtain, {
|
||||
opacity: 1,
|
||||
duration: 1
|
||||
openGallery({
|
||||
gsap: _gsap,
|
||||
curtain,
|
||||
gallery,
|
||||
setIsAnimating,
|
||||
setIsScrollLocked
|
||||
})
|
||||
|
||||
_gsap.to(gallery, {
|
||||
y: 0,
|
||||
ease: 'power3.inOut',
|
||||
duration: 1,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
props.setScrollable(false)
|
||||
props.setIsAnimating(false)
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
const slideDown: () => void = () => {
|
||||
// isAnimating is prechecked in isOpen effect
|
||||
props.setIsAnimating(true)
|
||||
|
||||
invariant(gallery, 'curtain is not defined')
|
||||
invariant(curtain, 'gallery is not defined')
|
||||
|
||||
_gsap.to(gallery, {
|
||||
y: '100%',
|
||||
ease: 'power3.inOut',
|
||||
duration: 1
|
||||
closeGallery({
|
||||
gsap: _gsap,
|
||||
curtain,
|
||||
gallery,
|
||||
setIsAnimating,
|
||||
setIsScrollLocked,
|
||||
onClosed: () => {
|
||||
lastIndex = -1
|
||||
}
|
||||
})
|
||||
|
||||
_gsap.to(curtain, {
|
||||
opacity: 0,
|
||||
duration: 1.2,
|
||||
delay: 0.4
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
// cleanup
|
||||
props.setScrollable(true)
|
||||
props.setIsAnimating(false)
|
||||
lastIndex = -1
|
||||
}, 1400)
|
||||
}
|
||||
|
||||
const galleryLoadImages: () => void = () => {
|
||||
let activeImagesIndex: number[] = []
|
||||
const _state = state()
|
||||
const currentIndex = _state.index
|
||||
const nextIndex = Math.min(currentIndex + 1, _state.length - 1)
|
||||
const prevIndex = Math.max(currentIndex - 1, 0)
|
||||
switch (navigateVector) {
|
||||
case 'next':
|
||||
activeImagesIndex = [nextIndex]
|
||||
break
|
||||
case 'prev':
|
||||
activeImagesIndex = [prevIndex]
|
||||
break
|
||||
case 'none':
|
||||
activeImagesIndex = [currentIndex, nextIndex, prevIndex]
|
||||
break
|
||||
}
|
||||
setLoads(removeDuplicates(activeImagesIndex), true)
|
||||
const currentIndex = mobile.index()
|
||||
|
||||
setLoads(
|
||||
removeDuplicates(
|
||||
getActiveImageIndexes(currentIndex, imageState().length, navigateVector)
|
||||
),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
const changeSlide: (slide: number) => void = (slide) => {
|
||||
// we are already in the gallery, don't need to
|
||||
// check mounted or libLoaded
|
||||
if (!swiperReady() || _swiper === undefined) return
|
||||
galleryLoadImages()
|
||||
_swiper.slideTo(slide, 0)
|
||||
}
|
||||
|
||||
// effects
|
||||
onMount(() => {
|
||||
window.addEventListener(
|
||||
'touchstart',
|
||||
() => {
|
||||
loadGsap()
|
||||
.then((g) => {
|
||||
_gsap = g
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
loadSwiper()
|
||||
.then((S) => {
|
||||
invariant(galleryInner, 'galleryInner is not defined')
|
||||
_swiper = new S(galleryInner, { spaceBetween: 20 })
|
||||
_swiper.on('slideChange', ({ realIndex }) => {
|
||||
setIndex(realIndex)
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
const ensureGalleryReady: () => Promise<void> = async () => {
|
||||
if (initPromise !== undefined) return await initPromise
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
const [g, S] = await Promise.all([loadGsap(), loadSwiper()])
|
||||
|
||||
_gsap = g
|
||||
|
||||
invariant(galleryInner, 'galleryInner is not defined')
|
||||
_swiper = new S(galleryInner, { spaceBetween: 20 })
|
||||
_swiper.on('slideChange', ({ realIndex }) => {
|
||||
setIndex(realIndex)
|
||||
})
|
||||
|
||||
setLibLoaded(true)
|
||||
},
|
||||
{ once: true, passive: true }
|
||||
)
|
||||
setSwiperReady(true)
|
||||
|
||||
const initialIndex = untrack(mobile.index)
|
||||
|
||||
if (initialIndex >= 0) {
|
||||
changeSlide(initialIndex)
|
||||
lastIndex = initialIndex
|
||||
}
|
||||
} catch (e) {
|
||||
initPromise = undefined
|
||||
setSwiperReady(false)
|
||||
console.log(e)
|
||||
}
|
||||
})()
|
||||
|
||||
await initPromise
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('touchstart', () => void ensureGalleryReady(), {
|
||||
once: true,
|
||||
passive: true
|
||||
})
|
||||
mounted = true
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => {
|
||||
state()
|
||||
},
|
||||
() => {
|
||||
const i = state().index
|
||||
if (i === lastIndex)
|
||||
return // change slide only when index is changed
|
||||
else if (lastIndex === -1)
|
||||
navigateVector = 'none' // lastIndex before set
|
||||
else if (i < lastIndex)
|
||||
navigateVector = 'prev' // set navigate vector for galleryLoadImages
|
||||
else if (i > lastIndex)
|
||||
navigateVector = 'next' // set navigate vector for galleryLoadImages
|
||||
else navigateVector = 'none' // default
|
||||
changeSlide(i) // change slide to new index
|
||||
lastIndex = i // update last index
|
||||
() => [swiperReady(), mobile.index()] as const,
|
||||
([ready, index]) => {
|
||||
if (!ready || index < 0) return
|
||||
if (index === lastIndex) return
|
||||
if (lastIndex === -1) navigateVector = 'none'
|
||||
else if (index < lastIndex) navigateVector = 'prev'
|
||||
else if (index > lastIndex) navigateVector = 'next'
|
||||
else navigateVector = 'none'
|
||||
changeSlide(index)
|
||||
lastIndex = index
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => {
|
||||
props.isOpen()
|
||||
},
|
||||
() => {
|
||||
if (props.isAnimating()) return
|
||||
if (props.isOpen()) slideUp()
|
||||
() => mobile.isOpen(),
|
||||
async (isOpen) => {
|
||||
if (isOpen && !swiperReady()) {
|
||||
await ensureGalleryReady()
|
||||
}
|
||||
|
||||
if (!libLoaded() || !swiperReady()) return
|
||||
if (mobile.isAnimating()) return
|
||||
if (isOpen) slideUp()
|
||||
else slideDown()
|
||||
},
|
||||
{ defer: true }
|
||||
@@ -215,26 +176,16 @@ export default function Gallery(props: {
|
||||
<div ref={gallery} class="gallery">
|
||||
<div ref={galleryInner} class="galleryInner">
|
||||
<div class="swiper-wrapper">
|
||||
<Show when={libLoaded()}>
|
||||
<For each={props.ijs}>
|
||||
{(ij, i) => (
|
||||
<div class="swiper-slide">
|
||||
<GalleryImage
|
||||
load={loads[i()]}
|
||||
ij={ij}
|
||||
loadingText={_loadingText}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
<For each={imageState().images}>
|
||||
{(ij, i) => (
|
||||
<div class="swiper-slide">
|
||||
<GalleryImage load={loads[i()]} ij={ij} loadingText={loadingText()} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<GalleryNav
|
||||
closeText={props.closeText}
|
||||
isAnimating={props.isAnimating}
|
||||
setIsOpen={props.setIsOpen}
|
||||
/>
|
||||
<GalleryNav closeText={props.closeText} />
|
||||
</div>
|
||||
<div ref={curtain} class="curtain" />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user