From cd0a53a8c8efc5d574fa19a41ac0e2504514ae42 Mon Sep 17 00:00:00 2001 From: daniyal Date: Thu, 25 Jul 2024 20:05:28 +0500 Subject: [PATCH] feat: implemented mod details page --- package-lock.json | 32 ++ package.json | 3 + src/components/ModForm.tsx | 45 +- src/controllers/relay.ts | 109 ++++- src/hooks/index.ts | 1 + src/hooks/useDidMount.ts | 12 + src/layout/header.tsx | 2 +- src/pages/innerMod.tsx | 845 +++++++++++++++++++++++++++++++++++++ src/routes/index.tsx | 9 + src/styles/comments.css | 479 +++++++++++++++++++++ src/styles/downloads.css | 249 +++++++++++ src/styles/post.css | 219 ++++++++++ src/styles/reactions.css | 100 +++++ src/styles/tabs.css | 58 +++ src/styles/tags.css | 36 ++ src/types/index.ts | 2 + src/types/mod.ts | 28 ++ src/utils/index.ts | 1 + src/utils/mod.ts | 44 ++ src/utils/nostr.ts | 31 +- src/utils/url.ts | 48 +++ 21 files changed, 2302 insertions(+), 51 deletions(-) create mode 100644 src/hooks/useDidMount.ts create mode 100644 src/pages/innerMod.tsx create mode 100644 src/styles/comments.css create mode 100644 src/styles/downloads.css create mode 100644 src/styles/post.css create mode 100644 src/styles/reactions.css create mode 100644 src/styles/tabs.css create mode 100644 src/styles/tags.css create mode 100644 src/types/index.ts create mode 100644 src/types/mod.ts create mode 100644 src/utils/mod.ts diff --git a/package-lock.json b/package-lock.json index 0a80398..01da9d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 54f4395..c87ebe7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index bb9d81f..257aff6 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -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) diff --git a/src/controllers/relay.ts b/src/controllers/relay.ts index 7eb668e..e1387a6 100644 --- a/src/controllers/relay.ts +++ b/src/controllers/relay.ts @@ -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 => { + // 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} - Returns a promise that resolves to the found event or null if not found. + */ + fetchEvent = async ( + filter: Filter, + relays: string[] = [] + ): Promise => { + // 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((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 + } } diff --git a/src/hooks/index.ts b/src/hooks/index.ts index c7c7b90..7d4ee84 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ export * from './redux' +export * from './useDidMount' diff --git a/src/hooks/useDidMount.ts b/src/hooks/useDidMount.ts new file mode 100644 index 0000000..5bac96a --- /dev/null +++ b/src/hooks/useDidMount.ts @@ -0,0 +1,12 @@ +import { useRef, useEffect } from 'react' + +export const useDidMount = (callback: () => void) => { + const didMount = useRef(false) + + useEffect(() => { + if (callback && !didMount.current) { + didMount.current = true + callback() + } + }) +} diff --git a/src/layout/header.tsx b/src/layout/header.tsx index 61a9187..759ec41 100644 --- a/src/layout/header.tsx +++ b/src/layout/header.tsx @@ -57,7 +57,7 @@ export const Header = () => {
diff --git a/src/pages/innerMod.tsx b/src/pages/innerMod.tsx new file mode 100644 index 0000000..48ced6f --- /dev/null +++ b/src/pages/innerMod.tsx @@ -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() + + 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(null) + const viewFullPostBtnRef = useRef(null) + const oldDownloadListRef = useRef(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 ( +
+
+
+
+
+
+ +
+
+
+
+

+ {modData.title} +

+
+
+
+
+

View

+
+
+
+ {modData.screenshotsUrls.map((url, index) => ( + {`ScreenShot-${index}`} + ))} +
+
+ {modData.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+
+
+
+ +
+
+ + + +
+

420

+
+
+
+
+ + + +
+

69k

+
+
+
+
+
+
+ + + +
+

4.2k

+
+
+
+
+
+
+ + + +
+

69

+
+
+
+
+
+
+
+
+
+ + + +

+ {formatDate( + (modData.published_at !== -1 + ? modData.published_at + : modData.edited_at) * 1000, + 'dd/m/yyyy' + )} +

+
+
+ + + +

+ {formatDate(modData.edited_at * 1000, 'dd/m/yyyy')} +

+
+ + + + +

+ {modData.site} +

+
+
+
+
+
+
+

Mod Download

+ {modData.downloadUrls.length > 0 && ( +
+ +
+ )} + {modData.downloadUrls.length > 1 && ( + <> +
+ +
+
+ {modData.downloadUrls + .slice(1) + .map((download, index) => ( + + ))} +
+ + )} +
+
+
+
+

Creator's Blog Posts

+
+ + + +
+
+
+
+
+

Comments

+
+
+
+ +
+ +
+
+ + +
+
+
+ +
+

+ Yo this article was insane to read! +

+
+
+
+
+ + + +

52

+
+
+
+
+
+ + + +

4

+
+
+
+
+
+ + + +

6

+
+
+
+
+
+ + + +

500K

+
+
+
+
+
+ + + +

12

+

+ Replies +

+
+
+

+ Reply +

+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ ) +} + +type DownloadProps = { + url: string +} + +const Download = ({ url }: DownloadProps) => ( +
+
+ +
+
+

Ratings:

+
+ +
+
+
+
+
+ + + +
+

420

+
+
+
+ + + +
+

420

+
+
+
+ + + +
+

420

+
+
+
+
+
+
+
+ + + +
+

4,200

+
+
+
+ + + +
+

4,200

+
+
+
+ + + +
+

4,200

+
+
+
+
+
+
+
+) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 9892824..803fc2c 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -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: }, + { + path: appRoutes.modsInner, + element: + }, { path: appRoutes.about, element: diff --git a/src/styles/comments.css b/src/styles/comments.css new file mode 100644 index 0000000..94bc835 --- /dev/null +++ b/src/styles/comments.css @@ -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%; +} + diff --git a/src/styles/downloads.css b/src/styles/downloads.css new file mode 100644 index 0000000..0ff3847 --- /dev/null +++ b/src/styles/downloads.css @@ -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); +} + diff --git a/src/styles/post.css b/src/styles/post.css new file mode 100644 index 0000000..fcc8294 --- /dev/null +++ b/src/styles/post.css @@ -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); +} + diff --git a/src/styles/reactions.css b/src/styles/reactions.css new file mode 100644 index 0000000..339c552 --- /dev/null +++ b/src/styles/reactions.css @@ -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); +} + diff --git a/src/styles/tabs.css b/src/styles/tabs.css new file mode 100644 index 0000000..840924f --- /dev/null +++ b/src/styles/tabs.css @@ -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; +} + diff --git a/src/styles/tags.css b/src/styles/tags.css new file mode 100644 index 0000000..1de956f --- /dev/null +++ b/src/styles/tags.css @@ -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); +} + diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..ce551f7 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './mod' +export * from './user' diff --git a/src/types/mod.ts b/src/types/mod.ts new file mode 100644 index 0000000..0997795 --- /dev/null +++ b/src/types/mod.ts @@ -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 { + published_at: number + edited_at: number + site: string + author: string + tags: string[] +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 1dd5f84..910cb10 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from './mod' export * from './nostr' export * from './url' export * from './utils' diff --git a/src/utils/mod.ts b/src/utils/mod.ts new file mode 100644 index 0000000..7b70260 --- /dev/null +++ b/src/utils/mod.ts @@ -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` 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) + ) + } +} diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index 92649b6..db2f5bc 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -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 +} diff --git a/src/utils/url.ts b/src/utils/url.ts index 29b9be0..8ccad5f 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -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)