├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── __mocks__
│ └── guilds.ts
├── assets
│ ├── default_avatar.png
│ ├── nestjs.png
│ ├── reactiflux.png
│ └── typescript.png
├── components
│ ├── AppBar.tsx
│ ├── GuildMenuItem.tsx
│ └── Spinner.tsx
├── index.css
├── index.tsx
├── pages
│ ├── CategoryPage.tsx
│ ├── GuildAnalyticsPage.tsx
│ ├── GuildBansPage.tsx
│ ├── GuildPrefixPage.tsx
│ ├── LoginPage.tsx
│ ├── MenuPage.tsx
│ └── WelcomeMessagePage.tsx
├── react-app-env.d.ts
├── reportWebVitals.ts
├── setupTests.ts
└── utils
│ ├── api.ts
│ ├── contexts
│ └── GuildContext.tsx
│ ├── helpers.ts
│ ├── hooks
│ ├── useFetchGuildBans.tsx
│ ├── useFetchGuildConfig.tsx
│ ├── useFetchGuilds.tsx
│ ├── useFetchUser.tsx
│ └── useWelcomePage.tsx
│ ├── styles
│ └── index.tsx
│ └── types.ts
├── tsconfig.json
└── yarn.lock
/.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 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Discord Bot Dashboard (2022)
2 |
3 | This is a Discord Bot Dashboard written in React. You might also want to take a look at the API and Discord Bot, the repositories can be found here:
4 |
5 | - [Dashboard NestJS API](https://github.com/stuyy/discord-dashboard-api)
6 | - [Discord Bot](https://github.com/stuyy/discord-dashboard-bot)
7 |
8 | Please do note all code provided in these projects are NOT for production use. They are only for eductional purposes only. These repositories are only a supplement to the new Discord Bot Dashboard Tutorial Series on my YouTube channel, which you can find [here](https://youtube.com/ansonthedeveloper). You can find the playlist with all episodes [here](https://www.youtube.com/playlist?list=PL_cUvD4qzbkyX4Wp8TAfjpttjUldDWJnp).
9 |
10 | For any further questions, you may raise an issue or contact me on Discord here: https://discord.gg/3S68xJnqZt
11 |
12 | # Instructions
13 |
14 | To setup this project, all you need to do is clone the repository.
15 |
16 | 1. Clone using SSH or HTTPS
17 |
18 | `git clone git@github.com:stuyy/discord-dashboard-react.git`
19 |
20 | The package manager I used for this project, and use generally, is `yarn`. You may use `npm` if you wish however if you run into any issues with installing dependencies or building, consider switching to `yarn`.
21 |
22 | 2. `cd` into the cloned project, and then install dependencies using `npm install` or `yarn install`.
23 |
24 | 3. Run `npm start` or `yarn start`, this should start the project on `http://localhost:3000`.
25 |
26 | In order to have the app fully working, you must have the NestJS API setup locally as well. Please visit the [Dashboard NestJS API](https://github.com/stuyy/discord-dashboard-api) project repository and go through the README for instructions on how to setup the Nest API for the dashboard.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "discord-dashboard-react",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.14.1",
7 | "@testing-library/react": "^12.0.0",
8 | "@testing-library/user-event": "^13.2.1",
9 | "@types/jest": "^27.0.1",
10 | "@types/node": "^16.7.13",
11 | "@types/react": "^17.0.20",
12 | "@types/react-dom": "^17.0.9",
13 | "axios": "^0.24.0",
14 | "chart.js": "^3.7.0",
15 | "react": "^17.0.2",
16 | "react-chartjs-2": "^4.0.0",
17 | "react-dom": "^17.0.2",
18 | "react-icons": "^4.3.1",
19 | "react-router-dom": "^6.2.1",
20 | "react-scripts": "5.0.0",
21 | "react-spinners": "^0.11.0",
22 | "styled-components": "^5.3.3",
23 | "typescript": "^4.4.2",
24 | "web-vitals": "^2.1.0"
25 | },
26 | "scripts": {
27 | "start:dev": "react-scripts start",
28 | "build": "react-scripts build",
29 | "test": "react-scripts test",
30 | "eject": "react-scripts eject"
31 | },
32 | "eslintConfig": {
33 | "extends": [
34 | "react-app",
35 | "react-app/jest"
36 | ]
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | },
50 | "devDependencies": {
51 | "@types/axios": "^0.14.0",
52 | "@types/styled-components": "^5.1.19"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuyy/discord-dashboard-react/17f081e9589dd9e2be5bb4abf9cf67038729866e/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
28 |
29 |
33 | React App
34 |
35 |
36 |
37 |
38 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuyy/discord-dashboard-react/17f081e9589dd9e2be5bb4abf9cf67038729866e/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuyy/discord-dashboard-react/17f081e9589dd9e2be5bb4abf9cf67038729866e/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Routes, Route } from 'react-router-dom';
3 | import { BarLoader } from 'react-spinners';
4 | import { AppBar } from './components/AppBar';
5 | import { Spinner } from './components/Spinner';
6 | import { CategoryPage } from './pages/CategoryPage';
7 | import { GuildAnalyticsPage } from './pages/GuildAnalyticsPage';
8 | import { GuildBansPage } from './pages/GuildBansPage';
9 | import { GuildPrefixPage } from './pages/GuildPrefixPage';
10 | import { LoginPage } from './pages/LoginPage';
11 | import { MenuPage } from './pages/MenuPage';
12 | import { WelcomeMessagePage } from './pages/WelcomeMessagePage';
13 | import { GuildContext } from './utils/contexts/GuildContext';
14 | import { useFetchUser } from './utils/hooks/useFetchUser';
15 | import { PartialGuild } from './utils/types';
16 |
17 | function App() {
18 | const [guild, setGuild] = useState();
19 | const { user, loading, error } = useFetchUser();
20 |
21 | const updateGuild = (guild: PartialGuild) => setGuild(guild);
22 |
23 | if (loading) return } />;
24 |
25 | return (
26 |
27 | {user && !error ? (
28 | <>
29 |
30 | } />
31 |
32 |
33 | } />
34 | } />
35 | {/* } /> */}
36 | } />
37 | } />
38 | } />
39 | }
42 | />
43 | } />
44 |
45 | >
46 | ) : (
47 |
48 | } />
49 | Not Found} />
50 |
51 | )}
52 |
53 | );
54 | }
55 |
56 | export default App;
57 |
--------------------------------------------------------------------------------
/src/__mocks__/guilds.ts:
--------------------------------------------------------------------------------
1 | import NestJSIcon from '../assets/nestjs.png';
2 | import ReactIcon from '../assets/reactiflux.png';
3 | import TypescriptIcon from '../assets/typescript.png';
4 |
5 | export const mockGuilds = [
6 | {
7 | id: '123',
8 | name: 'NestJS',
9 | icon: NestJSIcon,
10 | },
11 | {
12 | id: '124',
13 | name: 'React',
14 | icon: ReactIcon,
15 | },
16 | {
17 | id: '125',
18 | name: 'Typescript',
19 | icon: TypescriptIcon,
20 | },
21 | ];
22 |
--------------------------------------------------------------------------------
/src/assets/default_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuyy/discord-dashboard-react/17f081e9589dd9e2be5bb4abf9cf67038729866e/src/assets/default_avatar.png
--------------------------------------------------------------------------------
/src/assets/nestjs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuyy/discord-dashboard-react/17f081e9589dd9e2be5bb4abf9cf67038729866e/src/assets/nestjs.png
--------------------------------------------------------------------------------
/src/assets/reactiflux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuyy/discord-dashboard-react/17f081e9589dd9e2be5bb4abf9cf67038729866e/src/assets/reactiflux.png
--------------------------------------------------------------------------------
/src/assets/typescript.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuyy/discord-dashboard-react/17f081e9589dd9e2be5bb4abf9cf67038729866e/src/assets/typescript.png
--------------------------------------------------------------------------------
/src/components/AppBar.tsx:
--------------------------------------------------------------------------------
1 | import { AppBarStyle } from '../utils/styles';
2 | import NestJSIcon from '../assets/nestjs.png';
3 | import { useContext } from 'react';
4 | import { GuildContext } from '../utils/contexts/GuildContext';
5 | import { Navigate } from 'react-router-dom';
6 | import { getIconURL } from '../utils/helpers';
7 |
8 | export const AppBar = () => {
9 | const { guild } = useContext(GuildContext);
10 | console.log(guild);
11 |
12 | return guild ? (
13 |
14 |
15 | Configuring {guild.name}
16 |
17 |
26 |
27 | ) : (
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/GuildMenuItem.tsx:
--------------------------------------------------------------------------------
1 | import { getIconURL } from '../utils/helpers';
2 | import { GuildMenuItemStyle } from '../utils/styles';
3 | import { PartialGuild } from '../utils/types';
4 |
5 | type Props = {
6 | guild: PartialGuild;
7 | };
8 |
9 | export const GuildMenuItem = ({ guild }: Props) => (
10 |
11 |
18 | {guild.name}
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/src/components/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { Overlay } from '../utils/styles';
3 |
4 | export const Spinner: FC = ({ children }) => {children};
5 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | background-color: #292929;
4 | color: #fff;
5 | font-family: 'DM Sans';
6 | height: 100%;
7 | }
8 |
9 | html,
10 | #root {
11 | height: 100%;
12 | }
13 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 | import { BrowserRouter as Router } from 'react-router-dom';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
17 | // If you want to start measuring performance in your app, pass a function
18 | // to log results (for example: reportWebVitals(console.log))
19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
20 | reportWebVitals();
21 |
--------------------------------------------------------------------------------
/src/pages/CategoryPage.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { GuildContext } from '../utils/contexts/GuildContext';
3 | import {
4 | Container,
5 | Flex,
6 | Grid,
7 | Page,
8 | TextButton,
9 | Title,
10 | } from '../utils/styles';
11 | import {
12 | IoSettingsOutline,
13 | IoNewspaperOutline,
14 | IoInformationCircleOutline,
15 | } from 'react-icons/io5';
16 | import { useNavigate } from 'react-router-dom';
17 |
18 | export const CategoryPage = () => {
19 | const { guild } = useContext(GuildContext);
20 | const navigate = useNavigate();
21 |
22 | return (
23 |
24 |
25 |
26 |
27 | Guild Information
28 |
29 |
30 |
31 | navigate('/dashboard/analytics')}>
32 | Analytics
33 |
34 | navigate('/dashboard/bans')}>
35 | Guild Bans
36 |
37 |
38 |
39 |
40 |
41 | Basic Configurations
42 |
43 |
44 |
45 | navigate('/dashboard/prefix')}>
46 | Command Prefix
47 |
48 | navigate('/dashboard/message')}>
49 | Welcome Message
50 |
51 |
52 |
53 |
54 |
55 | Channel Logs
56 |
57 |
58 |
59 | Moderation Logs
60 | Bot Logs
61 |
62 |
63 |
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/pages/GuildAnalyticsPage.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from 'react';
2 | import { getGuildBanLogs, getGuildModLogs } from '../utils/api';
3 | import { GuildContext } from '../utils/contexts/GuildContext';
4 | import { GuildModLogType } from '../utils/types';
5 | import { Line } from 'react-chartjs-2';
6 | import {
7 | Chart as ChartJS,
8 | CategoryScale,
9 | LinearScale,
10 | PointElement,
11 | LineElement,
12 | } from 'chart.js';
13 | import { Flex, Title } from '../utils/styles';
14 |
15 | ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement);
16 |
17 | export const GuildAnalyticsPage = () => {
18 | const { guild } = useContext(GuildContext);
19 | const guildId = (guild && guild.id) || '';
20 |
21 | const [modLogs, setModLogs] = useState([]);
22 |
23 | const getLabels = () => {
24 | const currentDate = new Date();
25 | const last = currentDate.getDate();
26 | const start = last - 6;
27 | const labels = [];
28 | for (let i = start; i <= last; i++) {
29 | currentDate.setDate(i);
30 | labels.push(`${currentDate.getMonth() + 1}/${currentDate.getDate()}`);
31 | }
32 | return labels;
33 | };
34 |
35 | const prepareData = (data: GuildModLogType[]) => {
36 | const currentDate = new Date();
37 | const last = currentDate.getDate();
38 | const start = last - 6;
39 | const dataRecords = [];
40 | for (let i = start; i <= last; i++) {
41 | const records = data.filter(
42 | (banLog) => new Date(banLog.issuedOn).getDate() === i
43 | );
44 | dataRecords.push(records.length);
45 | }
46 | return dataRecords;
47 | };
48 |
49 | useEffect(() => {
50 | const currentDate = new Date();
51 | currentDate.setDate(currentDate.getDate() - 6);
52 | const fromDate = currentDate.toLocaleDateString();
53 | getGuildModLogs(guildId, fromDate)
54 | .then(({ data }) => {
55 | setModLogs(data);
56 | })
57 | .catch((err) => console.log(err));
58 | }, []);
59 |
60 | return (
61 |
62 |
68 |
69 |
Ban Analytics
70 | log.type === 'ban')
78 | ),
79 | borderColor: 'white',
80 | pointBorderColor: 'orange',
81 | },
82 | ],
83 | }}
84 | />
85 |
86 |
87 |
Kick Analytics
88 | log.type === 'kick')
96 | ),
97 | borderColor: 'white',
98 | pointBorderColor: 'orange',
99 | },
100 | ],
101 | }}
102 | />
103 |
104 |
105 |
Timeout Analytics
106 | log.type === 'timeout')
114 | ),
115 | borderColor: 'white',
116 | pointBorderColor: 'orange',
117 | },
118 | ],
119 | }}
120 | />
121 |
122 |
123 |
124 | );
125 | };
126 |
--------------------------------------------------------------------------------
/src/pages/GuildBansPage.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from 'react';
2 | import { MoonLoader } from 'react-spinners';
3 | import { GuildContext } from '../utils/contexts/GuildContext';
4 | import { useFetchGuildBans } from '../utils/hooks/useFetchGuildBans';
5 | import {
6 | Container,
7 | ContextMenuContainer,
8 | Flex,
9 | Page,
10 | UserBanCard,
11 | } from '../utils/styles';
12 | import { DiscordUserType, GuildBanType } from '../utils/types';
13 | import DefaultAvatar from '../assets/default_avatar.png';
14 | import { deleteGuildBan } from '../utils/api';
15 |
16 | export const GuildBansPage = () => {
17 | const { guild } = useContext(GuildContext);
18 | const guildId = (guild && guild.id) || '';
19 | const { bans, loading, updating, setUpdating } = useFetchGuildBans(guildId);
20 | const [showMenu, setShowMenu] = useState(false);
21 | const [points, setPoints] = useState({ x: 0, y: 0 });
22 | const [selectedBan, setSelectedBan] = useState();
23 |
24 | const getAvatarUrl = (user: DiscordUserType) =>
25 | `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`;
26 |
27 | useEffect(() => {
28 | const handleClick = () => setShowMenu(false);
29 | window.addEventListener('click', handleClick);
30 | return () => window.removeEventListener('click', handleClick);
31 | }, []);
32 |
33 | const handleUnban = async () => {
34 | if (!selectedBan) {
35 | console.log('No user was selected.');
36 | return;
37 | }
38 | try {
39 | console.log(`Unbanning User: ${selectedBan?.user.username}`);
40 | await deleteGuildBan(guildId, selectedBan.user.id);
41 | setUpdating(!updating);
42 | } catch (err) {
43 | console.log(err);
44 | }
45 | };
46 |
47 | return (
48 |
49 |
50 | {!loading ? (
51 |
58 | {bans.length ? (
59 | bans.map((ban) => (
60 |
{
62 | console.log('Context Menu Opened');
63 | e.preventDefault();
64 | setShowMenu(true);
65 | setPoints({ x: e.pageX, y: e.pageY });
66 | setSelectedBan(ban);
67 | }}
68 | >
69 |
70 | {ban.user.username}#{ban.user.discriminator}
71 |
72 |
81 |
82 | ))
83 | ) : (
84 |
No Bans
85 | )}
86 | {showMenu && (
87 |
88 |
89 | - Unban
90 | - Update Ban
91 |
92 |
93 | )}
94 |
95 | ) : (
96 |
97 |
98 |
99 | )}
100 |
101 |
102 | );
103 | };
104 |
--------------------------------------------------------------------------------
/src/pages/GuildPrefixPage.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { MoonLoader } from 'react-spinners';
3 | import { updateGuildPrefix } from '../utils/api';
4 | import { GuildContext } from '../utils/contexts/GuildContext';
5 | import { useGuildConfig } from '../utils/hooks/useFetchGuildConfig';
6 | import {
7 | Button,
8 | Container,
9 | Flex,
10 | InputField,
11 | Page,
12 | Title,
13 | } from '../utils/styles';
14 |
15 | export const GuildPrefixPage = () => {
16 | const { guild } = useContext(GuildContext);
17 | const guildId = (guild && guild.id) || '';
18 | const { config, loading, error, prefix, setPrefix } = useGuildConfig(guildId);
19 |
20 | const savePrefix = async (
21 | e: React.MouseEvent
22 | ) => {
23 | e.preventDefault();
24 | console.log(prefix);
25 | try {
26 | const res = await updateGuildPrefix(guildId, prefix);
27 | console.log(res);
28 | } catch (err) {
29 | console.log(err);
30 | }
31 | };
32 |
33 | return (
34 |
35 |
36 | {!loading && config ? (
37 | <>
38 | Update Command Prefix
39 |
64 | >
65 | ) : (
66 |
67 |
68 |
69 | )}
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/pages/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import { FaDiscord, FaQuestionCircle } from 'react-icons/fa';
2 | import { HomePageStyle, MainButton } from '../utils/styles';
3 |
4 | export const LoginPage = () => {
5 | const redirect = () => {
6 | window.location.href = 'http://localhost:3001/api/auth/login';
7 | };
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | Login with Discord
15 |
16 |
17 |
18 | Support Server
19 |
20 |
21 |
28 | Privacy Policy
29 | Terms of Service
30 | Contact Us
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/pages/MenuPage.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { MoonLoader } from 'react-spinners';
4 | import { GuildMenuItem } from '../components/GuildMenuItem';
5 | import { GuildContext } from '../utils/contexts/GuildContext';
6 | import { useFetchGuilds } from '../utils/hooks/useFetchGuilds';
7 | import { Container, Flex, Page } from '../utils/styles';
8 | import { PartialGuild } from '../utils/types';
9 | import { mockGuilds } from '../__mocks__/guilds';
10 |
11 | export const MenuPage = () => {
12 | const navigate = useNavigate();
13 | const { updateGuild } = useContext(GuildContext);
14 | const { guilds, loading, error } = useFetchGuilds();
15 |
16 | const handleClick = (guild: PartialGuild) => {
17 | updateGuild(guild);
18 | navigate('/dashboard/categories');
19 | };
20 |
21 | return (
22 |
23 |
24 | Select a Server
25 |
26 | {loading ? (
27 |
28 |
29 |
30 | ) : (
31 |
32 | {guilds &&
33 | guilds.map((guild) => (
34 |
handleClick(guild)}>
35 |
36 |
37 | ))}
38 |
39 | )}
40 |
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/pages/WelcomeMessagePage.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { MoonLoader } from 'react-spinners';
3 | import { updateWelcomeChannelId } from '../utils/api';
4 | import { GuildContext } from '../utils/contexts/GuildContext';
5 | import { useWelcomePage } from '../utils/hooks/useWelcomePage';
6 | import {
7 | Button,
8 | Container,
9 | Flex,
10 | Page,
11 | Select,
12 | TextArea,
13 | Title,
14 | } from '../utils/styles';
15 |
16 | export const WelcomeMessagePage = () => {
17 | const { guild } = useContext(GuildContext);
18 | const guildId = (guild && guild.id) || '';
19 | const { config, channels, selectedChannel, setSelectedChannel, loading } =
20 | useWelcomePage(guildId);
21 |
22 | const updateWelcomeChannel = async () => {
23 | console.log(selectedChannel);
24 | try {
25 | await updateWelcomeChannelId(guildId, selectedChannel || '');
26 | } catch (err) {
27 | console.log(err);
28 | }
29 | };
30 |
31 | return (
32 |
33 |
34 | Update Welcome Message
35 | {channels && config && !loading ? (
36 |
37 |
38 |
39 |
40 |
41 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
71 |
74 |
75 |
76 | ) : (
77 |
78 |
79 |
80 | )}
81 |
82 |
83 | );
84 | };
85 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/utils/api.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosRequestConfig } from 'axios';
2 | import {
3 | GuildModLogType,
4 | GuildConfigType,
5 | PartialGuild,
6 | User,
7 | GuildBanType,
8 | } from './types';
9 |
10 | const CONFIG: AxiosRequestConfig = { withCredentials: true };
11 | const API_URL = 'http://localhost:3001/api';
12 |
13 | export const getAuthStatus = () =>
14 | axios.get(`${API_URL}/auth/status`, CONFIG);
15 |
16 | export const getMutualGuilds = () =>
17 | axios.get(`${API_URL}/discord/guilds`, CONFIG);
18 |
19 | export const getGuildConfig = (guildId: string) =>
20 | axios.get(`${API_URL}/guilds/config/${guildId}`, CONFIG);
21 |
22 | export const updateGuildPrefix = (guildId: string, prefix: string) =>
23 | axios.post(
24 | `${API_URL}/guilds/${guildId}/config/prefix`,
25 | {
26 | prefix,
27 | },
28 | CONFIG
29 | );
30 |
31 | export const getGuildChannels = (guildId: string) =>
32 | axios.get(`${API_URL}/discord/guilds/${guildId}/channels`, CONFIG);
33 |
34 | export const updateWelcomeChannelId = (guildId: string, channelId: string) =>
35 | axios.post(
36 | `${API_URL}/guilds/${guildId}/config/welcome`,
37 | {
38 | channelId,
39 | },
40 | CONFIG
41 | );
42 |
43 | export const getGuildBanLogs = (guildId: string, fromDate: string) =>
44 | axios.get(
45 | `${API_URL}/guilds/${guildId}/bans?fromDate=${fromDate}`,
46 | CONFIG
47 | );
48 |
49 | export const getGuildModLogs = (guildId: string, fromDate: string) =>
50 | axios.get(
51 | `${API_URL}/guilds/${guildId}/logs?fromDate=${fromDate}`,
52 | CONFIG
53 | );
54 |
55 | export const getGuildBans = (guildId: string) =>
56 | axios.get(
57 | `${API_URL}/discord/guilds/${guildId}/bans`,
58 | CONFIG
59 | );
60 |
61 | export const deleteGuildBan = (guildId: string, userId: string) =>
62 | axios.delete(`${API_URL}/discord/guilds/${guildId}/bans/${userId}`, CONFIG);
63 |
--------------------------------------------------------------------------------
/src/utils/contexts/GuildContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 | import { PartialGuild } from '../types';
3 |
4 | type GuildContextType = {
5 | guild?: PartialGuild;
6 | updateGuild: (guild: PartialGuild) => void;
7 | };
8 |
9 | export const GuildContext = createContext({
10 | updateGuild: () => {},
11 | });
12 |
--------------------------------------------------------------------------------
/src/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | import { PartialGuild } from './types';
2 |
3 | export const getIconURL = (guild: PartialGuild) =>
4 | `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png`;
5 |
--------------------------------------------------------------------------------
/src/utils/hooks/useFetchGuildBans.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getGuildBans } from '../api';
3 | import { GuildBanType } from '../types';
4 |
5 | export function useFetchGuildBans(guildId: string) {
6 | const [bans, setBans] = useState([]);
7 | const [loading, setLoading] = useState(false);
8 | const [error, setError] = useState();
9 | const [updating, setUpdating] = useState(false);
10 |
11 | useEffect(() => {
12 | setLoading(true);
13 | getGuildBans(guildId)
14 | .then(({ data }) => {
15 | setBans(data);
16 | })
17 | .catch((err) => {
18 | console.log(err);
19 | setError(err);
20 | })
21 | .finally(() => setLoading(false));
22 | }, [updating]);
23 |
24 | return { bans, loading, error, updating, setUpdating };
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/hooks/useFetchGuildConfig.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getGuildConfig } from '../api';
3 | import { GuildConfigType } from '../types';
4 |
5 | export function useGuildConfig(guildId: string) {
6 | const [config, setConfig] = useState();
7 | const [prefix, setPrefix] = useState('');
8 | const [loading, setLoading] = useState(false);
9 | const [error, setError] = useState();
10 |
11 | useEffect(() => {
12 | setLoading(true);
13 | getGuildConfig(guildId)
14 | .then(({ data }) => {
15 | setConfig(data);
16 | setPrefix(data.prefix);
17 | })
18 | .catch((err) => {
19 | console.log(err);
20 | setError(err);
21 | })
22 | .finally(() => setTimeout(() => setLoading(false), 1000));
23 | }, []);
24 |
25 | return { config, prefix, setPrefix, loading, error };
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/hooks/useFetchGuilds.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getMutualGuilds } from '../api';
3 | import { PartialGuild } from '../types';
4 |
5 | export function useFetchGuilds() {
6 | const [guilds, setGuilds] = useState();
7 | const [loading, setLoading] = useState(false);
8 | const [error, setError] = useState();
9 |
10 | useEffect(() => {
11 | setLoading(true);
12 | getMutualGuilds()
13 | .then(({ data }) => {
14 | setGuilds(data);
15 | })
16 | .catch((err) => {
17 | console.log(err);
18 | setError(err);
19 | })
20 | .finally(() => {
21 | setLoading(false);
22 | });
23 | }, []);
24 |
25 | return { guilds, loading, error };
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/hooks/useFetchUser.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getAuthStatus } from '../api';
3 | import { User } from '../types';
4 |
5 | export function useFetchUser() {
6 | const [user, setUser] = useState();
7 | const [error, setError] = useState();
8 | const [loading, setLoading] = useState(false);
9 |
10 | useEffect(() => {
11 | setLoading(true);
12 | getAuthStatus()
13 | .then(({ data }) => {
14 | console.log(data);
15 | setUser(data);
16 | })
17 | .catch((err) => {
18 | console.log(err);
19 | setError(err);
20 | })
21 | .finally(() => setTimeout(() => setLoading(false), 1000));
22 | }, []);
23 |
24 | return { user, error, loading };
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/hooks/useWelcomePage.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getGuildChannels, getGuildConfig } from '../api';
3 | import { GuildConfigType, PartialGuildChannel } from '../types';
4 |
5 | export function useWelcomePage(guildId: string) {
6 | const [config, setConfig] = useState();
7 | const [channels, setChannels] = useState();
8 | const [selectedChannel, setSelectedChannel] = useState();
9 | const [loading, setLoading] = useState(false);
10 | useEffect(() => {
11 | setLoading(true);
12 | getGuildConfig(guildId)
13 | .then(({ data }) => {
14 | setConfig(data);
15 | setSelectedChannel(data.welcomeChannelId);
16 | return getGuildChannels(guildId);
17 | })
18 | .then(({ data }) => setChannels(data))
19 | .catch((err) => console.log(err))
20 | .finally(() => setLoading(false));
21 | }, []);
22 |
23 | return {
24 | config,
25 | channels,
26 | loading,
27 | selectedChannel,
28 | setSelectedChannel,
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/styles/index.tsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | export const MainButton = styled.div`
4 | display: flex;
5 | width: 350px;
6 | align-items: center;
7 | justify-content: space-between;
8 | background-color: #2121217d;
9 | padding: 4px 50px;
10 | box-sizing: border-box;
11 | border-radius: 5px;
12 | border: 1px solid #58585863;
13 | margin: 10px 0;
14 | box-shadow: 0px 1px 5px 0px #00000018;
15 | `;
16 |
17 | export const TextButton = styled(MainButton)`
18 | padding: 18px 28px;
19 | width: 100%;
20 | background-color: #272727;
21 | `;
22 |
23 | export const HomePageStyle = styled.div`
24 | height: 100%;
25 | padding: 100px 0;
26 | box-sizing: border-box;
27 | display: flex;
28 | flex-direction: column;
29 | justify-content: space-between;
30 | align-items: center;
31 | `;
32 |
33 | export const GuildMenuItemStyle = styled.div`
34 | display: flex;
35 | justify-content: space-between;
36 | align-items: center;
37 | padding: 8px 20px;
38 | background-color: #252525;
39 | border-radius: 5px;
40 | border: 1px solid #ffffff14;
41 | margin: 8px 0;
42 | `;
43 |
44 | export const Container = styled.div`
45 | width: 1200px;
46 | margin: 0 auto;
47 | `;
48 |
49 | export const AppBarStyle = styled.header`
50 | display: flex;
51 | justify-content: space-between;
52 | align-items: center;
53 | padding: 15px 35px;
54 | box-sizing: border-box;
55 | border-bottom: 1px solid #c9c9c921;
56 | `;
57 |
58 | export const Title = styled.p`
59 | font-size: 22px;
60 | `;
61 |
62 | type FlexProps = Partial<{
63 | alignItems: string;
64 | justifyContent: string;
65 | flexDirection: string;
66 | }>;
67 | export const Flex = styled.div`
68 | display: flex;
69 | align-items: ${({ alignItems }) => alignItems};
70 | justify-content: ${({ justifyContent }) => justifyContent};
71 | flex-direction: ${({ flexDirection }) => flexDirection};
72 | `;
73 |
74 | export const Grid = styled.div`
75 | display: grid;
76 | grid-template-columns: 1fr 1fr 1fr;
77 | column-gap: 10px;
78 | `;
79 |
80 | export const InputField = styled.input`
81 | padding: 14px 16px;
82 | box-sizing: border-box;
83 | font-size: 16px;
84 | color: #fff;
85 | font-family: 'DM Sans';
86 | background-color: #272727;
87 | border-radius: 5px;
88 | border: 1px solid #3f3f3f;
89 | outline: none;
90 | width: 100%;
91 | :focus {
92 | outline: 1px solid #ffffff5a;
93 | }
94 | `;
95 |
96 | export const TextArea = styled.textarea`
97 | padding: 14px 16px;
98 | box-sizing: border-box;
99 | font-size: 16px;
100 | color: #fff;
101 | font-family: 'DM Sans';
102 | background-color: #272727;
103 | border-radius: 5px;
104 | border: 1px solid #3f3f3f;
105 | outline: none;
106 | width: 100%;
107 | resize: none;
108 | :focus {
109 | outline: 1px solid #ffffff5a;
110 | }
111 | `;
112 |
113 | type ButtonProps = {
114 | variant: 'primary' | 'secondary';
115 | };
116 |
117 | export const Button = styled.button`
118 | padding: 10px 24px;
119 | border-radius: 5px;
120 | outline: none;
121 | border: none;
122 | font-size: 16px;
123 | color: #fff;
124 | font-family: 'DM Sans';
125 | cursor: pointer;
126 | ${({ variant }) =>
127 | variant === 'primary' &&
128 | css`
129 | background-color: #006ed3;
130 | `}
131 | ${({ variant }) =>
132 | variant === 'secondary' &&
133 | css`
134 | background-color: #3d3d3d;
135 | `}
136 | `;
137 |
138 | export const Page = styled.div`
139 | padding: 50px;
140 | `;
141 |
142 | export const Select = styled.select`
143 | width: 100%;
144 | padding: 10px;
145 | font-family: 'DM Sans';
146 | font-size: 18px;
147 | background-color: inherit;
148 | padding: 12px 16px;
149 | color: #fff;
150 | border: 1px solid #3f3f3f;
151 | border-radius: 5px;
152 | & > option {
153 | background-color: #292929;
154 | }
155 | `;
156 |
157 | export const Overlay = styled.div`
158 | height: 100%;
159 | width: 100%;
160 | background-color: #0000006c;
161 | display: flex;
162 | justify-content: center;
163 | align-items: center;
164 | position: fixed;
165 | top: 0;
166 | left: 0;
167 | `;
168 |
169 | export const UserBanCard = styled.div`
170 | display: flex;
171 | justify-content: space-between;
172 | align-items: center;
173 | border: 1px solid #ffffff1f;
174 | box-sizing: border-box;
175 | padding: 18px;
176 | border-radius: 4px;
177 | `;
178 |
179 | type ContextMenuContainerProps = {
180 | top: number;
181 | left: number;
182 | };
183 |
184 | export const ContextMenuContainer = styled.div`
185 | position: absolute;
186 | background-color: #353535;
187 | border-radius: 3px;
188 | box-shadow: 0px 1px 10px 0px rgba(0, 0, 0, 0.15);
189 | height: auto;
190 | width: 200px;
191 | box-sizing: border-box;
192 | ${({ top, left }) => css`
193 | top: ${top}px;
194 | left: ${left}px;
195 | `}
196 | ul {
197 | list-style-type: none;
198 | margin: 0;
199 | padding: 8px;
200 | box-sizing: border-box;
201 | }
202 |
203 | ul li {
204 | padding: 10px 14px;
205 | border-radius: 3px;
206 | }
207 |
208 | ul li:hover {
209 | background-color: #444444;
210 | cursor: pointer;
211 | }
212 | `;
213 |
--------------------------------------------------------------------------------
/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | export type User = {
2 | id: string;
3 | discordId: string;
4 | };
5 |
6 | export type PartialGuild = {
7 | id: string;
8 | name: string;
9 | icon: string;
10 | owner: boolean;
11 | permissions: string;
12 | features: string[];
13 | };
14 |
15 | export type GuildConfigType = {
16 | id: number;
17 | guildId: string;
18 | prefix: string;
19 | welcomeChannelId: string;
20 | };
21 |
22 | export type PartialGuildChannel = {
23 | id: string;
24 | last_message_id: string;
25 | type: number;
26 | name: string;
27 | position: number;
28 | parent_id?: string;
29 | topic?: string;
30 | guild_id: string;
31 | permission_overwrites: string[];
32 | nsfw: boolean;
33 | rate_limit_per_user: string;
34 | banner?: string;
35 | };
36 |
37 | export type ModerationActionType = 'ban' | 'kick' | 'timeout';
38 |
39 | export type GuildModLogType = {
40 | id: number;
41 | guildId: string;
42 | memberId: string;
43 | issuedBy: string;
44 | issuedOn: Date;
45 | reason?: string;
46 | type: ModerationActionType;
47 | };
48 |
49 | export type DiscordUserType = {
50 | id: string;
51 | username: string;
52 | discriminator: string;
53 | avatar?: string;
54 | bot?: boolean;
55 | system?: boolean;
56 | mfa_enabled?: boolean;
57 | banner?: string;
58 | accent_color?: number;
59 | locale?: string;
60 | verified?: boolean;
61 | email?: string;
62 | flags?: number;
63 | premium_type?: number;
64 | public_flags?: number;
65 | };
66 |
67 | export type GuildBanType = {
68 | reason?: string;
69 | user: DiscordUserType;
70 | };
71 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------