diff --git a/.env.example b/.env.example index 6404610..e6b55e6 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,10 @@ VITE_APP_RELAY=wss://relay.degmods.com VITE_ADMIN_NPUBS= # A dedicated npub used for reporting mods, blogs, profile and etc. -VITE_REPORTING_NPUB= \ No newline at end of file +VITE_REPORTING_NPUB= + +# if there's no featured image, or if the image breaks somewhere down the line, then it should default to this image +VITE_FALLBACK_MOD_IMAGE=https://image.nostr.build/1fa7afd3489302c2da8957993ac0fd6c4308eedd4b1b95b24ecfabe3651b2183.png + +# if there's no image, or if the image breaks somewhere down the line, then it should default to this image +VITE_FALLBACK_GAME_IMAGE=https://image.nostr.build/1fa7afd3489302c2da8957993ac0fd6c4308eedd4b1b95b24ecfabe3651b2183.png \ No newline at end of file diff --git a/.gitea/workflows/release-production.yaml b/.gitea/workflows/release-production.yaml index 1415a50..88cffa7 100644 --- a/.gitea/workflows/release-production.yaml +++ b/.gitea/workflows/release-production.yaml @@ -25,6 +25,8 @@ jobs: echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env + echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env + echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env cat .env - name: Create Build diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index c08b4e8..10e4bc4 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -25,6 +25,8 @@ jobs: echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env + echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env + echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env cat .env - name: Create Build diff --git a/index.html b/index.html index fef57eb..5d13bc4 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,10 @@ - + @@ -14,24 +17,33 @@ - + - + - - + DEG Mods - Liberating Game Mods
- - + diff --git a/package-lock.json b/package-lock.json index 3d378b3..6526152 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "react-router-dom": "^6.24.1", "react-toastify": "10.0.5", "react-window": "1.8.10", + "swiper": "11.1.11", "uuid": "10.0.0", "webln": "0.3.2" }, @@ -4932,6 +4933,24 @@ "node": ">=4" } }, + "node_modules/swiper": { + "version": "11.1.11", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.1.11.tgz", + "integrity": "sha512-077Aw3OrlZpkkBRf/6+44bGh/HZY/vsLEyate2db2KkJgYUIR5TvDgvvhcJtW/puXzw79w5KBc30DauEX6GZYQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index f4cec95..7fd0014 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "react-router-dom": "^6.24.1", "react-toastify": "10.0.5", "react-window": "1.8.10", + "swiper": "11.1.11", "uuid": "10.0.0", "webln": "0.3.2" }, diff --git a/src/components/GameCard.tsx b/src/components/GameCard.tsx index 9cd2c03..ade5f15 100644 --- a/src/components/GameCard.tsx +++ b/src/components/GameCard.tsx @@ -1,20 +1,23 @@ import '../styles/cardGames.css' +import { handleGameImageError } from '../utils' type GameCardProps = { - backgroundLink: string + title: string + imageUrl: string } -export const GameCard = ({ backgroundLink }: GameCardProps) => { +export const GameCard = ({ title, imageUrl }: GameCardProps) => { return (
-
+
+ +
-

This is a game title, the best game title

+

{title}

) diff --git a/src/components/ModCard.tsx b/src/components/ModCard.tsx index a47c927..86b7070 100644 --- a/src/components/ModCard.tsx +++ b/src/components/ModCard.tsx @@ -1,9 +1,10 @@ import '../styles/cardMod.css' +import { handleModImageError } from '../utils' type ModCardProps = { title: string summary: string - backgroundLink: string + imageUrl: string link: string handleClick: () => void } @@ -11,7 +12,7 @@ type ModCardProps = { export const ModCard = ({ title, summary, - backgroundLink, + imageUrl, link, handleClick }: ModCardProps) => { @@ -25,12 +26,13 @@ export const ModCard = ({ }} >
-
+
+ +

{title}

{summary}

diff --git a/src/constants.ts b/src/constants.ts index d141103..f0e7bff 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,2 +1,38 @@ export const T_TAG_VALUE = 'GameMod' export const MOD_FILTER_LIMIT = 20 +export const LANDING_PAGE_DATA = { + featuredSlider: [ + 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5cek8pnrwc34xgknyv33xqkngc34xyknscfjxsknzvp38quxgc33vejnqvqhqecq8', + 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vpcxs6nwwp3x5knyd3evckngetxxcknjdfkx5kngdfhvgukvwfjxsunseqnend73', + 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6', + 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5dp4xsex2e3cxuknsdryvvkngc3sxcknjef4vcknvvmyvcukyd3kvd3rxdgnuver5', + 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vf5x9nrxcekxvknjvmzxvkngcfsx5kkzcf3xqknsvmrvgenwe3j8p3nzwgka59vj' + ], + awesomeMods: [ + 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6', + 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5df5xccngvtrxqkkydpexukngvp4xgknsvp4vskkgdrxvgmkxdmp8quxycgx78rpf', + 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vrrvgmnjc33xuknwde4vskngvekxgknsenyxvkk2ctxvscrvenpvsmnxeqydygjx' + ], + featuredGames: [ + { + title: 'SUPERHOT', + imageUrl: '' + }, + { + title: 'The Bounce House', + imageUrl: '' + }, + { + title: 'Immortal Guns', + imageUrl: '' + }, + { + title: 'Magenta Horizon Act 1', + imageUrl: '' + }, + { + title: 'DEAD LETTER DEPT. Demo', + imageUrl: '' + } + ] +} diff --git a/src/controllers/relay.ts b/src/controllers/relay.ts index f688878..2e362ca 100644 --- a/src/controllers/relay.ts +++ b/src/controllers/relay.ts @@ -14,6 +14,7 @@ import { MetadataController, UserRelaysType } from './metadata' */ export class RelayController { private static instance: RelayController + private events = new Map() private debug = true public connectedRelays: Relay[] = [] @@ -151,6 +152,16 @@ export class RelayController { // Wait for all publish operations to complete (either fulfilled or rejected) await Promise.allSettled(publishPromises) + if (publishedOnRelays.length > 0) { + // if the event was successfully published to relays then check if it contains the `aTag` + // if so, then cache the event + + const aTag = event.tags.find((item) => item[0] === 'a') + if (aTag && aTag[1]) { + this.events.set(aTag[1], event) + } + } + // Return the list of relay URLs where the event was published return publishedOnRelays } @@ -335,13 +346,35 @@ export class RelayController { filter: Filter, relays: string[] = [] ): Promise => { + // first check if event is present in cached map then return that + // otherwise query relays + if (filter['#a']) { + const aTag = filter['#a'][0] + const cachedEvent = this.events.get(aTag) + + if (cachedEvent) return cachedEvent + } + const events = await this.fetchEvents(filter, relays) // Sort events by creation date in descending order events.sort((a, b) => b.created_at - a.created_at) - // Return the most recent event, or null if no events were received - return events[0] || null + if (events.length > 0) { + const event = events[0] + + // if the aTag was specified in filter then cache the fetched event before returning + if (filter['#a']) { + const aTag = filter['#a'][0] + this.events.set(aTag, event) + } + + // return the event + return event + } + + // return null if event array is empty + return null } /** @@ -358,6 +391,15 @@ export class RelayController { hexKey: string, userRelaysType: UserRelaysType ) => { + // first check if event is present in cached map then return that + // otherwise query relays + if (filter['#a']) { + const aTag = filter['#a'][0] + const cachedEvent = this.events.get(aTag) + + if (cachedEvent) return cachedEvent + } + // Get an instance of the MetadataController, which manages user metadata and relays const metadataController = await MetadataController.getInstance() diff --git a/src/pages/games.tsx b/src/pages/games.tsx index bdd5dc9..d2ed7bc 100644 --- a/src/pages/games.tsx +++ b/src/pages/games.tsx @@ -35,11 +35,26 @@ export const GamesPage = () => {
- - - - - + + + + +
diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 9a9cef3..f34ad81 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,123 +1,58 @@ +import { Filter, kinds, nip19 } from 'nostr-tools' +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { A11y, Navigation, Pagination } from 'swiper/modules' +import { Swiper, SwiperSlide } from 'swiper/react' import { BlogCard } from '../components/BlogCard' import { GameCard } from '../components/GameCard' import { ModCard } from '../components/ModCard' +import { LANDING_PAGE_DATA } from '../constants' +import { RelayController } from '../controllers' +import { useDidMount } from '../hooks' +import { appRoutes, getModsInnerPageRoute } from '../routes' +import { ModDetails } from '../types' +import { + extractModData, + fetchMods, + handleModImageError, + log, + LogType +} from '../utils' + import '../styles/cardLists.css' import '../styles/SimpleSlider.css' import '../styles/styles.css' +// Import Swiper styles +import 'swiper/css' +import 'swiper/css/navigation' +import 'swiper/css/pagination' + export const HomePage = () => { + const navigate = useNavigate() return (
-
-
-
-
-
-

Placeholder

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Integer nec odio. Praesent libero. Sed cursus ante - dapibus diam. Sed nisi. Nulla quis sem at nibh elementum - imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce - nec tellus sed augue semper porta. Mauris massa. - Vestibulum lacinia arcu eget nulla. className aptent - taciti sociosqu ad litora torquent per conubia nostra, - per inceptos himenaeos. Curabitur sodales ligula in - libero. -
-

- -
-
-
-
-
-

Placeholder

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Integer nec odio. Praesent libero. Sed cursus ante - dapibus diam. Sed nisi. Nulla quis sem at nibh elementum - imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce - nec tellus sed augue semper porta. Mauris massa. - Vestibulum lacinia arcu eget nulla. className aptent - taciti sociosqu ad litora torquent per conubia nostra, - per inceptos himenaeos. Curabitur sodales ligula in - libero. -
-

- -
-
-
-
-
-

Placeholder

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Integer nec odio. Praesent libero. Sed cursus ante - dapibus diam. Sed nisi. Nulla quis sem at nibh elementum - imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce - nec tellus sed augue semper porta. Mauris massa. - Vestibulum lacinia arcu eget nulla. className aptent - taciti sociosqu ad litora torquent per conubia nostra, - per inceptos himenaeos. Curabitur sodales ligula in - libero. -
-

- -
-
-
-
-
-
-
+ + {LANDING_PAGE_DATA.featuredSlider.map((naddr) => ( + + + + ))} +
@@ -126,20 +61,18 @@ export const HomePage = () => {
-

Cool Games (WIP)

+

Cool Games

- - - - - + {LANDING_PAGE_DATA.featuredGames.map((game) => ( + + ))}
navigate(appRoutes.games)} > View All @@ -147,114 +80,24 @@ export const HomePage = () => {
-

Awesome Mods (WIP)

+

Awesome Mods

- { - alert( - 'these are dummy mods. So navigation on these are not implemented yet' - ) - }} - /> - { - alert( - 'these are dummy mods. So navigation on these are not implemented yet' - ) - }} - /> - { - alert( - 'these are dummy mods. So navigation on these are not implemented yet' - ) - }} - /> + {LANDING_PAGE_DATA.awesomeMods.map((naddr) => ( + + ))}
-
-
-
-

Latest Mods (WIP)

-
-
- { - alert( - 'these are dummy mods. So navigation on these are not implemented yet' - ) - }} - /> - { - alert( - 'these are dummy mods. So navigation on these are not implemented yet' - ) - }} - /> - { - alert( - 'these are dummy mods. So navigation on these are not implemented yet' - ) - }} - /> - { - alert( - 'these are dummy mods. So navigation on these are not implemented yet' - ) - }} - /> -
- -
+

Blog Posts (WIP)

@@ -281,3 +124,189 @@ export const HomePage = () => {
) } + +type SlideContentProps = { + naddr: string +} + +const SlideContent = ({ naddr }: SlideContentProps) => { + const navigate = useNavigate() + const [mod, setMod] = useState() + + useDidMount(() => { + const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) + const { identifier, kind, pubkey, relays = [] } = decoded.data + + const filter: Filter = { + '#a': [identifier], + authors: [pubkey], + kinds: [kind] + } + + RelayController.getInstance() + .fetchEvent(filter, relays) + .then((event) => { + if (event) { + const extracted = extractModData(event) + setMod(extracted) + } + }) + .catch((err) => { + log( + true, + LogType.Error, + 'An error occurred in fetching mod details from relays', + err + ) + }) + }) + + if (!mod) return + + return ( + <> +
+ +
+
+

{mod.title}

+

+ {mod.summary} +
+

+ +
+ + ) +} + +type DisplayModProps = { + naddr: string +} + +const DisplayMod = ({ naddr }: DisplayModProps) => { + const navigate = useNavigate() + const [mod, setMod] = useState() + + useDidMount(() => { + const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) + const { identifier, kind, pubkey, relays = [] } = decoded.data + + const filter: Filter = { + '#a': [identifier], + authors: [pubkey], + kinds: [kind] + } + + RelayController.getInstance() + .fetchEvent(filter, relays) + .then((event) => { + if (event) { + const extracted = extractModData(event) + setMod(extracted) + } + }) + .catch((err) => { + log( + true, + LogType.Error, + 'An error occurred in fetching mod details from relays', + err + ) + }) + }) + + if (!mod) return + + const route = getModsInnerPageRoute(naddr) + + return ( + navigate(route)} + /> + ) +} + +const DisplayLatestMods = () => { + const navigate = useNavigate() + const [isFetchingLatestMods, setIsFetchingLatestMods] = useState(true) + const [latestMods, setLatestMods] = useState([]) + + useDidMount(() => { + fetchMods({ source: window.location.host, limit: 4 }) + .then((res) => { + setLatestMods(res) + }) + .finally(() => { + setIsFetchingLatestMods(false) + }) + }) + + return ( +
+
+

Latest Mods

+
+
+ {isFetchingLatestMods ? ( + + ) : ( + latestMods.map((mod) => { + const route = getModsInnerPageRoute( + nip19.naddrEncode({ + identifier: mod.aTag, + pubkey: mod.author, + kind: kinds.ClassifiedListing + }) + ) + + return ( + navigate(route)} + /> + ) + }) + )} +
+ + +
+ ) +} + +const Spinner = () => { + return ( +
+
+
+ ) +} diff --git a/src/pages/mods.tsx b/src/pages/mods.tsx index e61be15..0c3506c 100644 --- a/src/pages/mods.tsx +++ b/src/pages/mods.tsx @@ -91,7 +91,7 @@ export const ModsPage = () => { useEffect(() => { setIsFetching(true) - fetchMods(filterOptions.source) + fetchMods({ source: filterOptions.source }) .then((res) => { setMods(res) }) @@ -106,7 +106,10 @@ export const ModsPage = () => { const until = mods.length > 0 ? mods[mods.length - 1].published_at - 1 : undefined - fetchMods(filterOptions.source, until) + fetchMods({ + source: filterOptions.source, + until + }) .then((res) => { setMods(res) setPage((prev) => prev + 1) @@ -121,7 +124,10 @@ export const ModsPage = () => { const since = mods.length > 0 ? mods[0].published_at + 1 : undefined - fetchMods(filterOptions.source, undefined, since) + fetchMods({ + source: filterOptions.source, + since + }) .then((res) => { setMods(res) setPage((prev) => prev - 1) @@ -215,7 +221,7 @@ export const ModsPage = () => { key={mod.id} title={mod.title} summary={mod.summary} - backgroundLink={mod.featuredImageUrl} + imageUrl={mod.featuredImageUrl} link={`#${route}`} handleClick={() => navigate(route)} /> diff --git a/src/styles/SimpleSlider.css b/src/styles/SimpleSlider.css index 9ea9e1a..60bf918 100644 --- a/src/styles/SimpleSlider.css +++ b/src/styles/SimpleSlider.css @@ -1,6 +1,6 @@ .swiper-pagination-bullet-active { - background: rgba(255,255,255,0.5); - box-shadow: 0 0 4px 0 rgba(0,0,0,0.5); + background: rgba(255, 255, 255, 0.5); + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.5); } .simple-slider .swiper-slide { @@ -22,16 +22,22 @@ } } -.simple-slider .swiper-button-next, .simple-slider .swiper-button-prev { +.simple-slider .swiper-button-next, +.simple-slider .swiper-button-prev { width: 50px; margin-left: 00px; margin-right: 00px; - color: rgba(255,255,255,0.5); - background: linear-gradient(rgba(255,255,255,0.05), rgba(255,255,255,0.05)), linear-gradient(to top right, #262626, #292929, #262626), linear-gradient(to top right, #262626, #292929, #262626); + color: rgba(255, 255, 255, 0.5); + background: linear-gradient( + rgba(255, 255, 255, 0.05), + rgba(255, 255, 255, 0.05) + ), + linear-gradient(to top right, #262626, #292929, #262626), + linear-gradient(to top right, #262626, #292929, #262626); padding: 10px; height: 75px; border-radius: 10px; - box-shadow: 0 0 8px 0 rgb(0,0,0,0.1); + box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1); display: flex; flex-direction: column; justify-content: center; @@ -39,19 +45,27 @@ margin-top: -35px; } -.simple-slider .swiper-button-next:hover, .simple-slider .swiper-button-prev:hover { - background: linear-gradient(rgba(255,255,255,0.1), rgba(255,255,255,0.1)), linear-gradient(to top right, #262626, #292929, #262626), linear-gradient(to top right, #262626, #292929, #262626); +.simple-slider .swiper-button-next:hover, +.simple-slider .swiper-button-prev:hover { + background: linear-gradient( + rgba(255, 255, 255, 0.1), + rgba(255, 255, 255, 0.1) + ), + linear-gradient(to top right, #262626, #292929, #262626), + linear-gradient(to top right, #262626, #292929, #262626); } -.swiper-button-next:after, .swiper-button-prev:after { - font-size: 18px; +.swiper-button-next:after, +.swiper-button-prev:after { + font-size: 18px!important; } -@media (max-width:992px) { - .simple-slider .swiper-button-next, .simple-slider .swiper-button-prev { +@media (max-width: 992px) { + .simple-slider .swiper-button-next, + .simple-slider .swiper-button-prev { bottom: 0; top: unset; - width: 48%; + width: 45%; height: unset; padding: 10px; } @@ -115,7 +129,12 @@ bottom: 0; right: 0; left: 0; - background: linear-gradient(rgba(255,255,255,0.15), rgba(255,255,255,0.15)), linear-gradient(to top right, #262626, #292929, #262626), linear-gradient(to top right, #262626, #292929, #262626); + background: linear-gradient( + rgba(255, 255, 255, 0.15), + rgba(255, 255, 255, 0.15) + ), + linear-gradient(to top right, #262626, #292929, #262626), + linear-gradient(to top right, #262626, #292929, #262626); z-index: -1; border-radius: 10px; } @@ -129,12 +148,15 @@ opacity: 1; } -.swiper-container-horizontal > .swiper-pagination-bullets, .swiper-pagination-custom, .swiper-pagination-fraction { +.swiper-container-horizontal > .swiper-pagination-bullets, +.swiper-pagination-custom, +.swiper-pagination-fraction { width: 100%; bottom: 0; } -.swiper-button-next, .swiper-button-prev { +.swiper-button-next, +.swiper-button-prev { position: absolute; } @@ -161,23 +183,35 @@ .SliderWrapper { width: 100%; padding: 50px 0; - background: rgba(0,0,0,0.1); + background: rgba(0, 0, 0, 0.1); backdrop-filter: blur(5px); display: flex; flex-direction: column; justify-content: center; align-items: center; margin: -25px 0 0 0; - border-bottom: solid 1px rgba(255,255,255,0.05); + border-bottom: solid 1px rgba(255, 255, 255, 0.05); } .IBMSMSCWSPic { border-radius: 10px; overflow: hidden; - border: solid 1px rgba(255,255,255,0.05); - padding-top: 50%; + border: solid 1px rgba(255, 255, 255, 0.05); z-index: 1; - box-shadow: 0 0 8px 0 rgba(0,0,0,0.25); + box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.25); + width: 100%; + height: 100%; + object-fit: cover; /* Ensures the image covers the container like a background image */ +} + +.IBMSMSCWSPicWrapper { + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + overflow: hidden; + position: relative; } .IBMSMSCWSInfo { @@ -187,9 +221,15 @@ justify-content: center; padding: 25px; border-radius: 10px; - background: linear-gradient(rgba(255,255,255,0), rgba(255,255,255,0)), linear-gradient(to top right, rgb(38,38,38), rgb(41,41,41), rgb(38,38,38)); - box-shadow: 0 0 8px 0 rgb(0,0,0,0.1); - border: solid 1px rgba(255,255,255,0.05); + background: linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 0)), + linear-gradient( + to top right, + rgb(38, 38, 38), + rgb(41, 41, 41), + rgb(38, 38, 38) + ); + box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1); + border: solid 1px rgba(255, 255, 255, 0.05); } @media (max-width: 768px) { @@ -212,7 +252,7 @@ -webkit-line-clamp: 2; font-size: 20px; line-height: 1.25; - color: rgba(255,255,255,0.75); + color: rgba(255, 255, 255, 0.75); font-weight: bold; } @@ -221,7 +261,7 @@ -webkit-box-orient: vertical; overflow: hidden; -webkit-line-clamp: 8; - color: rgba(255,255,255,0.5); + color: rgba(255, 255, 255, 0.5); font-size: 15px; line-height: 1.5; flex-grow: 1; @@ -235,6 +275,8 @@ } .swiper-pagination { + display: none; + bottom: -10px !important; } @media (max-width: 992px) { @@ -244,7 +286,7 @@ } .swiper-pagination-bullet { - background: rgba(0,0,0,0.5); + background: rgba(0, 0, 0, 0.5); opacity: 1; width: 12px; height: 12px; @@ -252,6 +294,5 @@ } .swiper-pagination-bullet.swiper-pagination-bullet-active { - background: rgba(128,0,255,0.5); + background: rgba(128, 0, 255, 0.5); } - diff --git a/src/styles/cardGames.css b/src/styles/cardGames.css index 4aee260..1e6aadd 100644 --- a/src/styles/cardGames.css +++ b/src/styles/cardGames.css @@ -17,16 +17,24 @@ transform: scale(1); } -.cardGameMain { +.cardGameMainWrapper { + position: relative; padding-top: 150%; +} + +.cardGameMain { border-radius: 15px; - background: rgba(255,255,255,0.05); - box-shadow: 0 0 8px 0 rgb(0,0,0,0.1); + box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1); + width: 100%; + object-fit: cover; /* Ensures the image covers the container like a background image */ + position: absolute; + height: 100%; + top: 0; } .cardGameMainTitle { transition: ease 0.4s; - color: rgba(255,255,255,0.5); + color: rgba(255, 255, 255, 0.5); padding: 0 15px; font-weight: bold; display: -webkit-box; @@ -36,4 +44,3 @@ font-size: 18px; line-height: 1.5; } - diff --git a/src/styles/cardMod.css b/src/styles/cardMod.css index ec7b3ae..4bd8600 100644 --- a/src/styles/cardMod.css +++ b/src/styles/cardMod.css @@ -9,11 +9,18 @@ background: linear-gradient(to top right, #262626, #292929, #262626); } -.cMMPicture { +.cMMPictureWrapper { position: relative; width: 100%; padding-top: 56.25%; - background: rgba(0, 0, 0, 0.1); +} + +.cMMPicture { + position: absolute; + width: 100%; + height: 100%; + top: 0; + object-fit: cover; /* Ensures the image covers the container like a background image */ } .cMMBody { diff --git a/src/styles/styles.css b/src/styles/styles.css index 6c11cf6..d9a719f 100644 --- a/src/styles/styles.css +++ b/src/styles/styles.css @@ -655,3 +655,28 @@ a:hover { .errorMainText { } + +.spinner { + display: flex; + justify-content: center; + align-items: center; + height: 100%; +} + +.spinnerCircle { + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/src/styles/tags.css b/src/styles/tags.css index 9a94e80..bbcd86a 100644 --- a/src/styles/tags.css +++ b/src/styles/tags.css @@ -27,6 +27,7 @@ color: rgba(255, 255, 255, 0.5); box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1); background: rgba(255, 255, 255, 0.05); + text-decoration: unset; } .IBMSMSMBSSTagsTag:active { diff --git a/src/utils/mod.ts b/src/utils/mod.ts index ac2a9cc..6e3511d 100644 --- a/src/utils/mod.ts +++ b/src/utils/mod.ts @@ -134,6 +134,13 @@ export const initializeFormState = ( ] }) +interface FetchModsOptions { + source?: string + until?: number + since?: number + limit?: number +} + /** * Fetches a list of mods based on the provided source. * @@ -144,15 +151,16 @@ export const initializeFormState = ( * @returns A promise that resolves to an array of `ModDetails` objects. In case of an error, * it logs the error and shows a notification, then returns an empty array. */ -export const fetchMods = async ( - source: string, - until?: number, - since?: number -): Promise => { +export const fetchMods = async ({ + source, + until, + since, + limit +}: FetchModsOptions): Promise => { // Define the filter criteria for fetching mods const filter: Filter = { kinds: [kinds.ClassifiedListing], // Specify the kind of events to fetch - limit: MOD_FILTER_LIMIT, // Limit the number of events fetched to 20 + limit: limit || MOD_FILTER_LIMIT, // Limit the number of events fetched to 20 '#t': [T_TAG_VALUE], until, // Optional filter to fetch events until this timestamp since // Optional filter to fetch events from this timestamp diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 85dabeb..b321c91 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -123,3 +123,15 @@ export const abbreviateNumber = (value: number): string => { return value.toString() } } + +export const handleGameImageError = ( + e: React.SyntheticEvent +) => { + e.currentTarget.src = import.meta.env.VITE_FALLBACK_GAME_IMAGE +} + +export const handleModImageError = ( + e: React.SyntheticEvent +) => { + e.currentTarget.src = import.meta.env.VITE_FALLBACK_MOD_IMAGE +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 141f56d..1f3c47c 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -4,6 +4,8 @@ interface ImportMetaEnv { readonly VITE_APP_RELAY: string readonly VITE_ADMIN_NPUBS: string readonly VITE_REPORTING_NPUB: string + readonly VITE_FALLBACK_MOD_IMAGE: string + readonly VITE_FALLBACK_GAME_IMAGE: string // more env variables... }