-
Submit a mod
+ {title}
-
+ {isFetching ? (
+
+ ) : (
+
+ )}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 9892824..5fcf1a3 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,9 +13,11 @@ export const appRoutes = {
home: '/home',
games: '/games',
mods: '/mods',
+ modsInner: '/mods-inner/:nevent',
about: '/about',
blog: '/blog',
submitMod: '/submit-mod',
+ editMod: '/edit-mod/:nevent',
write: '/write',
settingsProfile: '/settings-profile',
settingsRelays: '/settings-relays',
@@ -22,6 +25,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 +45,10 @@ export const routes = [
path: appRoutes.mods,
element:
},
+ {
+ path: appRoutes.modsInner,
+ element:
+ },
{
path: appRoutes.about,
element:
@@ -51,6 +61,10 @@ export const routes = [
path: appRoutes.submitMod,
element:
},
+ {
+ path: appRoutes.editMod,
+ element:
+ },
{
path: appRoutes.write,
element:
diff --git a/src/styles/cardMod.css b/src/styles/cardMod.css
index e59ff12..5244511 100644
--- a/src/styles/cardMod.css
+++ b/src/styles/cardMod.css
@@ -3,7 +3,7 @@
height: 100%;
display: flex;
flex-direction: column;
- background: rgba(255,255,255,0.05);
+ background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
overflow: hidden;
background: linear-gradient(to top right, #262626, #292929, #262626);
@@ -13,12 +13,12 @@
position: relative;
width: 100%;
padding-top: 56.25%;
- background: rgba(0,0,0,0.1);
+ background: rgba(0, 0, 0, 0.1);
}
.cMMBody {
padding: 15px;
- color: rgba(255,255,255,0.5);
+ color: rgba(255, 255, 255, 0.5);
display: flex;
flex-direction: column;
grid-gap: 15px;
@@ -28,7 +28,7 @@
.cMMFoot {
width: 100%;
padding: 10px 25px;
- border-top: solid 1px rgba(255,255,255,0.05);
+ border-top: solid 1px rgba(255, 255, 255, 0.05);
font-size: 14px;
display: flex;
flex-direction: row;
@@ -43,7 +43,8 @@
transform: scale(1);
border-radius: 12px;
padding: 2px;
- box-shadow: 0 0 8px 0 rgb(0,0,0,0.05);
+ box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.05);
+ cursor: pointer;
}
.cardModMainWrapperLink:hover {
@@ -60,7 +61,12 @@
.cardModMainWrapperLink::before {
transition: ease 0.4s;
- background: linear-gradient(to top, #8000ff 0%, #232323 50%, rgba(255,255,255,0) 100%);
+ background: linear-gradient(
+ to top,
+ #8000ff 0%,
+ #232323 50%,
+ rgba(255, 255, 255, 0) 100%
+ );
content: '';
position: absolute;
top: 0;
@@ -84,7 +90,7 @@
-webkit-line-clamp: 2;
font-size: 20px;
line-height: 1.25;
- color: rgba(255,255,255,0.75);
+ color: rgba(255, 255, 255, 0.75);
font-weight: bold;
}
@@ -93,7 +99,7 @@
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 3;
- color: rgba(255,255,255,0.5);
+ color: rgba(255, 255, 255, 0.5);
font-size: 15px;
line-height: 1.5;
}
@@ -113,6 +119,5 @@
grid-gap: 5px;
justify-content: center;
align-items: center;
- color: rgba(255,255,255,0.25);
+ color: rgba(255, 255, 255, 0.25);
}
-
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/loadingSpinner.module.scss b/src/styles/loadingSpinner.module.scss
new file mode 100644
index 0000000..b83b70b
--- /dev/null
+++ b/src/styles/loadingSpinner.module.scss
@@ -0,0 +1,37 @@
+.loadingSpinnerOverlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 9999;
+
+ .loadingSpinnerContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .loadingSpinner {
+ border: 4px solid #f3f3f3;
+ border-top: 4px solid #3498db;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ animation: spin 1s linear infinite;
+ }
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/src/styles/pagination.css b/src/styles/pagination.css
index 2e739b1..a59fe41 100644
--- a/src/styles/pagination.css
+++ b/src/styles/pagination.css
@@ -35,19 +35,23 @@
flex-direction: column;
justify-content: center;
align-items: center;
- background: rgba(35,35,35,0);
+ background: rgba(35, 35, 35, 0);
border-radius: 10px;
height: 100%;
- box-shadow: 0 0 8px 0 rgba(0,0,0,0);
+ box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0);
transform: scale(1);
- border: solid 1px rgba(255,255,255,0);
- color: rgba(255,255,255,0.1);
+ border: solid 1px rgba(255, 255, 255, 0);
+ color: rgba(255, 255, 255, 0.1);
font-weight: bold;
}
.PaginationMainInsideBox.PaginationMainInsideBoxArrows {
}
+.PaginationMainInsideBox.PaginationMainInsideBoxArrows:disabled {
+ cursor: not-allowed;
+}
+
@media (max-width: 768px) {
.PaginationMainInsideBox.PaginationMainInsideBoxArrows {
order: 2;
@@ -60,10 +64,10 @@
text-decoration: unset;
color: unset;
background: linear-gradient(to top right, #232323, #262626, #232323);
- box-shadow: 0 0 16px 0 rgba(0,0,0,0.1);
+ box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.1);
transform: scale(1.01);
- border: solid 1px rgba(255,255,255,0.1);
- color: rgba(255,255,255,0.85);
+ border: solid 1px rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.85);
}
.PaginationMainInsideBox:active {
@@ -76,8 +80,8 @@
text-decoration: unset;
color: unset;
transform: scale(1.01);
- border: solid 1px rgba(255,255,255,0.1);
- color: rgba(255,255,255,0.75);
+ border: solid 1px rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.75);
}
.PMIBDots {
@@ -100,4 +104,3 @@
justify-content: space-around;
}
}
-
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..06650af
--- /dev/null
+++ b/src/types/mod.ts
@@ -0,0 +1,36 @@
+export interface ModFormState {
+ dTag: string
+ aTag: string
+ rTag: string
+ 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
{
+ id: string
+ published_at: number
+ edited_at: number
+ author: string
+ tags: string[]
+}
+
+export interface MuteLists {
+ authors: string[]
+ eventIds: 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..b85376f
--- /dev/null
+++ b/src/utils/mod.ts
@@ -0,0 +1,186 @@
+import { Event, Filter, kinds } from 'nostr-tools'
+import { getTagValue } from './nostr'
+import { ModFormState, ModDetails } from '../types'
+import { RelayController } from '../controllers'
+import { log, LogType } from './utils'
+import { toast } from 'react-toastify'
+import { T_TAG_VALUE } from '../constants'
+
+/**
+ * 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 {
+ id: event.id,
+ dTag: getFirstTagValue('d'),
+ aTag: getFirstTagValue('a'),
+ rTag: getFirstTagValue('r'),
+ author: event.pubkey,
+ edited_at: event.created_at,
+ body: event.content,
+ published_at: getIntTagValue('published_at'),
+ game: getFirstTagValue('game'),
+ title: getFirstTagValue('title'),
+ featuredImageUrl: getFirstTagValue('featuredImageUrl'),
+ summary: getFirstTagValue('summary'),
+ nsfw: getFirstTagValue('nsfw') === 'true',
+ screenshotsUrls: getTagValue(event, 'screenshotsUrls') || [],
+ tags: getTagValue(event, 'tags') || [],
+ downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) =>
+ JSON.parse(item)
+ )
+ }
+}
+
+/**
+ * Constructs a list of `ModDetails` objects from an array of events.
+ *
+ * This function filters out events that do not contain all required data,
+ * extracts the necessary information from the valid events, and constructs
+ * `ModDetails` objects.
+ *
+ * @param events - The array of event objects to be processed.
+ * @returns An array of `ModDetails` objects constructed from valid events.
+ */
+export const constructModListFromEvents = (events: Event[]): ModDetails[] => {
+ // Filter and extract mod details from events
+ const modDetailsList: ModDetails[] = events
+ .filter(isModDataComplete) // Filter out incomplete events
+ .map((event) => extractModData(event)) // Extract data and construct ModDetails
+
+ return modDetailsList
+}
+
+/**
+ * Checks if the provided event contains all the required data for constructing a `ModDetails` object.
+ *
+ * This function verifies that the event has the necessary tags and values to construct a `ModDetails` object.
+ *
+ * @param event - The event object to be checked.
+ * @returns `true` if the event contains all required data; `false` otherwise.
+ */
+export const isModDataComplete = (event: Event): boolean => {
+ // Helper function to check if a tag value is present and not empty
+ const hasTagValue = (tagIdentifier: string): boolean => {
+ const value = getTagValue(event, tagIdentifier)
+ return !!value && value.length > 0 && value[0].trim() !== ''
+ }
+
+ // Check if all required fields are present
+ return (
+ hasTagValue('d') &&
+ hasTagValue('a') &&
+ hasTagValue('t') &&
+ hasTagValue('published_at') &&
+ hasTagValue('game') &&
+ hasTagValue('title') &&
+ hasTagValue('featuredImageUrl') &&
+ hasTagValue('summary') &&
+ hasTagValue('nsfw') &&
+ getTagValue(event, 'screenshotsUrls') !== null &&
+ getTagValue(event, 'tags') !== null &&
+ getTagValue(event, 'downloadUrls') !== null
+ )
+}
+
+/**
+ * Initializes the form state with values from existing mod data or defaults.
+ *
+ * @param existingModData - An optional object containing existing mod details. If provided, its values will be used to populate the form state.
+ * @returns The initial state for the form, with values from existingModData if available, otherwise default values.
+ */
+export const initializeFormState = (
+ existingModData?: ModDetails
+): ModFormState => ({
+ dTag: existingModData?.dTag || '',
+ aTag: existingModData?.aTag || '',
+ rTag: existingModData?.rTag || window.location.host,
+ game: existingModData?.game || '',
+ title: existingModData?.title || '',
+ body: existingModData?.body || '',
+ featuredImageUrl: existingModData?.featuredImageUrl || '',
+ summary: existingModData?.summary || '',
+ nsfw: existingModData?.nsfw || false,
+ screenshotsUrls: existingModData?.screenshotsUrls || [''],
+ tags: existingModData?.tags.join(',') || '',
+ downloadUrls: existingModData?.downloadUrls || [
+ {
+ url: '',
+ hash: '',
+ signatureKey: '',
+ malwareScanLink: '',
+ modVersion: '',
+ customNote: ''
+ }
+ ]
+})
+
+/**
+ * Fetches a list of mods based on the provided source.
+ *
+ * @param source - The source URL to filter the mods. If it matches the current window location,
+ * it adds a filter condition to the request.
+ * @param until - Optional timestamp to filter events until this time.
+ * @param since - Optional timestamp to filter events from this time.
+ * @returns A promise that resolves to an array of `ModDetails` objects. In case of an error,
+ * it logs the error and shows a notification, then returns an empty array.
+ */
+export const fetchMods = async (
+ source: string,
+ until?: number,
+ since?: number
+): Promise => {
+ // Define the filter criteria for fetching mods
+ const filter: Filter = {
+ kinds: [kinds.ClassifiedListing], // Specify the kind of events to fetch
+ limit: 20, // Limit the number of events fetched to 20
+ '#t': [T_TAG_VALUE],
+ until, // Optional filter to fetch events until this timestamp
+ since // Optional filter to fetch events from this timestamp
+ }
+
+ // If the source matches the current window location, add a filter condition
+ if (source === window.location.host) {
+ filter['#r'] = [window.location.host] // Add a tag filter for the current host
+ }
+
+ // Fetch events from the relay using the defined filter
+ return RelayController.getInstance()
+ .fetchEvents(filter, []) // Pass the filter and an empty array of options
+ .then((events) => {
+ console.log('events :>> ', events)
+
+ // Convert the fetched events into a list of mods
+ const modList = constructModListFromEvents(events)
+ return modList // Return the list of mods
+ })
+ .catch((err) => {
+ // Log the error and show a notification if fetching fails
+ log(
+ true,
+ LogType.Error,
+ 'An error occurred in fetching mods from relays',
+ err
+ )
+ toast.error('An error occurred in fetching mods from relays') // Show error notification
+ return [] as ModDetails[] // Return an empty array in case of an error
+ })
+}
diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts
index 92649b6..0aa99d1 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,62 @@ 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
+}
+
+/**
+ * @param hexKey hex private or public key
+ * @returns whether or not is key valid
+ */
+const validateHex = (hexKey: string) => {
+ return hexKey.match(/^[a-f0-9]{64}$/)
+}
+
+/**
+ * NPUB provided - it will convert NPUB to HEX
+ * HEX provided - it will return HEX
+ *
+ * @param pubKey in NPUB, HEX format
+ * @returns HEX format
+ */
+export const npubToHex = (pubKey: string): string | null => {
+ // If key is NPUB
+ if (pubKey.startsWith('npub1')) {
+ try {
+ return nip19.decode(pubKey).data as string
+ } catch (error) {
+ return null
+ }
+ }
+
+ // valid hex key
+ if (validateHex(pubKey)) return pubKey
+
+ // Not a valid hex key
+ return null
+}
diff --git a/src/utils/url.ts b/src/utils/url.ts
index 29b9be0..711432c 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)
@@ -20,3 +68,25 @@ export const isReachable = async (url: string) => {
return false
}
}
+
+/**
+ * Extracts a filename from a given URL.
+ *
+ * @param url - The URL from which to extract the filename.
+ * @returns The filename extracted from the URL. If no filename can be extracted, a default name is provided.
+ */
+export const getFilenameFromUrl = (url: string): string => {
+ // Create a URL object to parse the provided URL string
+ const urlObj = new URL(url)
+
+ // Extract the pathname from the URL object
+ const pathname = urlObj.pathname
+
+ // Extract the filename from the pathname. The filename is the last segment after the last '/'
+ // If pathname is empty or does not end with a filename, use 'downloaded_file' as the default
+ const filename =
+ pathname.substring(pathname.lastIndexOf('/') + 1) || 'downloaded_file'
+
+ // Return the extracted filename
+ return filename
+}
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 504d101..96b45ee 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -34,3 +34,36 @@ export const timeout = (ms: number = 60000) => {
}, ms) // Timeout duration in milliseconds
})
}
+
+/**
+ * Copies the given text to the clipboard.
+ *
+ * @param text - The text to be copied to the clipboard.
+ * @returns A promise that resolves to a boolean indicating success or failure.
+ */
+export const copyTextToClipboard = async (text: string): Promise => {
+ try {
+ // Check if the Clipboard API is available
+ if (navigator.clipboard) {
+ // Use the Clipboard API to write the text to the clipboard
+ await navigator.clipboard.writeText(text)
+ return true // Successfully copied
+ } else {
+ // Clipboard API is not available, fall back to a manual method
+ const textarea = document.createElement('textarea')
+ textarea.value = text
+ // Ensure the textarea is not visible to the user
+ textarea.style.position = 'absolute'
+ textarea.style.left = '-9999px'
+ document.body.appendChild(textarea)
+ textarea.select()
+ // Attempt to copy the text to the clipboard
+ const successful = document.execCommand('copy')
+ document.body.removeChild(textarea)
+ return successful
+ }
+ } catch (error) {
+ console.error('Failed to copy text to clipboard', error)
+ return false // Failed to copy
+ }
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 3e09383..a399374 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -2,6 +2,7 @@
interface ImportMetaEnv {
readonly VITE_APP_RELAY: string
+ readonly VITE_ADMIN_NPUBS: string
// more env variables...
}