toggle(e)} ref={ref} />
39 | )
40 | }
41 |
42 | Card.Header = (props: Props) =>
43 | Card.Content = (props: Props) =>
--------------------------------------------------------------------------------
/src/styles/Guild.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 | .togglesBoxWrapper {
8 | justify-content: center;
9 | flex-wrap: wrap;
10 | background-position: center;
11 | display: flex;
12 |
13 | .toggleBox {
14 | width: 49%;
15 | background-color: none;
16 | padding: 10px 20px;
17 | display: inline-block;
18 | margin: 10px auto;
19 | background-color: var(--luny-background);
20 | border-radius: 10px;
21 | border: 1px solid themed('luny-ui', 10);
22 | color: white;
23 |
24 | .checkedIcon {
25 | display: none;
26 | float: right;
27 | vertical-align: middle;
28 | margin: auto;
29 | margin-right: -22.5px;
30 | margin-top: -12.5px;
31 | padding: 3px;
32 | border-radius: 15px;
33 | background-color: themed('luny-band');
34 | color: white;
35 | }
36 | }
37 |
38 | .toggleBox[data-checked] {
39 | border: 2px solid themed('luny-band', 100);
40 | padding: 9px 19px;
41 |
42 | .checkedIcon {
43 | display: flex;
44 | }
45 | }
46 | }
47 |
48 | .newItemButton {
49 | background-color: themed('luny-ui', 20);
50 | font-weight: 100;
51 |
52 | &:hover {
53 | background-color: themed('luny-ui', 40);
54 | }
55 |
56 | &:disabled {
57 | background-color: themed('luny-ui', 5);
58 | }
59 | }
--------------------------------------------------------------------------------
/src/styles/Auth.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 | .container {
8 | padding-right: 15px;
9 | padding-left: 15px;
10 | margin-right: auto;
11 | margin-left: auto;
12 |
13 | .help {
14 | padding-top: 95px;
15 | }
16 |
17 | .loginInfos {
18 | position: absolute;
19 | left: 50%;
20 | top: 50%;
21 | transform: translate(-50%, -50%);
22 | text-align: center;
23 |
24 | img {
25 | width: 125px;
26 | border-radius: 50%;
27 | display: block;
28 | }
29 |
30 | .placeholder {
31 | width: 125px;
32 | height: 120px;
33 | display: block;
34 | border-radius: 50%;
35 | background-color: themed('luny-band');
36 | }
37 |
38 | label {
39 | display: block;
40 | font-size: 25px;
41 | margin-top: 10px;
42 | margin-bottom: 10px;
43 | color: themed('luny-text');
44 | }
45 |
46 | button:hover {
47 | background-color: themed('luny-band', 60);
48 | }
49 | }
50 |
51 | @media (min-width: 768px) {
52 | & {
53 | width: 750px;
54 | }
55 | }
56 |
57 | @media (min-width: 992px) {
58 | & {
59 | width: 970px;
60 | }
61 | }
62 |
63 | @media (min-width: 1200px) {
64 | & {
65 | width: 1170px;
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/src/components/form/switch.tsx:
--------------------------------------------------------------------------------
1 | import React, { DetailedHTMLProps, HTMLAttributes, InputHTMLAttributes } from 'react';
2 | import styled from 'styled-components';
3 |
4 | type Props = Omit
, 'children' | 'type'> & {
5 |
6 | }
7 |
8 | const SwitchWrapper = styled.label`
9 | display: inline-block;
10 | height: 40px;
11 | position: relative;
12 | width: 56px;
13 |
14 | & > input {
15 | height: 0px;
16 | opacity: 0;
17 | width: 0px;
18 | }
19 |
20 | & > span {
21 | -webkit-transition: .4s;
22 | background-color: var(--luny-background);
23 | border-radius: 30px;
24 | bottom: 0;
25 | cursor: pointer;
26 | left: 0;
27 | position: absolute;
28 | right: 0;
29 | top: 0;
30 | transition: .4s;
31 | border: 1px solid var(--luny-ui-15);
32 |
33 | &:before {
34 | -webkit-transition: .4s;
35 | background-color: var(--luny-ui-40);
36 | border-radius: 50%;
37 | bottom: 4.5px;
38 | content: "";
39 | height: 21px;
40 | left: 4px;
41 | position: absolute;
42 | transition: .25s;
43 | width: 22px;
44 | }
45 | }
46 |
47 | & > input:checked + span:before {
48 | -ms-transform: translateX(24px);
49 | -webkit-transform: translateX(24px);
50 | transform: translateX(24px);
51 | background-color: var(--luny-band-100);
52 | }
53 |
54 | & > input:checked:disabled + span {
55 | cursor: no-drop;
56 |
57 | &:before {
58 | background-color: var(--luny-band-40);
59 | }
60 | }
61 | `;
62 |
63 | export const Switch: React.FC = (props) => (
64 |
65 |
66 |
67 |
68 | );
--------------------------------------------------------------------------------
/src/styles/globals.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 | * {
8 | margin: 0;
9 | padding: 0;
10 | box-sizing: border-box;
11 | font-family: 'Poppins';
12 | // font-family: 'Poppins', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
13 | // Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
14 | }
15 |
16 | html,
17 | body {
18 | padding: 0;
19 | margin: 0;
20 | background: linear-gradient(90deg, var(--luny-background) 21px, transparent 1%) 50%, linear-gradient(var(--luny-background) 21px,transparent 1%) 50%, #333;
21 | background-size: 22px 22px;
22 | overflow-x: hidden;
23 | }
24 |
25 | ::-webkit-scrollbar {
26 | width: 5px;
27 | }
28 |
29 | ::-webkit-scrollbar-track {
30 | background: var(--luny-colors-flow-80);
31 | box-shadow: inset 0 0 6px var(--luny-colors-flow-20);
32 | }
33 |
34 | ::-webkit-scrollbar-thumb {
35 | background-color: var(--luny-colors-band-100);
36 | border-radius: 0.5em;
37 | }
38 |
39 | a {
40 | color: inherit;
41 | text-decoration: none;
42 |
43 | &:hover {
44 | color: #000000a2
45 | }
46 | }
47 |
48 | p {
49 | &[data-size="xs"] {
50 | font-size: 0.75rem;
51 | }
52 |
53 | &[data-size="sm"] {
54 | font-size: 0.875rem;
55 | }
56 |
57 | &[data-size="md"] {
58 | font-size: 1rem;
59 | }
60 |
61 | &[data-size="lg"] {
62 | font-size: 1.125rem;
63 | }
64 | }
65 |
66 | .backgroundGradient {
67 | position: absolute;
68 | top: 0;
69 | width: 100vw;
70 | height: 900px;
71 | background: linear-gradient(175deg, themed('luny-band', 15), transparent, transparent);
72 | z-index: -1;
73 | }
74 |
75 | /*
76 | --fontSizes-xxs: 0.625rem;
77 | --fontSizes-xs: 0.75rem;
78 | --fontSizes-sm: 0.875rem;
79 | --fontSizes-md: 1rem;
80 | --fontSizes-lg: 1.125rem;
81 | --fontSizes-xl: 1.25rem;
82 | --fontSizes-2xl: 1.5rem;
83 | */
--------------------------------------------------------------------------------
/src/pages/auth/discord.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import qs from 'qs';
3 | import { gql } from '@apollo/client';
4 |
5 | import { User } from '../../@types';
6 |
7 | import { Button } from '../../components'
8 |
9 | import { createAPIClient } from '../../services/ApiService';
10 |
11 | import styles from '../../styles/Auth.module.scss';
12 |
13 | export default function AuthDiscord() {
14 | const [user, setUser] = useState();
15 |
16 | useEffect(() => {
17 | const loginButton = document.getElementById('loginButton');
18 |
19 | const { token } = qs.parse(location.search, { ignoreQueryPrefix: true });
20 |
21 | if(!token) {
22 | window.location.href = '/';
23 | } else {
24 | request();
25 |
26 | loginButton.addEventListener('click', () => {
27 | localStorage.setItem('auth_token', token);
28 | window.location.href = '/dashboard/@me';
29 | });
30 | }
31 |
32 | async function request() {
33 | const { data, errors } = await createAPIClient(token).query({
34 | query: gql`
35 | query User {
36 | CurrentUser {
37 | username
38 | id
39 | avatar
40 | }
41 | }
42 | `,
43 | });
44 |
45 | if(errors) {
46 | console.log(errors)
47 |
48 | return;
49 | }
50 |
51 | setUser(data.CurrentUser)
52 | }
53 | }, []);
54 |
55 | console.log(user);
56 |
57 | return (
58 |
59 |
60 | { user?.avatar ?
:
}
61 |
{user?.username}
62 |
63 |
64 | Logar
65 |
66 |
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 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Home
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Home
61 |
62 |
63 |
64 |
65 |
66 | )
67 | }
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '../styles/globals.scss';
2 |
3 | import { createGlobalStyle } from 'styled-components';
4 | import Head from 'next/head';
5 | import { useState, useEffect } from 'react';
6 | import { DashboardLayout } from '../components/dashboard';
7 |
8 | import Theme from '../utils/theme';
9 |
10 | import themesStyle from '../styles/theme.module.scss';
11 | import { useRouter } from 'next/router';
12 | import { APIProvider } from '../contexts/TestAPIContext';
13 |
14 | export default function MyApp({ Component, pageProps }) {
15 | const [_mode, setMode] = useState<'dark'|'light'|null>(null);
16 | const router = useRouter();
17 |
18 | useEffect(() => {
19 | const body = document.querySelector('body');
20 |
21 | if(!_mode) {
22 | setMode(localStorage.getItem('theme') as 'dark' | 'light' | null || 'dark')
23 | }
24 |
25 | const mode = (localStorage.getItem('theme') as 'dark' | 'light' | null) || 'dark';
26 |
27 | body.setAttribute('data-theme', mode);
28 |
29 | window.changeMode = (mode) => {
30 | localStorage.setItem('theme', mode);
31 | document.querySelector('[data-styled]').innerHTML = `:root {${Theme({ mode: mode || 'dark' }).toString()}}`
32 | body.setAttribute('data-theme', mode);
33 | }
34 | });
35 |
36 | const Children = router.pathname.startsWith('/dashboard') ? (
37 |
38 |
39 |
40 | ) :
41 |
42 | return (
43 | <>
44 |
45 | Luna
46 |
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 | {Children}
57 |
58 | >
59 | )
60 | }
--------------------------------------------------------------------------------
/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import Link from 'next/link';
3 |
4 | import styles from '../styles/Header.module.scss';
5 |
6 | export function Header() {
7 | useEffect(() => {
8 | const dropdowns = document.querySelectorAll('[data-dropdown]');
9 |
10 | dropdowns.forEach((dropdown) => {
11 | dropdown.addEventListener('click', (e) => {
12 | const classList = dropdown.classList;
13 |
14 | if(classList.toString().includes(styles.open)) {
15 | classList.remove(styles.open)
16 | } else {
17 | classList.add(styles.open)
18 | }
19 | });
20 |
21 | document.addEventListener('click', () => {
22 | dropdown.classList.remove(styles.open);
23 | });
24 | });
25 | }, []);
26 |
27 | return (
28 |
73 | )
74 | }
--------------------------------------------------------------------------------
/src/components/dashboard/guilds/permissions/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Card } from '../../../card';
4 | import { Select, Switch } from '../../../form';
5 |
6 | import styles from './styles.module.scss';
7 | import { useAPI } from '../../../../hooks/useAPI';
8 |
9 | export const GuildPermissions: React.FC<{}> = (props) => {
10 | const { guild } = useAPI();
11 |
12 | return (
13 |
14 |
15 |
18 |
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 | setValue(!value)} role={'checkbox'}>
19 |
20 |
21 |
22 | {props.children}
23 |
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 => ({
40 | label: channel.name,
41 | value: channel.id,
42 | })) || []}
43 | placeholder={'Select a channel'}
44 | />
45 |
46 |
47 |
48 |
49 |
50 | Canal de Modlogs
51 |
52 |
53 | 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 | {
83 | return ({
84 | label: key.replace(/([A-Z])|^([a-z])/g, (w) => ` ${w.toUpperCase()}`),
85 | value: value,
86 | default: key == ''
87 | })
88 | }),
89 | ]}
90 | customId={'banCommandPermissions'}
91 | placeholder={'Select a permission'}
92 | maxValues={flagsEntries.length}
93 | />
94 |
95 |
96 |
97 | Manatory Reason
98 | Requires a reason for the ban.
99 |
100 |
101 | Notify User
102 | Notify the user that they have been banned.
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | Add permissions
120 |
121 |
122 |
123 |
124 |
125 |
126 | )
127 | }
128 |
129 | export default DashboardGuildModeration;
--------------------------------------------------------------------------------
/src/contexts/TestAPIContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext } from 'react';
2 |
3 | import { User, UserWithGuilds, AbstractGuild, Guild } from '../@types';
4 | import { Utils } from '../utils';
5 |
6 | const guildUrlString = /\/dashboard\/guilds\/(\d*)(\/.*)?/i
7 |
8 | interface APIContextData {
9 | signed: boolean;
10 | loading: boolean;
11 | user: UserWithGuilds;
12 | guild?: Guild;
13 |
14 | checkGuild: (guildId: string) => boolean;
15 | fetchGuild: (id: string) => Promise;
16 | fetchUserGuilds: () => Promise;
17 | }
18 |
19 | const APIContext = createContext({} as APIContextData);
20 |
21 | export class APIProvider extends React.Component {
27 |
28 | constructor(props: React.PropsWithChildren) {
29 | super(props);
30 |
31 | this.state = {
32 | loading: true,
33 | user: null,
34 | guild: null,
35 | token: null,
36 | }
37 | }
38 |
39 | componentDidMount(): void {
40 | this.setState({
41 | user: {
42 | id: '842170079520096276',
43 | username: 'Myka',
44 | discriminator: '0806',
45 | avatar: '74a291872af71565d98d870384d8d666',
46 | public_flags: 0,
47 | guilds: null
48 | },
49 | loading: false,
50 | token: 'N/A',
51 | });
52 | }
53 |
54 | async componentDidUpdate() {
55 | const pathname = window.location.pathname;
56 |
57 | if(guildUrlString.test(pathname)) {
58 | const guildId = pathname.replace(guildUrlString, '$1');
59 |
60 | if(this.state.guild?.id !== guildId) {
61 | await this.fetchGuild(guildId);
62 | }
63 | } else if(this.state.guild) {
64 | this.setState({ guild: null });
65 | }
66 | }
67 |
68 | checkGuild(guildId: string) {
69 | return this.state.guild?.id === guildId;
70 | }
71 |
72 | async fetchGuild(id: string) {
73 | const guild: Guild = {
74 | id,
75 | name: '🌇 Lunar City',
76 | banner: null,
77 | channels: [
78 | {
79 | id: '002',
80 | name: 'rules',
81 | nsfw: false,
82 | parent_id: '001',
83 | position: 1,
84 | type: 0,
85 | },
86 | {
87 | id: '005',
88 | name: 'news',
89 | nsfw: false,
90 | parent_id: '001',
91 | position: 1,
92 | type: 5,
93 | },
94 | {
95 | id: '004',
96 | name: 'chat',
97 | nsfw: false,
98 | parent_id: '003',
99 | position: 1,
100 | type: 0,
101 | },
102 | ],
103 | roles: [
104 | {
105 | id: '787668624797466675',
106 | name: '『🚀 [Dev] Space Engineer』',
107 | permissions: 1649267441655,
108 | position: 40,
109 | color: 10494192,
110 | hoist: true,
111 | managed: false,
112 | mentionable: false
113 | },
114 | {
115 | id: '787668626403753984',
116 | name: '『🙇♀️[Suporte] Atendentes』',
117 | permissions: 693741547328,
118 | position: 34,
119 | color: 13942765,
120 | hoist: true,
121 | managed: false,
122 | mentionable: false
123 | },
124 | {
125 | id: '787668627943718913',
126 | name: '『🌿 Users』',
127 | permissions: 6546640448,
128 | position: 23,
129 | color: 15109595,
130 | hoist: true,
131 | managed: false,
132 | mentionable: false
133 | },
134 | ],
135 | features: ['ANIMATED_ICON'],
136 | icon: 'a_e32c439c0f444ee342ff89e631957af9',
137 | owner_id: '842170079520096276'
138 | };
139 |
140 | this.setState({ guild });
141 |
142 | return guild;
143 | }
144 |
145 | async fetchUserGuilds() {
146 | this.setState({
147 | user: {
148 | ...this.state.user,
149 | guilds: [
150 | {
151 | id: '787667696077504552',
152 | name: '🌇 Lunar City',
153 | features: ['ANIMATED_ICON'],
154 | icon: 'a_e32c439c0f444ee342ff89e631957af9',
155 | owner: true,
156 | permissions: 8,
157 | }
158 | ]
159 | }
160 | })
161 | }
162 |
163 | render(): React.ReactNode {
164 | const {
165 | props: { children },
166 | state: { loading, user, guild },
167 | fetchUserGuilds,
168 | fetchGuild,
169 | checkGuild,
170 | } = this;
171 |
172 | return (
173 |
182 | {children}
183 |
184 | )
185 | }
186 | }
187 |
188 | export default APIContext;
--------------------------------------------------------------------------------
/src/components/form/select/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, PropsWithChildren, DetailedHTMLProps, HTMLAttributes, useId, useEffect, useRef } from 'react';
2 | import styles from './styles.module.scss';
3 | import { Utils } from '../../../utils';
4 |
5 | type Props = PropsWithChildren, HTMLDivElement>>;
6 |
7 | interface Option {
8 | label: string;
9 | value: string|number|bigint;
10 | icon?: {
11 | url: string;
12 | };
13 | default?: boolean;
14 | color?: string;
15 | }
16 |
17 | interface SelectProps extends Props {
18 | placeholder: string;
19 | customId: string;
20 | options: Option[];
21 | maxValues?: number;
22 |
23 | backgroundColor?: string;
24 | }
25 |
26 | export class Select extends React.Component {
27 | ref: React.RefObject;
28 | _id: string;
29 |
30 | constructor(props: SelectProps) {
31 | super(props);
32 |
33 | this.state = {
34 | opened: false,
35 | values: props.options.filter(option => option.default),
36 | }
37 |
38 | this.ref = React.createRef();
39 |
40 | this._id = Utils.uuid();
41 | }
42 |
43 | componentDidMount(): void {
44 | window.addEventListener('click', e => {
45 | if(this.ref.current && this.state.opened && !this.ref.current.contains(e.target as Node)) {
46 | this.setState({ opened: false })
47 | }
48 | }, { capture: true });
49 | }
50 |
51 | setValue(value: Option['value']) {
52 | const { props, state } = this;
53 |
54 | const _values = state.values.map(({ value }) => value);
55 |
56 | const isMultiple = props.maxValues > 1;
57 |
58 | if(isMultiple && state.values.length >= props.maxValues) {
59 | return false;
60 | }
61 |
62 | if(_values.includes(value)) {
63 | this.removeValue(value);
64 | this.setState({ opened: false })
65 | } else {
66 | const option = props.options.find(option => option.value == value);
67 |
68 | this.setState({
69 | values: [option, ...(isMultiple ? state.values : [])].splice(0, props.maxValues),
70 | opened: false
71 | });
72 | }
73 | }
74 |
75 | removeValue(value: Option['value']) {
76 | return this.setState({ values: this.state.values.filter((option) => option.value != value) });
77 | }
78 |
79 | render() {
80 | const { props, state: { values, opened }, _id: id } = this;
81 |
82 | const _values = values.map(({ value }) => value);
83 |
84 | const isMultiple = props.maxValues > 1;
85 |
86 | const style = {backgroundColor: props.backgroundColor};
87 |
88 | const Placeholder = () => {
89 | let placeholder: string|JSX.Element[] = props.placeholder;
90 |
91 | if (values.length > 0) {
92 | if(isMultiple) {
93 | placeholder = values.map((option) => (
94 | this.removeValue(option.value)}>
95 | {option.label}
96 |
97 | ))
98 | } else {
99 | placeholder = values[0].label;
100 | }
101 | }
102 |
103 | return (
104 |
105 | {placeholder}
106 |
107 | )
108 | }
109 |
110 | const Options = () => {
111 | const options = isMultiple ? props.options.filter((option) => !_values.includes(option.value)) : props.options;
112 |
113 | if(options.length > 0) {
114 | return options.map((option) => {
115 | const props = {
116 | 'data-value': option.value,
117 | style: {},
118 | };
119 |
120 | if(_values.includes(option.value)) {
121 | props['data-selected'] = true;
122 | }
123 |
124 | if(option.color) {
125 | const c = hexToRgb(option.color);
126 |
127 | if(c) props.style = {
128 | color: `rgb(${c.r}, ${c.g}, ${c.b})`
129 | }
130 | }
131 |
132 | return (
133 | this.setValue(option.value)}>
134 | {option.icon &&
}
135 |
{option.label}
136 |
137 | )
138 | });
139 | }
140 |
141 | return (Não há nada para ver aqui )
142 | }
143 |
144 | const selectProps = {};
145 |
146 | if(opened) selectProps['data-opened'] = true;
147 | if(values.length >= props.maxValues && isMultiple) selectProps['data-full-values'] = true;
148 |
149 | return (
150 |
151 |
152 |
this.setState({ opened: !opened })}>
153 |
156 |
157 |
158 |
159 | {Options()}
160 |
161 |
162 |
163 | )
164 | }
165 | }
166 |
167 | function hexToRgb(hex: string) {
168 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
169 | return result ? {
170 | r: parseInt(result[1], 16),
171 | g: parseInt(result[2], 16),
172 | b: parseInt(result[3], 16)
173 | } : null;
174 | }
--------------------------------------------------------------------------------
/src/components/dashboard/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import Link from 'next/link';
3 | import { useRouter } from 'next/router';
4 |
5 | import { Utils } from '../../utils';
6 |
7 | import styles from '../../styles/Sidebar.module.scss';
8 | import { Dots } from '../dots';
9 | import { useAPI } from '../../hooks/useAPI';
10 |
11 | import { URLS } from '../../utils/Constants';
12 |
13 | export function DashboardSidebar() {
14 | const [opened, setOpen] = useState(false);
15 | const { user, guild, fetchUserGuilds } = useAPI();
16 |
17 | const router = useRouter();
18 |
19 | const urls = URLS[guild ? 'GUILD' : 'USER'];
20 |
21 | useEffect(() => {
22 | const serversMenu = document.querySelector(`.${styles.serversMenuWrapper}`) as HTMLElement;
23 | const profile = document.querySelector(`.${styles.profile}`) as HTMLElement;
24 |
25 | window.addEventListener('click', (e) => {
26 | if(opened && !serversMenu.contains(e.target as Node) && !profile.contains(e.target as Node)) {
27 | setOpen(false);
28 | }
29 | });
30 |
31 | if(opened && !user.guilds) {
32 | fetchUserGuilds();
33 | }
34 | });
35 |
36 | const openedProps = opened ? {'data-opened': true} : {}
37 |
38 | return (
39 |
40 |
41 |
42 |
setOpen(!opened)}>
43 |
44 | { guild ? (guild.icon ? : {Utils.stringAcronym(guild.name)}
) : }
45 |
46 |
47 |
48 | {guild?.name || user?.username}
49 | {(guild || user)?.id}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | {user?.username}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | {user?.guilds?.map(guild => (
73 |
74 |
75 |
76 |
77 | { guild.icon ? : {Utils.stringAcronym(guild.name)}
}
78 |
79 |
80 |
81 | {guild.name}
82 |
83 |
84 |
85 |
86 | )) ?? }
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | Invite
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | {Object.entries(urls).map(([category, urls], index) => (
111 |
112 |
113 | {category.charAt(0).toUpperCase() + category.slice(1).toLowerCase()}
114 |
115 |
116 | {urls.map((url, index) => (
117 |
118 |
119 |
120 | {url.label}
121 |
122 |
123 | ))}
124 |
125 | ))}
126 |
127 |
128 | )
129 | }
--------------------------------------------------------------------------------
/src/contexts/APIContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useEffect, useState } from 'react';
2 |
3 | import { User, UserWithGuilds, AbstractGuild, Guild } from '../@types';
4 | import { createAPIClient } from '../services/ApiService';
5 | import { ApolloError, gql, NetworkStatus, ServerError, ServerParseError } from '@apollo/client';
6 | import { Client } from '../services/ClientService';
7 | import { NetworkError } from '@apollo/client/errors';
8 | import { Utils } from '../utils';
9 |
10 | const guildUrlString = /\/dashboard\/guilds\/(\d*)(\/.*)?/i
11 |
12 | interface APIContextData {
13 | signed: boolean;
14 | loading: boolean;
15 | user: UserWithGuilds;
16 | guild?: Guild;
17 |
18 | checkGuild: (guildId: string) => boolean;
19 | fetchGuild: (id: string) => Promise;
20 | fetchUserGuilds: () => Promise;
21 | }
22 |
23 | const APIContext = createContext({} as APIContextData);
24 |
25 | export class APIProvider extends React.Component {
31 | public client = new Client();
32 |
33 | constructor(props: React.PropsWithChildren) {
34 | super(props);
35 |
36 | this.state = {
37 | loading: true,
38 | user: null,
39 | guild: null,
40 | token: null,
41 | }
42 | }
43 |
44 | componentDidMount(): void {
45 | const storagedToken = localStorage.getItem('auth_token');
46 |
47 | const loadUser = async() => {
48 | this.client.setToken(storagedToken);
49 |
50 | try {
51 | const { data, errors } = await this.client.api.query({
52 | query: gql`
53 | query User {
54 | CurrentUser {
55 | username
56 | id
57 | public_flags
58 | avatar
59 | discriminator
60 | }
61 | }
62 | `,
63 | });
64 |
65 | if(errors?.length > 0) {
66 | console.log(errors);
67 | return errors;
68 | }
69 |
70 | if(data) {
71 | this.setState({
72 | user: { ...data.CurrentUser, guilds: null },
73 | loading: false,
74 | token: storagedToken,
75 | });
76 | }
77 | } catch(error) {
78 | const err = (error as ApolloError).graphQLErrors[0] as any;
79 |
80 | if(err.status == 401) {
81 | localStorage.removeItem('auth_token')
82 | window.location.href = '/login';
83 | }
84 |
85 | console.log(err);
86 | }
87 | }
88 |
89 | if(storagedToken) {
90 | loadUser()
91 | } else if(window.location.pathname.startsWith('/dashboard')) {
92 | window.location.href = '/login';
93 | }
94 | }
95 |
96 | async componentDidUpdate() {
97 | const pathname = window.location.pathname;
98 |
99 | if(guildUrlString.test(pathname)) {
100 | const guildId = pathname.replace(guildUrlString, '$1');
101 |
102 | if(this.state.guild?.id !== guildId) {
103 | await this.fetchGuild(guildId);
104 | }
105 | } else if(this.state.guild) {
106 | this.setState({ guild: null });
107 | }
108 | }
109 |
110 | checkGuild(guildId: string) {
111 | return this.state.guild?.id === guildId;
112 | }
113 |
114 | async fetchGuild(id: string) {
115 | const { data, errors } = await this.client.api.query({
116 | query: gql`
117 | query Guild($guild_id: String!) {
118 | Guild(id: $guild_id) {
119 | id
120 | name
121 | features
122 | icon
123 | owner_id
124 | banner
125 | channels {
126 | id
127 | type
128 | name
129 | nsfw
130 | parent_id
131 | position
132 | }
133 | roles {
134 | color
135 | hoist
136 | id
137 | managed
138 | mentionable
139 | name
140 | permissions
141 | position
142 | }
143 | }
144 | }
145 | `,
146 | variables: {
147 | guild_id: id,
148 | },
149 | });
150 |
151 | const guild = data?.Guild as Guild;
152 |
153 | this.setState({ guild });
154 |
155 | return guild;
156 | }
157 |
158 | async fetchUserGuilds() {
159 | const { data, errors } = await this.client.api.query({
160 | query: gql`
161 | query UserGuilds($filter_guilds: Boolean) {
162 | CurrentUserGuilds(filter: $filter_guilds) {
163 | id
164 | name
165 | features
166 | icon
167 | owner
168 | permissions
169 | }
170 | }
171 | `,
172 | variables: {
173 | filter_guilds: true,
174 | }
175 | });
176 |
177 | const guilds = data.CurrentUserGuilds as AbstractGuild[];
178 |
179 | this.setState({
180 | user: { ...this.state.user, guilds },
181 | });
182 |
183 | return guilds;
184 | }
185 |
186 | render(): React.ReactNode {
187 | const {
188 | props: { children },
189 | state: { loading, user, guild },
190 | fetchUserGuilds,
191 | fetchGuild,
192 | checkGuild,
193 | } = this;
194 |
195 | return (
196 |
205 | {children}
206 |
207 | )
208 | }
209 | }
210 |
211 | export default APIContext;
--------------------------------------------------------------------------------
/src/styles/Sidebar.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 | .sidebar {
8 | position: fixed;
9 | top: 0;
10 | left: 0;
11 | height: 100%;
12 | width: 250px;
13 | padding: 10px 14px;
14 | background: var(--luny-background);
15 | transition: all 0.5s ease;
16 | z-index: 100;
17 | border-right: 1px solid themed('luny-ui', 15);
18 |
19 | header {
20 | position: relative;
21 |
22 | .container {
23 | display: flex;
24 | align-items: center;
25 |
26 | .profile {
27 | display: flex;
28 | align-items: center;
29 | width: 100%;
30 | border-radius: 6px;
31 | padding: 5px;
32 | border: 1px solid transparent;
33 | cursor: pointer;
34 | -webkit-touch-callout: none;
35 | -webkit-user-select: none;
36 | -khtml-user-select: none;
37 | -moz-user-select: none;
38 | -ms-user-select: none;
39 | user-select: none;
40 |
41 | &:hover {
42 | background-color: themed('luny-ui', 15);
43 | }
44 |
45 | &[data-opened] {
46 | padding: 4px -1px;
47 | background-color: themed('luny-ui', 5);
48 | border-color: themed('luny-band')
49 | }
50 |
51 | .image {
52 | display: flex;
53 | align-items: center;
54 | justify-content: center;
55 | min-width: 60px;
56 |
57 | img {
58 | width: 50px;
59 | border-radius: 15px;
60 | }
61 |
62 | div {
63 | text-align: center;
64 | border-radius: 100%;
65 | width: 50px;
66 | height: 50px;
67 | background-color: themed('luny-band', 80);
68 | overflow: hidden;
69 | text-overflow: ellipsis;
70 | line-height: 50px;
71 | color: themed('luny-text');
72 | }
73 | }
74 |
75 | .text {
76 | display: flex;
77 | flex-direction: column;
78 | max-width: 100%;
79 | white-space: nowrap;
80 | pointer-events: none;
81 | margin-left: 10px;
82 | color: themed('luny-text', 60);
83 |
84 | .name {
85 | margin-top: 2px;
86 | font-size: 14px;
87 | font-weight: 600;
88 | }
89 |
90 | .id {
91 | font-size: 10px;
92 | margin-top: -2px;
93 | display: block;
94 | }
95 |
96 | .name,
97 | .id {
98 | overflow: hidden;
99 | text-overflow: ellipsis;
100 | }
101 | }
102 | }
103 |
104 | section.serversMenuWrapper {
105 | display: none;
106 | position: absolute;
107 | float: left;
108 | top: 65px;
109 | background-color: var(--luny-backgroundSecondary);
110 | border: 1px solid themed('luny-ui', 15);
111 | border-radius: 10px;
112 | padding: 0;
113 | width: 100%;
114 | z-index: 1000;
115 | box-shadow: 0 0.25rem 0.5rem themed('luny-ui', 5);
116 | max-height: 300px;
117 | -webkit-touch-callout: none;
118 | -webkit-user-select: none;
119 | -khtml-user-select: none;
120 | -moz-user-select: none;
121 | -ms-user-select: none;
122 | user-select: none;
123 |
124 | .serversMenu {
125 | border-radius: 8px;
126 | width: 100%;
127 | max-height: 298px;
128 | overflow-y: auto;
129 | padding: 0 5px;
130 |
131 | hr {
132 | margin: 0;
133 | border: none;
134 | border-bottom: 1px solid themed('luny-ui', 15);
135 | }
136 |
137 | li {
138 | margin-top: 5px;
139 | margin-bottom: 5px;
140 | border-radius: 6px;
141 | max-width: 110%;
142 | overflow: hidden;
143 | text-overflow: ellipsis;
144 | cursor: pointer;
145 |
146 | .server {
147 | width: 110%;
148 | list-style: none;
149 | height: 100%;
150 | background-color: transparent;
151 | display: flex;
152 | align-items: center;
153 | height: 100%;
154 | width: 100%;
155 | border-radius: 6px;
156 | text-decoration: none;
157 | transition: all 0.3s ease;
158 | padding: 8px;
159 |
160 | .image {
161 | position: relative;
162 | left: 2.5px;
163 |
164 | div {
165 | color: white;
166 | text-align: center;
167 | border-radius: 100%;
168 | width: 36px;
169 | height: 36px;
170 | background-color: themed('luny-band', 80);
171 | overflow: hidden;
172 | text-overflow: ellipsis;
173 | line-height: 36px;
174 | }
175 |
176 | img {
177 | border-radius: 50%;
178 | width: 36px;
179 | height: 36px;
180 | }
181 | }
182 |
183 | .name {
184 | font-size: 13px;
185 | font-weight: 500;
186 | white-space: nowrap;
187 | opacity: 1;
188 | margin-left: 10px;
189 | color: themed('luny-text', 60);
190 | }
191 | }
192 |
193 | &[data-selected] {
194 | background-color: themed('luny-ui', 5);
195 | }
196 |
197 | &.invite {
198 | background-color: themed('luny-band', 15);
199 | }
200 |
201 | &:hover {
202 | background-color: themed('luny-ui', 15);
203 | }
204 | }
205 |
206 | &::-webkit-scrollbar-track {
207 | background: transparent;
208 | height: calc(100% - 10px);
209 | margin-top: 5px;
210 | margin-bottom: 5px;
211 | }
212 | }
213 |
214 | &[data-opened] {
215 | display: block;
216 |
217 | .serversMenu {
218 | background-color: themed('luny-ui', 15)
219 | }
220 | }
221 | }
222 | }
223 | }
224 |
225 | .links {
226 | margin-top: 5px;
227 | padding: 10px 0;
228 | overflow: auto;
229 | height: auto;
230 | flex-direction: column;
231 | justify-content: space-between;
232 | overflow-y: scroll;
233 |
234 | li {
235 | height: 50px;
236 | list-style: none;
237 | display: flex;
238 | align-items: center;
239 |
240 | &[data-selected] a {
241 | background-color: themed('luny-ui', 5);
242 |
243 | i {
244 | color: themed('luny-band', 60);
245 | }
246 | }
247 |
248 | span {
249 | color: themed('luny-text', 60);
250 | font-size: 17px;
251 | font-weight: 500;
252 | white-space: nowrap;
253 | opacity: 1;
254 | transition: all 0.3s ease;
255 | }
256 |
257 | a {
258 | transition: all 0.3s ease;
259 | list-style: none;
260 | height: 100%;
261 | background-color: transparent;
262 | display: flex;
263 | align-items: center;
264 | height: 100%;
265 | width: 100%;
266 | border-radius: 6px;
267 | text-decoration: none;
268 |
269 | &:hover {
270 | background-color: themed('luny-band');
271 | }
272 |
273 | i {
274 | color: themed('luny-text', 60);
275 | min-width: 60px;
276 | border-radius: 6px;
277 | height: 100%;
278 | display: flex;
279 | align-items: center;
280 | justify-content: center;
281 | font-size: 20px;
282 | transition: all 0.3s ease;
283 | }
284 |
285 | &:hover {
286 | i,
287 | span {
288 | color: themed('luny-flow')
289 | }
290 | }
291 | }
292 | }
293 |
294 | & {
295 | overflow-y: scroll;
296 | }
297 |
298 | &::-webkit-scrollbar {
299 | display: none;
300 | }
301 | }
302 | }
303 |
304 | .content {
305 | position: absolute;
306 | top: 0;
307 | left: 250px;
308 | height: 100vh;
309 | width: calc(100% - 250px);
310 | background-color: var(var(--luny-colors-background));
311 | transition: var(--tran-05);
312 | padding: 12px 60px;
313 | }
314 |
315 | .content main {
316 | margin-top: 25px;
317 | justify-content: center;
318 | display: flex;
319 | flex-wrap: wrap;
320 | background-position: center;
321 | background-size: cover;
322 | }
323 |
324 | @media screen and (max-width: 1024px) {
325 | .sidebar {
326 | display: none;
327 | }
328 |
329 | .content {
330 | width: 100%;
331 | left: 0;
332 | padding: 16px;
333 | }
334 | }
335 |
336 | // @media screen and (min-width: 1750px) {
337 | // .sidebar {
338 | // width: 350px;
339 | // }
340 |
341 | // .content {
342 | // width: calc(100% - 350px);
343 | // left: 350px;
344 | // }
345 |
346 | // .content main {
347 | // margin-left: auto;
348 | // margin-right: auto;
349 | // width: 80%;
350 | // }
351 | // }
--------------------------------------------------------------------------------