From ca52afa6c43276cbb63996be93590edfb30ef051 Mon Sep 17 00:00:00 2001 From: zekro Date: Sat, 3 Sep 2022 05:50:15 +0000 Subject: [PATCH 01/25] add all guild settings entries to navbar --- web.new/public/locales/en-US/components.json | 7 +++- web.new/src/assets/api.svg | 8 +++++ web.new/src/assets/data.svg | 9 +++-- web.new/src/assets/karma.svg | 8 +++-- web.new/src/components/Navbar/Navbar.tsx | 35 ++++++++++++++++++++ 5 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 web.new/src/assets/api.svg diff --git a/web.new/public/locales/en-US/components.json b/web.new/public/locales/en-US/components.json index 6eb19f0c9..c02b3bf17 100644 --- a/web.new/public/locales/en-US/components.json +++ b/web.new/public/locales/en-US/components.json @@ -14,7 +14,12 @@ "general": "General", "backup": "Backups", "antiraid": "Antiraid", - "codeexec": "Code Execution" + "codeexec": "Code Execution", + "verification": "Verification", + "karma": "Karma", + "logs": "Logs", + "data": "Data", + "api": "API" } }, "logout": "Logout", diff --git a/web.new/src/assets/api.svg b/web.new/src/assets/api.svg new file mode 100644 index 000000000..ef2410386 --- /dev/null +++ b/web.new/src/assets/api.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/web.new/src/assets/data.svg b/web.new/src/assets/data.svg index ee3e558ac..62d9440f1 100644 --- a/web.new/src/assets/data.svg +++ b/web.new/src/assets/data.svg @@ -1,3 +1,6 @@ - - - \ No newline at end of file + + + + + + diff --git a/web.new/src/assets/karma.svg b/web.new/src/assets/karma.svg index 508b0ed1d..36521b562 100644 --- a/web.new/src/assets/karma.svg +++ b/web.new/src/assets/karma.svg @@ -1,3 +1,5 @@ - - - \ No newline at end of file + + + + + diff --git a/web.new/src/components/Navbar/Navbar.tsx b/web.new/src/components/Navbar/Navbar.tsx index 4d6e95898..78391c766 100644 --- a/web.new/src/components/Navbar/Navbar.tsx +++ b/web.new/src/components/Navbar/Navbar.tsx @@ -3,14 +3,19 @@ import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router'; import styled from 'styled-components'; import { ReactComponent as AntiraidIcon } from '../../assets/antiraid.svg'; +import { ReactComponent as APIIcon } from '../../assets/api.svg'; import { ReactComponent as BackupIcon } from '../../assets/backup.svg'; import { ReactComponent as CodeIcon } from '../../assets/code.svg'; +import { ReactComponent as DataIcon } from '../../assets/data.svg'; import { ReactComponent as HammerIcon } from '../../assets/hammer.svg'; +import { ReactComponent as KarmaIcon } from '../../assets/karma.svg'; +import { ReactComponent as LogsIcon } from '../../assets/logs.svg'; import { ReactComponent as SettingsIcon } from '../../assets/settings.svg'; import { ReactComponent as SPBrand } from '../../assets/sp-brand.svg'; import SPIcon from '../../assets/sp-icon.png'; import { ReactComponent as TriangleIcon } from '../../assets/triangle.svg'; import { ReactComponent as UsersIcon } from '../../assets/users.svg'; +import { ReactComponent as VerificationIcon } from '../../assets/verification.svg'; import { useApi } from '../../hooks/useApi'; import { useGuilds } from '../../hooks/useGuilds'; import { usePerms } from '../../hooks/usePerms'; @@ -204,6 +209,36 @@ export const Navbar: React.FC = () => { {t('section.guildsettings.codeexec')} )} + {isAllowed('sp.guild.config.verification') && ( + + + {t('section.guildsettings.verification')} + + )} + {isAllowed('sp.guild.config.karma') && ( + + + {t('section.guildsettings.karma')} + + )} + {isAllowed('sp.guild.config.logs') && ( + + + {t('section.guildsettings.logs')} + + )} + {isAllowed('sp.guild.admin.flushdata') && ( + + + {t('section.guildsettings.data')} + + )} + {isAllowed('sp.guild.config.api') && ( + + + {t('section.guildsettings.api')} + + )} )} From 5171787a981701d56adda7446578c33c42aa27ab Mon Sep 17 00:00:00 2001 From: zekro Date: Sat, 3 Sep 2022 05:50:45 +0000 Subject: [PATCH 02/25] add verification route --- .../routes.guildsettings.verification.json | 10 +++ web.new/src/App.tsx | 20 +++++ web.new/src/lib/shinpuru-ts/src/bindings.ts | 8 +- .../Dashboard/GuildSettings/Verification.tsx | 85 +++++++++++++++++++ 4 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 web.new/public/locales/en-US/routes.guildsettings.verification.json create mode 100644 web.new/src/routes/Dashboard/GuildSettings/Verification.tsx diff --git a/web.new/public/locales/en-US/routes.guildsettings.verification.json b/web.new/public/locales/en-US/routes.guildsettings.verification.json new file mode 100644 index 000000000..92b00d76f --- /dev/null +++ b/web.new/public/locales/en-US/routes.guildsettings.verification.json @@ -0,0 +1,10 @@ +{ + "heading": "User Verification", + "explanation": "Lorem ipsum", + "enable": "Enable user verification", + "save": "Save settings", + + "notifications": { + "saved": "Settings saved." + } +} diff --git a/web.new/src/App.tsx b/web.new/src/App.tsx index 6076c5f06..ab41f0210 100644 --- a/web.new/src/App.tsx +++ b/web.new/src/App.tsx @@ -19,6 +19,10 @@ const GuildGeneralRoute = React.lazy(() => import('./routes/Dashboard/GuildSetti const GuildBackupsRoute = React.lazy(() => import('./routes/Dashboard/GuildSettings/Backup')); const GuildAntiraidRoute = React.lazy(() => import('./routes/Dashboard/GuildSettings/Antiraid')); const GuildCodeexecRoute = React.lazy(() => import('./routes/Dashboard/GuildSettings/Codeexec')); +const GuildVerificationRoute = React.lazy( + () => import('./routes/Dashboard/GuildSettings/Verification'), +); +const GuildKarmaRoute = React.lazy(() => import('./routes/Dashboard/GuildSettings/Karma')); const GlobalStyle = createGlobalStyle` body { @@ -114,6 +118,22 @@ export const App: React.FC = () => { } /> + + + + } + /> + + + + } + /> {import.meta.env.DEV && } />} diff --git a/web.new/src/lib/shinpuru-ts/src/bindings.ts b/web.new/src/lib/shinpuru-ts/src/bindings.ts index 3d7a6fb0f..068f552fc 100644 --- a/web.new/src/lib/shinpuru-ts/src/bindings.ts +++ b/web.new/src/lib/shinpuru-ts/src/bindings.ts @@ -1,5 +1,5 @@ import { Client } from './client'; -import { GuildLogEntry, User } from './models'; +import { GuildLogEntry, GuildSettingsVerification, User } from './models'; import { AccessTokenModel, APIToken, @@ -402,12 +402,12 @@ export class GuildSettingsClient extends SubClient { return this.req('POST', 'logs/state', { state }); } - verification(): Promise { + verification(): Promise { return this.req('GET', 'verification'); } - setVerification(state: boolean): Promise { - return this.req('POST', 'verification', { state }); + setVerification(state: GuildSettingsVerification): Promise { + return this.req('POST', 'verification', state); } } diff --git a/web.new/src/routes/Dashboard/GuildSettings/Verification.tsx b/web.new/src/routes/Dashboard/GuildSettings/Verification.tsx new file mode 100644 index 000000000..a3ad4de4e --- /dev/null +++ b/web.new/src/routes/Dashboard/GuildSettings/Verification.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useReducer } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router'; +import { Button } from '../../../components/Button'; +import { Controls } from '../../../components/Controls'; +import { Loader } from '../../../components/Loader'; +import { MaxWidthContainer } from '../../../components/MaxWidthContainer'; +import { NotificationType } from '../../../components/Notifications'; +import { Small } from '../../../components/Small'; +import { Switch } from '../../../components/Switch'; +import { useApi } from '../../../hooks/useApi'; +import { useNotifications } from '../../../hooks/useNotifications'; +import { GuildSettingsVerification } from '../../../lib/shinpuru-ts/src'; + +type Props = {}; + +const settingsReducer = ( + state: Partial, + [type, payload]: ['set_state', Partial] | ['set_enabled', boolean], +) => { + switch (type) { + case 'set_state': + return { ...state, ...payload }; + case 'set_enabled': + return { ...state, enabled: payload }; + default: + return state; + } +}; + +const CodeexecRoute: React.FC = ({}) => { + const { t } = useTranslation('routes.guildsettings.codeexec'); + const { pushNotification } = useNotifications(); + const { guildid } = useParams(); + const fetch = useApi(); + const [settings, dispatchSettings] = useReducer( + settingsReducer, + {} as Partial, + ); + + const _saveSettings = () => { + if (!guildid) return; + + fetch((c) => c.guilds.settings(guildid).setVerification(settings as GuildSettingsVerification)) + .then(() => + pushNotification({ + message: t('notifications.saved'), + type: NotificationType.SUCCESS, + }), + ) + .catch(); + }; + + useEffect(() => { + if (!guildid) return; + + fetch((c) => c.guilds.settings(guildid).verification()) + .then((res) => dispatchSettings(['set_state', res])) + .catch(); + }, [guildid]); + + return ( + +

{t('heading')}

+ {t('explaination')} + +

Settings

+ {(settings.enabled !== undefined && ( + dispatchSettings(['set_enabled', e])} + labelAfter={t('enable')} + /> + )) || } + + + + +
+ ); +}; + +export default CodeexecRoute; From d5e516a25ece3af1da21e7c0ea0bacae37ce7b29 Mon Sep 17 00:00:00 2001 From: zekro Date: Sat, 3 Sep 2022 05:51:47 +0000 Subject: [PATCH 03/25] update codeexec route --- web.new/src/components/SplitContainer.tsx | 16 ++++++++++++++++ .../routes/Dashboard/GuildSettings/Codeexec.tsx | 9 ++++----- 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 web.new/src/components/SplitContainer.tsx diff --git a/web.new/src/components/SplitContainer.tsx b/web.new/src/components/SplitContainer.tsx new file mode 100644 index 000000000..f9713b3c0 --- /dev/null +++ b/web.new/src/components/SplitContainer.tsx @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +export const SplitContainer = styled.div` + width: 100%; + display: flex; + gap: 1em; + + > section { + width: 100%; + } + + @media (orientation: portrait) { + flex-direction: column; + } +`; + diff --git a/web.new/src/routes/Dashboard/GuildSettings/Codeexec.tsx b/web.new/src/routes/Dashboard/GuildSettings/Codeexec.tsx index cbd34f484..8dcbc4c50 100644 --- a/web.new/src/routes/Dashboard/GuildSettings/Codeexec.tsx +++ b/web.new/src/routes/Dashboard/GuildSettings/Codeexec.tsx @@ -1,7 +1,6 @@ -import React, { useEffect, useReducer, useState } from 'react'; +import React, { useEffect, useReducer } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; -import { uid } from 'react-uid'; import styled, { useTheme } from 'styled-components'; import { Button } from '../../../components/Button'; import { Controls } from '../../../components/Controls'; @@ -57,8 +56,8 @@ const MarginSmall = styled(Small)` margin-top: 1.5em; `; -const CodeexecRoute: React.FC = ({}) => { - const { t } = useTranslation('routes.guildsettings.codeexec'); +const VerificationRoute: React.FC = ({}) => { + const { t } = useTranslation('routes.guildsettings.verification'); const theme = useTheme(); const { pushNotification } = useNotifications(); const { guildid } = useParams(); @@ -159,4 +158,4 @@ const CodeexecRoute: React.FC = ({}) => { ); }; -export default CodeexecRoute; +export default VerificationRoute; From 91677f8bb67c1e18b3e1725532291549f1467ca5 Mon Sep 17 00:00:00 2001 From: zekro Date: Sun, 4 Sep 2022 15:31:38 +0000 Subject: [PATCH 04/25] add karma settings --- .../webserver/v1/controllers/guildsettings.go | 4 +- web.new/package.json | 11 +- web.new/public/locales/en-US/components.json | 14 + .../en-US/routes.guildsettings.karma.json | 44 +++ web.new/src/components/Embed.tsx | 4 + .../components/KarmaRule/KarmaRuleEntry.tsx | 79 +++++ .../components/KarmaRule/KarmaRuleInput.tsx | 121 +++++++ web.new/src/components/KarmaRule/index.ts | 2 + web.new/src/components/KarmaRule/shared.ts | 32 ++ .../src/components/RoleInput/RoleInput.tsx | 26 ++ web.new/src/components/RoleInput/index.ts | 1 + web.new/src/lib/shinpuru-ts/src/bindings.ts | 4 +- web.new/src/lib/shinpuru-ts/src/models.ts | 11 +- .../Dashboard/GuildSettings/General.tsx | 19 +- .../routes/Dashboard/GuildSettings/Karma.tsx | 318 ++++++++++++++++++ .../routes/Dashboard/Guilds/GuildModlog.tsx | 19 +- web.new/src/util/reducer.ts | 17 + web.new/yarn.lock | 5 + 18 files changed, 694 insertions(+), 37 deletions(-) create mode 100644 web.new/public/locales/en-US/routes.guildsettings.karma.json create mode 100644 web.new/src/components/KarmaRule/KarmaRuleEntry.tsx create mode 100644 web.new/src/components/KarmaRule/KarmaRuleInput.tsx create mode 100644 web.new/src/components/KarmaRule/index.ts create mode 100644 web.new/src/components/KarmaRule/shared.ts create mode 100644 web.new/src/components/RoleInput/RoleInput.tsx create mode 100644 web.new/src/components/RoleInput/index.ts create mode 100644 web.new/src/routes/Dashboard/GuildSettings/Karma.tsx create mode 100644 web.new/src/util/reducer.ts diff --git a/internal/services/webserver/v1/controllers/guildsettings.go b/internal/services/webserver/v1/controllers/guildsettings.go index 894f8620b..aa2ff0a15 100644 --- a/internal/services/webserver/v1/controllers/guildsettings.go +++ b/internal/services/webserver/v1/controllers/guildsettings.go @@ -397,7 +397,7 @@ func (c *GuildsSettingsController) getGuildSettingsKarmaBlocklist(ctx *fiber.Ctx // @Produce json // @Param id path string true "The ID of the guild." // @Param memberid path string true "The ID of the guild." -// @Success 200 {object} models.Status +// @Success 200 {object} models.Member // @Failure 400 {object} models.Error // @Failure 401 {object} models.Error // @Failure 404 {object} models.Error @@ -426,7 +426,7 @@ func (c *GuildsSettingsController) putGuildSettingsKarmaBlocklist(ctx *fiber.Ctx return err } - return ctx.JSON(models.Ok) + return ctx.JSON(memb) } // @Summary Remove Guild Karma Blocklist Entry diff --git a/web.new/package.json b/web.new/package.json index 4a024a698..e31f883a2 100644 --- a/web.new/package.json +++ b/web.new/package.json @@ -7,6 +7,7 @@ "color": "^4.2.1", "date-fns": "^2.28.0", "debounce": "^1.2.1", + "emoji.json": "^13.1.0", "i18next": "^21.6.14", "i18next-browser-languagedetector": "^6.1.3", "i18next-http-backend": "^1.4.0", @@ -24,15 +25,15 @@ "zustand": "^3.7.0" }, "devDependencies": { + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^12.1.2", + "@testing-library/user-event": "^13.5.0", + "@types/color": "^3", + "@types/debounce": "^1", "@types/react": "^18", "@types/react-dom": "^18", "@types/react-router-dom": "^5", "@types/styled-components": "^5", - "@types/color": "^3", - "@types/debounce": "^1", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^12.1.2", - "@testing-library/user-event": "^13.5.0", "@vitejs/plugin-react": "^1.2.0", "eslint-plugin-prettier": "^4.0.0", "typescript": "^4.7", diff --git a/web.new/public/locales/en-US/components.json b/web.new/public/locales/en-US/components.json index c02b3bf17..49bcd40af 100644 --- a/web.new/public/locales/en-US/components.json +++ b/web.new/public/locales/en-US/components.json @@ -126,5 +126,19 @@ "accept": "Unban request has sucessfully been acceped and the member is now unbanned.", "decline": "Unban request has successfully been rejected and the member remains banned." } + }, + "karmarule": { + "text": "When a users karma <1/> <2/> points then <3/> <4/>.", + "trigger": { + "dropsbelow": "drops below", + "risesabove": "rises above" + }, + "action": { + "togglerole": "toggle role", + "kick": "kick member", + "ban": "ban member", + "sendmessage": "send message" + }, + "apply": "Apply rule" } } diff --git a/web.new/public/locales/en-US/routes.guildsettings.karma.json b/web.new/public/locales/en-US/routes.guildsettings.karma.json new file mode 100644 index 000000000..1fe5323d1 --- /dev/null +++ b/web.new/public/locales/en-US/routes.guildsettings.karma.json @@ -0,0 +1,44 @@ +{ + "heading": "Karma", + "explanation": "Lorem ipsum", + "enable": "Enable karma system", + "save": "Save settings", + + "notifications": { + "saved": "Settings saved.", + "ruleapplied": "Karma rule has been applied.", + "ruleremoved": "Karma rule has been removed.", + "blocklistadded": "Block list entry has been added.", + "blocklistremoved": "Block list entry has been removed." + }, + + "emotes": { + "heading": "Emotes", + "increase": "Increase karma", + "decrease": "Decrease karma" + }, + + "limit": { + "heading": "Limit" + }, + + "penalty": { + "heading": "Penalty", + "switch": "Enable karma penalty" + }, + + "rules": { + "heading": "Rules" + }, + + "blocklist": { + "heading": "Block list", + "addmember": "Add member", + "table": { + "id": "ID", + "name": "Name", + "nick": "Nick", + "unblock": "Unblock" + } + } +} diff --git a/web.new/src/components/Embed.tsx b/web.new/src/components/Embed.tsx index 1f44f88d4..2408ba567 100644 --- a/web.new/src/components/Embed.tsx +++ b/web.new/src/components/Embed.tsx @@ -9,3 +9,7 @@ export const Embed = styled.span` background-color: rgba(0 0 0 / 10%); width: fit-content; `; + +export const EmbedWrapper: React.FC<{ value: string | number | JSX.Element | undefined }> = ({ + value, +}) => {value}; diff --git a/web.new/src/components/KarmaRule/KarmaRuleEntry.tsx b/web.new/src/components/KarmaRule/KarmaRuleEntry.tsx new file mode 100644 index 000000000..758a63bb3 --- /dev/null +++ b/web.new/src/components/KarmaRule/KarmaRuleEntry.tsx @@ -0,0 +1,79 @@ +import { useMemo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { ReactComponent as IconDelete } from '../../assets/delete.svg'; +import { Guild, KarmaRule } from '../../lib/shinpuru-ts/src'; +import { Container } from '../Container'; +import { EmbedWrapper } from '../Embed'; +import { getActionOptions, getTriggerOptions } from './shared'; + +type Props = { + guild: Guild; + rule: KarmaRule; + onRemove: () => void; +}; + +const RuleContainer = styled(Container)` + margin: 1em 0; + display: flex; + align-items: center; + justify-content: space-between; + + &:hover > button { + opacity: 1; + } + + > button { + background: none; + border: none; + padding: 0; + cursor: pointer; + opacity: 0; + transition: all 0.25s ease; + } +`; + +export const KarmaRuleEntry: React.FC = ({ guild, rule, onRemove }) => { + const { t } = useTranslation('components'); + + const triggerOptions = useMemo(() => getTriggerOptions(t), [t]); + const actionOptions = useMemo(() => getActionOptions(t), [t]); + + const triggerElem = ( + o.value === rule.trigger)?.display} /> + ); + const valueElem = ; + const actionElem = ( + o.value === rule.action)?.display} /> + ); + const argumentElem = (() => { + switch (rule.action) { + case 'SEND_MESSAGE': + return ; + case 'TOGGLE_ROLE': + return r.id === rule.argument)?.name} />; + default: + return <>; + } + })(); + + return ( + +
+ +
+ +
+ ); +}; diff --git a/web.new/src/components/KarmaRule/KarmaRuleInput.tsx b/web.new/src/components/KarmaRule/KarmaRuleInput.tsx new file mode 100644 index 000000000..237754091 --- /dev/null +++ b/web.new/src/components/KarmaRule/KarmaRuleInput.tsx @@ -0,0 +1,121 @@ +import { useMemo, useReducer } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { Guild, KarmaRule, KarmaRuleAction, KarmaRuleTrigger } from '../../lib/shinpuru-ts/src'; +import { Button } from '../Button'; +import { Input } from '../Input'; +import { Select } from '../Select'; +import { getActionOptions, getRoleOptions, getTriggerOptions } from './shared'; + +type Props = { + guild: Guild; + onApply: (r: KarmaRule) => void; +}; + +const ruleReducer = ( + state: KarmaRule, + [type, payload]: + | ['set', KarmaRule] + | ['set_trigger', KarmaRuleTrigger] + | ['set_vaule', number] + | ['set_action', KarmaRuleAction] + | ['set_argument', string], +) => { + switch (type) { + case 'set': + return payload; + case 'set_trigger': + return { ...state, trigger: payload }; + case 'set_vaule': + return { ...state, value: payload }; + case 'set_action': + return { ...state, action: payload, argument: '' }; + case 'set_argument': + return { ...state, argument: payload }; + default: + return state; + } +}; + +const StyledSelect = styled(Select)``; + +const RuleContainer = styled.div` + ${StyledSelect}, ${Input} { + display: inline-block; + width: 10em; + margin: 0.4em 0.2em; + } + + ${Button} { + margin: 1em 0; + width: 100%; + } +`; + +export const KarmaRuleInput: React.FC = ({ guild, onApply }) => { + const { t } = useTranslation('components'); + const [rule, dispatchRule] = useReducer(ruleReducer, {} as KarmaRule); + + const triggerOptions = useMemo(() => getTriggerOptions(t), [t]); + const actionOptions = useMemo(() => getActionOptions(t), [t]); + const roleOptions = useMemo(() => getRoleOptions(t, guild), [t, guild]); + + const triggerSelect = ( + o.value === rule.trigger)} + onElementSelect={(e) => dispatchRule(['set_trigger', e.value as KarmaRuleTrigger])} + /> + ); + const valueInput = ( + dispatchRule(['set_vaule', parseInt(e.currentTarget.value)])} + /> + ); + const actionSelect = ( + o.value === rule.action)} + onElementSelect={(e) => dispatchRule(['set_action', e.value as KarmaRuleAction])} + /> + ); + const argumentInput = (() => { + switch (rule.action) { + case 'SEND_MESSAGE': + return ( + dispatchRule(['set_argument', e.currentTarget.value])} + /> + ); + case 'TOGGLE_ROLE': + return ( + r.id === rule.argument)} + onElementSelect={(e) => dispatchRule(['set_argument', e.value as string])} + /> + ); + default: + return <>; + } + })(); + + return ( + + + + + ); +}; diff --git a/web.new/src/components/KarmaRule/index.ts b/web.new/src/components/KarmaRule/index.ts new file mode 100644 index 000000000..fd180a797 --- /dev/null +++ b/web.new/src/components/KarmaRule/index.ts @@ -0,0 +1,2 @@ +export * from './KarmaRuleInput'; +export * from './KarmaRuleEntry'; diff --git a/web.new/src/components/KarmaRule/shared.ts b/web.new/src/components/KarmaRule/shared.ts new file mode 100644 index 000000000..b3e1b2b1b --- /dev/null +++ b/web.new/src/components/KarmaRule/shared.ts @@ -0,0 +1,32 @@ +import { Guild, KarmaRuleAction, KarmaRuleTrigger } from '../../lib/shinpuru-ts/src'; +import { Element } from '../Select'; + +type TranslateFunc = (v: string) => string; + +export const getTriggerOptions = (t: TranslateFunc) => + [ + { + id: 'below', + value: KarmaRuleTrigger.BELOW, + display: t('karmarule.trigger.dropsbelow'), + }, + { + id: 'above', + value: KarmaRuleTrigger.ABOVE, + display: t('karmarule.trigger.risesabove'), + }, + ] as Element[]; + +export const getActionOptions = (t: TranslateFunc) => + (['TOGGLE_ROLE', 'KICK', 'BAN', 'SEND_MESSAGE'] as KarmaRuleAction[]).map((a) => ({ + id: a, + display: t(`karmarule.action.${a.toLowerCase().replaceAll('_', '')}`), + value: a, + })) as Element[]; + +export const getRoleOptions = (t: TranslateFunc, g: Guild) => + (g?.roles ?? []).map((r) => ({ + id: r.id, + display: r.name, + value: r.id, + })) as Element[]; diff --git a/web.new/src/components/RoleInput/RoleInput.tsx b/web.new/src/components/RoleInput/RoleInput.tsx new file mode 100644 index 000000000..ea0a8c0a0 --- /dev/null +++ b/web.new/src/components/RoleInput/RoleInput.tsx @@ -0,0 +1,26 @@ +import { Guild, Role } from '../../lib/shinpuru-ts/src'; +import { TagElement, TagsInput } from '../TagsInput'; + +type Props = { + guild: Guild; + selected?: Role[]; + onChange: (v: Role[]) => void; + placeholder?: string; +}; + +export const RoleInput: React.FC = ({ guild, selected, onChange, placeholder }) => { + const roleTagOptions = + guild?.roles?.map( + (r) => + ({ id: r.id, display: r.name, keywords: [r.id, r.name], value: r } as TagElement), + ) ?? []; + + return ( + roleTagOptions.find((e) => e.id === r.id)!)} + onChange={(v) => onChange(v.map((e) => e.value))} + placeholder={placeholder} + /> + ); +}; diff --git a/web.new/src/components/RoleInput/index.ts b/web.new/src/components/RoleInput/index.ts new file mode 100644 index 000000000..b0a0e6e1c --- /dev/null +++ b/web.new/src/components/RoleInput/index.ts @@ -0,0 +1 @@ +export * from './RoleInput'; diff --git a/web.new/src/lib/shinpuru-ts/src/bindings.ts b/web.new/src/lib/shinpuru-ts/src/bindings.ts index 068f552fc..42d4bbbbe 100644 --- a/web.new/src/lib/shinpuru-ts/src/bindings.ts +++ b/web.new/src/lib/shinpuru-ts/src/bindings.ts @@ -354,7 +354,7 @@ export class GuildSettingsClient extends SubClient { return this.req('GET', 'karma/blocklist'); } - addKarmaBlocklist(memberId: string): Promise { + addKarmaBlocklist(memberId: string): Promise { return this.req('PUT', `karma/blocklist/${memberId}`); } @@ -366,7 +366,7 @@ export class GuildSettingsClient extends SubClient { return this.req('GET', 'karma/rules'); } - addKarmaRules(rule: KarmaRule): Promise> { + addKarmaRule(rule: KarmaRule): Promise { return this.req('POST', 'karma/rules', rule); } diff --git a/web.new/src/lib/shinpuru-ts/src/models.ts b/web.new/src/lib/shinpuru-ts/src/models.ts index af0e7d1d5..2c3328f9c 100644 --- a/web.new/src/lib/shinpuru-ts/src/models.ts +++ b/web.new/src/lib/shinpuru-ts/src/models.ts @@ -341,12 +341,19 @@ export interface AccessTokenModel { expires: string; } +export enum KarmaRuleTrigger { + BELOW = 0, + ABOVE = 1, +} + +export type KarmaRuleAction = 'TOGGLE_ROLE' | 'KICK' | 'BAN' | 'SEND_MESSAGE'; + export interface KarmaRule { id: string; guildid: string; - trigger: number; + trigger: KarmaRuleTrigger; value: number; - action: string; + action: KarmaRuleAction; argument: string; } diff --git a/web.new/src/routes/Dashboard/GuildSettings/General.tsx b/web.new/src/routes/Dashboard/GuildSettings/General.tsx index 70359ff6e..92b1bd68a 100644 --- a/web.new/src/routes/Dashboard/GuildSettings/General.tsx +++ b/web.new/src/routes/Dashboard/GuildSettings/General.tsx @@ -10,9 +10,10 @@ import { Input } from '../../../components/Input'; import { Loader } from '../../../components/Loader'; import { MaxWidthContainer } from '../../../components/MaxWidthContainer'; import { NotificationType } from '../../../components/Notifications'; +import { RoleInput } from '../../../components/RoleInput'; import { Element, Select } from '../../../components/Select'; import { Small } from '../../../components/Small'; -import { TagElement, TagsInput } from '../../../components/TagsInput/TagsInput'; +import { TagElement } from '../../../components/TagsInput/TagsInput'; import { useApi } from '../../../hooks/useApi'; import { useGuild } from '../../../hooks/useGuild'; import { useNotifications } from '../../../hooks/useNotifications'; @@ -22,7 +23,7 @@ import { Channel, ChannelType, GuildSettings, Role } from '../../../lib/shinpuru type Props = {}; type GuildSettingsVM = { - autoroles: TagElement[]; + autoroles: Role[]; modlogchannel?: Element; voicelogchannel?: Element; joinmessagechannel?: Element; @@ -35,7 +36,7 @@ const guildSettingsReducer = ( state: GuildSettingsVM, [type, payload]: | ['set_state', Partial] - | ['set_autoroles', TagElement[]] + | ['set_autoroles', Role[]] | [ ( | 'set_modlogchannel' @@ -135,8 +136,7 @@ const GeneralRoute: React.FC = () => { const gs = {} as GuildSettings; - if (isAllowed('sp.guild.config.autorole')) - gs.autoroles = settings.autoroles.map((r) => r.value.id); + if (isAllowed('sp.guild.config.autorole')) gs.autoroles = settings.autoroles.map((r) => r.id); if (isAllowed('sp.guild.config.modlog')) gs.modlogchannel = settings.modlogchannel?.value.id ?? '__RESET__'; @@ -169,8 +169,7 @@ const GeneralRoute: React.FC = () => { 'set_autoroles', (res.autoroles ?? []) .map((rid) => guild.roles!.find((r) => r.id === rid)) - .filter((r) => !!r) - .map((r) => ({ id: r!.id, display: r!.name, keywords: [r!.id, r!.name], value: r! })), + .filter((r) => !!r) as Role[], ]); const modlogchannel = guild.channels!.find((c) => c.id === res.modlogchannel)!; @@ -253,9 +252,9 @@ const GeneralRoute: React.FC = () => { {isAllowed('sp.guild.config.autorole') && (

{t('autoroles.title')}

- dispatchSettings(['set_autoroles', v])} placeholder={t('autoroles.placeholder')} /> diff --git a/web.new/src/routes/Dashboard/GuildSettings/Karma.tsx b/web.new/src/routes/Dashboard/GuildSettings/Karma.tsx new file mode 100644 index 000000000..c618c93f8 --- /dev/null +++ b/web.new/src/routes/Dashboard/GuildSettings/Karma.tsx @@ -0,0 +1,318 @@ +import emojis from 'emoji.json'; +import React, { useEffect, useReducer, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router'; +import { uid } from 'react-uid'; +import styled from 'styled-components'; +import { Button } from '../../../components/Button'; +import { Controls } from '../../../components/Controls'; +import { Input } from '../../../components/Input'; +import { KarmaRuleEntry, KarmaRuleInput } from '../../../components/KarmaRule'; +import { Loader } from '../../../components/Loader'; +import { MaxWidthContainer } from '../../../components/MaxWidthContainer'; +import { NotificationType } from '../../../components/Notifications'; +import { Small } from '../../../components/Small'; +import { SplitContainer } from '../../../components/SplitContainer'; +import { Switch } from '../../../components/Switch'; +import { TagElement, TagsInput } from '../../../components/TagsInput'; +import { useApi } from '../../../hooks/useApi'; +import { useGuild } from '../../../hooks/useGuild'; +import { useNotifications } from '../../../hooks/useNotifications'; +import { KarmaRule, KarmaSettings, Member } from '../../../lib/shinpuru-ts/src'; +import { listReducer } from '../../../util/reducer'; + +type Props = {}; + +const settingsReducer = ( + state: Partial, + [type, payload]: + | ['set_state', Partial] + | ['set_enabled' | 'set_penalty', boolean] + | ['set_increase' | 'set_decrease', string[]] + | ['set_tokens', number], +) => { + switch (type) { + case 'set_state': + return { ...state, ...payload }; + case 'set_enabled': + return { ...state, state: payload }; + case 'set_increase': + return { ...state, emotes_increase: payload }; + case 'set_decrease': + return { ...state, emotes_decrease: payload }; + case 'set_tokens': + return { ...state, tokens: payload > 0 ? payload : 1 }; + case 'set_penalty': + return { ...state, penalty: payload }; + default: + return state; + } +}; + +const rulesReducer = listReducer; + +const blocklistReducer = ( + state: Member[], + [type, payload]: ['set', Member[]] | ['add', Member | Member[]] | ['remove', Member], +) => { + switch (type) { + case 'set': + return payload; + case 'add': + return [...state, ...(Array.isArray(payload) ? payload : [payload])]; + case 'remove': + return state.filter((e) => e.user.id !== payload.user.id); + default: + return state; + } +}; + +const InputContainer = styled.section` + > input, + label { + display: block; + width: 100%; + } + + > label { + margin-bottom: 1em; + } +`; + +const BlocklistInputContainer = styled.div` + display: flex; + gap: 1em; + white-space: nowrap; + + > ${Input} { + width: 100%; + } +`; + +const StyledTable = styled.table` + width: 100%; + margin-top: 2em; + + th { + text-align: start; + } + + ${Button} { + padding: 0.5em; + } +`; + +const KarmaRoute: React.FC = ({}) => { + const { t } = useTranslation('routes.guildsettings.karma'); + const { pushNotification } = useNotifications(); + const { guildid } = useParams(); + const guild = useGuild(guildid); + const fetch = useApi(); + const [settings, dispatchSettings] = useReducer(settingsReducer, {} as Partial); + const [rules, dispatchRules] = useReducer(rulesReducer, []); + const [blocklist, dispatchBlocklist] = useReducer(blocklistReducer, []); + const [blocklistInput, setBlocklistInput] = useState(''); + + const _saveSettings = () => { + if (!guildid) return; + + fetch((c) => c.guilds.settings(guildid).setKarma(settings as KarmaSettings)) + .then(() => + pushNotification({ + message: t('notifications.saved'), + type: NotificationType.SUCCESS, + }), + ) + .catch(); + }; + + const _addKarmaRule = (r: KarmaRule) => { + if (!guildid) return; + fetch((c) => c.guilds.settings(guildid).addKarmaRule(r)) + .then((res) => { + dispatchRules(['add', res]); + pushNotification({ + message: t('notifications.ruleapplied'), + type: NotificationType.SUCCESS, + }); + }) + .catch(); + }; + + const _removeKarmaRule = (r: KarmaRule) => { + if (!guildid) return; + fetch((c) => c.guilds.settings(guildid).removeKarmaRule(r.id)).then((res) => { + dispatchRules(['remove', r]); + pushNotification({ + message: t('notifications.ruleremoved'), + type: NotificationType.SUCCESS, + }); + }); + }; + + const _addBlocklist = (v: string) => { + if (!guildid) return; + fetch((c) => c.guilds.settings(guildid).addKarmaBlocklist(v)) + .then((res) => { + dispatchBlocklist(['add', res]); + setBlocklistInput(''); + pushNotification({ + message: t('notifications.blocklistadded'), + type: NotificationType.SUCCESS, + }); + }) + .catch(); + }; + + const _removeBlocklist = (v: Member) => { + if (!guildid) return; + fetch((c) => c.guilds.settings(guildid).removeKarmaBlocklist(v.user.id)) + .then((res) => { + dispatchBlocklist(['remove', v]); + pushNotification({ + message: t('notifications.blocklistremoved'), + type: NotificationType.SUCCESS, + }); + }) + .catch(); + }; + + useEffect(() => { + if (!guildid) return; + + fetch((c) => c.guilds.settings(guildid).karma()) + .then((res) => dispatchSettings(['set_state', res])) + .catch(); + + fetch((c) => c.guilds.settings(guildid).karmaRules()) + .then((res) => dispatchRules(['set', res.data])) + .catch(); + + fetch((c) => c.guilds.settings(guildid).karmaBlocklist()) + .then((res) => dispatchBlocklist(['set', res.data])) + .catch(); + }, [guildid]); + + const emojiOptions: TagElement[] = emojis.map((e) => ({ + id: e.codes, + value: e.char, + display: e.char, + keywords: e.name.split(' '), + })); + + return ( + +

{t('heading')}

+ {t('explaination')} + +

Settings

+ {(settings.state !== undefined && ( + dispatchSettings(['set_enabled', e])} + labelAfter={t('enable')} + /> + )) || } + +

{t('emotes.heading')}

+ + + + {(settings.state !== undefined && ( + settings.emotes_increase?.includes(e.value))} + options={emojiOptions} + onChange={(e) => dispatchSettings(['set_increase', e.map((e) => e.value)])} + /> + )) || } + + + + {(settings.state !== undefined && ( + settings.emotes_decrease?.includes(e.value))} + options={emojiOptions} + onChange={(e) => dispatchSettings(['set_decrease', e.map((e) => e.value)])} + /> + )) || } + + + +

{t('limit.heading')}

+ + {(settings.state !== undefined && ( + dispatchSettings(['set_tokens', parseInt(e.currentTarget.value)])} + /> + )) || } + + +

{t('penalty.heading')}

+ + {(settings.state !== undefined && ( + dispatchSettings(['set_penalty', e])} + labelAfter={t('penalty.switch')} + /> + )) || } + + + + + + +

{t('rules.heading')}

+ {(guild !== undefined && ( +
+ _addKarmaRule(r)} /> + {rules.map((r) => ( + _removeKarmaRule(r)} + /> + ))} +
+ )) || } + +

{t('blocklist.heading')}

+ + setBlocklistInput(e.currentTarget.value)} /> + + + + + + {t('blocklist.table.id')} + {t('blocklist.table.name')} + {t('blocklist.table.nick')} + {t('blocklist.table.unblock')} + + {blocklist.map((m) => ( + + {m.user.id} + + {m.user.username}#{m.user.discriminator} + + {m.nick || m.user.username} + + + + + ))} + + +
+ ); +}; + +export default KarmaRoute; diff --git a/web.new/src/routes/Dashboard/Guilds/GuildModlog.tsx b/web.new/src/routes/Dashboard/Guilds/GuildModlog.tsx index 5fc9ff93a..878d02803 100644 --- a/web.new/src/routes/Dashboard/Guilds/GuildModlog.tsx +++ b/web.new/src/routes/Dashboard/Guilds/GuildModlog.tsx @@ -10,6 +10,7 @@ import { UnbanRequestWrapper, } from '../../../components/Modals/ModalProcessUnbanRequest'; import { ReportsList } from '../../../components/Report'; +import { SplitContainer } from '../../../components/SplitContainer'; import { UnbanRequestTile } from '../../../components/UnbanRequestTile'; import { useApi } from '../../../hooks/useApi'; import { usePerms } from '../../../hooks/usePerms'; @@ -19,20 +20,6 @@ type Props = {}; const StyledReprtList = styled(ReportsList)``; -const Container = styled.div` - width: 100%; - display: flex; - gap: 1em; - - > section { - width: 100%; - } - - @media (orientation: portrait) { - flex-direction: column; - } -`; - const GuildModlogRoute: React.FC = () => { const { t } = useTranslation('routes.guildmodlog'); const fetch = useApi(); @@ -73,7 +60,7 @@ const GuildModlogRoute: React.FC = () => { }; return ( - + = () => {
)} - + ); }; diff --git a/web.new/src/util/reducer.ts b/web.new/src/util/reducer.ts new file mode 100644 index 000000000..ed4d1eee8 --- /dev/null +++ b/web.new/src/util/reducer.ts @@ -0,0 +1,17 @@ +type Entity = { id: string }; + +export const listReducer = ( + state: T[], + [type, payload]: ['set', T[]] | ['add', T | T[]] | ['remove', T], +) => { + switch (type) { + case 'set': + return payload; + case 'add': + return [...state, ...(Array.isArray(payload) ? payload : [payload])]; + case 'remove': + return state.filter((e) => e.id !== payload.id); + default: + return state; + } +}; diff --git a/web.new/yarn.lock b/web.new/yarn.lock index aceea62ec..0e6456e07 100644 --- a/web.new/yarn.lock +++ b/web.new/yarn.lock @@ -4109,6 +4109,11 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +emoji.json@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/emoji.json/-/emoji.json-13.1.0.tgz#116e46dc57c97af7ec2605ec88cfff31e9bcbade" + integrity sha512-ibJCYVe3Ilic4euofl0ozWqkmqXXsCuhuOuwhwAoG9qsMizNGI8aa7P4QIKlZ8NLgXx+pnVT2t6ZTvQczIv/ZA== + emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" From 34b9956b82991d8a689ca9e06bdc7bd391006124 Mon Sep 17 00:00:00 2001 From: zekro Date: Sun, 4 Sep 2022 21:09:49 +0000 Subject: [PATCH 05/25] add descriptions for karma settings --- .../locales/en-US/routes.guildsettings.karma.json | 10 ++++++++-- web.new/src/routes/Dashboard/GuildSettings/Karma.tsx | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/web.new/public/locales/en-US/routes.guildsettings.karma.json b/web.new/public/locales/en-US/routes.guildsettings.karma.json index 1fe5323d1..da688fe9c 100644 --- a/web.new/public/locales/en-US/routes.guildsettings.karma.json +++ b/web.new/public/locales/en-US/routes.guildsettings.karma.json @@ -1,6 +1,7 @@ { "heading": "Karma", "explanation": "Lorem ipsum", + "settings": "Settings", "enable": "Enable karma system", "save": "Save settings", @@ -14,25 +15,30 @@ "emotes": { "heading": "Emotes", + "description": "The emotes a user can react to a message with to increase or decrease the authors karma.", "increase": "Increase karma", "decrease": "Decrease karma" }, "limit": { - "heading": "Limit" + "heading": "Limit", + "description": "The amount of up and downvotes a user can perform per hour." }, "penalty": { "heading": "Penalty", + "description": "When enabled, every karma decrease is paid with 1 karma point of the executors own karma account.", "switch": "Enable karma penalty" }, "rules": { - "heading": "Rules" + "heading": "Rules", + "description": "Actions which will be performed when a user surpasses a specific karma level." }, "blocklist": { "heading": "Block list", + "description": "Blocked members are neither able to give or remove karma of other members nor able to gain karma points from other users.", "addmember": "Add member", "table": { "id": "ID", diff --git a/web.new/src/routes/Dashboard/GuildSettings/Karma.tsx b/web.new/src/routes/Dashboard/GuildSettings/Karma.tsx index c618c93f8..2604f2d74 100644 --- a/web.new/src/routes/Dashboard/GuildSettings/Karma.tsx +++ b/web.new/src/routes/Dashboard/GuildSettings/Karma.tsx @@ -205,7 +205,7 @@ const KarmaRoute: React.FC = ({}) => {

{t('heading')}

{t('explaination')} -

Settings

+

{t('settings')}

{(settings.state !== undefined && ( = ({}) => { )) || }

{t('emotes.heading')}

+ {t('emotes.description')} @@ -239,6 +240,7 @@ const KarmaRoute: React.FC = ({}) => {

{t('limit.heading')}

+ {t('limit.description')} {(settings.state !== undefined && ( = ({}) => {

{t('penalty.heading')}

+ {t('penalty.description')} {(settings.state !== undefined && ( = ({}) => {

{t('rules.heading')}

+ {t('rules.description')} {(guild !== undefined && (
_addKarmaRule(r)} /> @@ -283,6 +287,7 @@ const KarmaRoute: React.FC = ({}) => { )) || }

{t('blocklist.heading')}

+ {t('blocklist.description')} setBlocklistInput(e.currentTarget.value)} />