├── src ├── styles │ ├── Home.module.scss │ ├── theme.module.scss │ ├── Button.module.scss │ ├── Guild.module.scss │ ├── Auth.module.scss │ ├── globals.scss │ ├── Header.module.scss │ ├── Card.module.scss │ └── Sidebar.module.scss ├── utils │ ├── index.ts │ ├── Constants.ts │ ├── theme.ts │ └── Utils.ts ├── components │ ├── text │ │ ├── index.ts │ │ └── text.tsx │ ├── dashboard │ │ ├── index.ts │ │ ├── guilds │ │ │ ├── index.ts │ │ │ ├── reasons │ │ │ │ ├── styles.module.scss │ │ │ │ └── index.tsx │ │ │ └── permissions │ │ │ │ ├── styles.module.scss │ │ │ │ └── index.tsx │ │ ├── layout.tsx │ │ └── sidebar.tsx │ ├── form │ │ ├── index.ts │ │ ├── button.tsx │ │ ├── duration │ │ │ ├── styles.module.scss │ │ │ └── index.tsx │ │ ├── switch.tsx │ │ └── select │ │ │ ├── styles.module.scss │ │ │ └── index.tsx │ ├── index.ts │ ├── dots.tsx │ ├── card.tsx │ └── header.tsx ├── config │ └── config.exemple.ts ├── hooks │ └── useAPI.ts ├── services │ ├── ApiService.ts │ └── ClientService.ts ├── @types │ ├── globals.d.ts │ └── index.d.ts ├── pages │ ├── dashboard │ │ ├── guilds │ │ │ └── [guild] │ │ │ │ ├── index.tsx │ │ │ │ ├── reasons.tsx │ │ │ │ └── moderation.tsx │ │ └── @me │ │ │ └── index.tsx │ ├── login.tsx │ ├── _document.jsx │ ├── index.tsx │ ├── auth │ │ └── discord.tsx │ └── _app.tsx └── contexts │ ├── TestAPIContext.tsx │ └── APIContext.tsx ├── .gitpod.yml ├── next-env.d.ts ├── next.config.js ├── .gitignore ├── tsconfig.json └── package.json /src/styles/Home.module.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Utils'; -------------------------------------------------------------------------------- /src/components/text/index.ts: -------------------------------------------------------------------------------- 1 | export * from './text'; -------------------------------------------------------------------------------- /src/components/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './layout'; 2 | export * from './guilds'; -------------------------------------------------------------------------------- /src/components/dashboard/guilds/index.ts: -------------------------------------------------------------------------------- 1 | export * from './permissions'; 2 | export * from './reasons'; -------------------------------------------------------------------------------- /src/config/config.exemple.ts: -------------------------------------------------------------------------------- 1 | const ApiUrl = 'http://localhost:1500/graphql' 2 | 3 | export { ApiUrl }; -------------------------------------------------------------------------------- /src/styles/theme.module.scss: -------------------------------------------------------------------------------- 1 | @function themed($title, $tonalit: 100) { 2 | @return var(--#{$title}-#{$tonalit}); 3 | } -------------------------------------------------------------------------------- /src/components/form/index.ts: -------------------------------------------------------------------------------- 1 | export * from './select'; 2 | export * from './button'; 3 | export * from './switch'; 4 | export * from './duration'; -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dashboard'; 2 | export * from './text'; 3 | export * from './dots'; 4 | export * from './card'; 5 | export * from './form'; -------------------------------------------------------------------------------- /src/hooks/useAPI.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import APIContext from '../contexts/TestAPIContext'; 3 | 4 | export const useAPI = () => useContext(APIContext); -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: yarn install && yarn run build 3 | command: yarn run start 4 | vscode: 5 | extensions: 6 | - dbaeumer.vscode-eslint 7 | ports: 8 | - port: 3000 9 | onOpen: open-preview -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | reactStrictMode: false, 6 | swcMinify: true, 7 | sassOptions: { 8 | includePaths: [path.join(__dirname, 'styles')], 9 | }, 10 | }; 11 | 12 | module.exports = nextConfig; -------------------------------------------------------------------------------- /src/components/text/text.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, CSSProperties, FC } from 'react'; 2 | 3 | interface TextProps extends PropsWithChildren { 4 | css: CSSProperties; 5 | size?: 'xs' | 'sm' | 'md' | 'lg'; 6 | } 7 | 8 | export const Text: FC = ({ children, size = 'sm' }: TextProps) => ( 9 |

{children}

10 | ) -------------------------------------------------------------------------------- /src/components/dashboard/guilds/reasons/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/theme.module.scss'; 2 | 3 | .textInput { 4 | background-color: var(--luny-backgroundSecondary); 5 | color: themed('luny-text', 60); 6 | font-size: 16px; 7 | padding: 5px; 8 | min-width: 100%; 9 | width: 100%; 10 | max-width: 100%; 11 | height: 75px; 12 | border-radius: 7px; 13 | } -------------------------------------------------------------------------------- /src/services/ApiService.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache } from '@apollo/client' 2 | import { ApiUrl } from '../config/config'; 3 | 4 | export const api = new ApolloClient({ 5 | uri: ApiUrl, 6 | cache: new InMemoryCache(), 7 | }); 8 | 9 | export const createAPIClient = (token: string | null) => new ApolloClient({ 10 | uri: ApiUrl, 11 | cache: new InMemoryCache(), 12 | headers: token ? { 'Authorization': token } : undefined, 13 | }); -------------------------------------------------------------------------------- /src/@types/globals.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | namespace NodeJS { 5 | interface ProcessEnv { 6 | DISCORD_CLIENT_ID: string; 7 | OAUTH2_DISCORD_REDIRECT_URI: string; 8 | API_URL: string; 9 | NODE_ENV: 'development' | 'production'; 10 | } 11 | } 12 | 13 | interface Window { 14 | changeMode: (mode: "dark" | "light") => void; 15 | toggleFavorite: (guildID: string) => void; 16 | } 17 | } -------------------------------------------------------------------------------- /src/pages/dashboard/guilds/[guild]/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAPI } from '../../../../hooks/useAPI'; 3 | 4 | import { Switch, Card } from '../../../../components'; 5 | 6 | const DashboardGuild: React.FC = () => { 7 | const { user, guild } = useAPI(); 8 | 9 | return ( 10 |
11 | {guild?.name} 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | ) 20 | } 21 | 22 | export default DashboardGuild; -------------------------------------------------------------------------------- /src/services/ClientService.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; 2 | import { ApiUrl as uri } from '../config/config'; 3 | import { createAPIClient } from './ApiService'; 4 | 5 | export class Client { 6 | public api: ApolloClient; 7 | public token: string; 8 | 9 | constructor(token?: string) { 10 | this.api = createAPIClient(token); 11 | 12 | this.token = token; 13 | } 14 | 15 | setToken(token: string) { 16 | delete this.api; 17 | 18 | this.token = token; 19 | this.api = createAPIClient(token); 20 | } 21 | } -------------------------------------------------------------------------------- /src/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import { Utils } from '../utils' 3 | 4 | export default function Login() { 5 | return ( 6 |

Oh...

7 | ) 8 | } 9 | 10 | export const getServerSideProps: GetServerSideProps = async() => { 11 | return { 12 | redirect: { 13 | destination: Utils.generateOAuth2Discord({ 14 | clientId: process.env.DISCORD_CLIENT_ID, 15 | scopes: ['identify', 'guilds'], 16 | redirectUri: process.env.OAUTH2_DISCORD_REDIRECT_URI, 17 | }), 18 | permanent: false, 19 | }, 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | yarn.lock 8 | package-lock.json 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | .env 35 | firebase.config.json 36 | src/config/config.ts 37 | 38 | # vercel 39 | .vercel 40 | 41 | # swr 42 | .swc 43 | 44 | # Apollo Extension Config 45 | apollo.config.js 46 | schema.graphql -------------------------------------------------------------------------------- /src/styles/Button.module.scss: -------------------------------------------------------------------------------- 1 | @import "theme.module"; 2 | 3 | @import "node_modules/bootstrap/scss/functions"; 4 | @import "node_modules/bootstrap/scss/variables"; 5 | @import "node_modules/bootstrap/scss/mixins"; 6 | 7 | .button { 8 | border: none; 9 | border-radius: 7px; 10 | cursor: pointer; 11 | background-color: themed('luny-band'); 12 | color: white; 13 | padding: 10px; 14 | transition: all ease .3s; 15 | 16 | span { 17 | font-size: 17px; 18 | 19 | &:is(.icon) { 20 | margin-right: 5px; 21 | } 22 | } 23 | 24 | &:hover { 25 | background-color: themed('luny-band', 80); 26 | } 27 | 28 | &:disabled { 29 | background-color: themed('luny-band', 20); 30 | } 31 | } -------------------------------------------------------------------------------- /src/components/form/button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ButtonHTMLAttributes, PropsWithChildren, DetailedHTMLProps, HTMLAttributes } from 'react'; 2 | 3 | import styles from '../../styles/Button.module.scss'; 4 | 5 | type Props = PropsWithChildren, HTMLDivElement>>; 6 | 7 | function Button(props: ButtonHTMLAttributes) { 8 | return ( 9 | 67 | 68 | 69 | ) 70 | } -------------------------------------------------------------------------------- /src/pages/dashboard/@me/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { Card, Select, Button } from '../../../components'; 4 | import { useAPI } from '../../../hooks/useAPI'; 5 | 6 | export default function Index() { 7 | const context = useAPI(); 8 | 9 | return ( 10 |
11 | 12 | 13 |

14 | select 15 |

16 |
17 | 18 | 19 | b.position - a.position).map(role => ({ 21 | label: role.name, 22 | value: role.id, 23 | color: role.color ? `#${role.color.toString(16)}` : undefined 24 | })) || []} 25 | placeholder={'Select a role'} 26 | maxValues={1} 27 | backgroundColor={'var(--luny-backgroundSecondary)'} 28 | /> 29 | 30 |
31 | 32 | 33 |
34 |
35 | Ban Members 36 |
37 | 38 |
39 |
40 | Kick Members 41 |
42 | 43 |
44 |
45 | Mute Members 46 |
47 | 48 |
49 |
50 | Adv Members 51 |
52 | 53 |
54 |
55 | View History 56 |
57 | 58 |
59 |
60 | Manage History 61 |
62 | 63 |
64 |
65 |
66 |
67 | ) 68 | } -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | interface IThemeColors { 2 | band?: string; 3 | mode?: 'dark' | 'light'; 4 | } 5 | 6 | interface IBaseColors { 7 | background: string; 8 | backgroundSecondary: string; 9 | ui: string; 10 | flow: string; 11 | text: string; 12 | overlay: string; 13 | icon: string | null; 14 | gradient: string; 15 | } 16 | 17 | const blackColor = '#000000'; 18 | const whiteColor = '#ffffff'; 19 | 20 | const basesColors = { 21 | dark: { 22 | background: '#141520', 23 | backgroundSecondary: '#20212B', 24 | ui: whiteColor, 25 | flow: blackColor, 26 | text: whiteColor, 27 | overlay: blackColor, 28 | icon: whiteColor, 29 | gradient: 'rgba(160, 32, 240, 0.03)', 30 | }, 31 | light: { 32 | background: '#CCE', 33 | backgroundSecondary: '#DDF', 34 | ui: blackColor, 35 | flow: whiteColor, 36 | text: blackColor, 37 | overlay: whiteColor, 38 | icon: null, 39 | gradient: 'rgba(160, 32, 240, 0.13)', 40 | }, 41 | }; 42 | 43 | const tonalits = Object.entries({ 44 | "5": '0d', 45 | "10": '1a', 46 | "15": '26', 47 | "20": '33', 48 | "40": '66', 49 | "60": '99', 50 | "80": 'cc', 51 | "100": '' 52 | }); 53 | 54 | function ThemeCSSVariables({ band = "#A020F0", mode = 'dark' }: IThemeColors = {}) { 55 | const baseColors: IBaseColors = basesColors[mode] || basesColors['dark']; 56 | 57 | const obj = { 58 | "--luny-background": baseColors.background, 59 | "--luny-backgroundSecondary": baseColors.backgroundSecondary, 60 | "--luny-band": band, 61 | "--luny-text": baseColors.text, 62 | "--luny-ui": baseColors.ui, 63 | "--luny-flow": baseColors.flow, 64 | ...mapTonalits("--luny-band", band), 65 | ...mapTonalits("--luny-ui", baseColors.ui), 66 | ...mapTonalits("--luny-flow", baseColors.flow), 67 | ...mapTonalits("--luny-text", baseColors.text), 68 | "--luny-overlay": baseColors.overlay + "E6", 69 | "--luny-icon": baseColors.icon || band, 70 | "--luny-green": "#61fe80", 71 | "--luny-red": "#fe4854", 72 | "--luny-blue": "#0d97fb", 73 | "--luny-gradient": `${baseColors.gradient}`, 74 | }; 75 | 76 | return { 77 | ...obj, 78 | toString: () => { 79 | return Object.entries({ ...obj }).map(([key, value]) => `${key}:${value};`).join("") 80 | } 81 | }; 82 | }; 83 | 84 | function mapTonalits(key: string, color: string) { 85 | return Object.fromEntries( 86 | tonalits.map(([tonalit, value]) => [`${key}-${tonalit}`, `${color}${value}`]) 87 | ); 88 | }; 89 | 90 | export default ThemeCSSVariables; 91 | module.exports.mapTonalits = mapTonalits; -------------------------------------------------------------------------------- /src/components/form/duration/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useId } from 'react'; 2 | 3 | import styles from './styles.module.scss'; 4 | import { Utils } from '../../../utils/Utils'; 5 | 6 | interface Props { 7 | stages: { 8 | name: string; 9 | label?: string; 10 | min: number; 11 | max: number; 12 | ms: number; 13 | default?: number; 14 | }[]; 15 | disable?: boolean; 16 | max?: number; 17 | } 18 | 19 | export class DurationInput extends React.Component, disabled: boolean }> { 20 | readonly type = 'duration'; 21 | 22 | _id = Utils.uuid(); 23 | 24 | constructor(props) { 25 | super(props); 26 | 27 | this.state = { 28 | values: Object.fromEntries( 29 | props.stages.map(stage => ([stage.name, stage.default || 0])) 30 | ), 31 | disabled: props.disabled, 32 | } 33 | } 34 | 35 | calcDuration(values: Record): number { 36 | return Object.entries(values).map(([name, value]) => (this.props.stages.find(stage => stage.name === name)?.ms || 0) * value).reduce((a, b) => a + b, 0); 37 | } 38 | 39 | #calc(name: string, value: number | string) { 40 | const newValue = { ...this.state.values, [name]: Number((event.target as HTMLInputElement).value + `${value}`) }; 41 | 42 | const v = this.calcDuration(newValue); 43 | 44 | console.log(v, newValue); 45 | 46 | return v; 47 | } 48 | 49 | get value(): number { 50 | return this.calcDuration(this.state.values); 51 | } 52 | 53 | setDisable(disabled: boolean) { 54 | this.setState({ disabled }); 55 | } 56 | 57 | render() { 58 | const { props, _id, state: { values, disabled } } = this; 59 | return ( 60 |
61 | {props.stages.map((stage, i) => ( 62 |
63 | this.setState({ 75 | values: { ...values, [stage.name]: Number(event.target.value) ?? 0 }, 76 | })} 77 | 78 | onKeyPress={event => (([',', '.', '-', 'e']).includes(event.key) || (props.max && this.#calc(stage.name, event.key) > props.max)) && event.preventDefault()} 79 | /> 80 | 81 | {stage.label || stage.name} 82 | 83 |
84 | ))} 85 |
86 | ) 87 | } 88 | } -------------------------------------------------------------------------------- /src/styles/Header.module.scss: -------------------------------------------------------------------------------- 1 | @import "theme.module"; 2 | 3 | @import "node_modules/bootstrap/scss/functions"; 4 | @import "node_modules/bootstrap/scss/variables"; 5 | @import "node_modules/bootstrap/scss/mixins"; 6 | 7 | .header { 8 | width: 100%; 9 | height: 64px; 10 | display: flex; 11 | align-items: center; 12 | background-color: var(--luny-background); 13 | font-size: 1rem; 14 | font-weight: 500; 15 | border: 1px solid themed('luny-ui', 15); 16 | border-radius: 0.75rem; 17 | color: themed('luny-text', 60); 18 | padding: 0 1rem; 19 | 20 | ul { 21 | list-style-type: none; 22 | padding: 0; 23 | 24 | li { 25 | display: inline; 26 | position: relative; 27 | cursor: pointer; 28 | padding: 0 10px; 29 | 30 | i { 31 | font-size: 1.25rem; 32 | } 33 | 34 | span.text { 35 | padding-left: 10px; 36 | } 37 | 38 | a { 39 | text-decoration: none; 40 | display: inline-block; 41 | transition: background .3s; 42 | padding: 0 10px; 43 | color: themed('luny-text', 60); 44 | } 45 | 46 | 47 | ul { 48 | display: none; 49 | left: 0; 50 | position: absolute; 51 | background-color: var(--luny-background); 52 | margin-top: 10px; 53 | border: 1px solid themed('luny-ui', 15); 54 | border-radius: 0.75rem; 55 | width: 200px; 56 | 57 | li { 58 | a { 59 | display: block; 60 | border-radius: 0.3rem; 61 | padding: 10px; 62 | 63 | &:hover { 64 | background-color: themed('luny-ui', 15); 65 | } 66 | } 67 | } 68 | } 69 | 70 | &.open { 71 | ul { 72 | display: grid; 73 | grid-gap: 10px; 74 | padding: 10px 0; 75 | animation: subMenuHover 0.25s; 76 | z-index: 1000; 77 | } 78 | } 79 | 80 | &:hover { 81 | ul { 82 | a { 83 | color: themed('luny-text', 60); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | .header .sidebar { 92 | display: none; 93 | } 94 | 95 | @keyframes subMenuHover { 96 | 0% { 97 | transform: translateY(-10px); 98 | } 99 | 100% { 100 | transform: translateY(0); 101 | } 102 | } 103 | 104 | @media screen and (max-width: 1024px) { 105 | .header { 106 | .sidebar { 107 | display: inline; 108 | } 109 | } 110 | } 111 | 112 | @media screen and (max-width: 768px) { 113 | .header { 114 | ul { 115 | li { 116 | a { 117 | span.text { 118 | display: none; 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/styles/Card.module.scss: -------------------------------------------------------------------------------- 1 | @import "theme.module"; 2 | 3 | @import "node_modules/bootstrap/scss/functions"; 4 | @import "node_modules/bootstrap/scss/variables"; 5 | @import "node_modules/bootstrap/scss/mixins"; 6 | 7 | .card { 8 | position: relative; 9 | margin-top: 25px; 10 | border-radius: 6px; 11 | background-color: themed('luny-ui', 5); 12 | border: 1px solid themed('luny-ui', 15); 13 | min-width: 96%; 14 | width: 96%; 15 | max-width: 96%; 16 | margin-left: auto; 17 | margin-right: auto; 18 | padding: 12px; 19 | color: themed('luny-text', 60); 20 | -webkit-touch-callout: none; 21 | -webkit-user-select: none; 22 | -khtml-user-select: none; 23 | -moz-user-select: none; 24 | -ms-user-select: none; 25 | user-select: none; 26 | 27 | hr { 28 | margin-top: 10px; 29 | margin-bottom: 10px; 30 | border: 1px solid themed('luny-ui', 10); 31 | } 32 | 33 | header { 34 | display: flex; 35 | justify-content: space-between; 36 | align-items: center; 37 | height: 50px; 38 | width: 100%; 39 | // margin-top: 10px; 40 | 41 | h2 { 42 | margin-bottom: 15px; 43 | font-weight: 700; 44 | white-space: nowrap; 45 | max-width: 100%; 46 | text-overflow: ellipsis; 47 | overflow: hidden; 48 | } 49 | 50 | i { 51 | font-size: 16px; 52 | } 53 | } 54 | 55 | code { 56 | font-size: 16px; 57 | font-weight: 500; 58 | color: var(--luny-text-100); 59 | margin-bottom: 10px; 60 | background-color: var(--luny-background); 61 | border: 1px solid var(--luny-ui-20); 62 | border-radius: 6px; 63 | padding: 0px 3px; 64 | } 65 | 66 | span { 67 | font-size: 14px; 68 | font-weight: 500; 69 | // margin-bottom: 10px; 70 | } 71 | 72 | main { 73 | justify-content: space-between; 74 | align-items: center; 75 | width: 100%; 76 | position: relative; 77 | margin-top: -1px; 78 | padding: 7px; 79 | display: block; 80 | } 81 | 82 | &[data-size='small'] { 83 | min-width: 46%; 84 | width: 46%; 85 | max-width: 46%; 86 | } 87 | 88 | &[data-retractable] { 89 | padding-bottom: 0; 90 | 91 | header { 92 | position: relative; 93 | cursor: pointer; 94 | top: 1px; 95 | padding-bottom: 7px; 96 | } 97 | 98 | main { 99 | display: none; 100 | } 101 | 102 | &[data-opened] { 103 | header { 104 | border-bottom: 1px solid themed('luny-ui', 10); 105 | } 106 | 107 | main { 108 | animation: drop 0.25s; 109 | display: block; 110 | } 111 | } 112 | } 113 | } 114 | 115 | @media screen and (max-width: 768px) { 116 | .card { 117 | &[data-size='small'] { 118 | min-width: 100%; 119 | width: 100%; 120 | max-width: 100%; 121 | } 122 | } 123 | } 124 | 125 | @keyframes drop { 126 | 0% { 127 | transform: translateY(-10px); 128 | } 129 | 100% { 130 | transform: translateY(0); 131 | } 132 | } -------------------------------------------------------------------------------- /src/components/dashboard/guilds/reasons/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { InputHTMLAttributes, useEffect } from 'react'; 2 | 3 | import { Card } from '../../../card'; 4 | 5 | import styles from './styles.module.scss'; 6 | import { DurationInput } from '../../../form/duration'; 7 | import { Utils } from '../../../../utils/Utils'; 8 | 9 | type Props = InputHTMLAttributes; 10 | 11 | const maxDurationLength = 28 * 1000 * 60 * 60 * 24; 12 | 13 | export const GuildPredefinedReason: React.FC<{}> = () => { 14 | const _id = Utils.uuid(); 15 | 16 | useEffect(() => { 17 | const card = document.querySelector(`div[id="${_id}"]`); 18 | const span = document.querySelector(`span[id="${_id}"]`) as HTMLDivElement; 19 | const textarea = document.querySelector(`textarea[id="${_id}"]`) as HTMLTextAreaElement; 20 | 21 | if(card) { 22 | const observer = new MutationObserver(mutations => { 23 | if(!mutations.some(mutation => mutation.attributeName == 'data-opened')) return; 24 | 25 | const isOpened = card.getAttribute('data-opened'); 26 | 27 | if(!isOpened) span.innerHTML = textarea.value; 28 | 29 | // @ts-ignore 30 | span.style = `display: ${isOpened ? 'none' : 'block'}` 31 | }); 32 | 33 | observer.observe(card, { attributes: true }); 34 | } 35 | }, [_id]); 36 | 37 | return ( 38 | 39 | 40 |

41 | #1 Rule 42 |
43 | Lorem ipsum dolor sit amet consectetur, adipiscing elit praesent vehicula duis integer, bibendum nisi per molestie. Donec vitae parturient pretium pulvinar fermentum ultricies nec elementum eu massa vestibulum, tempus viverra porttitor vulputate taciti torquent gravida vel hac nisi, dictumst vivamus tortor litora maecenas consequat sociis mattis nisl pellentesque. Nec nostra cubilia habitant ut interdum nam feugiat litora potenti vel accumsan ad, vitae euismod dapibus molestie eros non id venenatis integer. 44 |

45 |
46 | 47 | 48 | Reason Text: 49 | 50 |
51 | 52 | Mute Duration: 78 |
79 | * Duração maxima de 28 dias 80 |
81 |
82 |
83 | ) 84 | } -------------------------------------------------------------------------------- /src/utils/Utils.ts: -------------------------------------------------------------------------------- 1 | import { AbstractGuild, User } from '../@types'; 2 | 3 | type TScope = 4 | 'applications.builds.read' 5 | | 'applications.commands' 6 | | 'applications.entitlements' 7 | | 'applications.store.update' 8 | | 'bot' 9 | | 'connections' 10 | | 'email' 11 | | 'identify' 12 | | 'guilds' 13 | | 'guilds.join' 14 | | 'gdm.join' 15 | | 'webhook.incoming' 16 | 17 | interface DiscordOAuth2 { 18 | clientId: string; 19 | scopes: TScope[]; 20 | permissions?: bigint | number; 21 | guildId?: string; 22 | redirectUri?: string; 23 | state?: string; 24 | responseType?: string; 25 | prompt?: string; 26 | disableGuildSelect?: boolean; 27 | } 28 | 29 | export class Utils { 30 | static uuid(): string { 31 | let d = Date.now(); 32 | let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0; 33 | 34 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 35 | let r = Math.random() * 16; 36 | if(d > 0) { 37 | r = (d + r) % 16 | 0; 38 | d = Math.floor(d / 16); 39 | } else { 40 | r = (d2 + r) % 16 | 0; 41 | d2 = Math.floor(d2 / 16); 42 | } 43 | return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); 44 | }); 45 | } 46 | 47 | static stringAcronym(string: string): string { 48 | return string 49 | .replace(/'s /g, ' ') 50 | .replace(/\w+/g, w => w[0]) 51 | .replace(/\s/g, ''); 52 | } 53 | 54 | static getUserAvatar(user: User, options: { size: 128 | 256 | 512 | 1024 | 2048, dynamic?: boolean } = { size: 1024 }): string { 55 | if (user?.avatar) { 56 | return `https://cdn.discordapp.com/avatars/${user?.id}/${user?.avatar}.webp?size=${options.size}`; 57 | } else { 58 | return `https://cdn.discordapp.com/embed/avatars/${Number(user?.discriminator || '0000') % 5}.png`; 59 | } 60 | } 61 | 62 | static getGuildIcon(guild: AbstractGuild, options: { size: 128 | 256 | 512 | 1024 | 2048, dynamic?: boolean } = { size: 1024 }): string { 63 | if (guild.icon) { 64 | return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.${options.dynamic && guild.icon.startsWith('a_') && guild.features.includes('ANIMATED_ICON') ? 'gif' : 'png'}?size=${options.size}`; 65 | } else { 66 | return undefined; 67 | } 68 | } 69 | 70 | static generateOAuth2Discord({ 71 | clientId = process.env.DISCORD_CLIENT_ID, 72 | scopes, 73 | permissions = BigInt(0), 74 | guildId = null, 75 | redirectUri = '/', 76 | responseType = 'code', 77 | state = null, 78 | disableGuildSelect = false, 79 | prompt = null 80 | }: DiscordOAuth2) { 81 | const query = new URLSearchParams({ 82 | client_id: clientId, 83 | scope: scopes.join(' '), 84 | }); 85 | 86 | if (permissions) { 87 | query.set('permissions', Number(permissions).toString()); 88 | }; 89 | 90 | if (guildId) { 91 | query.set('guild_id', guildId); 92 | if(disableGuildSelect) { 93 | query.set('disable_guild_select', 'true'); 94 | } 95 | }; 96 | 97 | if(redirectUri) { 98 | query.set('redirect_uri', redirectUri); 99 | query.set('response_type', responseType); 100 | if(state) { 101 | query.set('state', state); 102 | }; 103 | if(prompt) { 104 | query.set('prompt', prompt); 105 | }; 106 | }; 107 | 108 | return `https://discord.com/api/oauth2/authorize?${query.toString()}`; 109 | } 110 | } -------------------------------------------------------------------------------- /src/components/form/select/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../../styles/theme.module"; 2 | 3 | @import "node_modules/bootstrap/scss/functions"; 4 | @import "node_modules/bootstrap/scss/variables"; 5 | @import "node_modules/bootstrap/scss/mixins"; 6 | 7 | .select { 8 | position: relative; 9 | user-select: none; 10 | width: 100%; 11 | 12 | .container { 13 | position: relative; 14 | display: flex; 15 | flex-direction: column; 16 | 17 | .placeholderWrapper { 18 | .placeholder { 19 | div { 20 | position: relative; 21 | display: flex; 22 | align-items: center; 23 | padding: 4px 10px; 24 | font-size: 14px; 25 | font-weight: 500; 26 | color: themed('luny-text', 100); 27 | height: auto; 28 | min-height: 40px; 29 | line-height: 50%; 30 | background: var(--luny-background); 31 | cursor: pointer; 32 | border-radius: 10px; 33 | flex-wrap: wrap; 34 | 35 | &:after { 36 | content: ""; 37 | position: absolute; 38 | top: 50%; 39 | right: 10px; 40 | transform: translateY(-50%); 41 | width: 0; 42 | height: 0; 43 | border-left: 7px solid transparent; 44 | border-right: 7px solid transparent; 45 | border-top: 7px solid themed('luny-band', 100); 46 | animation: rotate 0.2s ease; 47 | } 48 | } 49 | 50 | .option { 51 | background-color: var(--luny-background); 52 | color: themed('luny-text', 100); 53 | padding: 0 4px; 54 | border-radius: 6px; 55 | border: 1px solid themed('luny-ui', 15); 56 | font-size: 12px; 57 | font-weight: 500; 58 | cursor: pointer; 59 | transition: all 0.2s; 60 | line-height: 25px; 61 | height: 25px; 62 | margin-bottom: 5px; 63 | margin-right: 5px; 64 | margin-top: 5px; 65 | } 66 | } 67 | } 68 | 69 | .options { 70 | display: none; 71 | position: absolute; 72 | top: 100%; 73 | left: 10px; 74 | margin-top: 10px; 75 | width: calc(100% - 20px); 76 | flex-direction: column; 77 | background-color: var(--luny-background); 78 | border: 1px solid themed('luny-ui', 15); 79 | border-radius: 6px; 80 | z-index: 1; 81 | overflow-y: auto; 82 | overflow-x: hidden; 83 | max-height: 300px; 84 | padding: 4px; 85 | 86 | .option { 87 | color: themed('luny-text', 100); 88 | padding: 0 5px; 89 | border-radius: 6px; 90 | font-size: 15px; 91 | font-weight: 500; 92 | cursor: pointer; 93 | line-height: 30px; 94 | height: 30px; 95 | 96 | img { 97 | height: 30px; 98 | width: 30px; 99 | border-radius: 50%; 100 | margin-right: 10px; 101 | } 102 | 103 | span { 104 | top: 5px; 105 | color: themed('luny-text', 60); 106 | font-size: 12px; 107 | height: 30px; 108 | } 109 | 110 | &[data-selected] { 111 | background-color: themed('luny-ui', 10); 112 | } 113 | 114 | &:hover { 115 | background-color: themed('luny-ui', 15); 116 | } 117 | } 118 | 119 | &::-webkit-scrollbar-track { 120 | background: transparent; 121 | height: calc(100% - 10px); 122 | margin-top: 5px; 123 | margin-bottom: 5px; 124 | } 125 | } 126 | } 127 | 128 | &[data-opened] { 129 | .container { 130 | .placeholderWrapper { 131 | border-color: themed('luny-band', 100); 132 | 133 | .placeholder { 134 | div:after { 135 | transform: translateY(-50%) rotate(180deg); 136 | transition: transform 0.2s ease-in-out; 137 | } 138 | } 139 | } 140 | 141 | .options { 142 | display: block; 143 | } 144 | } 145 | } 146 | 147 | &[data-full-values] { 148 | .container { 149 | .options { 150 | pointer-events: none; 151 | opacity: 0.5; 152 | cursor: not-allowed; 153 | } 154 | } 155 | } 156 | } 157 | 158 | @keyframes rotate { 159 | from { 160 | transform: translateY(50%) rotate(0deg); 161 | } 162 | 163 | to { 164 | transform: translateY(-50%) rotate(180deg); 165 | } 166 | } -------------------------------------------------------------------------------- /src/pages/dashboard/guilds/[guild]/moderation.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { useAPI } from '../../../../hooks/useAPI'; 5 | 6 | import { Switch, Card, Select, GuildPermissions, Button } from '../../../../components'; 7 | 8 | import { PermissionFlagsBits } from 'discord-api-types/v10'; 9 | 10 | import styles from '../../../../styles/Guild.module.scss'; 11 | 12 | const flagsEntries = Object.entries(PermissionFlagsBits); 13 | 14 | const ToggleBox: React.FC = (props) => { 15 | const [value, setValue] = useState(!!props?.defaultValue); 16 | 17 | return ( 18 | 24 | ) 25 | } 26 | 27 | const DashboardGuildModeration: React.FC = () => { 28 | const { user, guild } = useAPI(); 29 | 30 | return ( 31 |
32 | 33 | 34 |

Canal de Punicões

35 |
36 | 37 | a.position - b.position).map(channel => ({ 56 | label: channel.name, 57 | value: channel.id, 58 | })) || []} 59 | placeholder={'Select a channel'} 60 | /> 61 | 62 |
63 | 64 | 65 | 66 |

67 | 68 | 69 | ban user {/* */} 70 | user 71 | reason 72 | notify-dm 73 | days 74 | 75 |
76 | Ban a user from the guild. 77 |

78 |
79 | 80 |