feat: implemented mod details page #3

Merged
s merged 6 commits from mods-inner into master 2024-08-06 10:57:28 +00:00
21 changed files with 2302 additions and 51 deletions
Showing only changes of commit cd0a53a8c8 - Show all commits

32
package-lock.json generated
View File

@ -10,6 +10,8 @@
"dependencies": {
"@nostr-dev-kit/ndk": "2.8.2",
"@reduxjs/toolkit": "2.2.6",
"date-fns": "3.6.0",
"dompurify": "3.1.6",
"lodash": "4.17.21",
"nostr-login": "1.5.2",
"nostr-tools": "2.7.1",
@ -24,6 +26,7 @@
"uuid": "10.0.0"
},
"devDependencies": {
"@types/dompurify": "3.0.5",
"@types/lodash": "4.17.7",
"@types/papaparse": "5.3.14",
"@types/react": "^18.3.3",
@ -1538,6 +1541,15 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"dev": true,
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@ -1610,6 +1622,12 @@
"@types/react": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
@ -2213,6 +2231,15 @@
"node": ">= 12"
}
},
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
@ -2319,6 +2346,11 @@
"node": ">=6.0.0"
}
},
"node_modules/dompurify": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz",
"integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.823",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.823.tgz",

View File

@ -12,6 +12,8 @@
"dependencies": {
"@nostr-dev-kit/ndk": "2.8.2",
"@reduxjs/toolkit": "2.2.6",
"date-fns": "3.6.0",
"dompurify": "3.1.6",
"lodash": "4.17.21",
"nostr-login": "1.5.2",
"nostr-tools": "2.7.1",
@ -26,6 +28,7 @@
"uuid": "10.0.0"
},
"devDependencies": {
"@types/dompurify": "3.0.5",
"@types/lodash": "4.17.7",
"@types/papaparse": "5.3.14",
"@types/react": "^18.3.3",

View File

@ -1,5 +1,5 @@
import _ from 'lodash'
import { Event, kinds, UnsignedEvent } from 'nostr-tools'
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools'
import Papa from 'papaparse'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'react-toastify'
@ -17,27 +17,9 @@ import {
} from '../utils'
import { CheckboxField, InputError, InputField } from './Inputs'
import { RelayController } from '../controllers'
interface DownloadUrl {
url: string
hash: string
signatureKey: string
malwareScanLink: string
modVersion: string
customNote: string
}
interface FormState {
game: string
title: string
body: string
featuredImageUrl: string
summary: string
nsfw: boolean
screenshotsUrls: string[]
tags: string
downloadUrls: DownloadUrl[]
}
import { useNavigate } from 'react-router-dom'
import { getModsInnerPageRoute } from '../routes'
import { DownloadUrl, FormState } from '../types'
interface FormErrors {
game?: string
@ -59,6 +41,7 @@ interface GameOption {
let processedCSV = false
export const ModForm = () => {
const navigate = useNavigate()
const userState = useAppSelector((state) => state.user)
const [isPublishing, setIsPublishing] = useState(false)
@ -250,6 +233,7 @@ export const ModForm = () => {
const signedEvent = await window.nostr
?.signEvent(unsignedEvent)
.then((event) => event as Event)
.catch((err) => {
toast.error('Failed to sign the event!')
log(true, LogType.Error, 'Failed to sign the event!', err)
@ -265,14 +249,6 @@ export const ModForm = () => {
signedEvent as Event
)
console.log('publishedOnRelays :>> ', publishedOnRelays)
if (!publishedOnRelays) {
toast.error('Failed to publish event!')
setIsPublishing(false)
return
}
// Handle cases where publishing failed or succeeded
if (publishedOnRelays.length === 0) {
toast.error('Failed to publish event on any relay')
@ -282,6 +258,15 @@ export const ModForm = () => {
'\n'
)}`
)
const nevent = nip19.neventEncode({
id: signedEvent.id,
author: signedEvent.pubkey,
kind: signedEvent.kind,
relays: publishedOnRelays
})
navigate(getModsInnerPageRoute(nevent))
}
setIsPublishing(false)

View File

@ -1,7 +1,6 @@
import { Relay, Event } from 'nostr-tools'
import { log, LogType, timeout } from '../utils'
import { Event, Filter, Relay } from 'nostr-tools'
import { log, LogType, normalizeWebSocketURL, timeout } from '../utils'
import { MetadataController } from './metadata'
import _ from 'lodash'
/**
* Singleton class to manage relay operations.
@ -15,7 +14,8 @@ export class RelayController {
private connectRelay = async (relayUrl: string) => {
const relay = this.connectedRelays.find(
(relay) => _.trimEnd(relay.url, '/') === _.trimEnd(relayUrl, '/')
(relay) =>
normalizeWebSocketURL(relay.url) === normalizeWebSocketURL(relayUrl)
)
if (relay) {
// already connected, skip
@ -51,38 +51,55 @@ export class RelayController {
return RelayController.instance
}
publish = async (event: Event) => {
/**
* Publishes an event to multiple relays.
*
* This method connects to the application relay and a set of write relays
* obtained from the `MetadataController`. It then publishes the event to
* all connected relays and returns a list of relays where the event was successfully published.
*
* @param event - The event to be published.
* @returns A promise that resolves to an array of URLs of relays where the event was published,
* or an empty array if no relays were connected or the event could not be published.
*/
publish = async (event: Event): Promise<string[]> => {
// Connect to the application relay specified by environment variable
const appRelayPromise = this.connectRelay(import.meta.env.VITE_APP_RELAY)
// todo: window.nostr.getRelays() is not implemented yet in nostr-login, implement the logic once its done
// Retrieve the list of write relays for the event's public key
// Use a timeout to handle cases where retrieving write relays takes too long
const writeRelaysPromise = MetadataController.getInstance().findWriteRelays(
event.pubkey
)
log(this.debug, LogType.Info, `Finding user's write relays`)
log(this.debug, LogType.Info, ` Finding user's write relays`)
// Use Promise.race to either get the write relay URLs or timeout
const writeRelayUrls = await Promise.race([
writeRelaysPromise,
timeout()
timeout() // This is a custom timeout function that rejects the promise after a specified time
]).catch((err) => {
log(this.debug, LogType.Error, err)
return [] as string[]
return [] as string[] // Return an empty array if an error occurs
})
// Connect to all write relays obtained from MetadataController
const relayPromises = writeRelayUrls.map((relayUrl) =>
this.connectRelay(relayUrl)
)
// Wait for all relay connections to settle (either fulfilled or rejected)
await Promise.allSettled([appRelayPromise, ...relayPromises])
// Check if any relays are connected; if not, log an error and return null
if (this.connectedRelays.length === 0) {
log(this.debug, LogType.Error, 'No relay is connected!')
return null
return []
}
const publishedOnRelays: string[] = []
const publishedOnRelays: string[] = [] // List to track which relays successfully published the event
// Create a promise for publishing the event to each connected relay
const publishPromises = this.connectedRelays.map((relay) => {
log(
this.debug,
@ -91,7 +108,10 @@ export class RelayController {
event
)
return Promise.race([relay.publish(event), timeout(30000)])
return Promise.race([
relay.publish(event), // Publish the event to the relay
timeout(30000) // Set a timeout to handle cases where publishing takes too long
])
.then((res) => {
log(
this.debug,
@ -99,8 +119,7 @@ export class RelayController {
`⬆️ nostr (${relay.url}): Publish result:`,
res
)
publishedOnRelays.push(relay.url)
publishedOnRelays.push(relay.url) // Add the relay URL to the list of successfully published relays
})
.catch((err) => {
log(
@ -112,10 +131,64 @@ export class RelayController {
})
})
// Wait for all publish operations to complete (either fulfilled or rejected)
await Promise.allSettled(publishPromises)
console.log('publishedOnRelays :>> ', publishedOnRelays)
// Return the list of relay URLs where the event was published
return publishedOnRelays
}
/**
* Asynchronously retrieves an event from a set of relays based on a provided filter.
* If no relays are specified, it defaults to using connected relays.
*
* @param {Filter} filter - The filter criteria to find the event.
* @param {string[]} [relays] - An optional array of relay URLs to search for the event.
* @returns {Promise<Event | null>} - Returns a promise that resolves to the found event or null if not found.
*/
fetchEvent = async (
filter: Filter,
relays: string[] = []
): Promise<Event | null> => {
// Connect to all specified relays
const relayPromises = relays.map((relayUrl) => this.connectRelay(relayUrl))
await Promise.allSettled(relayPromises)
// Check if any relays are connected
if (this.connectedRelays.length === 0) {
log(this.debug, LogType.Error, 'No relay is connected to fetch events!')
throw new Error('No relay is connected to fetch events!')
}
const events: Event[] = []
// Create a promise for each relay subscription
const subPromises = this.connectedRelays.map((relay) => {
return new Promise<void>((resolve) => {
// Subscribe to the relay with the specified filter
const sub = relay.subscribe([filter], {
// Handle incoming events
onevent: (e) => {
log(this.debug, LogType.Info, ` ${relay.url} : Received Event`, e)
events.push(e)
},
// Handle the End-Of-Stream (EOSE) message
oneose: () => {
log(this.debug, LogType.Info, ` ${relay.url} : EOSE`)
sub.close() // Close the subscription
resolve() // Resolve the promise when EOSE is received
}
})
})
})
// Wait for all subscriptions to complete
await Promise.allSettled(subPromises)
// 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
}
}

View File

@ -1 +1,2 @@
export * from './redux'
export * from './useDidMount'

12
src/hooks/useDidMount.ts Normal file
View File

@ -0,0 +1,12 @@
import { useRef, useEffect } from 'react'
export const useDidMount = (callback: () => void) => {
const didMount = useRef<boolean>(false)
useEffect(() => {
if (callback && !didMount.current) {
didMount.current = true
callback()
}
})
}

View File

@ -57,7 +57,7 @@ export const Header = () => {
<div className={navStyles.NMTI_Sec_HomeLink_Logo}>
<img
className={navStyles.NMTI_Sec_HomeLink_LogoImg}
src='assets/img/DEG%20Mods%20Logo%20With%20Text.svg'
src='/assets/img/DEG%20Mods%20Logo%20With%20Text.svg'
/>
</div>
</Link>

845
src/pages/innerMod.tsx Normal file
View File

@ -0,0 +1,845 @@
import DOMPurify from 'dompurify'
import { Filter, nip19 } from 'nostr-tools'
import { useRef, useState } from 'react'
import { useParams } from 'react-router-dom'
import { BlogCard } from '../components/BlogCard'
import { ProfileSection } from '../components/ProfileSection'
import { RelayController } from '../controllers'
import { useDidMount } from '../hooks'
import '../styles/comments.css'
import '../styles/downloads.css'
import '../styles/innerPage.css'
import '../styles/post.css'
import '../styles/reactions.css'
import '../styles/styles.css'
import '../styles/tabs.css'
import '../styles/tags.css'
import '../styles/write.css'
import { ModDetails } from '../types'
import { extractModData } from '../utils'
import { formatDate } from 'date-fns'
export const InnerModPage = () => {
const { nevent } = useParams()
const [modData, setModData] = useState<ModDetails>()
useDidMount(async () => {
if (nevent) {
const decoded = nip19.decode<'nevent'>(nevent as `nevent1${string}`)
const eventId = decoded.data.id
const kind = decoded.data.kind
const author = decoded.data.author
const relays = decoded.data.relays || []
const filter: Filter = {
ids: [eventId]
}
if (kind) filter.kinds = [kind]
if (author) filter.authors = [author]
RelayController.getInstance()
.fetchEvent(filter, relays)
.then((event) => {
console.log('event :>> ', event)
if (event) {
const extracted = extractModData(event)
setModData(extracted)
}
})
.catch((err) => {
console.log('err :>> ', err)
})
}
})
const postBodyRef = useRef<HTMLDivElement>(null)
const viewFullPostBtnRef = useRef<HTMLDivElement>(null)
const oldDownloadListRef = useRef<HTMLDivElement>(null)
const viewFullPost = () => {
if (postBodyRef.current && viewFullPostBtnRef.current) {
postBodyRef.current.style.maxHeight = 'unset'
postBodyRef.current.style.padding = 'unset'
viewFullPostBtnRef.current.style.display = 'none'
}
}
const handleViewOldLinks = () => {
if (oldDownloadListRef.current) {
// Toggle styles
if (oldDownloadListRef.current.style.height === '0px') {
// Enable styles
oldDownloadListRef.current.style.padding = ''
oldDownloadListRef.current.style.height = ''
oldDownloadListRef.current.style.border = ''
} else {
// Disable styles
oldDownloadListRef.current.style.padding = '0'
oldDownloadListRef.current.style.height = '0'
oldDownloadListRef.current.style.border = 'unset'
}
}
}
if (!modData) return null
return (
<div className='InnerBodyMain'>
<div className='ContainerMain'>
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
<div className='IBMSMSplitMain'>
<div className='IBMSMSplitMainBigSide'>
<div className='IBMSMSplitMainBigSideSec'>
<div className='IBMSMSMBSSModFor'>
<p className='IBMSMSMBSSModForPara'>
Mod for:&nbsp;
<a className='IBMSMSMBSSModForLink' href='search.html'>
{modData.game}
</a>
</p>
<div
className='dropdown dropdownMain'
style={{ flexGrow: 'unset' }}
>
<button
className='btn btnMain btnMainDropdown'
aria-expanded='false'
type='button'
style={{
borderRadius: '5px',
background: 'unset',
padding: '5px'
}}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-192 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M64 360C94.93 360 120 385.1 120 416C120 446.9 94.93 472 64 472C33.07 472 8 446.9 8 416C8 385.1 33.07 360 64 360zM64 200C94.93 200 120 225.1 120 256C120 286.9 94.93 312 64 312C33.07 312 8 286.9 8 256C8 225.1 33.07 200 64 200zM64 152C33.07 152 8 126.9 8 96C8 65.07 33.07 40 64 40C94.93 40 120 65.07 120 96C120 126.9 94.93 152 64 152z'></path>
</svg>
</button>
<div
className={`dropdown-menu dropdown-menu-end dropdownMainMenu`}
>
<a
className='dropdown-item dropdownMainMenuItem'
href='submit-mod.html'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMSSS_Author_Top_Icon'
>
<path d='M362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32zM421.7 220.3L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3z'></path>
</svg>
Edit
</a>
<a
className='dropdown-item dropdownMainMenuItem'
href='#'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMSSS_Author_Top_Icon'
>
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
</svg>
Copy URL
</a>
<a
className='dropdown-item dropdownMainMenuItem'
href='#'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMSSS_Author_Top_Icon'
>
<path d='M503.7 226.2l-176 151.1c-15.38 13.3-39.69 2.545-39.69-18.16V272.1C132.9 274.3 66.06 312.8 111.4 457.8c5.031 16.09-14.41 28.56-28.06 18.62C39.59 444.6 0 383.8 0 322.3c0-152.2 127.4-184.4 288-186.3V56.02c0-20.67 24.28-31.46 39.69-18.16l176 151.1C514.8 199.4 514.8 216.6 503.7 226.2z'></path>
</svg>
Share
</a>
<a
className='dropdown-item dropdownMainMenuItem'
id='reportPost'
href='#'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMSSS_Author_Top_Icon'
>
<path d='M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z'></path>
</svg>
Report
</a>
<a
className='dropdown-item dropdownMainMenuItem'
href='#'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-32 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMSSS_Author_Top_Icon'
>
<path d='M323.5 51.25C302.8 70.5 284 90.75 267.4 111.1C240.1 73.62 206.2 35.5 168 0C69.75 91.12 0 210 0 281.6C0 408.9 100.2 512 224 512s224-103.1 224-230.4C448 228.4 396 118.5 323.5 51.25zM304.1 391.9C282.4 407 255.8 416 226.9 416c-72.13 0-130.9-47.73-130.9-125.2c0-38.63 24.24-72.64 65.13-83.3c10.14-2.656 19.94 4.78 19.94 15.27c0 6.941-4.469 13.16-11.16 15.19c-17.5 4.578-34.41 23.94-34.41 52.84c0 50.81 39.31 94.81 91.41 94.81c24.66 0 45.22-6.5 63.19-18.75c11.75-8 27.91 3.469 23.91 16.69C314.6 384.7 309.8 388.4 304.1 391.9z'></path>
</svg>
Block Post
</a>
</div>
</div>
</div>
<div className='IBMSMSMBSSPost'>
<div
className='IBMSMSMBSSPostPicture'
style={{
background: `url(${modData.featuredImageUrl}) center / cover no-repeat`
}}
></div>
<div className='IBMSMSMBSSPostInside'>
<div className='IBMSMSMBSSPostTitle'>
<h1 className='IBMSMSMBSSPostTitleHeading'>
{modData.title}
</h1>
</div>
<div
ref={postBodyRef}
className='IBMSMSMBSSPostBody'
style={{ maxHeight: '250px', padding: '0 10px' }}
>
<div
className='IBMSMSMBSSPostTitleText'
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(modData.body)
}}
/>
<div
ref={viewFullPostBtnRef}
className='IBMSMSMBSSPostBodyHide'
>
<p onClick={viewFullPost}>View</p>
</div>
</div>
<div className='IBMSMSMBSSShots'>
{modData.screenshotsUrls.map((url, index) => (
<img
className='IBMSMSMBSSShotsImg'
src={url}
alt={`ScreenShot-${index}`}
key={`ScreenShot-${index}`}
/>
))}
</div>
<div className='IBMSMSMBSSTags'>
{modData.tags.map((tag, index) => (
<a
className='IBMSMSMBSSTagsTag'
href='#'
key={`tag-${index}`}
>
{tag}
</a>
))}
</div>
</div>
</div>
<div className='IBMSMSplitMainBigSideSec'>
<div className='IBMSMSMBSS_Details'>
<a
href='#commentsArea'
style={{ textDecoration: 'unset', color: 'unset' }}
>
<div className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CComments'>
<div className='IBMSMSMBSS_Details_CardVisual'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSS_Details_CardVisualIcon'
>
<path d='M256 31.1c-141.4 0-255.1 93.12-255.1 208c0 49.62 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734c1.249 3 4.021 4.766 7.271 4.766c66.25 0 115.1-31.76 140.6-51.39c32.63 12.25 69.02 19.39 107.4 19.39c141.4 0 255.1-93.13 255.1-207.1S397.4 31.1 256 31.1zM127.1 271.1c-17.75 0-32-14.25-32-31.1s14.25-32 32-32s32 14.25 32 32S145.7 271.1 127.1 271.1zM256 271.1c-17.75 0-31.1-14.25-31.1-31.1s14.25-32 31.1-32s31.1 14.25 31.1 32S273.8 271.1 256 271.1zM383.1 271.1c-17.75 0-32-14.25-32-31.1s14.25-32 32-32s32 14.25 32 32S401.7 271.1 383.1 271.1z'></path>
</svg>
</div>
<p className='IBMSMSMBSS_Details_CardText'>420</p>
</div>
</a>
<div
id='reactBolt'
className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CBolt'
>
<div className='IBMSMSMBSS_Details_CardVisual'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-64 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSS_Details_CardVisualIcon'
>
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
</svg>
</div>
<p className='IBMSMSMBSS_Details_CardText'>69k</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
<div
id='reactUp'
className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactUp IBMSMSMBSS_D_CRUActive'
>
<div className='IBMSMSMBSS_Details_CardVisual'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSS_Details_CardVisualIcon'
>
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
</svg>
</div>
<p className='IBMSMSMBSS_Details_CardText'>4.2k</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
<div
id='reactDown'
className='IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactDown'
>
<div className='IBMSMSMBSS_Details_CardVisual'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSS_Details_CardVisualIcon'
>
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
</svg>
</div>
<p className='IBMSMSMBSS_Details_CardText'>69</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
</div>
</div>
<div className='IBMSMSplitMainBigSideSec'>
<div className='IBMSMSMBSSPost_PostDetails'>
<div
data-bs-toggle='tooltip'
data-bs-placement='left'
className='IBMSMSMBSSPost_PDElement'
title='Publish date'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSPost_PDElementIcon'
data-bs-toggle='tooltip'
data-bss-tooltip
aria-label='Publish date'
>
<path d='M480 32H128C110.3 32 96 46.33 96 64v336C96 408.8 88.84 416 80 416S64 408.8 64 400V96H32C14.33 96 0 110.3 0 128v288c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V64C512 46.33 497.7 32 480 32zM272 416h-96C167.2 416 160 408.8 160 400C160 391.2 167.2 384 176 384h96c8.836 0 16 7.162 16 16C288 408.8 280.8 416 272 416zM272 320h-96C167.2 320 160 312.8 160 304C160 295.2 167.2 288 176 288h96C280.8 288 288 295.2 288 304C288 312.8 280.8 320 272 320zM432 416h-96c-8.836 0-16-7.164-16-16c0-8.838 7.164-16 16-16h96c8.836 0 16 7.162 16 16C448 408.8 440.8 416 432 416zM432 320h-96C327.2 320 320 312.8 320 304C320 295.2 327.2 288 336 288h96C440.8 288 448 295.2 448 304C448 312.8 440.8 320 432 320zM448 208C448 216.8 440.8 224 432 224h-256C167.2 224 160 216.8 160 208v-96C160 103.2 167.2 96 176 96h256C440.8 96 448 103.2 448 112V208z' />
</svg>
<p className='IBMSMSMBSSPost_PDElementText'>
{formatDate(
(modData.published_at !== -1
? modData.published_at
: modData.edited_at) * 1000,
'dd/m/yyyy'
)}
</p>
</div>
<div
data-bs-toggle='tooltip'
data-bs-placement='left'
className='IBMSMSMBSSPost_PDElement'
title='Last modified'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSPost_PDElementIcon'
>
<path d='M362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32zM421.7 220.3L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3z' />
</svg>
<p className='IBMSMSMBSSPost_PDElementText'>
{formatDate(modData.edited_at * 1000, 'dd/m/yyyy')}
</p>
</div>
<a
data-bs-toggle='tooltip'
data-bs-placement='left'
className='IBMSMSMBSSPost_PDElement IBMSMSMBSSPost_PDElementLink'
href='#'
title='Published on'
target='_blank'
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 -64 640 640'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSPost_PDElementIcon'
>
<path d='M172.5 131.1C228.1 75.51 320.5 75.51 376.1 131.1C426.1 181.1 433.5 260.8 392.4 318.3L391.3 319.9C381 334.2 361 337.6 346.7 327.3C332.3 317 328.9 297 339.2 282.7L340.3 281.1C363.2 249 359.6 205.1 331.7 177.2C300.3 145.8 249.2 145.8 217.7 177.2L105.5 289.5C73.99 320.1 73.99 372 105.5 403.5C133.3 431.4 177.3 435 209.3 412.1L210.9 410.1C225.3 400.7 245.3 404 255.5 418.4C265.8 432.8 262.5 452.8 248.1 463.1L246.5 464.2C188.1 505.3 110.2 498.7 60.21 448.8C3.741 392.3 3.741 300.7 60.21 244.3L172.5 131.1zM467.5 380C411 436.5 319.5 436.5 263 380C213 330 206.5 251.2 247.6 193.7L248.7 192.1C258.1 177.8 278.1 174.4 293.3 184.7C307.7 194.1 311.1 214.1 300.8 229.3L299.7 230.9C276.8 262.1 280.4 306.9 308.3 334.8C339.7 366.2 390.8 366.2 422.3 334.8L534.5 222.5C566 191 566 139.1 534.5 108.5C506.7 80.63 462.7 76.99 430.7 99.9L429.1 101C414.7 111.3 394.7 107.1 384.5 93.58C374.2 79.2 377.5 59.21 391.9 48.94L393.5 47.82C451 6.731 529.8 13.25 579.8 63.24C636.3 119.7 636.3 211.3 579.8 267.7L467.5 380z' />
</svg>
<p className='IBMSMSMBSSPost_PDElementText'>
{modData.site}
</p>
</a>
</div>
</div>
</div>
<div className='IBMSMSplitMainBigSideSec'>
<div className='IBMSMSMBSSDownloadsWrapper'>
<h4 className='IBMSMSMBSSDownloadsTitle'>Mod Download</h4>
{modData.downloadUrls.length > 0 && (
<div className='IBMSMSMBSSDownloadsPrime'>
<Download url={modData.downloadUrls[0].url} />
</div>
)}
{modData.downloadUrls.length > 1 && (
<>
<div className='IBMSMSMBSSDownloadsActions'>
<button
className='btn btnMain'
id='viewOldLinks'
type='button'
onClick={handleViewOldLinks}
>
View other links
</button>
</div>
<div
ref={oldDownloadListRef}
id='oldDownloadList'
className='IBMSMSMBSSDownloads'
style={{ padding: 0, height: '0px', border: 'unset' }}
>
{modData.downloadUrls
.slice(1)
.map((download, index) => (
<Download
key={`downloadUrl-${index}`}
url={download.url}
/>
))}
</div>
</>
)}
</div>
</div>
<div className='IBMSMSplitMainBigSideSec'>
<div className='IBMSMSMBSSPostsWrapper'>
<h4 className='IBMSMSMBSSPostsTitle'>Creator's Blog Posts</h4>
<div className='IBMSMList IBMSMListAlt'>
<BlogCard backgroundLink='https://nichegamer.com/wp-content/uploads/2023/01/onimai-01-07-2023.jpg' />
<BlogCard backgroundLink='https://nichegamer.com/wp-content/uploads/2023/01/onimai-01-07-2023.jpg' />
<BlogCard backgroundLink='https://nichegamer.com/wp-content/uploads/2023/01/onimai-01-07-2023.jpg' />
</div>
</div>
</div>
<div className='IBMSMSplitMainBigSideSec'>
<div className='IBMSMSMBSSCommentsWrapper'>
<h4 className='IBMSMSMBSSTitle'>Comments</h4>
<div id='ArticleComments-1' className='IBMSMSMBSSComments'>
<div className='IBMSMSMBSSCommentsCreation'>
<div className='IBMSMSMBSSCC_Top'>
<textarea
id='commentBox-1'
className='IBMSMSMBSSCC_Top_Box'
></textarea>
</div>
<div className='IBMSMSMBSSCC_Bottom'>
<a className='IBMSMSMBSSCC_BottomButton'>
Comment
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</a>
</div>
</div>
<div className='CommentsToggle'>
<button
className='btn btnMain CommentsToggleBtn CommentsToggleActive'
type='button'
>
All Comments
</button>
<button
className='btn btnMain CommentsToggleBtn'
type='button'
>
Creator Comments
</button>
</div>
<div className='IBMSMSMBSSCommentsList'>
<div className='IBMSMSMBSSCL_Comment'>
<div className='IBMSMSMBSSCL_CommentTop'>
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
<a
className='IBMSMSMBSSCL_CommentTopPP'
href='profile.html'
style={{
background: `url('assets/img/media-cache%20(4).png') center / cover no-repeat`
}}
></a>
</div>
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
<div className='IBMSMSMBSSCL_CommentTopDetails'>
<a
className='IBMSMSMBSSCL_CTD_Name'
href='profile.html'
>
FreakoverseFreakoverseFreakoverse
</a>
<a
className='IBMSMSMBSSCL_CTD_Address'
href='profile.html'
>
npub18n4ysp43ux5c98fs6h9c57qpr4p8r3j8f6e32v0vj8egzy878aqqyzzk9r
</a>
</div>
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
<a
className='IBMSMSMBSSCL_CADTime'
href='feed-note.html'
>
8:45 PM
</a>
<a
className='IBMSMSMBSSCL_CADDate'
href='feed-note.html'
>
02/05/2024
</a>
</div>
</div>
</div>
<div className='IBMSMSMBSSCL_CommentBottom'>
<p className='IBMSMSMBSSCL_CBText'>
Yo this article was insane to read!
</p>
</div>
<div className='IBMSMSMBSSCL_CommentActions'>
<div className='IBMSMSMBSSCL_CommentActionsInside'>
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp IBMSMSMBSSCL_CAEUpActive'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSCL_CAElementIcon'
>
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
</svg>
<p className='IBMSMSMBSSCL_CAElementText'>52</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSCL_CAElementIcon'
>
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
</svg>
<p className='IBMSMSMBSSCL_CAElementText'>4</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAERepost IBMSMSMBSSCL_CAERepostActive'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 -64 640 640'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSCL_CAElementIcon'
>
<path d='M614.2 334.8C610.5 325.8 601.7 319.1 592 319.1H544V176C544 131.9 508.1 96 464 96h-128c-17.67 0-32 14.31-32 32s14.33 32 32 32h128C472.8 160 480 167.2 480 176v143.1h-48c-9.703 0-18.45 5.844-22.17 14.82s-1.656 19.29 5.203 26.16l80 80.02C499.7 445.7 505.9 448 512 448s12.28-2.344 16.97-7.031l80-80.02C615.8 354.1 617.9 343.8 614.2 334.8zM304 352h-128C167.2 352 160 344.8 160 336V192h48c9.703 0 18.45-5.844 22.17-14.82s1.656-19.29-5.203-26.16l-80-80.02C140.3 66.34 134.1 64 128 64S115.7 66.34 111 71.03l-80 80.02C24.17 157.9 22.11 168.2 25.83 177.2S38.3 192 48 192H96V336C96 380.1 131.9 416 176 416h128c17.67 0 32-14.31 32-32S321.7 352 304 352z'></path>
</svg>
<p className='IBMSMSMBSSCL_CAElementText'>6</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEBolt IBMSMSMBSSCL_CAEBoltActive'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-64 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSCL_CAElementIcon'
>
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
</svg>
<p className='IBMSMSMBSSCL_CAElementText'>500K</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
</div>
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReplies'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSCL_CAElementIcon'
>
<path d='M256 32C114.6 32 .0272 125.1 .0272 240c0 49.63 21.35 94.98 56.97 130.7c-12.5 50.37-54.27 95.27-54.77 95.77c-2.25 2.25-2.875 5.734-1.5 8.734C1.979 478.2 4.75 480 8 480c66.25 0 115.1-31.76 140.6-51.39C181.2 440.9 217.6 448 256 448c141.4 0 255.1-93.13 255.1-208S397.4 32 256 32z'></path>
</svg>
<p className='IBMSMSMBSSCL_CAElementText'>12</p>
<p className='IBMSMSMBSSCL_CAElementText'>
Replies
</p>
</div>
<div className='IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEReply'>
<p className='IBMSMSMBSSCL_CAElementText'>
Reply
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<ProfileSection />
</div>
</div>
</div>
</div>
)
}
type DownloadProps = {
url: string
}
const Download = ({ url }: DownloadProps) => (
<div className='IBMSMSMBSSDownloadsElement'>
<div className='IBMSMSMBSSDownloadsElementInside'>
<button
className='btn btnMain IBMSMSMBSSDownloadsElementBtn'
type='button'
onClick={() => console.log(url)}
>
Download
</button>
</div>
<div className='IBMSMSMBSSDownloadsElementInside'>
<p>Ratings:</p>
<div className='tabsMain'>
<ul className='nav nav-tabs tabsMainTop' role='tablist'>
<li className='nav-item tabsMainTopTab' role='presentation'>
<a
className='nav-link active tabsMainTopTabLink'
role='tab'
data-bs-toggle='tab'
href='#tab-1'
>
WoT
</a>
</li>
<li className='nav-item tabsMainTopTab' role='presentation'>
<a
className='nav-link tabsMainTopTabLink'
role='tab'
data-bs-toggle='tab'
href='#tab-2'
>
All
</a>
</li>
</ul>
<div className='tab-content tabsMainBottom'>
<div
className='tab-pane active tabsMainBottomContent'
role='tabpanel'
id='tab-1'
>
<div className='IBMSMSMBSSDownloadsElementInsideReactions'>
<div
data-bs-toggle='tooltip'
data-bss-tooltip=''
className='IBMSMSMBSSDEIReactionsElement IBMSMSMBSSDEIReactionsElementActive'
title='Clean'
>
<div className='IBMSMSMBSSDEIReactionsElementIconWrapper'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSDEIReactionsElementIcon'
>
<path d='M512 165.4c0 127.9-70.05 235.3-175.3 270.1c-20.04 7.938-41.83 12.46-64.69 12.46c-64.9 0-125.2-36.51-155.7-94.47c-54.13 49.93-68.71 107-68.96 108.1C44.72 472.6 34.87 480 24.02 480c-1.844 0-3.727-.2187-5.602-.6562c-12.89-3.098-20.84-16.08-17.75-28.96c9.598-39.5 90.47-226.4 335.3-226.4C344.8 224 352 216.8 352 208S344.8 192 336 192C228.6 192 151 226.6 96.29 267.6c.1934-10.82 1.242-21.84 3.535-33.05c13.47-65.81 66.04-119 131.4-134.2c28.33-6.562 55.68-6.013 80.93-.0054c56 13.32 118.2-7.412 149.3-61.24c5.664-9.828 20.02-9.516 24.66 .8282C502.7 76.76 512 121.9 512 165.4z' />
</svg>
</div>
<p className='IBMSMSMBSSDEIReactionsElementText'>420</p>
</div>
<div
data-bs-toggle='tooltip'
data-bss-tooltip=''
className='IBMSMSMBSSDEIReactionsElement'
title='Broken link'
>
<div className='IBMSMSMBSSDEIReactionsElementIconWrapper'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 -64 640 640'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSDEIReactionsElementIcon'
>
<path d='M185.7 120.3C242.5 75.82 324.7 79.73 376.1 131.1C420.1 175.1 430.9 239.6 406.7 293.5L438.6 318.4L534.5 222.5C566 191 566 139.1 534.5 108.5C506.7 80.63 462.7 76.1 430.7 99.9L429.1 101C414.7 111.3 394.7 107.1 384.5 93.58C374.2 79.2 377.5 59.21 391.9 48.94L393.5 47.82C451 6.732 529.8 13.25 579.8 63.24C636.3 119.7 636.3 211.3 579.8 267.7L489.3 358.2L630.8 469.1C641.2 477.3 643.1 492.4 634.9 502.8C626.7 513.2 611.6 515.1 601.2 506.9L9.196 42.89C-1.236 34.71-3.065 19.63 5.112 9.196C13.29-1.236 28.37-3.065 38.81 5.112L185.7 120.3zM238.1 161.1L353.4 251.7C359.3 225.5 351.7 197.2 331.7 177.2C306.6 152.1 269.1 147 238.1 161.1V161.1zM263 380C233.1 350.1 218.7 309.8 220.9 270L406.6 416.4C357.4 431 301.9 418.9 263 380V380zM116.6 187.9L167.2 227.8L105.5 289.5C73.99 320.1 73.99 372 105.5 403.5C133.3 431.4 177.3 435 209.3 412.1L210.9 410.1C225.3 400.7 245.3 404 255.5 418.4C265.8 432.8 262.5 452.8 248.1 463.1L246.5 464.2C188.1 505.3 110.2 498.7 60.21 448.8C3.741 392.3 3.741 300.7 60.21 244.3L116.6 187.9z' />
</svg>
</div>
<p className='IBMSMSMBSSDEIReactionsElementText'>420</p>
</div>
<div
data-bs-toggle='tooltip'
data-bss-tooltip=''
className='IBMSMSMBSSDEIReactionsElement'
title='Has virus'
>
<div className='IBMSMSMBSSDEIReactionsElementIconWrapper'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSDEIReactionsElementIcon'
>
<path d='M288 43.55C288 93.44 348.3 118.4 383.6 83.15L391.8 74.98C404.3 62.48 424.5 62.48 437 74.98C449.5 87.48 449.5 107.7 437 120.2L368.7 188.6C352.7 204.3 341.4 224.8 337.6 249.8L168.3 420.1C148.4 448.6 104.5 448.6 84.63 420.1C64.73 391.5 64.73 347.6 84.63 319.1L172.4 231.3C208.7 197 233.6 154.3 233.6 104.4C233.6 54.47 173.3 29.5 138 64.73L130.1 72.9C117.6 85.4 97.41 85.4 84.94 72.9C72.44 60.4 72.44 40.17 84.94 27.67L153.2-41.52C194.8-83.6 295.2-83.6 336.7-41.52C375.5 2.213 377.4 55.78 288 43.55zM121.4 169.3L44.88 245.8C25.35 265.6 25.35 295.7 44.88 315.5L138.4 408.9C147.4 417.1 166.5 421.6 181.5 417.1L296.7 295.7C306.2 272.4 318.5 253.3 333.3 239.8L237.2 142.2C222.4 123.2 204.1 105.3 181.5 105.3C159.1 105.3 141.3 123.2 126.6 142.2L121.4 169.3zM250.7 419.8L299.4 428.2C316.8 433.6 335.5 423.6 336.8 410.8L342.8 236.8C344.1 225 336.8 213.2 325.4 211.4L146.2 160.1C134.7 158.8 120.5 174.7 121.1 185.7L127.8 359.6C128.4 370.9 143.8 382.3 156.1 376.4L250.7 419.8z' />
</svg>
</div>
<p className='IBMSMSMBSSDEIReactionsElementText'>420</p>
</div>
</div>
</div>
<div
className='tab-pane tabsMainBottomContent'
role='tabpanel'
id='tab-2'
>
<div className='IBMSMSMBSSDownloadsElementInsideReactions'>
<div
data-bs-toggle='tooltip'
data-bss-tooltip=''
className='IBMSMSMBSSDEIReactionsElement IBMSMSMBSSDEIReactionsElementActive'
title='Clean'
>
<div className='IBMSMSMBSSDEIReactionsElementIconWrapper'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSDEIReactionsElementIcon'
>
<path d='M512 165.4c0 127.9-70.05 235.3-175.3 270.1c-20.04 7.938-41.83 12.46-64.69 12.46c-64.9 0-125.2-36.51-155.7-94.47c-54.13 49.93-68.71 107-68.96 108.1C44.72 472.6 34.87 480 24.02 480c-1.844 0-3.727-.2187-5.602-.6562c-12.89-3.098-20.84-16.08-17.75-28.96c9.598-39.5 90.47-226.4 335.3-226.4C344.8 224 352 216.8 352 208S344.8 192 336 192C228.6 192 151 226.6 96.29 267.6c.1934-10.82 1.242-21.84 3.535-33.05c13.47-65.81 66.04-119 131.4-134.2c28.33-6.562 55.68-6.013 80.93-.0054c56 13.32 118.2-7.412 149.3-61.24c5.664-9.828 20.02-9.516 24.66 .8282C502.7 76.76 512 121.9 512 165.4z' />
</svg>
</div>
<p className='IBMSMSMBSSDEIReactionsElementText'>4,200</p>
</div>
<div
data-bs-toggle='tooltip'
data-bss-tooltip=''
className='IBMSMSMBSSDEIReactionsElement'
title='Broken link'
>
<div className='IBMSMSMBSSDEIReactionsElementIconWrapper'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 -64 640 640'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSDEIReactionsElementIcon'
>
<path d='M185.7 120.3C242.5 75.82 324.7 79.73 376.1 131.1C420.1 175.1 430.9 239.6 406.7 293.5L438.6 318.4L534.5 222.5C566 191 566 139.1 534.5 108.5C506.7 80.63 462.7 76.1 430.7 99.9L429.1 101C414.7 111.3 394.7 107.1 384.5 93.58C374.2 79.2 377.5 59.21 391.9 48.94L393.5 47.82C451 6.732 529.8 13.25 579.8 63.24C636.3 119.7 636.3 211.3 579.8 267.7L489.3 358.2L630.8 469.1C641.2 477.3 643.1 492.4 634.9 502.8C626.7 513.2 611.6 515.1 601.2 506.9L9.196 42.89C-1.236 34.71-3.065 19.63 5.112 9.196C13.29-1.236 28.37-3.065 38.81 5.112L185.7 120.3zM238.1 161.1L353.4 251.7C359.3 225.5 351.7 197.2 331.7 177.2C306.6 152.1 269.1 147 238.1 161.1V161.1zM263 380C233.1 350.1 218.7 309.8 220.9 270L406.6 416.4C357.4 431 301.9 418.9 263 380V380zM116.6 187.9L167.2 227.8L105.5 289.5C73.99 320.1 73.99 372 105.5 403.5C133.3 431.4 177.3 435 209.3 412.1L210.9 410.1C225.3 400.7 245.3 404 255.5 418.4C265.8 432.8 262.5 452.8 248.1 463.1L246.5 464.2C188.1 505.3 110.2 498.7 60.21 448.8C3.741 392.3 3.741 300.7 60.21 244.3L116.6 187.9z' />
</svg>
</div>
<p className='IBMSMSMBSSDEIReactionsElementText'>4,200</p>
</div>
<div
data-bs-toggle='tooltip'
data-bss-tooltip=''
className='IBMSMSMBSSDEIReactionsElement'
title='Has virus'
>
<div className='IBMSMSMBSSDEIReactionsElementIconWrapper'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMBSSDEIReactionsElementIcon'
>
<path d='M288 43.55C288 93.44 348.3 118.4 383.6 83.15L391.8 74.98C404.3 62.48 424.5 62.48 437 74.98C449.5 87.48 449.5 107.7 437 120.2L368.7 188.6C352.7 204.3 341.4 224.8 337.6 249.8L168.3 420.1C148.4 448.6 104.5 448.6 84.63 420.1C64.73 391.5 64.73 347.6 84.63 319.1L172.4 231.3C208.7 197 233.6 154.3 233.6 104.4C233.6 54.47 173.3 29.5 138 64.73L130.1 72.9C117.6 85.4 97.41 85.4 84.94 72.9C72.44 60.4 72.44 40.17 84.94 27.67L153.2-41.52C194.8-83.6 295.2-83.6 336.7-41.52C375.5 2.213 377.4 55.78 288 43.55zM121.4 169.3L44.88 245.8C25.35 265.6 25.35 295.7 44.88 315.5L138.4 408.9C147.4 417.1 166.5 421.6 181.5 417.1L296.7 295.7C306.2 272.4 318.5 253.3 333.3 239.8L237.2 142.2C222.4 123.2 204.1 105.3 181.5 105.3C159.1 105.3 141.3 123.2 126.6 142.2L121.4 169.3zM250.7 419.8L299.4 428.2C316.8 433.6 335.5 423.6 336.8 410.8L342.8 236.8C344.1 225 336.8 213.2 325.4 211.4L146.2 160.1C134.7 158.8 120.5 174.7 121.1 185.7L127.8 359.6C128.4 370.9 143.8 382.3 156.1 376.4L250.7 419.8z' />
</svg>
</div>
<p className='IBMSMSMBSSDEIReactionsElementText'>4,200</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)

View File

@ -2,6 +2,7 @@ import { AboutPage } from '../pages/about'
import { BlogsPage } from '../pages/blogs'
import { GamesPage } from '../pages/games'
import { HomePage } from '../pages/home'
import { InnerModPage } from '../pages/innerMod'
import { ModsPage } from '../pages/mods'
import { SettingsPage } from '../pages/settings'
import { SubmitModPage } from '../pages/submitMod'
@ -12,6 +13,7 @@ export const appRoutes = {
home: '/home',
games: '/games',
mods: '/mods',
modsInner: '/mods-inner/:nevent',
about: '/about',
blog: '/blog',
submitMod: '/submit-mod',
@ -22,6 +24,9 @@ export const appRoutes = {
settingsAdmin: '/settings-admin'
}
export const getModsInnerPageRoute = (eventId: string) =>
appRoutes.modsInner.replace(':nevent', eventId)
export const routes = [
{
path: appRoutes.index,
@ -39,6 +44,10 @@ export const routes = [
path: appRoutes.mods,
element: <ModsPage />
},
{
path: appRoutes.modsInner,
element: <InnerModPage />
},
{
path: appRoutes.about,
element: <AboutPage />

479
src/styles/comments.css Normal file
View File

@ -0,0 +1,479 @@
.IBMSMSMBSSComments {
width: 100%;
display: flex;
flex-direction: column;
grid-gap: 25px;
}
.IBMSMSMBSSCommentsList {
width: 100%;
display: grid;
grid-template-columns: 1fr;
grid-gap: 25px;
}
.IBMSMSMBSSCL_Comment {
width: 100%;
display: grid;
grid-template-columns: 1fr;
grid-gap: 20px;
}
.IBMSMSMBSSCL_CommentTop {
width: 100%;
display: flex;
flex-direction: row;
grid-gap: 15px;
padding: 0;
}
.IBMSMSMBSSCL_CommentTopPP {
border-radius: 10px;
width: 60px;
height: 60px;
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
}
.IBMSMSMBSSCL_CommentTopDetails {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.IBMSMSMBSSCL_CommentBottom {
padding: 20px;
color: rgba(255,255,255,0.75);
background: linear-gradient(to top right, #262626, #292929, #262626);
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
border-radius: 10px;
/*border: solid 1px rgba(255,255,255,0.1);*/
}
.IBMSMSMBSSCL_CommentTopPPWrapper {
display: flex;
flex-direction: column;
justify-content: end;
align-items: center;
}
.IBMSMSMBSSCL_CBText {
}
.IBMSMSMBSSCL_CommentActions {
margin: -10px 0 0 0;
display: grid;
grid-template-columns: 1;
grid-gap: 25px;
}
@media (max-width: 576px) {
.IBMSMSMBSSCL_CommentActions {
margin: -10px 0 0 0;
display: grid;
grid-template-columns: 1fr;
grid-gap: 25px;
}
}
.IBMSMSMBSSCL_CAElement {
transition: ease 0.4s;
display: flex;
flex-direction: row;
align-items: center;
grid-gap: 10px;
padding: 5px 15px;
border-radius: 10px;
color: rgba(255,255,255,0.25);
font-weight: bold;
position: relative;
cursor: pointer;
font-size: 14px;
overflow: hidden;
transform: scale(1);
flex-wrap: wrap;
}
@media (max-width: 576px) {
.IBMSMSMBSSCL_CAElement {
flex-grow: 1;
justify-content: center;
}
}
.IBMSMSMBSSCL_CAElement:hover::before {
transition: ease 0.4s;
background: linear-gradient(to top right, #262626, #292929, #262626);
opacity: 1;
}
.IBMSMSMBSSCL_CAElement::before {
transition: ease 0.4s;
background: linear-gradient(to top right, #262626, #292929, #262626);
opacity: 0;
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: -1;
border-radius: 10px;
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
}
.IBMSMSMBSSCL_CAElementText {
}
.IBMSMSMBSSCL_CAElementIcon {
background: rgba(255,255,255,0);
font-size: 14px;
}
.IBMSMSMBSSCL_CTD_Name {
font-weight: bold;
color: rgba(255,255,255,0.5);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 200px;
}
.IBMSMSMBSSCL_CTD_Address {
color: rgba(255,255,255,0.25);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 150px;
}
.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAEReply {
border: solid 1px rgba(255,255,255,0.05);
}
.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAEReply:hover {
transition: ease 0.4s;
border: solid 1px rgba(255,255,255,0.05);
color: rgba(255,255,255,0.5);
}
.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAEReplies:hover {
transition: ease 0.4s;
color: rgba(173,90,255,0.75);
}
.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAERepost.IBMSMSMBSSCL_CAERepostActive {
color: rgba(255,255,255,0.75);
}
.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAERepost:hover {
transition: ease 0.4s;
color: rgba(255,255,255,0.75);
}
.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAEDown:hover {
transition: ease 0.4s;
color: rgba(255,114,54,0.85);
}
.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAEUp:hover {
transition: ease 0.4s;
color: rgba(255,70,70,0.85);
}
.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAEBolt:hover {
transition: ease 0.4s;
color: rgba(255,255,0,0.85);
}
.IBMSMSMBSSCL_CAElement:hover {
transition: ease 0.4s;
transform: scale(1.05);
}
.IBMSMSMBSSCL_CAElement:active {
transition: ease 0.1s;
transform: scale(0.95);
}
.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAEUp.IBMSMSMBSSCL_CAEUpActive {
color: rgba(255,70,70,0.85);
}
.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAElement.IBMSMSMBSSCL_CAEBolt.IBMSMSMBSSCL_CAEBoltActive {
color: rgba(255,255,0,0.85);
}
.IBMSMSMBSSCL_CommentActionsInside {
display: flex;
flex-direction: row;
justify-content: end;
flex-wrap: wrap;
grid-gap: 10px;
}
.IBMSMSMBSSCL_CommentActionsDetails {
color: rgba(255,255,255,0.25);
font-size: 16px;
display: flex;
flex-direction: column;
justify-content: start;
align-items: end;
grid-gap: 5px;
line-height: 1;
flex-grow: 1;
}
@media (max-width: 576px) {
.IBMSMSMBSSCL_CommentActionsDetails {
flex-direction: row;
justify-content: end;
grid-gap: 10px;
}
}
.IBMSMSMBSSCL_CADDate {
transition: ease 0.4s;
color: rgba(255,255,255,0.25);
}
.IBMSMSMBSSCL_CADTime {
transition: ease 0.4s;
color: rgba(255,255,255,0.25);
}
.IBMSMSMBSSCL_CommentTopDetailsWrapper {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 15px;
flex-wrap: wrap;
align-items: end;
}
@media (max-width: 576px) {
.IBMSMSMBSSCL_CommentTopDetailsWrapper {
grid-template-columns: 1fr;
}
}
.IBMSMSMBSSCommentsCreation {
padding: 0;
display: flex;
flex-direction: column;
grid-gap: 15px;
}
.IBMSMSMBSSCC_Top {
}
.IBMSMSMBSSCC_Bottom {
display: flex;
flex-direction: row;
justify-content: end;
align-items: start;
grid-gap: 10px;
}
.IBMSMSMBSSCC_Top_Box {
transition: border, background, box-shadow ease 0.4s;
width: 100%;
background: rgba(0,0,0,0.05);
border: solid 1px rgba(255,255,255,0.05);
box-shadow: inset 0 0 8px 0 rgb(0,0,0,0.1);
border-radius: 10px;
min-height: 100px;
height: 100px;
min-width: 100%;
outline: unset;
padding: 15px 20px;
color: rgba(255,255,255,0.75);
}
@media (max-width: 576px) {
.IBMSMSMBSSCC_Top_Box {
padding: 15px 15px;
height: 100px;
}
}
.IBMSMSMBSSCC_Top_Box:focus, hover {
transition: border, background, box-shadow ease 0.4s;
background: rgba(0,0,0,0.1);
border: solid 1px rgba(255,255,255,0.1);
box-shadow: inset 0 0 8px 0 rgb(0,0,0,0.15);
outline: unset;
}
.IBMSMSMBSSCC_BottomButton {
transition: ease 0.4s;
text-decoration: unset;
color: rgba(255,255,255,0.25);
font-weight: bold;
padding: 10px 20px;
border-radius: 10px;
box-shadow: 0 0 8px 0 rgba(0,0,0,0);
font-size: 16px;
transform: scale(1);
position: relative;
cursor: pointer;
border: solid 1px rgba(255,255,255,0.1);
overflow: hidden;
}
.IBMSMSMBSSCC_BottomButton:hover {
transition: ease 0.4s;
text-decoration: unset;
color: rgba(255,255,255,0.75);
border-radius: 10px;
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
font-size: 16px;
transform: scale(1.03);
/*border: solid 1px rgba(255,255,255,0);*/
}
.IBMSMSMBSSCC_BottomButton:active {
transition: ease 0.1s;
transform: scale(0.98);
}
.IBMSMSMBSSCC_BottomButton::before {
transition: ease 0.4s;
background: linear-gradient(to top right, #262626, #292929, #262626);
opacity: 0;
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: -1;
border-radius: 10px;
}
.IBMSMSMBSSCC_BottomButton:hover::before {
transition: ease 0.4s;
background: linear-gradient(to top right, #262626, #292929, #262626);
opacity: 1;
}
.IBMSMSMBSSCL_CommentTopOther {
display: flex;
flex-direction: row;
justify-content: end;
align-items: end;
flex-grow: 1;
grid-gap: 10px;
}
.IBMSMSMBSSCL_CTO {
transition: ease 0.4s;
display: flex;
flex-direction: row;
border-radius: 10px;
border: solid 1px rgba(255,255,255,0.1);
overflow: hidden;
color: rgba(255,255,255,0.25);
font-size: 14px;
}
@media (max-width: 576px) {
.IBMSMSMBSSCL_CTO {
width: 100%;
}
}
.IBMSMSMBSSCL_CTOLink {
transition: ease 0.4s;
padding: 5px 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(255,255,255,0.05);
color: rgba(255,255,255,0.25);
}
.IBMSMSMBSSCL_CTOLink:hover {
transition: ease 0.4s;
background: rgba(255,255,255,0.1);
color: rgba(255,255,255,0.5);
}
.IBMSMSMBSSCL_CTOLink:active > .IBMSMSMBSSCL_CTOLinkIcon {
transition: ease 0.1s;
transform: scale(0.9);
}
.IBMSMSMBSSCL_CTOText {
transition: ease 0.4s;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 5px 10px;
}
.CommentsToggle {
width: 100%;
display: flex;
flex-direction: row;
grid-gap: 15px;
/*padding: 10px;*/
/*background: rgba(0,0,0,0.05);*/
border-radius: 10px;
/*border: solid 1px rgba(255,255,255,0.05);*/
}
@media (max-width: 576px) {
.CommentsToggle {
flex-direction: column;
}
}
.btnMain.CommentsToggleBtn {
flex-grow: 1;
background: unset;
box-shadow: unset;
font-weight: normal;
border-radius: 7px;
}
.btnMain.CommentsToggleBtn.CommentsToggleActive {
background: rgba(255,255,255,0.1);
font-weight: bold;
}
.IBMSMSMBSSCommentsWrapper {
display: flex;
flex-direction: column;
grid-gap: 25px;
}
.IBMSMSMBSSTitle {
color: rgba(255,255,255,0.5);
}
.IBMSMSMBSSCL_CommentNoteRepliesTitle {
color: rgba(255,255,255,0.5);
}
.IBMSMSMBSSCL_CAElementLoadWrapper {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
display: flex;
flex-direction: row;
}
.IBMSMSMBSSCL_CAElementLoad {
background: rgba(255,255,255,0.5);
width: 0%;
}
.btnMain.IBMSMSMBSSCL_CTOBtn {
padding: 5px 10px;
height: 100%;
}

249
src/styles/downloads.css Normal file
View File

@ -0,0 +1,249 @@
.IBMSMSMBSSDownloadsWrapper {
width: 100%;
display: flex;
flex-direction: column;
grid-gap: 15px;
}
.IBMSMSMBSSDownloads {
width: 100%;
border-radius: 10px;
display: grid;
grid-template-columns: 1fr;
grid-gap: 15px;
border: solid 1px rgba(255,255,255,0.05);
overflow: auto;
max-height: 550px;
padding: 15px;
}
@media (max-width: 768px) {
.IBMSMSMBSSDownloads {
grid-template-columns: 1fr;
}
}
.IBMSMSMBSSDownloadsPrime {
}
.IBMSMSMBSSDownloadsTitle {
color: rgba(255,255,255,0.5);
}
.IBMSMSMBSSDownloadsElement {
transition: ease 0.4s;
width: 100%;
display: grid;
grid-template-columns: 1fr;
grid-gap: 10px;
border: solid 1px rgba(255,255,255,0);
background: rgba(255,255,255,0.05);
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
}
@media (max-width: 768px) {
.IBMSMSMBSSDownloadsElement {
grid-template-columns: 1fr;
}
}
.btnMain.IBMSMSMBSSDownloadsElementBtn {
background: rgba(255,255,255,0.05);
border-radius: 10px;
width: 100%;
}
@media (max-width: 768px) {
.btnMain.IBMSMSMBSSDownloadsElementBtn {
order: 3;
}
}
.btnMain.IBMSMSMBSSDownloadsElementBtn:hover {
background: rgba(255,255,255,0.1);
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
}
.IBMSMSMBSSDownloadsElementInside {
display: flex;
flex-direction: column;
justify-content: start;
align-items: start;
color: rgba(255,255,255,0.5);
grid-gap: 10px;
}
.IBMSMSMBSSDownloadsElementInsideReactions {
width: 100%;
display: flex;
flex-direction: row;
grid-gap: 10px;
height: 100%;
}
@media (max-width: 576px) {
.IBMSMSMBSSDownloadsElementInsideReactions {
flex-direction: column;
}
}
.IBMSMSMBSSDEIReactionsElement {
transition: ease 0.4s;
display: grid;
grid-template-columns: 0.5fr 1.5fr;
grid-gap: 0px;
justify-content: center;
align-items: center;
width: 100%;
background: rgba(255,255,255,0);
overflow: hidden;
border-radius: 10px;
border: solid 1px rgba(255,255,255,0.05);
cursor: pointer;
}
.IBMSMSMBSSDEIReactionsElement:hover {
transition: ease 0.4s;
background: rgba(255,255,255,0.05);
color: rgba(255,255,255,0.75);
border: solid 1px rgba(255,255,255,0);
}
.IBMSMSMBSSDEIReactionsElement:hover > .IBMSMSMBSSDEIReactionsElementIconWrapper {
transition: ease 0.4s;
background: rgba(255,255,255,0.05);
border-right: solid 1px rgba(255,255,255,0);
}
.IBMSMSMBSSDEIReactionsElement:hover > .IBMSMSMBSSDEIReactionsElementIconWrapper > .IBMSMSMBSSDEIReactionsElementIcon {
transform: scale(1.1);
}
.IBMSMSMBSSDEIReactionsElementIcon {
transition: ease 0.4s;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.IBMSMSMBSSDEIReactionsElementText {
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
padding: 5px 5px;
}
.IBMSMSMBSSDEIReactionsElementIconWrapper {
transition: ease 0.4s;
font-size: 18px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
background: rgba(255,255,255,0);
padding: 10px 5px;
border-right: solid 1px rgba(255,255,255,0.05);
}
.IBMSMSMBSSDownloadsElementInsideDetails {
width: 100%;
display: flex;
flex-direction: column;
grid-gap: 10px;
}
.IBMSMSMBSSDEIReactionsElement.IBMSMSMBSSDEIReactionsElementActive {
background: rgba(255,255,255,0.05);
border: solid 1px rgba(255,255,255,0);
}
.IBMSMSMBSSDEIReactionsElement.IBMSMSMBSSDEIReactionsElementActive > .IBMSMSMBSSDEIReactionsElementIconWrapper {
background: rgba(255,255,255,0.05);
border-right: solid 1px rgba(255,255,255,0);
}
.IBMSMSMBSSDEIReactionsElement.IBMSMSMBSSDEIReactionsElementActive > .IBMSMSMBSSDEIReactionsElementIconWrapper > .IBMSMSMBSSDEIReactionsElementIcon {
color: rgba(255,255,255,0.75);
}
.IBMSMSMBSSDownloadsActions {
width: 100%;
display: flex;
flex-direction: row;
}
.IBMSMSMBSSDownloadsElementInside.IBMSMSMBSSDownloadsElementInsideAlt {
align-items: center;
}
.IBMSMSMBSSDownloadsElementInsideAltTable {
width: 100%;
display: flex;
flex-direction: column;
border-radius: 10px;
border: solid 1px rgba(255,255,255,0.1);
overflow: auto;
grid-gap: 1px;
}
.IBMSMSMBSSDownloadsElementInsideAltTableRow {
transition: ease 0.4s;
display: flex;
flex-direction: row;
grid-gap: 0px;
}
.IBMSMSMBSSDownloadsElementInsideAltTableRow:hover {
transition: ease 0.4s;
background: rgba(255,255,255,0.05);
}
@media (max-width: 576px) {
.IBMSMSMBSSDownloadsElementInsideAltTableRow {
flex-direction: column;
}
}
.IBMSMSMBSSDownloadsElementInsideAltTableRowCol {
width: 100%;
text-align: start;
padding: 10px 15px;
}
.IBMSMSMBSSDownloadsElementInsideAltTableRowCol.IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst {
text-align: center;
font-weight: bold;
max-width: 200px;
background: rgba(255,255,255,0.05);
display: flex;
justify-content: center;
align-items: center;
}
@media (max-width: 576px) {
.IBMSMSMBSSDownloadsElementInsideAltTableRowCol.IBMSMSMBSSDownloadsElementInsideAltTableRowColFirst {
max-width: unset;
}
}
.IBMSMSMBSSDownloadsElementInsideAltText {
transition: ease 0.4s;
cursor: pointer;
font-weight: 400;
color: rgba(255,255,255,0.25);
}
.IBMSMSMBSSDownloadsElementInsideAltText:hover {
transition: ease 0.4s;
cursor: pointer;
font-weight: 600;
color: rgba(255,255,255,0.75);
}

219
src/styles/post.css Normal file
View File

@ -0,0 +1,219 @@
.IBMSMSMBSSPost {
width: 100%;
overflow: hidden;
border-radius: 15px;
display: flex;
flex-direction: column;
align-items: center;
grid-gap: 25px;
background: rgba(255,255,255,0.05);
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
position: relative;
padding: 0 0 50px 0;
}
.IBMSMSMBSSPostPicture {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 56.25%;
}
.IBMSMSMBSSPostTitle {
width: 100%;
padding: 15px;
padding: 0px;
display: flex;
flex-direction: column;
align-items: center;
}
.IBMSMSMBSSPostBody {
width: 100%;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
overflow: hidden;
}
.IBMSMSMBSSPostTitleHeading {
width: 100%;
}
.IBMSMSMBSSPostTitleText {
width: 100%;
}
.IBMSMSMBSSPostInside {
display: flex;
flex-direction: column;
grid-gap: 25px;
padding: 0 15px;
width: 100%;
max-width: 775px;
}
.IBMSMSMBSSPostImg {
width: 100%;
margin: 15px 0;
background: #232323;
border-radius: 10px;
}
.IBMSMSMBSSPost_PostDetails {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
grid-gap: 5px;
border-radius: 15px;
overflow: hidden;
padding: 5px 15px;
border: solid 1px rgba(255,255,255,0.1);
justify-content: space-around;
}
@media (max-width: 576px) {
.IBMSMSMBSSPost_PostDetails {
flex-direction: column;
}
}
.IBMSMSMBSSPost_PDElement {
transition: ease 0.4s;
/*width: 100%;*/
display: flex;
flex-direction: row;
grid-gap: 10px;
justify-content: start;
align-items: center;
color: rgba(255,255,255,0.25);
padding: 10px 15px;
border-radius: 10px;
position: relative;
}
.IBMSMSMBSSPost_PDElementLink::before {
transition: ease 0.4s;
background: linear-gradient(to top right, #262626, #292929, #262626);
opacity: 0;
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: -1;
border-radius: 10px;
}
.IBMSMSMBSSPost_PDElementLink:hover::before {
transition: ease 0.4s;
background: linear-gradient(to top right, #262626, #292929, #262626);
opacity: 1;
}
.IBMSMSMBSSPost_PDElementIcon {
}
.IBMSMSMBSSPost_PDElementText {
}
.IBMSMSMBSSPost_PDElement.IBMSMSMBSSPost_PDElementLink {
transition: ease 0.4s;
text-decoration: unset;
}
.IBMSMSMBSSPost_PDElement.IBMSMSMBSSPost_PDElementLink:hover {
transition: ease 0.4s;
text-decoration: unset;
color: rgba(255,255,255,0.75);
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
}
.IBMSMSMBSSPostBodyHide {
bottom: 0;
left: 0;
right: 0;
height: 100%;
position: absolute;
border: solid 1px rgba(255,255,255,0.1);
border-radius: 10px;
background: linear-gradient(rgba(0,0,0,0) 0%, #232323 100%);
display: flex;
flex-direction: column;
justify-content: end;
align-items: center;
padding: 15px;
color: rgba(255,255,255,0.75);
font-weight: bold;
cursor: pointer;
box-shadow: inset 0 0 8px 0 rgb(0,0,0,0.1);
}
.IBMSMSMBSSModFor {
width: 100%;
border-radius: 10px;
padding: 15px;
color: rgba(255,255,255,0.65);
background: rgba(255,255,255,0.05);
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.IBMSMSMBSSModForPara {
font-weight: bold;
}
.IBMSMSMBSSModForLink {
transition: ease 0.4s;
font-weight: normal;
color: rgba(255,255,255,0.5);
text-decoration: none;
}
.IBMSMSMBSSModForLink:hover {
transition: ease 0.4s;
color: rgba(255,255,255,0.75);
text-decoration: underline;
}
.IBMSMSMBSSShots {
max-width: 100%;
min-width: 0px;
overflow-x: auto;
display: flex;
flex-direction: row;
grid-gap: 10px;
background: rgba(0,0,0,0.1);
border-radius: 10px;
padding: 10px;
box-shadow: inset 0 0 8px 0 rgb(0,0,0,0.1);
}
.IBMSMSMBSSShotsImg {
min-width: 250px;
border-radius: 10px;
overflow: hidden;
height: 140.625px;
object-fit: cover;
cursor: pointer;
}
.IBMSMSMBSSPostsWrapper {
display: flex;
flex-direction: column;
grid-gap: 15px;
}
.IBMSMSMBSSPostsTitle {
color: rgba(255,255,255,0.5);
}

100
src/styles/reactions.css Normal file
View File

@ -0,0 +1,100 @@
.IBMSMSMBSS_Details {
width: 100%;
display: flex;
flex-direction: row;
grid-gap: 15px;
/*background: linear-gradient(to top right, #262626, #292929, #262626);*/
/*box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);*/
flex-wrap: wrap;
}
@media (max-width: 768px) {
.IBMSMSMBSS_Details {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
.IBMSMSMBSS_Details_Card {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background: linear-gradient(to top right, #262626, #292929, #262626);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
color: rgba(255,255,255,0.25);
cursor: pointer;
position: relative;
}
.IBMSMSMBSS_Details_Card:hover > .IBMSMSMBSS_Details_CardVisual > .IBMSMSMBSS_Details_CardVisualIcon {
transition: ease 0.4s;
transform: scale(1.1);
}
.IBMSMSMBSS_Details_Card:active > .IBMSMSMBSS_Details_CardVisual > .IBMSMSMBSS_Details_CardVisualIcon {
transition: ease 0.2s;
transform: scale(0.95);
}
.IBMSMSMBSS_Details_CardVisual {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 15px;
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
background: rgba(255,255,255,0.05);
font-size: 20px;
}
.IBMSMSMBSS_Details_CardText {
transition: ease 0.4s;
text-align: center;
width: 100%;
font-weight: bold;
margin: 0 15px;
min-width: 50px;
}
.IBMSMSMBSS_Details_Card.IBMSMSMBSS_D_CBolt:hover {
color: rgba(255,255,0,0.85);
}
.IBMSMSMBSS_Details_Card.IBMSMSMBSS_D_CComments:hover {
color: rgba(173,90,255,0.75);
}
.IBMSMSMBSS_Details_Card.IBMSMSMBSS_D_CReactUp:hover {
color: rgba(255,70,70,0.85);
}
.IBMSMSMBSS_Details_Card.IBMSMSMBSS_D_CReactDown:hover {
color: rgba(255,114,54,0.85);
}
.IBMSMSMBSS_Details_CardText:hover {
transition: ease 0.4s;
}
.IBMSMSMBSS_Details_CardVisualIcon {
transition: ease 0.4s;
}
.HBLA_Details_Card:hover {
}
.IBMSMSMBSS_Details_Card.IBMSMSMBSS_D_CReactUp.IBMSMSMBSS_D_CRUActive {
color: rgba(255,70,70,0.85);
}
.IBMSMSMBSS_Details_Card.IBMSMSMBSS_D_CReactDown.IBMSMSMBSS_D_CRDActive {
color: rgba(255,114,54,0.85);
}
.IBMSMSMBSS_Details_Card.IBMSMSMBSS_D_CBolt.IBMSMSMBSS_D_CBActive {
color: rgba(255,255,0,0.85);
}

58
src/styles/tabs.css Normal file
View File

@ -0,0 +1,58 @@
.tabsMain {
width: 100%;
display: flex;
flex-direction: column;
grid-gap: 10px;
padding: 10px;
background: rgba(0,0,0,0.1);
border-radius: 10px;
border: solid 1px rgba(255,255,255,0.05);
}
.tabsMainTop {
width: 100%;
display: flex;
flex-direction: row;
grid-gap: 10px;
border: unset;
padding: 0;
background: rgba(0,0,0,0);
}
.tabsMainTopTab {
flex-grow: 1;
text-align: center;
}
.nav-link.tabsMainTopTabLink {
color: rgba(255,255,255,0.5);
font-weight: normal;
background: rgba(255,255,255,0);
border: unset;
border-radius: 8px;
padding: 5px;
}
.nav-link.active.tabsMainTopTabLink {
color: rgba(255,255,255,0.75);
font-weight: bold;
background: rgba(255,255,255,0.05);
}
.tabsMainBottom {
}
.tab-pane.tabsMainBottomContent {
}
.tab-pane.active.tabsMainBottomContent {
}
.tabsMain.tabsMainAlt {
border-radius: 0px;
border: unset;
border-bottom: solid 1px rgba(255,255,255,0.05);
padding: 20px 10px;
grid-gap: 20px;
}

36
src/styles/tags.css Normal file
View File

@ -0,0 +1,36 @@
.IBMSMSMBSSTags {
width: 100%;
display: flex;
flex-direction: row;
justify-content: start;
align-items: start;
grid-gap: 10px;
flex-wrap: wrap;
}
.IBMSMSMBSSTagsTag {
transition: ease 0.4s;
padding: 5px 15px;
border-radius: 10px;
background: rgba(255,255,255,0);
color: rgba(255,255,255,0.25);
text-decoration: unset;
text-align: center;
cursor: pointer;
box-shadow: 0 0 8px 0 rgba(0,0,0,0);
border: solid 1px rgba(255,255,255,0.05);
}
.IBMSMSMBSSTagsTag:hover {
transition: ease 0.4s;
transform: scale(1.02);
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);
}
.IBMSMSMBSSTagsTag:active {
transition: ease 0.1s;
transform: scale(0.98);
}

2
src/types/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './mod'
export * from './user'

28
src/types/mod.ts Normal file
View File

@ -0,0 +1,28 @@
export interface FormState {
game: string
title: string
body: string
featuredImageUrl: string
summary: string
nsfw: boolean
screenshotsUrls: string[]
tags: string
downloadUrls: DownloadUrl[]
}
export interface DownloadUrl {
url: string
hash: string
signatureKey: string
malwareScanLink: string
modVersion: string
customNote: string
}
export interface ModDetails extends Omit<FormState, 'tags'> {
published_at: number
edited_at: number
site: string
author: string
tags: string[]
}

View File

@ -1,3 +1,4 @@
export * from './mod'
export * from './nostr'
export * from './url'
export * from './utils'

44
src/utils/mod.ts Normal file
View File

@ -0,0 +1,44 @@
import { Event } from 'nostr-tools'
import { getTagValue } from './nostr'
import { ModDetails } from '../types'
/**
* Extracts and normalizes mod data from an event.
*
* This function extracts specific tag values from an event and maps them to properties
* of a `PageData` object. It handles default values and type conversions as needed.
*
* @param event - The event object from which to extract data.
* @returns A `Partial<PageData>` object containing extracted data.
*/
export const extractModData = (event: Event): ModDetails => {
// Helper function to safely get the first value of a tag or return a default value
const getFirstTagValue = (tagIdentifier: string, defaultValue = '') => {
const tagValue = getTagValue(event, tagIdentifier)
return tagValue ? tagValue[0] : defaultValue
}
// Helper function to safely parse integer values from tags
const getIntTagValue = (tagIdentifier: string, defaultValue: number = -1) => {
const tagValue = getTagValue(event, tagIdentifier)
return tagValue ? parseInt(tagValue[0], 10) : defaultValue
}
return {
author: event.pubkey,
edited_at: event.created_at,
body: event.content,
site: getFirstTagValue('t'),
published_at: getIntTagValue('published_at'),
game: getFirstTagValue('game'),
title: getFirstTagValue('title'),
featuredImageUrl: getFirstTagValue('featuredImageUrl'),
summary: getFirstTagValue('summary'),
nsfw: Boolean(getFirstTagValue('nsfw')),
screenshotsUrls: getTagValue(event, 'screenshotsUrls') || [],
tags: getTagValue(event, 'tags') || [],
downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) =>
JSON.parse(item)
)
}
}

View File

@ -1,4 +1,4 @@
import { nip19 } from 'nostr-tools'
import { nip19, Event } from 'nostr-tools'
/**
* Get the current time in seconds since the Unix epoch (January 1, 1970).
@ -9,7 +9,7 @@ import { nip19 } from 'nostr-tools'
*
* @returns {number} The current time in seconds since the Unix epoch.
*/
export const now = () => Math.round(Date.now() / 1000)
export const now = (): number => Math.round(Date.now() / 1000)
/**
* Converts a hexadecimal public key to an npub format.
@ -25,3 +25,30 @@ export const hexToNpub = (hexPubkey: string): `npub1${string}` => {
// Convert the hexadecimal public key to npub format using the nip19 encoder
return nip19.npubEncode(hexPubkey)
}
/**
* Retrieves the value associated with a specific tag identifier from an event.
*
* This function searches the `tags` array of an event to find a tag that matches the given
* `tagIdentifier`. If a matching tag is found, it returns the associated value(s).
* If no matching tag is found, it returns `null`.
*
* @param event - The event object containing the tags.
* @param tagIdentifier - The identifier of the tag to search for.
* @returns {string | null} The value(s) associated with the specified tag identifier, or `null` if the tag is not found.
*/
export const getTagValue = (
event: Event,
tagIdentifier: string
): string[] | null => {
// Find the tag in the event's tags array where the first element matches the tagIdentifier.
const tag = event.tags.find((item) => item[0] === tagIdentifier)
// If a matching tag is found, return the rest of the elements in the tag (i.e., the values).
if (tag) {
return tag.slice(1) // Slice to remove the identifier, returning only the values.
}
// Return null if no matching tag is found.
return null
}

View File

@ -1,3 +1,51 @@
/**
* Normalizes a given URL by performing the following operations:
*
* 1. Ensures that the URL has a protocol by defaulting to 'wss://' if no protocol is provided.
* 2. Creates a `URL` object to easily manipulate and normalize the URL components.
* 3. Normalizes the pathname by:
* - Replacing multiple consecutive slashes with a single slash.
* - Removing the trailing slash if it exists.
* 4. Removes the port number if it is the default port for the protocol:
* - Port `80` for 'ws:' (WebSocket) protocol.
* - Port `443` for 'wss:' (WebSocket Secure) protocol.
* 5. Sorts the query parameters alphabetically.
* 6. Clears any fragment (hash) identifier from the URL.
*
* @param urlString - The URL string to be normalized.
* @returns A normalized URL string.
*/
export function normalizeWebSocketURL(urlString: string): string {
// If the URL string does not contain a protocol (e.g., "http://", "https://"),
// prepend "wss://" (WebSocket Secure) by default.
if (urlString.indexOf('://') === -1) urlString = 'wss://' + urlString
// Create a URL object from the provided URL string.
const url = new URL(urlString)
// Normalize the pathname by replacing multiple consecutive slashes with a single slash.
url.pathname = url.pathname.replace(/\/+/g, '/')
// Remove the trailing slash from the pathname if it exists.
if (url.pathname.endsWith('/')) url.pathname = url.pathname.slice(0, -1)
// Remove the port number if it is 80 for "ws:" protocol or 443 for "wss:" protocol, as these are default ports.
if (
(url.port === '80' && url.protocol === 'ws:') ||
(url.port === '443' && url.protocol === 'wss:')
)
url.port = ''
// Sort the search parameters alphabetically.
url.searchParams.sort()
// Clear any hash fragment from the URL.
url.hash = ''
// Return the normalized URL as a string.
return url.toString()
}
export const isValidUrl = (url: string) => {
try {
new URL(url)