degmods.com/src/pages/home.tsx

477 lines
13 KiB
TypeScript
Raw Normal View History

2024-11-05 16:22:08 +01:00
import { kinds, nip19 } from 'nostr-tools'
import { useMemo, useState } from 'react'
2024-11-13 14:30:58 +01:00
import { Link, useNavigate, useNavigation } from 'react-router-dom'
import { A11y, Autoplay, Navigation, Pagination } from 'swiper/modules'
2024-09-02 13:47:16 +05:00
import { Swiper, SwiperSlide } from 'swiper/react'
2024-07-11 18:34:12 +05:00
import { BlogCard } from '../components/BlogCard'
2024-07-11 17:15:03 +05:00
import { GameCard } from '../components/GameCard'
2024-07-11 17:52:48 +05:00
import { ModCard } from '../components/ModCard'
import { LANDING_PAGE_DATA, PROFILE_BLOG_FILTER_LIMIT } from '../constants'
2024-10-14 13:24:43 +05:00
import {
2024-11-11 22:37:49 +05:00
useAppSelector,
2024-10-14 13:24:43 +05:00
useDidMount,
useGames,
useLocalStorage,
2024-10-14 13:24:43 +05:00
useMuteLists,
useNDKContext,
useNSFWList,
useRepostList
2024-10-14 13:24:43 +05:00
} from '../hooks'
import { appRoutes, getModPageRoute } from '../routes'
import { BlogCardDetails, ModDetails, NSFWFilter, SortBy } from '../types'
2024-11-05 16:22:08 +01:00
import {
extractBlogCardDetails,
2024-11-05 16:22:08 +01:00
extractModData,
handleModImageError,
log,
LogType,
npubToHex
} from '../utils'
2024-09-02 13:47:16 +05:00
2024-07-11 16:19:12 +05:00
import '../styles/cardLists.css'
import '../styles/SimpleSlider.css'
import '../styles/styles.css'
2024-09-02 13:47:16 +05:00
// Import Swiper styles
2024-11-13 10:58:30 +01:00
import { NDKFilter } from '@nostr-dev-kit/ndk'
2024-09-02 13:47:16 +05:00
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
2024-11-13 14:30:58 +01:00
import { LoadingSpinner } from 'components/LoadingSpinner'
2024-11-14 11:29:01 +01:00
import { Spinner } from 'components/Spinner'
import { isInWoT } from 'utils/wot'
2024-09-02 13:47:16 +05:00
2024-07-11 16:19:12 +05:00
export const HomePage = () => {
2024-09-02 13:47:16 +05:00
const navigate = useNavigate()
const games = useGames()
const featuredGames = useMemo(() => {
return games.filter((game) =>
LANDING_PAGE_DATA.featuredGames.includes(game['Game Name'])
)
}, [games])
2024-07-11 16:19:12 +05:00
return (
<div className='InnerBodyMain'>
<div className='SliderWrapper'>
<div className='ContainerMain'>
<div className='IBMSecMain'>
<div className='simple-slider IBMSMSlider'>
2024-09-02 13:47:16 +05:00
<Swiper
className='swiper-container IBMSMSliderContainer'
wrapperClass='swiper-wrapper IBMSMSliderContainerWrapper'
2024-09-02 16:04:18 +00:00
modules={[Navigation, Pagination, A11y, Autoplay]}
2024-09-02 13:47:16 +05:00
pagination={{ clickable: true, dynamicBullets: true }}
slidesPerView={1}
autoplay={{ delay: 5000 }}
speed={1000}
navigation
loop
>
{LANDING_PAGE_DATA.featuredSlider.map((naddr) => (
<SwiperSlide
key={naddr}
className='swiper-slide IBMSMSliderContainerWrapperSlider'
>
2024-09-02 13:47:16 +05:00
<SlideContent naddr={naddr} />
</SwiperSlide>
))}
</Swiper>
2024-07-11 16:19:12 +05:00
</div>
</div>
</div>
</div>
<div className='ContainerMain'>
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'>
2024-09-02 13:47:16 +05:00
<h2 className='IBMSMTitleMainHeading'>Cool Games</h2>
2024-07-11 16:19:12 +05:00
</div>
<div className='IBMSMList IBMSMListFeaturedAlt'>
{featuredGames.map((game) => (
<GameCard
key={game['Game Name']}
title={game['Game Name']}
imageUrl={game['Boxart image']}
/>
2024-09-02 13:47:16 +05:00
))}
2024-07-11 16:19:12 +05:00
</div>
<div className='IBMSMAction'>
<a
className='btn btnMain IBMSMActionBtn'
role='button'
2024-09-02 13:47:16 +05:00
onClick={() => navigate(appRoutes.games)}
2024-07-11 16:19:12 +05:00
>
View All
</a>
</div>
</div>
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'>
2024-09-02 13:47:16 +05:00
<h2 className='IBMSMTitleMainHeading'>Awesome Mods</h2>
2024-07-11 16:19:12 +05:00
</div>
<div className='IBMSMList IBMSMListAlt'>
2024-09-02 13:47:16 +05:00
{LANDING_PAGE_DATA.awesomeMods.map((naddr) => (
<DisplayMod key={naddr} naddr={naddr} />
))}
2024-07-11 16:19:12 +05:00
</div>
<div className='IBMSMAction'>
<a
className='btn btnMain IBMSMActionBtn'
role='button'
2024-09-02 13:47:16 +05:00
onClick={() => navigate(appRoutes.mods)}
2024-07-11 16:19:12 +05:00
>
View All
</a>
</div>
</div>
2024-09-02 13:47:16 +05:00
<DisplayLatestMods />
2024-11-05 16:22:08 +01:00
<DisplayLatestBlogs />
2024-07-11 16:19:12 +05:00
</div>
</div>
</div>
)
}
2024-09-02 13:47:16 +05:00
type SlideContentProps = {
naddr: string
}
const SlideContent = ({ naddr }: SlideContentProps) => {
const navigate = useNavigate()
2024-10-14 19:20:43 +05:00
const { fetchEvent } = useNDKContext()
2024-09-02 13:47:16 +05:00
const [mod, setMod] = useState<ModDetails>()
useDidMount(() => {
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
const { identifier, kind, pubkey } = decoded.data
2024-09-02 13:47:16 +05:00
2024-10-14 13:24:43 +05:00
const ndkFilter: NDKFilter = {
2024-09-02 13:47:16 +05:00
'#a': [identifier],
authors: [pubkey],
kinds: [kind]
}
fetchEvent(ndkFilter)
2024-10-14 19:20:43 +05:00
.then((ndkEvent) => {
if (ndkEvent) {
2024-10-14 13:24:43 +05:00
const extracted = extractModData(ndkEvent)
2024-09-02 13:47:16 +05:00
setMod(extracted)
}
})
.catch((err) => {
log(
true,
LogType.Error,
'An error occurred in fetching mod details from relays',
err
)
})
})
if (!mod) return <Spinner />
return (
<>
2024-09-02 09:03:41 +00:00
<div className='IBMSMSCWSPicWrapper'>
<img
src={mod.featuredImageUrl}
onError={handleModImageError}
className='IBMSMSCWSPic'
/>
</div>
2024-09-02 13:47:16 +05:00
<div className='IBMSMSCWSInfo'>
<h3 className='IBMSMSCWSInfoHeading'>{mod.title}</h3>
<div className='IBMSMSCWSInfoTextWrapper'>
<p className='IBMSMSCWSInfoText'>
{mod.summary}
<br />
</p>
</div>
2024-09-03 17:56:33 +00:00
<p className='IBMSMSCWSInfoText IBMSMSCWSInfoText2'>
{mod.game}
2024-09-03 17:56:33 +00:00
<br />
</p>
2024-09-02 13:47:16 +05:00
<div className='IBMSMSliderContainerWrapperSliderAction'>
<a
className='btn btnMain IBMSMSliderContainerWrapperSliderActionbtn'
role='button'
onClick={() => navigate(getModPageRoute(naddr))}
2024-09-02 13:47:16 +05:00
>
Check it out
</a>
</div>
</div>
</>
)
}
type DisplayModProps = {
naddr: string
}
const DisplayMod = ({ naddr }: DisplayModProps) => {
const [mod, setMod] = useState<ModDetails>()
2024-10-14 19:20:43 +05:00
const { fetchEvent } = useNDKContext()
2024-10-14 13:24:43 +05:00
2024-09-02 13:47:16 +05:00
useDidMount(() => {
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
const { identifier, kind, pubkey } = decoded.data
2024-09-02 13:47:16 +05:00
2024-10-14 13:24:43 +05:00
const ndkFilter: NDKFilter = {
2024-09-02 13:47:16 +05:00
'#a': [identifier],
authors: [pubkey],
kinds: [kind]
}
fetchEvent(ndkFilter)
2024-10-14 19:20:43 +05:00
.then((ndkEvent) => {
if (ndkEvent) {
2024-10-14 13:24:43 +05:00
const extracted = extractModData(ndkEvent)
2024-09-02 13:47:16 +05:00
setMod(extracted)
}
})
.catch((err) => {
log(
true,
LogType.Error,
'An error occurred in fetching mod details from relays',
err
)
})
})
if (!mod) return <Spinner />
return <ModCard {...mod} />
2024-09-02 13:47:16 +05:00
}
const DisplayLatestMods = () => {
const navigate = useNavigate()
2024-10-14 13:24:43 +05:00
const { fetchMods } = useNDKContext()
const { siteWot, siteWotLevel, userWot, userWotLevel } = useAppSelector(
(state) => state.wot
)
2024-09-02 13:47:16 +05:00
const [isFetchingLatestMods, setIsFetchingLatestMods] = useState(true)
const [latestMods, setLatestMods] = useState<ModDetails[]>([])
const muteLists = useMuteLists()
const nsfwList = useNSFWList()
const repostList = useRepostList()
2024-09-02 13:47:16 +05:00
useDidMount(() => {
fetchMods({ source: window.location.host })
2024-10-14 13:24:43 +05:00
.then((mods) => {
// Sort by the latest (published_at descending)
mods.sort((a, b) => b.published_at - a.published_at)
setLatestMods(mods)
2024-09-02 13:47:16 +05:00
})
.finally(() => {
setIsFetchingLatestMods(false)
})
})
const filteredMods = useMemo(() => {
const mutedAuthors = [...muteLists.admin.authors, ...muteLists.user.authors]
const mutedEvents = [
...muteLists.admin.replaceableEvents,
...muteLists.user.replaceableEvents
]
const filtered = latestMods.filter(
(mod) =>
!mutedAuthors.includes(mod.author) &&
!mutedEvents.includes(mod.aTag) &&
!nsfwList.includes(mod.aTag) &&
!mod.nsfw &&
isInWoT(siteWot, siteWotLevel, mod.author) &&
isInWoT(userWot, userWotLevel, mod.author)
)
// Add repost tag if missing
for (let i = 0; i < filtered.length; i++) {
const mod = filtered[i]
const isMissingRepostTag =
!mod.repost && mod.aTag && repostList.includes(mod.aTag)
if (isMissingRepostTag) {
mod.repost = true
}
}
return filtered.slice(0, 4)
}, [
latestMods,
muteLists.admin.authors,
muteLists.admin.replaceableEvents,
muteLists.user.authors,
muteLists.user.replaceableEvents,
nsfwList,
repostList,
siteWot,
siteWotLevel,
userWot,
userWotLevel
])
2024-09-02 13:47:16 +05:00
return (
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Latest Mods</h2>
</div>
<div className='IBMSMList'>
{isFetchingLatestMods ? (
<Spinner />
) : (
filteredMods.map((mod) => {
return <ModCard key={mod.id} {...mod} />
2024-09-02 13:47:16 +05:00
})
)}
</div>
<div className='IBMSMAction'>
<a
className='btn btnMain IBMSMActionBtn'
role='button'
onClick={() => navigate(appRoutes.mods)}
>
View All
</a>
</div>
</div>
)
}
2024-11-05 16:22:08 +01:00
const DisplayLatestBlogs = () => {
const [blogs, setBlogs] = useState<Partial<BlogCardDetails>[]>()
const { fetchEvents } = useNDKContext()
const [filterOptions] = useLocalStorage('filter-blog-curated', {
sort: SortBy.Latest,
nsfw: NSFWFilter.Hide_NSFW
})
2024-11-13 14:30:58 +01:00
const navigation = useNavigation()
2024-11-05 16:22:08 +01:00
useDidMount(() => {
const fetchBlogs = async () => {
try {
// Show maximum of 4 blog posts
// 2 should be featured and the most recent 2 from blog npubs
// Populate the filter from known naddr (constants.ts)
const filter: NDKFilter = {
kinds: [kinds.LongFormArticle],
authors: [],
'#d': []
}
2024-11-05 16:22:08 +01:00
for (let i = 0; i < LANDING_PAGE_DATA.featuredBlogPosts.length; i++) {
try {
const naddr = LANDING_PAGE_DATA.featuredBlogPosts[i]
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
const { pubkey, identifier } = decoded.data
if (!filter.authors?.includes(pubkey)) {
filter.authors?.push(pubkey)
}
if (!filter.authors?.includes(identifier)) {
filter['#d']?.push(identifier)
2024-11-05 16:22:08 +01:00
}
} catch (error) {
// Silently ignore
}
}
// Prepare filter for the latest
2024-11-05 16:22:08 +01:00
const blogNpubs = import.meta.env.VITE_BLOG_NPUBS.split(',')
const blogHexkeys = blogNpubs
.map(npubToHex)
.filter((hexkey) => hexkey !== null)
// We fetch more posts in case of duplicates (from featured)
const latestFilter: NDKFilter = {
2024-11-05 16:22:08 +01:00
authors: blogHexkeys,
kinds: [kinds.LongFormArticle],
limit: PROFILE_BLOG_FILTER_LIMIT
}
// Filter by NSFW tag
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
latestFilter['#L'] = ['content-warning']
}
const results = await Promise.allSettled([
fetchEvents(filter),
fetchEvents(latestFilter)
])
const events: Partial<BlogCardDetails>[] = []
// Get featured blogs posts result
results.forEach((r) => {
// Add events from both promises to the array
if (r.status === 'fulfilled' && r.value) {
events.push(
...r.value
.map(extractBlogCardDetails) // Extract the blog card details
.sort(
// Sort each result by published_at in descending order
// We can't sort everything at once we'd lose prefered
(a, b) =>
a.published_at && b.published_at
? b.published_at - a.published_at
: 0
)
)
}
2024-11-05 16:22:08 +01:00
})
// Remove duplicates
const unique = Array.from(
events
.filter((b) => b.id)
2024-11-05 16:22:08 +01:00
.reduce((map, obj) => {
map.set(obj.id!, obj)
2024-11-05 16:22:08 +01:00
return map
}, new Map<string, Partial<BlogCardDetails>>())
2024-11-05 16:22:08 +01:00
.values()
).filter(
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
2024-11-05 16:22:08 +01:00
)
const latest = unique.slice(0, 4)
setBlogs(latest)
2024-11-05 16:22:08 +01:00
} catch (error) {
log(
true,
LogType.Error,
'An error occurred in fetching blog details from relays',
error
)
return null
}
}
fetchBlogs()
})
return (
<div className='IBMSecMain IBMSMListWrapper'>
{navigation.state !== 'idle' && <LoadingSpinner desc={'Fetching...'} />}
2024-11-05 16:22:08 +01:00
<div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Blog Posts</h2>
</div>
<div className='IBMSMList'>
{blogs?.map((b) => (
<BlogCard key={b.id} {...b} />
))}
</div>
<div className='IBMSMAction'>
<Link
className='btn btnMain IBMSMActionBtn'
role='button'
to={appRoutes.blogs}
>
View All
</Link>
</div>
</div>
)
}