From 38280d151a7353dae21b57aaa73541636b2679bc Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 21 Oct 2024 17:24:53 +0200 Subject: [PATCH 01/14] fix: rename the workflow --- .gitea/workflows/release-production.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/release-production.yaml b/.gitea/workflows/release-production.yaml index 88cffa7..981e1be 100644 --- a/.gitea/workflows/release-production.yaml +++ b/.gitea/workflows/release-production.yaml @@ -1,4 +1,4 @@ -name: Release to Staging +name: Release to Production on: push: branches: From 63333b38c3accab42aae7f9e0018ad6af9d03c49 Mon Sep 17 00:00:00 2001 From: freakoverse Date: Tue, 22 Oct 2024 06:02:17 +0000 Subject: [PATCH 02/14] Update src/assets/games/Games_SteamManual.csv --- src/assets/games/Games_SteamManual.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/games/Games_SteamManual.csv b/src/assets/games/Games_SteamManual.csv index 7d18729..d38ccaf 100644 --- a/src/assets/games/Games_SteamManual.csv +++ b/src/assets/games/Games_SteamManual.csv @@ -1,2 +1,2 @@ -Game Name,16 by 9 image,Boxart image -Marvel's Spider-Man 2,,https://s7.ezgif.com/tmp/ezgif-7-9ad5dabde6.webp \ No newline at end of file +Game Name,16 by 9 image,Boxart image +Marvel's Spider-Man 2,,https://image.nostr.build/b5ef5ef8bd99daab73148145b024a1e6177160fd287ce829f82ba46e821490b6.jpg \ No newline at end of file From 0102f414039dd4ce8e5d7c8cdc6b055a7edc9425 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 23 Oct 2024 17:13:29 +0200 Subject: [PATCH 03/14] chore(prettier): css format --- src/styles/profile.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/styles/profile.css b/src/styles/profile.css index c72d15a..556b066 100644 --- a/src/styles/profile.css +++ b/src/styles/profile.css @@ -22,6 +22,5 @@ flex-wrap: wrap; grid-gap: 10px; padding: 10px 0 0 0; - border-top: solid 1px rgba(255,255,255,0.1); + border-top: solid 1px rgba(255, 255, 255, 0.1); } - From a97a03417828aa54b938c4743c5bccbd18cfbf56 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 23 Oct 2024 17:19:05 +0200 Subject: [PATCH 04/14] feat: add Tabs component --- src/components/Tabs.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/components/Tabs.tsx diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx new file mode 100644 index 0000000..ebe96a5 --- /dev/null +++ b/src/components/Tabs.tsx @@ -0,0 +1,26 @@ +interface TabsProps { + tabs: string[] + tab: number + setTab: React.Dispatch> +} + +export const Tabs = ({ tabs, tab, setTab }: TabsProps) => { + return ( +
+ {tabs.map((t, i) => { + return ( + + ) + })} +
+ ) +} From 88106734925ebcc5821ee5470e9b6ad5008f75fa Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 23 Oct 2024 17:23:48 +0200 Subject: [PATCH 05/14] refactor: extend checkbox field input --- src/components/Inputs.tsx | 20 +++++++++++++++++--- src/components/ModForm.tsx | 1 + src/pages/write.tsx | 1 + 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/Inputs.tsx b/src/components/Inputs.tsx index 5a30009..e40cc91 100644 --- a/src/components/Inputs.tsx +++ b/src/components/Inputs.tsx @@ -89,13 +89,27 @@ interface CheckboxFieldProps { name: string isChecked: boolean handleChange: (e: React.ChangeEvent) => void + type?: 'default' | 'stylized' } export const CheckboxField = React.memo( - ({ label, name, isChecked, handleChange }: CheckboxFieldProps) => ( -
- + ({ + label, + name, + isChecked, + handleChange, + type = 'default' + }: CheckboxFieldProps) => ( +
+ { name='nsfw' isChecked={formState.nsfw} handleChange={handleCheckboxChange} + type='stylized' />
diff --git a/src/pages/write.tsx b/src/pages/write.tsx index 230edd7..58813a5 100644 --- a/src/pages/write.tsx +++ b/src/pages/write.tsx @@ -55,6 +55,7 @@ export const WritePage = () => { name='nsfw' isChecked={false} handleChange={() => {}} + type='stylized' />
+ +
+
+ + + + {/* Tabs Content */} + {tab === 0 && ( + <> + + +
+ {filteredModList.map((mod) => ( + + ))} +
+ + + + )} + + {tab === 1 && <>USER's BLOGS WIP} + {tab === 2 && <>USER's POSTS WIP} +
+
+ + + + + {showReportPopUp && ( + setShowReportPopUp(false)} + /> + )} + + + ) +} + +type ReportUserPopupProps = { + reportedPubkey: string + handleClose: () => void +} + +const USER_REPORT_REASONS = [ + { label: `User posts actual CP`, key: 'user_actuallyCP' }, + { label: `User is a spammer`, key: 'user_spam' }, + { label: `User is a scammer`, key: 'user_scam' }, + { label: `User posts malware`, key: 'user_malware' }, + { label: `User posts non-mods`, key: 'user_notAGameMod' }, + { label: `User doesn't tag NSFW`, key: 'user_wasntTaggedNSFW' }, + { label: `Other (user)`, key: 'user_otherReason' } +] + +const ReportUserPopup = ({ + reportedPubkey, + handleClose +}: ReportUserPopupProps) => { + const { ndk, fetchEventFromUserRelays, publish } = useNDKContext() + const userState = useAppSelector((state) => state.user) + const [selectedOptions, setSelectedOptions] = useState( + USER_REPORT_REASONS.reduce((acc: { [key: string]: boolean }, cur) => { + acc[cur.key] = false + return acc + }, {}) + ) + const [isLoading, setIsLoading] = useState(false) + const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + + const handleCheckboxChange = (option: keyof typeof selectedOptions) => { + setSelectedOptions((prevState) => ({ + ...prevState, + [option]: !prevState[option] + })) + } + + const handleSubmit = async () => { + const selectedOptionsCount = Object.values(selectedOptions).filter( + (isSelected) => isSelected + ).length + + if (selectedOptionsCount === 0) { + toast.error('At least one option should be checked!') + return + } + + setIsLoading(true) + setLoadingSpinnerDesc('Getting user pubkey') + let userHexKey: string + if (userState.auth && userState.user?.pubkey) { + userHexKey = userState.user.pubkey as string + } else { + userHexKey = (await window.nostr?.getPublicKey()) as string + } + + if (!userHexKey) { + toast.error('Could not get pubkey for reporting user!') + setIsLoading(false) + return + } + + const reportingNpub = import.meta.env.VITE_REPORTING_NPUB + const reportingPubkey = npubToHex(reportingNpub) + + if (reportingPubkey === userHexKey) { + setLoadingSpinnerDesc(`Finding user's mute list`) + // Define the event filter to search for the user's mute list events. + // We look for events of a specific kind (Mutelist) authored by the given hexPubkey. + const filter: NDKFilter = { + kinds: [NDKKind.MuteList], + authors: [userHexKey] + } + + // Fetch the mute list event from the relays. This returns the event containing the user's mute list. + const muteListEvent = await fetchEventFromUserRelays( + filter, + userHexKey, + UserRelaysType.Write + ) + + let unsignedEvent: UnsignedEvent + + if (muteListEvent) { + // get a list of tags + const tags = muteListEvent.tags + const alreadyExists = + tags.findIndex( + (item) => item[0] === 'p' && item[1] === reportedPubkey + ) !== -1 + + if (alreadyExists) { + setIsLoading(false) + return toast.warn( + `Reporter user's pubkey is already in the mute list` + ) + } + + tags.push(['p', reportedPubkey]) + + unsignedEvent = { + pubkey: muteListEvent.pubkey, + kind: NDKKind.MuteList, + content: muteListEvent.content, + created_at: now(), + tags: [...tags] + } + } else { + unsignedEvent = { + pubkey: userHexKey, + kind: NDKKind.MuteList, + content: '', + created_at: now(), + tags: [['p', reportedPubkey]] + } + } + + setLoadingSpinnerDesc('Updating mute list event') + const isUpdated = await signAndPublish(unsignedEvent, ndk, publish) + if (isUpdated) handleClose() + } else { + const href = window.location.href + let message = `I'd like to report ${href} due to following reasons:\n` + + Object.entries(selectedOptions).forEach(([key, value]) => { + if (value) { + message += `* ${key}\n` + } + }) + + setLoadingSpinnerDesc('Sending report') + const isSent = await sendDMUsingRandomKey( + message, + reportingPubkey!, + ndk, + publish + ) + if (isSent) handleClose() + } + setIsLoading(false) + } + + return ( + <> + {isLoading && } +
+
+
+
+
+
+

Report Post

+
+
+ + + +
+
+
+
+
+ + {USER_REPORT_REASONS.map((r) => ( + handleCheckboxChange(r.key)} + /> + ))} + +
+
+
+
+
+
+
+ + ) } diff --git a/src/types/modsFilter.ts b/src/types/modsFilter.ts index 8d724ec..a10542d 100644 --- a/src/types/modsFilter.ts +++ b/src/types/modsFilter.ts @@ -22,4 +22,5 @@ export interface FilterOptions { nsfw: NSFWFilter source: string moderated: ModeratedFilter + author?: string } From bb653fa356fce6667acf112217c6a46e9c951178 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 23 Oct 2024 17:52:53 +0200 Subject: [PATCH 08/14] fix: add more pages --- src/layout/feed.tsx | 10 ++++++++++ src/pages/404.tsx | 30 ++++++++++++++++++++++++++++++ src/pages/feed.tsx | 3 +++ src/pages/notifications.tsx | 3 +++ 4 files changed, 46 insertions(+) create mode 100644 src/layout/feed.tsx create mode 100644 src/pages/404.tsx create mode 100644 src/pages/feed.tsx create mode 100644 src/pages/notifications.tsx diff --git a/src/layout/feed.tsx b/src/layout/feed.tsx new file mode 100644 index 0000000..2566f31 --- /dev/null +++ b/src/layout/feed.tsx @@ -0,0 +1,10 @@ +import { Outlet } from 'react-router-dom' + +export const FeedLayout = () => { + return ( + <> +

WIP

+ + + ) +} diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 0000000..1c53760 --- /dev/null +++ b/src/pages/404.tsx @@ -0,0 +1,30 @@ +import { Link } from 'react-router-dom' +import { appRoutes } from 'routes' + +export const NotFoundPage = () => { + return ( +
+
+
+
+
+

Page not found

+
+
+

The page you're attempting to visit doesn't exist

+
+
+ + Go home + +
+
+
+
+
+ ) +} diff --git a/src/pages/feed.tsx b/src/pages/feed.tsx new file mode 100644 index 0000000..bcfc305 --- /dev/null +++ b/src/pages/feed.tsx @@ -0,0 +1,3 @@ +export const FeedPage = () => { + return

Feed

+} diff --git a/src/pages/notifications.tsx b/src/pages/notifications.tsx new file mode 100644 index 0000000..5681bdd --- /dev/null +++ b/src/pages/notifications.tsx @@ -0,0 +1,3 @@ +export const NotificationsPage = () => { + return

Notifications

+} From 7393940027c00c9ce170b5808b28c05f903b3b72 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 23 Oct 2024 17:54:33 +0200 Subject: [PATCH 09/14] feat: update routes --- src/routes/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 668194b..991105e 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -13,7 +13,7 @@ import { GamePage } from 'pages/game' export const appRoutes = { index: '/', - home: '/home', + home: '/', games: '/games', game: '/game/:name', mods: '/mods', @@ -28,7 +28,7 @@ export const appRoutes = { settingsRelays: '/settings-relays', settingsPreferences: '/settings-preferences', settingsAdmin: '/settings-admin', - profile: '/profile/:nprofile' + profile: '/profile/:nprofile?' } export const getGamePageRoute = (name: string) => From 76478ad57257978237e72962c83e152f463213fb Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 23 Oct 2024 17:58:41 +0200 Subject: [PATCH 10/14] fix: quick nav buttons and active state --- src/layout/socialNav.tsx | 58 ++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/src/layout/socialNav.tsx b/src/layout/socialNav.tsx index 022f555..e38bd88 100644 --- a/src/layout/socialNav.tsx +++ b/src/layout/socialNav.tsx @@ -1,24 +1,17 @@ +import { useAppSelector } from 'hooks' import { useState } from 'react' -import { Link, useLocation } from 'react-router-dom' +import { NavLink, NavLinkProps } from 'react-router-dom' import { appRoutes, getProfilePageRoute } from 'routes' import 'styles/socialNav.css' export const SocialNav = () => { - const location = useLocation() - const currentPath = location.pathname const [isCollapsed, setIsCollapsed] = useState(false) + const userState = useAppSelector((state) => state.user) const toggleNav = () => { setIsCollapsed(!isCollapsed) } - const isOnHomePage = - currentPath === appRoutes.index || currentPath === appRoutes.home - const isOnSearchPage = currentPath === appRoutes.search - const isOnProfilePage = new RegExp( - `^${appRoutes.profile.replace(':nprofile', '[^/]+')}$` - ).test(currentPath) - return (
{
- + {!!userState.auth && ( + + )}
)}
@@ -78,19 +68,25 @@ export const SocialNav = () => { ) } -interface NavButtonProps { - to: string - isActive: boolean +interface NavButtonProps extends NavLinkProps { svgPath: string viewBox?: string } -const NavButton = ({ to, isActive, svgPath, viewBox = '0 0 512 512' }: NavButtonProps) => ( - ( + + `btn btnMain socialNavInsideBtn ${ + isActive ? 'socialNavInsideBtnActive' : '' + }` + } > - -); - - + +) From 99ce338502149f135c3cfc7f483a0a8b44d5e845 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 23 Oct 2024 18:00:53 +0200 Subject: [PATCH 11/14] feat: browser router and SPA 404.html --- index.html | 31 ++++++++ public/404.html | 51 ++++++++++++ src/App.tsx | 19 +---- src/layout/socialNav.tsx | 4 +- src/main.tsx | 11 +-- src/routes/index.tsx | 164 +++++++++++++++++++++++---------------- 6 files changed, 186 insertions(+), 94 deletions(-) create mode 100644 public/404.html diff --git a/index.html b/index.html index 9b3186d..2f1f87c 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,37 @@ DEG Mods - Liberating Game Mods + + + +
diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..8912b25 --- /dev/null +++ b/public/404.html @@ -0,0 +1,51 @@ + + + + + + Single Page Apps for GitHub Pages + + + + diff --git a/src/App.tsx b/src/App.tsx index 60b4df3..fe0ccd1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,6 @@ -import { Route, Routes } from 'react-router-dom' -import { Layout } from './layout' -import { routes } from './routes' +import { RouterProvider } from 'react-router-dom' import { useEffect } from 'react' +import { router } from 'routes' import './styles/styles.css' function App() { @@ -22,19 +21,7 @@ function App() { } }, []) - return ( - - }> - {routes.map((route, index) => ( - - ))} - - - ) + return } export default App diff --git a/src/layout/socialNav.tsx b/src/layout/socialNav.tsx index e38bd88..914e8c0 100644 --- a/src/layout/socialNav.tsx +++ b/src/layout/socialNav.tsx @@ -29,11 +29,11 @@ export const SocialNav = () => { svgPath='M511.8 287.6L512.5 447.7C512.5 450.5 512.3 453.1 512 455.8V472C512 494.1 494.1 512 472 512H456C454.9 512 453.8 511.1 452.7 511.9C451.3 511.1 449.9 512 448.5 512H392C369.9 512 352 494.1 352 472V384C352 366.3 337.7 352 320 352H256C238.3 352 224 366.3 224 384V472C224 494.1 206.1 512 184 512H128.1C126.6 512 125.1 511.9 123.6 511.8C122.4 511.9 121.2 512 120 512H104C81.91 512 64 494.1 64 472V360C64 359.1 64.03 358.1 64.09 357.2V287.6H32.05C14.02 287.6 0 273.5 0 255.5C0 246.5 3.004 238.5 10.01 231.5L266.4 8.016C273.4 1.002 281.4 0 288.4 0C295.4 0 303.4 2.004 309.5 7.014L416 100.7V64C416 46.33 430.3 32 448 32H480C497.7 32 512 46.33 512 64V185L564.8 231.5C572.8 238.5 576.9 246.5 575.8 255.5C575.8 273.5 560.8 287.6 543.8 287.6L511.8 287.6z' /> - - - - - - + + + + ) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 991105e..18e4d63 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,3 +1,5 @@ +import { createBrowserRouter } from 'react-router-dom' +import { Layout } from 'layout' import { SearchPage } from 'pages/search' import { AboutPage } from '../pages/about' import { BlogsPage } from '../pages/blogs' @@ -10,6 +12,10 @@ import { SettingsPage } from '../pages/settings' import { SubmitModPage } from '../pages/submitMod' import { WritePage } from '../pages/write' import { GamePage } from 'pages/game' +import { NotFoundPage } from 'pages/404' +import { FeedLayout } from 'layout/feed' +import { FeedPage } from 'pages/feed' +import { NotificationsPage } from 'pages/notifications' export const appRoutes = { index: '/', @@ -28,7 +34,9 @@ export const appRoutes = { settingsRelays: '/settings-relays', settingsPreferences: '/settings-preferences', settingsAdmin: '/settings-admin', - profile: '/profile/:nprofile?' + profile: '/profile/:nprofile?', + feed: '/feed', + notifications: '/notifications' } export const getGamePageRoute = (name: string) => @@ -43,73 +51,91 @@ export const getModsEditPageRoute = (eventId: string) => export const getProfilePageRoute = (nprofile: string) => appRoutes.profile.replace(':nprofile', nprofile) -export const routes = [ +export const router = createBrowserRouter([ { - path: appRoutes.index, - element: - }, - { - path: appRoutes.home, - element: - }, - { - path: appRoutes.games, - element: - }, - { - path: appRoutes.game, - element: - }, - { - path: appRoutes.mods, - element: - }, - { - path: appRoutes.mod, - element: - }, - { - path: appRoutes.about, - element: - }, - { - path: appRoutes.blog, - element: - }, - { - path: appRoutes.submitMod, - element: - }, - { - path: appRoutes.editMod, - element: - }, - { - path: appRoutes.write, - element: - }, - { - path: appRoutes.search, - element: - }, - { - path: appRoutes.settingsProfile, - element: - }, - { - path: appRoutes.settingsRelays, - element: - }, - { - path: appRoutes.settingsPreferences, - element: - }, - { - path: appRoutes.settingsAdmin, - element: - }, - { - path: appRoutes.profile, - element: + element: , + children: [ + { + path: appRoutes.index, + element: + }, + { + path: appRoutes.games, + element: + }, + { + path: appRoutes.game, + element: + }, + { + path: appRoutes.mods, + element: + }, + { + path: appRoutes.mod, + element: + }, + { + path: appRoutes.about, + element: + }, + { + path: appRoutes.blog, + element: + }, + { + path: appRoutes.submitMod, + element: + }, + { + path: appRoutes.editMod, + element: + }, + { + path: appRoutes.write, + element: + }, + { + path: appRoutes.search, + element: + }, + { + path: appRoutes.settingsProfile, + element: + }, + { + path: appRoutes.settingsRelays, + element: + }, + { + path: appRoutes.settingsPreferences, + element: + }, + { + path: appRoutes.settingsAdmin, + element: + }, + { + path: appRoutes.profile, + element: + }, + { + element: , + children: [ + { + path: appRoutes.feed, + element: + }, + { + path: appRoutes.notifications, + element: + } + ] + }, + { + path: '*', + element: + } + ] } -] +]) From 53861a36d34cf969c8d8547e1b2c41b43221c3dd Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 24 Oct 2024 10:48:49 +0200 Subject: [PATCH 12/14] fix: props and placeholder wip text --- src/layout/socialNav.tsx | 8 +++----- src/pages/profile.tsx | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/layout/socialNav.tsx b/src/layout/socialNav.tsx index 914e8c0..cdc142c 100644 --- a/src/layout/socialNav.tsx +++ b/src/layout/socialNav.tsx @@ -74,14 +74,12 @@ interface NavButtonProps extends NavLinkProps { } const NavButton = ({ - to, - end, svgPath, - viewBox = '0 0 512 512' + viewBox = '0 0 512 512', + ...rest }: NavButtonProps) => ( `btn btnMain socialNavInsideBtn ${ isActive ? 'socialNavInsideBtnActive' : '' diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 017b57b..ca3d17d 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -499,8 +499,8 @@ export const ProfilePage = () => { )} - {tab === 1 && <>USER's BLOGS WIP} - {tab === 2 && <>USER's POSTS WIP} + {tab === 1 && <>WIP} + {tab === 2 && <>WIP}
From 84cb5b691278f867bfde7f8c19bf756060a3375e Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 24 Oct 2024 11:12:31 +0200 Subject: [PATCH 13/14] fix: add missing InnerBodyMain div feed layout --- src/layout/feed.tsx | 4 ++-- src/pages/feed.tsx | 2 +- src/pages/notifications.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/layout/feed.tsx b/src/layout/feed.tsx index 2566f31..6f68a29 100644 --- a/src/layout/feed.tsx +++ b/src/layout/feed.tsx @@ -2,9 +2,9 @@ import { Outlet } from 'react-router-dom' export const FeedLayout = () => { return ( - <> +

WIP

- +
) } diff --git a/src/pages/feed.tsx b/src/pages/feed.tsx index bcfc305..88694d9 100644 --- a/src/pages/feed.tsx +++ b/src/pages/feed.tsx @@ -1,3 +1,3 @@ export const FeedPage = () => { - return

Feed

+ return

Feed

} diff --git a/src/pages/notifications.tsx b/src/pages/notifications.tsx index 5681bdd..6632a58 100644 --- a/src/pages/notifications.tsx +++ b/src/pages/notifications.tsx @@ -1,3 +1,3 @@ export const NotificationsPage = () => { - return

Notifications

+ return

Notifications

} From 8ee6f98654c87c4a59ac4a0c58d01c48e79e1220 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 24 Oct 2024 13:14:42 +0200 Subject: [PATCH 14/14] fix: hash router backwards compatibility --- index.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/index.html b/index.html index 2f1f87c..bd4a70a 100644 --- a/index.html +++ b/index.html @@ -10,6 +10,16 @@ DEG Mods - Liberating Game Mods + + + +