mirror of
https://github.com/koreanbots/core.git
synced 2025-12-15 06:10:22 +00:00
style: code style changes
This commit is contained in:
parent
9a36919657
commit
899f948fa6
30
.eslintrc.js
30
.eslintrc.js
@ -4,7 +4,7 @@ module.exports = {
|
|||||||
node: true,
|
node: true,
|
||||||
es6: true,
|
es6: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
es2021: true
|
es2021: true,
|
||||||
},
|
},
|
||||||
ignorePatterns: ['node_modules/*', '.next/*', '.out/*', '!.prettierrc.js'],
|
ignorePatterns: ['node_modules/*', '.next/*', '.out/*', '!.prettierrc.js'],
|
||||||
extends: [
|
extends: [
|
||||||
@ -12,20 +12,17 @@ module.exports = {
|
|||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
'plugin:jsx-a11y/recommended'
|
'plugin:jsx-a11y/recommended',
|
||||||
],
|
],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true
|
jsx: true,
|
||||||
},
|
},
|
||||||
ecmaVersion: 12,
|
ecmaVersion: 12,
|
||||||
sourceType: 'module'
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: ['react', '@typescript-eslint'],
|
||||||
'react',
|
|
||||||
'@typescript-eslint'
|
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
'jsx-quotes': ['error', 'prefer-single'],
|
'jsx-quotes': ['error', 'prefer-single'],
|
||||||
'react/no-unescaped-entities': 'off',
|
'react/no-unescaped-entities': 'off',
|
||||||
@ -36,17 +33,8 @@ module.exports = {
|
|||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': ['warn'],
|
'@typescript-eslint/no-unused-vars': ['warn'],
|
||||||
indent: [
|
indent: ['error', 'tab'],
|
||||||
'error',
|
quotes: ['error', 'single'],
|
||||||
'tab'
|
semi: ['error', 'never'],
|
||||||
],
|
},
|
||||||
quotes: [
|
|
||||||
'error',
|
|
||||||
'single'
|
|
||||||
],
|
|
||||||
semi: [
|
|
||||||
'error',
|
|
||||||
'never'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
14
.github/ISSUE_TEMPLATE/bug.md
vendored
14
.github/ISSUE_TEMPLATE/bug.md
vendored
@ -1,32 +1,38 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
name: 🐛 버그 제보
|
name: 🐛 버그 제보
|
||||||
about: 버그를 제보해주세요!
|
about: 버그를 제보해주세요!
|
||||||
title: "[버그] "
|
title: '[버그] '
|
||||||
labels: 'bug'
|
labels: 'bug'
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
---## 재현방법
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 재현방법
|
|
||||||
> 어떻게하면 발생시킬 수 있나요?
|
> 어떻게하면 발생시킬 수 있나요?
|
||||||
|
|
||||||
## 예상되는 정상적인 동작
|
## 예상되는 정상적인 동작
|
||||||
|
|
||||||
> 정상이라면 어떻게 되야하나요?
|
> 정상이라면 어떻게 되야하나요?
|
||||||
|
|
||||||
## 발생한 문제
|
## 발생한 문제
|
||||||
|
|
||||||
> 어떤 문제가 발생하나요?
|
> 어떤 문제가 발생하나요?
|
||||||
|
|
||||||
## 클라이언트 버전
|
## 클라이언트 버전
|
||||||
|
|
||||||
> 클라이언트 버전을 알려주세요!
|
> 클라이언트 버전을 알려주세요!
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
클라이언트 버전을 가져오실 줄 모르신다면 아래 링크를 참고해주세요
|
클라이언트 버전을 가져오실 줄 모르신다면 아래 링크를 참고해주세요
|
||||||
|
|
||||||
https://github.com/koreanbots/docs/blob/master/version.md
|
https://github.com/koreanbots/docs/blob/master/version.md
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 사양
|
## 사양
|
||||||
|
|
||||||
> OS 정보와 브라우저의 버전을 알려주세요!
|
> OS 정보와 브라우저의 버전을 알려주세요!
|
||||||
|
|
||||||
## 확인
|
## 확인
|
||||||
|
|
||||||
- [ ] 중복되는 이슈는 없나요?
|
- [ ] 중복되는 이슈는 없나요?
|
||||||
- [ ] 해당 버그를 다시 발생시킬 수 있나요?
|
- [ ] 해당 버그를 다시 발생시킬 수 있나요?
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/reviewdog.yml
vendored
2
.github/workflows/reviewdog.yml
vendored
@ -12,4 +12,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
reporter: github-pr-review
|
reporter: github-pr-review
|
||||||
reviewdog_version: latest
|
reviewdog_version: latest
|
||||||
eslint_flags: --ext js,jsx,ts,tsx .
|
eslint_flags: --ext js,jsx,ts,tsx .
|
||||||
|
|||||||
90
.github/workflows/testing.yml
vendored
90
.github/workflows/testing.yml
vendored
@ -7,57 +7,57 @@ jobs:
|
|||||||
name: ESLint
|
name: ESLint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: install node v14
|
- name: install node v14
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
- name: yarn install
|
- name: yarn install
|
||||||
run: yarn install
|
run: yarn install
|
||||||
- name: run eslint
|
- name: run eslint
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
test:
|
test:
|
||||||
name: Run Test
|
name: Run Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: install node v14
|
- name: install node v14
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
- name: yarn install
|
- name: yarn install
|
||||||
run: yarn install
|
run: yarn install
|
||||||
- name: run jest
|
- name: run jest
|
||||||
run: yarn test
|
run: yarn test
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: install node v14
|
- name: install node v14
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
- name: yarn install
|
- name: yarn install
|
||||||
run: yarn install
|
run: yarn install
|
||||||
- name: Generate RSA Key Pair
|
- name: Generate RSA Key Pair
|
||||||
run: |
|
run: |
|
||||||
ssh-keygen -b 2048 -t rsa -f key -q -P ""
|
ssh-keygen -b 2048 -t rsa -f key -q -P ""
|
||||||
ssh-keygen -b 2048 -e -m pem -f key -q -P "" > private.key
|
ssh-keygen -b 2048 -e -m pem -f key -q -P "" > private.key
|
||||||
mv key public.pem
|
mv key public.pem
|
||||||
rm key.pub
|
rm key.pub
|
||||||
- name: Setup environments
|
- name: Setup environments
|
||||||
run: |
|
run: |
|
||||||
mv .env.demo.local .env.production.local
|
mv .env.demo.local .env.production.local
|
||||||
printf 'MARIADB_ROOT_PASSWORD=YOUSHALLNOTPASS\nCOMMIT_HASH=${{ github.sha }}' > .env
|
printf 'MARIADB_ROOT_PASSWORD=YOUSHALLNOTPASS\nCOMMIT_HASH=${{ github.sha }}' > .env
|
||||||
- name: Create needed files
|
- name: Create needed files
|
||||||
run: echo '{"tester":"DEMO_KEY"}' > secret.json
|
run: echo '{"tester":"DEMO_KEY"}' > secret.json
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build
|
run: yarn build
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
# docker:
|
# docker:
|
||||||
# needs:
|
# needs:
|
||||||
# - eslint
|
# - eslint
|
||||||
@ -75,7 +75,7 @@ jobs:
|
|||||||
# run: |
|
# run: |
|
||||||
# ssh-keygen -b 2048 -t rsa -f key -q -P ""
|
# ssh-keygen -b 2048 -t rsa -f key -q -P ""
|
||||||
# ssh-keygen -b 2048 -e -m pem -f key -q -P "" > private.key
|
# ssh-keygen -b 2048 -e -m pem -f key -q -P "" > private.key
|
||||||
# mv key public.pem
|
# mv key public.pem
|
||||||
# rm key.pub
|
# rm key.pub
|
||||||
# - name: Setup environments
|
# - name: Setup environments
|
||||||
# run: |
|
# run: |
|
||||||
@ -84,4 +84,4 @@ jobs:
|
|||||||
# - name: Create needed files
|
# - name: Create needed files
|
||||||
# run: echo '{"tester":"DEMO_KEY"}' > secret.json
|
# run: echo '{"tester":"DEMO_KEY"}' > secret.json
|
||||||
# - name: Docker Compose
|
# - name: Docker Compose
|
||||||
# run: docker-compose up -d
|
# run: docker-compose up -d
|
||||||
|
|||||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.next/
|
||||||
|
node_modules/
|
||||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
109
app.css
109
app.css
@ -3,111 +3,114 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Uni Sans Heavy CAPS";
|
font-family: 'Uni Sans Heavy CAPS';
|
||||||
src: url("/logofont.otf");
|
src: url('/logofont.otf');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logofont {
|
.logofont {
|
||||||
font-family: "Uni Sans Heavy CAPS";
|
font-family: 'Uni Sans Heavy CAPS';
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-dropdown {
|
.animation-dropdown {
|
||||||
animation: dropdown 0.1s linear;
|
animation: dropdown 0.1s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iu-is-the-best {
|
.iu-is-the-best {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.__control--is-focused {
|
.__control--is-focused {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
width: 20px
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html * ::-webkit-scrollbar {
|
html * ::-webkit-scrollbar {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html * ::-webkit-scrollbar-thumb {
|
html * ::-webkit-scrollbar-thumb {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
-webkit-transition: color .2s ease;
|
-webkit-transition: color 0.2s ease;
|
||||||
transition: color .2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
html .dark * ::-webkit-scrollbar-thumb {
|
html .dark * ::-webkit-scrollbar-thumb {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #202225;
|
background: #202225;
|
||||||
-webkit-transition: color .2s ease;
|
-webkit-transition: color 0.2s ease;
|
||||||
transition: color .2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
html * ::-webkit-scrollbar-track {
|
html * ::-webkit-scrollbar-track {
|
||||||
background: #f2f2f2;
|
background: #f2f2f2;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: 4px solid transparent;
|
border: 4px solid transparent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html .dark * ::-webkit-scrollbar-track {
|
html .dark * ::-webkit-scrollbar-track {
|
||||||
background: #2e3338;
|
background: #2e3338;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .__multi-value, .dark .__multi-value__label, .dark .__multi-value__remove {
|
.dark .__multi-value,
|
||||||
background: #2e3338 !important;
|
.dark .__multi-value__label,
|
||||||
|
.dark .__multi-value__remove {
|
||||||
|
background: #2e3338 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-none {
|
.scroll-none {
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-none ::-webkit-scrollbar {
|
.scroll-none ::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-selector-button {
|
.emoji-selector-button {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-image: url("https://unpkg.com/emoji-datasource-twitter@5.0.1/img/twitter/sheets-256/64.png");
|
background-image: url('https://unpkg.com/emoji-datasource-twitter@5.0.1/img/twitter/sheets-256/64.png');
|
||||||
background-size: 5700% 5700%;
|
background-size: 5700% 5700%;
|
||||||
background-position: 53.5714% 62.5%;
|
background-position: 53.5714% 62.5%;
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-selector-button:hover {
|
.emoji-selector-button:hover {
|
||||||
filter: grayscale(0%);
|
filter: grayscale(0%);
|
||||||
transform: scale(1.1, 1.1);
|
transform: scale(1.1, 1.1);
|
||||||
opacity: 90%;
|
opacity: 90%;
|
||||||
transition: ease-in 100ms;
|
transition: ease-in 100ms;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-mart-category-list > *, .emoji-mart-emoji > span {
|
.emoji-mart-category-list > *,
|
||||||
cursor: pointer;
|
.emoji-mart-emoji > span {
|
||||||
}
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,33 +1,45 @@
|
|||||||
import Logger from '@utils/Logger'
|
import Logger from '@utils/Logger'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
const Advertisement = ({ size='short' }:AdvertisementProps): JSX.Element => {
|
const Advertisement = ({ size = 'short' }: AdvertisementProps): JSX.Element => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
window.adsbygoogle = window.adsbygoogle || []
|
window.adsbygoogle = window.adsbygoogle || []
|
||||||
window.adsbygoogle.push({})
|
window.adsbygoogle.push({})
|
||||||
}
|
}
|
||||||
Logger.debug('Ads Pushed')
|
Logger.debug('Ads Pushed')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div className={`z-0 mx-auto w-full text-center text-white ${process.env.NODE_ENV === 'production' ? '' : 'py-12 bg-gray-700'}`} style={size === 'short' ? { height: '90px' } : { height: '330px'}}>
|
return (
|
||||||
{
|
<div
|
||||||
process.env.NODE_ENV === 'production' ? <ins
|
className={`z-0 mx-auto w-full text-center text-white ${
|
||||||
className='adsbygoogle mb-5 w-full'
|
process.env.NODE_ENV === 'production' ? '' : 'py-12 bg-gray-700'
|
||||||
style={{ display: 'inline-block', height: '90px' }}
|
}`}
|
||||||
data-ad-client='ca-pub-4856582423981759'
|
style={size === 'short' ? { height: '90px' } : { height: '330px' }}
|
||||||
data-ad-slot='3250141451'
|
>
|
||||||
data-adtest='on'
|
{process.env.NODE_ENV === 'production' ? (
|
||||||
data-full-width-responsive='true'
|
<ins
|
||||||
></ins> : 'Advertisement'
|
className='adsbygoogle mb-5 w-full'
|
||||||
}</div>
|
style={{ display: 'inline-block', height: '90px' }}
|
||||||
|
data-ad-client='ca-pub-4856582423981759'
|
||||||
|
data-ad-slot='3250141451'
|
||||||
|
data-adtest='on'
|
||||||
|
data-full-width-responsive='true'
|
||||||
|
></ins>
|
||||||
|
) : (
|
||||||
|
'Advertisement'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window { adsbygoogle: {
|
interface Window {
|
||||||
loaded?: boolean
|
adsbygoogle: {
|
||||||
push(obj: unknown): void
|
loaded?: boolean
|
||||||
} }
|
push(obj: unknown): void
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdvertisementProps {
|
interface AdvertisementProps {
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import DiscordAvatar from './DiscordAvatar'
|
import DiscordAvatar from './DiscordAvatar'
|
||||||
|
|
||||||
const Application = ({ type, id, name }:ApplicationProps):JSX.Element => {
|
const Application = ({ type, id, name }: ApplicationProps): JSX.Element => {
|
||||||
return <Link href={`/developers/applications/${type+'s'}/${id}`}>
|
return (
|
||||||
<div className='relative py-4 px-2 bg-little-white dark:bg-discord-black text-center transform hover:-translate-y-1 transition duration-100 ease-in cursor-pointer rounded-lg'>
|
<Link href={`/developers/applications/${type + 's'}/${id}`}>
|
||||||
<DiscordAvatar userID={id} className='w-full rounded-xl px-2' />
|
<div className='relative px-2 py-4 text-center dark:bg-discord-black bg-little-white rounded-lg cursor-pointer transform hover:-translate-y-1 transition duration-100 ease-in'>
|
||||||
<h2 className='text-xl font-medium pt-2 whitespace-nowrap truncate'>{name}</h2>
|
<DiscordAvatar userID={id} className='px-2 w-full rounded-xl' />
|
||||||
</div>
|
<h2 className='pt-2 whitespace-nowrap text-xl font-medium truncate'>{name}</h2>
|
||||||
</Link>
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApplicationProps {
|
interface ApplicationProps {
|
||||||
type: 'bot'
|
type: 'bot'
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Application
|
export default Application
|
||||||
|
|||||||
@ -8,19 +8,34 @@ import Divider from '@components/Divider'
|
|||||||
import Tag from '@components/Tag'
|
import Tag from '@components/Tag'
|
||||||
import DiscordAvatar from '@components/DiscordAvatar'
|
import DiscordAvatar from '@components/DiscordAvatar'
|
||||||
|
|
||||||
const BotCard = ({ manage=false, bot }: BotProps): JSX.Element => {
|
const BotCard = ({ manage = false, bot }: BotProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className='container mb-16 transform hover:-translate-y-1 transition duration-100 ease-in'>
|
<div className='container mb-16 transform hover:-translate-y-1 transition duration-100 ease-in'>
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='h-full'>
|
<div className='h-full'>
|
||||||
<div className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl' style={checkBotFlag(bot.flags, 'trusted') && bot.banner ? { background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${bot.banner}") center top / cover no-repeat`, color: 'white' } : {}}>
|
<div
|
||||||
|
className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl'
|
||||||
|
style={
|
||||||
|
checkBotFlag(bot.flags, 'trusted') && bot.banner
|
||||||
|
? {
|
||||||
|
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${bot.banner}") center top / cover no-repeat`,
|
||||||
|
color: 'white',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
<Link href={makeBotURL(bot)}>
|
<Link href={makeBotURL(bot)}>
|
||||||
<a className='cursor-pointer'>
|
<a className='cursor-pointer'>
|
||||||
<div className='flex h-44'>
|
<div className='flex h-44'>
|
||||||
<div className='w-2/3'>
|
<div className='w-2/3'>
|
||||||
<div className='flex justify-start'>
|
<div className='flex justify-start'>
|
||||||
<DiscordAvatar size={128} userID={bot.id} alt='Avatar' className='rounded-full absolute -left-2 -top-8 mx-auto w-32 h-32 bg-white'/>
|
<DiscordAvatar
|
||||||
|
size={128}
|
||||||
|
userID={bot.id}
|
||||||
|
alt='Avatar'
|
||||||
|
className='absolute -left-2 -top-8 mx-auto w-32 h-32 bg-white rounded-full'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-28 px-4'>
|
<div className='mt-28 px-4'>
|
||||||
@ -28,9 +43,7 @@ const BotCard = ({ manage=false, bot }: BotProps): JSX.Element => {
|
|||||||
<i className={`fas fa-circle text-${Status[bot.status]?.color}`} />
|
<i className={`fas fa-circle text-${Status[bot.status]?.color}`} />
|
||||||
{Status[bot.status]?.text}
|
{Status[bot.status]?.text}
|
||||||
</h2>
|
</h2>
|
||||||
<h1 className='mb-3 text-left text-2xl font-bold truncate'>
|
<h1 className='mb-3 text-left text-2xl font-bold truncate'>{bot.name}</h1>
|
||||||
{bot.name}
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid grid-cols-1 pr-5 py-5 w-1/3 h-0'>
|
<div className='grid grid-cols-1 pr-5 py-5 w-1/3 h-0'>
|
||||||
@ -42,43 +55,53 @@ const BotCard = ({ manage=false, bot }: BotProps): JSX.Element => {
|
|||||||
}
|
}
|
||||||
dark
|
dark
|
||||||
/>
|
/>
|
||||||
<Tag blurple text={bot.servers ? <>{formatNumber(bot.servers)} 서버</> : 'N/A'} dark />
|
<Tag
|
||||||
|
blurple
|
||||||
|
text={bot.servers ? <>{formatNumber(bot.servers)} 서버</> : 'N/A'}
|
||||||
|
dark
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className='px-4 text-left text-gray-400 text-sm font-medium mb-10 h-6'>{bot.intro}</p>
|
<p className='mb-10 px-4 h-6 text-left text-gray-400 text-sm font-medium'>
|
||||||
<div className='category px-2 flex flex-wrap'>
|
{bot.intro}
|
||||||
|
</p>
|
||||||
|
<div className='category flex flex-wrap px-2'>
|
||||||
{bot.category.slice(0, 3).map(el => (
|
{bot.category.slice(0, 3).map(el => (
|
||||||
<Tag key={el} text={el} href={`/categories/${el}`} dark/>
|
<Tag key={el} text={el} href={`/categories/${el}`} dark />
|
||||||
))} {
|
))}{' '}
|
||||||
bot.category.length > 3 && <Tag text={`+${bot.category.length - 3}`} dark />
|
{bot.category.length > 3 && <Tag text={`+${bot.category.length - 3}`} dark />}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className='flex justify-evenly'>
|
<div className='flex justify-evenly'>
|
||||||
<Link
|
<Link href={makeBotURL(bot)}>
|
||||||
href={makeBotURL(bot)}
|
<a className='py-3 w-full text-center text-koreanbots-blue hover:text-white text-sm font-bold hover:bg-koreanbots-blue rounded-bl-2xl hover:shadow-lg transition duration-100 ease-in'>
|
||||||
>
|
|
||||||
<a className='rounded-bl-2xl py-3 w-full text-center text-koreanbots-blue hover:text-white text-sm font-bold hover:bg-koreanbots-blue hover:shadow-lg transition duration-100 ease-in'>
|
|
||||||
보기
|
보기
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{
|
{manage ? (
|
||||||
manage ? <Link href={`/manage/${bot.id}`}>
|
<Link href={`/manage/${bot.id}`}>
|
||||||
<a
|
<a className='py-3 w-full text-center text-green-500 hover:text-white text-sm font-bold hover:bg-green-500 rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'>
|
||||||
className='rounded-br-2xl py-3 w-full text-center text-green-500 hover:text-white text-sm font-bold hover:bg-green-500 hover:shadow-lg transition duration-100 ease-in'
|
관리하기
|
||||||
>
|
|
||||||
관리하기
|
|
||||||
</a>
|
|
||||||
</Link> : <Link href={bot.url || `https://discordapp.com/oauth2/authorize?client_id=${bot.id}&scope=bot&permissions=0`}>
|
|
||||||
<a rel='noopener noreferrer' target='_blank'
|
|
||||||
className='rounded-br-2xl py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple hover:shadow-lg transition duration-100 ease-in'
|
|
||||||
>
|
|
||||||
초대하기
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
}
|
) : (
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
bot.url ||
|
||||||
|
`https://discordapp.com/oauth2/authorize?client_id=${bot.id}&scope=bot&permissions=0`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
className='py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'
|
||||||
|
>
|
||||||
|
초대하기
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,24 +1,49 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
const Button = ({ type='button', className, children, href, onClick }: ButtonProps):JSX.Element => {
|
const Button = ({
|
||||||
return href ? <Link href={href}>
|
type = 'button',
|
||||||
<a className={`cursor-pointer rounded-md px-4 py-2 m-1 transition duration-300 ease select-none outline-none foucs:outline-none ${className ?? 'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}>
|
className,
|
||||||
{ children }
|
children,
|
||||||
</a>
|
href,
|
||||||
</Link> : onClick ? <button type={type} onKeyDown={onClick} onClick={onClick} className={`cursor-pointer rounded-md px-4 py-2 m-0.5 transition duration-300 ease select-none outline-none foucs:outline-none ${className ?? 'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}>
|
onClick,
|
||||||
{ children }
|
}: ButtonProps): JSX.Element => {
|
||||||
</button> : <button type={type} className={`cursor-pointer rounded-md px-4 py-2 m-0.5 transition duration-300 ease select-none outline-none foucs:outline-none ${className ?? 'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}>
|
return href ? (
|
||||||
{ children }
|
<Link href={href}>
|
||||||
</button>
|
<a
|
||||||
|
className={`cursor-pointer rounded-md px-4 py-2 m-1 transition duration-300 ease select-none outline-none foucs:outline-none ${className ??
|
||||||
|
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
) : onClick ? (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
onKeyDown={onClick}
|
||||||
|
onClick={onClick}
|
||||||
|
className={`cursor-pointer rounded-md px-4 py-2 m-0.5 transition duration-300 ease select-none outline-none foucs:outline-none ${className ??
|
||||||
|
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
className={`cursor-pointer rounded-md px-4 py-2 m-0.5 transition duration-300 ease select-none outline-none foucs:outline-none ${className ??
|
||||||
|
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
type?: 'button' | 'submit' | 'reset'
|
type?: 'button' | 'submit' | 'reset'
|
||||||
className?: string
|
className?: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
href?: string
|
href?: string
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Button
|
export default Button
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
const ColorCard = ({ header, first, second, className }:ColorCardProps):JSX.Element => {
|
const ColorCard = ({ header, first, second, className }: ColorCardProps): JSX.Element => {
|
||||||
return <div className={`rounded-lg p-10 ${className} shadow-lg`}>
|
return (
|
||||||
<h2 className='text-2xl font-bold'>{header}</h2>
|
<div className={`rounded-lg p-10 ${className} shadow-lg`}>
|
||||||
<p className='opacity-80'>
|
<h2 className='text-2xl font-bold'>{header}</h2>
|
||||||
{first} <br/>
|
<p className='opacity-80'>
|
||||||
{second}
|
{first} <br />
|
||||||
</p>
|
{second}
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColorCardProps {
|
interface ColorCardProps {
|
||||||
header: string
|
header: string
|
||||||
first: string
|
first: string
|
||||||
second: string
|
second: string
|
||||||
className: string
|
className: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ColorCard
|
export default ColorCard
|
||||||
|
|||||||
@ -8,9 +8,9 @@ const Container = ({
|
|||||||
}: ContainerProps): JSX.Element => {
|
}: ContainerProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${ignoreColor ? '' : 'text-black dark:text-gray-100'} ${
|
||||||
ignoreColor ? '' : 'text-black dark:text-gray-100'
|
paddingTop ? 'pt-20' : ''
|
||||||
} ${paddingTop ? 'pt-20' : ''}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`container mx-auto px-4 ${className}`}>{children}</div>
|
<div className={`container mx-auto px-4 ${className}`}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,10 @@ import SEO from './SEO'
|
|||||||
const Docs = ({ title, header, description, subheader, children }: DocsProps): JSX.Element => {
|
const Docs = ({ title, header, description, subheader, children }: DocsProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEO title={typeof header === 'string' ? header : title} description={description || subheader} />
|
<SEO
|
||||||
|
title={typeof header === 'string' ? header : title}
|
||||||
|
description={description || subheader}
|
||||||
|
/>
|
||||||
<div className='dark:bg-discord-black bg-discord-blurple'>
|
<div className='dark:bg-discord-black bg-discord-blurple'>
|
||||||
<Container className='pb-10 pt-20' ignoreColor>
|
<Container className='pb-10 pt-20' ignoreColor>
|
||||||
<h1 className='mt-10 text-center text-gray-100 text-4xl font-bold sm:text-left'>
|
<h1 className='mt-10 text-center text-gray-100 text-4xl font-bold sm:text-left'>
|
||||||
@ -21,7 +24,7 @@ const Docs = ({ title, header, description, subheader, children }: DocsProps): J
|
|||||||
</div>
|
</div>
|
||||||
<Wave
|
<Wave
|
||||||
color='currentColor'
|
color='currentColor'
|
||||||
className='dark:text-discord-black text-discord-blurple dark:bg-discord-dark bg-white hidden md:block'
|
className='hidden dark:text-discord-black text-discord-blurple dark:bg-discord-dark bg-white md:block'
|
||||||
/>
|
/>
|
||||||
<Container>
|
<Container>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
|
|||||||
@ -5,10 +5,14 @@ import Wave from '@components/Wave'
|
|||||||
import Toggle from './Toggle'
|
import Toggle from './Toggle'
|
||||||
import { Theme } from '@types'
|
import { Theme } from '@types'
|
||||||
|
|
||||||
const Footer = ({ color, theme, setTheme }:FooterProps): JSX.Element => {
|
const Footer = ({ color, theme, setTheme }: FooterProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className='releative z-30'>
|
<div className='releative z-30'>
|
||||||
<Wave color='currentColor' className={`${color ?? 'dark:text-discord-dark text-white bg-discord-black'} hidden md:block`} />
|
<Wave
|
||||||
|
color='currentColor'
|
||||||
|
className={`${color ??
|
||||||
|
'dark:text-discord-dark text-white bg-discord-black'} hidden md:block`}
|
||||||
|
/>
|
||||||
<div className='bottom-0 text-white bg-discord-black'>
|
<div className='bottom-0 text-white bg-discord-black'>
|
||||||
<Container className='pb-20 pt-10 w-11/12 lg:flex lg:pt-0 lg:w-4/5' ignoreColor>
|
<Container className='pb-20 pt-10 w-11/12 lg:flex lg:pt-0 lg:w-4/5' ignoreColor>
|
||||||
<div className='w-full md:w-2/5'>
|
<div className='w-full md:w-2/5'>
|
||||||
@ -28,8 +32,8 @@ const Footer = ({ color, theme, setTheme }:FooterProps): JSX.Element => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-grow grid grid-cols-2 md:grid-cols-7 gap-2'>
|
<div className='grid flex-grow gap-2 grid-cols-2 md:grid-cols-7'>
|
||||||
<div className='mb-2 col-span-2'>
|
<div className='col-span-2 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>한국 디스코드봇 리스트</h2>
|
<h2 className='text-koreanbots-blue text-base font-bold'>한국 디스코드봇 리스트</h2>
|
||||||
<ul className='text-sm'>
|
<ul className='text-sm'>
|
||||||
<li>
|
<li>
|
||||||
@ -44,7 +48,7 @@ const Footer = ({ color, theme, setTheme }:FooterProps): JSX.Element => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-2 col-span-1'>
|
<div className='col-span-1 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>커뮤니티</h2>
|
<h2 className='text-koreanbots-blue text-base font-bold'>커뮤니티</h2>
|
||||||
<ul className='text-sm'>
|
<ul className='text-sm'>
|
||||||
<li>
|
<li>
|
||||||
@ -59,7 +63,7 @@ const Footer = ({ color, theme, setTheme }:FooterProps): JSX.Element => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-2 col-span-1'>
|
<div className='col-span-1 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>정책</h2>
|
<h2 className='text-koreanbots-blue text-base font-bold'>정책</h2>
|
||||||
<ul className='text-sm'>
|
<ul className='text-sm'>
|
||||||
<li>
|
<li>
|
||||||
@ -79,19 +83,21 @@ const Footer = ({ color, theme, setTheme }:FooterProps): JSX.Element => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-2 col-span-2'>
|
<div className='col-span-2 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>기타</h2>
|
<h2 className='text-koreanbots-blue text-base font-bold'>기타</h2>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<a className='hover:text-gray-300 mr-2'>다크모드</a>
|
<a className='mr-2 hover:text-gray-300'>다크모드</a>
|
||||||
<Toggle checked={theme === 'dark'} onChange={() => {
|
<Toggle
|
||||||
const t = theme === 'dark' ? 'light' : 'dark'
|
checked={theme === 'dark'}
|
||||||
setTheme(t)
|
onChange={() => {
|
||||||
localStorage.setItem('theme', t)
|
const t = theme === 'dark' ? 'light' : 'dark'
|
||||||
}} />
|
setTheme(t)
|
||||||
|
localStorage.setItem('theme', t)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { Field } from 'formik'
|
import { Field } from 'formik'
|
||||||
|
|
||||||
const CheckBox = ({ name, ...props }:CheckBoxProps):JSX.Element => {
|
const CheckBox = ({ name, ...props }: CheckBoxProps): JSX.Element => {
|
||||||
return <Field type='checkbox' name={name} className='mr-1 h-4 w-4 rounded' {...props}/>
|
return <Field type='checkbox' name={name} className='mr-1 w-4 h-4 rounded' {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CheckBoxProps {
|
interface CheckBoxProps {
|
||||||
name: string
|
name: string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CheckBox
|
export default CheckBox
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Field } from 'formik'
|
import { Field } from 'formik'
|
||||||
|
|
||||||
const CsrfToken = ({ token }:CsrfTokenProps):JSX.Element => {
|
const CsrfToken = ({ token }: CsrfTokenProps): JSX.Element => {
|
||||||
return <Field name='_csrf' hidden value={token} readOnly />
|
return <Field name='_csrf' hidden value={token} readOnly />
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CsrfTokenProps {
|
interface CsrfTokenProps {
|
||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CsrfToken
|
export default CsrfToken
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
import { Field } from 'formik'
|
import { Field } from 'formik'
|
||||||
|
|
||||||
const Input = ({ name, placeholder }:InputProps):JSX.Element => {
|
const Input = ({ name, placeholder }: InputProps): JSX.Element => {
|
||||||
return <Field name={name} className='border border-grey-light dark:border-transparent text-black dark:bg-very-black dark:text-white w-full h-10 rounded px-3 relative outline-none' placeholder={placeholder}/>
|
return (
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
className='border-grey-light relative px-3 w-full h-10 text-black dark:text-white dark:bg-very-black border dark:border-transparent rounded outline-none'
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputProps {
|
interface InputProps {
|
||||||
name: string
|
name: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Input
|
export default Input
|
||||||
|
|||||||
@ -1,33 +1,48 @@
|
|||||||
const Label = ({ For, children, label, labelDesc, error=null, grid=true, short=false, required=false }:LabelProps):JSX.Element => {
|
const Label = ({
|
||||||
return <>
|
For,
|
||||||
<label className={grid ? 'grid grid-cols-1 xl:grid-cols-4 gap-2 my-4' : 'inline-flex items-center'} htmlFor={For}>
|
children,
|
||||||
{
|
label,
|
||||||
label && <div className='col-span-1 text-sm'>
|
labelDesc,
|
||||||
<h3 className='text-lg font-bold text-discord-blurple'>{label}
|
error = null,
|
||||||
{
|
grid = true,
|
||||||
required && <span className='text-base font-semibold align-text-top text-red-500'> *</span>
|
short = false,
|
||||||
}
|
required = false,
|
||||||
</h3>
|
}: LabelProps): JSX.Element => {
|
||||||
{labelDesc}
|
return (
|
||||||
|
<>
|
||||||
|
<label
|
||||||
|
className={grid ? 'grid grid-cols-1 xl:grid-cols-4 gap-2 my-4' : 'inline-flex items-center'}
|
||||||
|
htmlFor={For}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<div className='col-span-1 text-sm'>
|
||||||
|
<h3 className='text-discord-blurple text-lg font-bold'>
|
||||||
|
{label}
|
||||||
|
{required && (
|
||||||
|
<span className='align-text-top text-red-500 text-base font-semibold'> *</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
{labelDesc}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={short ? 'col-span-1' : 'col-span-3'}>
|
||||||
|
{children}
|
||||||
|
<div className='mt-1 text-red-500 text-xs font-light'>{error}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</label>
|
||||||
<div className={short ? 'col-span-1' : 'col-span-3'}>
|
</>
|
||||||
{children}
|
)
|
||||||
<div className='text-red-500 text-xs font-light mt-1'>{error}</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LabelProps {
|
interface LabelProps {
|
||||||
For: string
|
For: string
|
||||||
children: JSX.Element | JSX.Element[]
|
children: JSX.Element | JSX.Element[]
|
||||||
label?: string
|
label?: string
|
||||||
labelDesc?: string | JSX.Element
|
labelDesc?: string | JSX.Element
|
||||||
error?: string | null
|
error?: string | null
|
||||||
grid?: boolean
|
grid?: boolean
|
||||||
short?: boolean
|
short?: boolean
|
||||||
required?: boolean
|
required?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Label
|
export default Label
|
||||||
|
|||||||
@ -1,28 +1,43 @@
|
|||||||
import ReactSelect from 'react-select'
|
import ReactSelect from 'react-select'
|
||||||
|
|
||||||
const Select = ({ placeholder, options, handleChange, handleTouch }:SelectProps):JSX.Element => {
|
const Select = ({ placeholder, options, handleChange, handleTouch }: SelectProps): JSX.Element => {
|
||||||
return <ReactSelect styles={{
|
return (
|
||||||
control: (provided) => {
|
<ReactSelect
|
||||||
return { ...provided, border: 'none' }
|
styles={{
|
||||||
},
|
control: provided => {
|
||||||
option: (provided) => {
|
return { ...provided, border: 'none' }
|
||||||
return { ...provided, cursor: 'pointer', ':hover': {
|
},
|
||||||
opacity: '0.7'
|
option: provided => {
|
||||||
} }
|
return {
|
||||||
}
|
...provided,
|
||||||
}} className='border border-grey-light dark:border-transparent rounded' classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white ' placeholder={placeholder || '선택해주세요.'} options={options} onChange={handleChange} onBlur={handleTouch} noOptionsMessage={() => '검색 결과가 없습니다.'}/>
|
cursor: 'pointer',
|
||||||
|
':hover': {
|
||||||
|
opacity: '0.7',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
className='border-grey-light border dark:border-transparent rounded'
|
||||||
|
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white '
|
||||||
|
placeholder={placeholder || '선택해주세요.'}
|
||||||
|
options={options}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleTouch}
|
||||||
|
noOptionsMessage={() => '검색 결과가 없습니다.'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectProps {
|
interface SelectProps {
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
handleChange: (value: Option) => void
|
handleChange: (value: Option) => void
|
||||||
handleTouch: () => void
|
handleTouch: () => void
|
||||||
options: Option[]
|
options: Option[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
value: string
|
value: string
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Select
|
export default Select
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
const Loader = ({ text, visible=true }:LoaderProps):JSX.Element => {
|
const Loader = ({ text, visible = true }: LoaderProps): JSX.Element => {
|
||||||
return <div className={`${visible ? '' : 'hidden '}w-full h-full fixed block top-0 left-0 bg-gray-500 bg-opacity-75 z-50 dark:text-black`}>
|
return (
|
||||||
<h1 className='text-2xl font-semibold opacity-100 top-1/2 my-0 mx-auto block relative text-center'>
|
<div
|
||||||
{ text }
|
className={`${
|
||||||
</h1>
|
visible ? '' : 'hidden '
|
||||||
</div>
|
}w-full h-full fixed block top-0 left-0 bg-gray-500 bg-opacity-75 z-50 dark:text-black`}
|
||||||
|
>
|
||||||
|
<h1 className='relative top-1/2 block mx-auto my-0 text-center text-2xl font-semibold opacity-100'>
|
||||||
|
{text}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoaderProps {
|
interface LoaderProps {
|
||||||
text: string
|
text: string
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Loader
|
export default Loader
|
||||||
|
|||||||
@ -3,26 +3,107 @@ import MarkdownView from 'react-showdown'
|
|||||||
import sanitizeHtml from 'sanitize-html'
|
import sanitizeHtml from 'sanitize-html'
|
||||||
import Emoji from 'node-emoji'
|
import Emoji from 'node-emoji'
|
||||||
|
|
||||||
const Markdown = ({ text }:MarkdownProps):JSX.Element => {
|
const Markdown = ({ text }: MarkdownProps): JSX.Element => {
|
||||||
return <div className='w-full markdown-body'>
|
return (
|
||||||
<MarkdownView markdown={Emoji.emojify(text)} extensions={[ twemoji, customEmoji, anchorHeader ]} options={{ openLinksInNewWindow: true, underline: true, omitExtraWLInCodeBlocks: true, literalMidWordUnderscores: true, simplifiedAutoLink: true, tables: true, strikethrough: true, smoothLivePreview: true, tasklists: true, ghCompatibleHeaderId: true, encodeEmails: true }} sanitizeHtml={(html)=> sanitizeHtml(html, {
|
<div className='markdown-body w-full'>
|
||||||
allowedTags: [
|
<MarkdownView
|
||||||
'addr', 'address', 'article', 'aside', 'h1', 'h2', 'h3', 'h4',
|
markdown={Emoji.emojify(text)}
|
||||||
'h5', 'h6', 'section', 'blockquote', 'dd', 'div',
|
extensions={[twemoji, customEmoji, anchorHeader]}
|
||||||
'dl', 'dt', 'hr', 'li', 'ol', 'p', 'pre',
|
options={{
|
||||||
'ul', 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn',
|
openLinksInNewWindow: true,
|
||||||
'em', 'i', 'kbd', 'mark', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp',
|
underline: true,
|
||||||
'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr', 'caption',
|
omitExtraWLInCodeBlocks: true,
|
||||||
'col', 'colgroup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'del',
|
literalMidWordUnderscores: true,
|
||||||
'img', 'svg', 'path', 'input'
|
simplifiedAutoLink: true,
|
||||||
],
|
tables: true,
|
||||||
allowedAttributes: false
|
strikethrough: true,
|
||||||
})} />
|
smoothLivePreview: true,
|
||||||
</div>
|
tasklists: true,
|
||||||
|
ghCompatibleHeaderId: true,
|
||||||
|
encodeEmails: true,
|
||||||
|
}}
|
||||||
|
sanitizeHtml={html =>
|
||||||
|
sanitizeHtml(html, {
|
||||||
|
allowedTags: [
|
||||||
|
'addr',
|
||||||
|
'address',
|
||||||
|
'article',
|
||||||
|
'aside',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'section',
|
||||||
|
'blockquote',
|
||||||
|
'dd',
|
||||||
|
'div',
|
||||||
|
'dl',
|
||||||
|
'dt',
|
||||||
|
'hr',
|
||||||
|
'li',
|
||||||
|
'ol',
|
||||||
|
'p',
|
||||||
|
'pre',
|
||||||
|
'ul',
|
||||||
|
'a',
|
||||||
|
'abbr',
|
||||||
|
'b',
|
||||||
|
'bdi',
|
||||||
|
'bdo',
|
||||||
|
'br',
|
||||||
|
'cite',
|
||||||
|
'code',
|
||||||
|
'data',
|
||||||
|
'dfn',
|
||||||
|
'em',
|
||||||
|
'i',
|
||||||
|
'kbd',
|
||||||
|
'mark',
|
||||||
|
'q',
|
||||||
|
'rb',
|
||||||
|
'rp',
|
||||||
|
'rt',
|
||||||
|
'rtc',
|
||||||
|
'ruby',
|
||||||
|
's',
|
||||||
|
'samp',
|
||||||
|
'small',
|
||||||
|
'span',
|
||||||
|
'strong',
|
||||||
|
'sub',
|
||||||
|
'sup',
|
||||||
|
'time',
|
||||||
|
'u',
|
||||||
|
'var',
|
||||||
|
'wbr',
|
||||||
|
'caption',
|
||||||
|
'col',
|
||||||
|
'colgroup',
|
||||||
|
'table',
|
||||||
|
'tbody',
|
||||||
|
'td',
|
||||||
|
'tfoot',
|
||||||
|
'th',
|
||||||
|
'thead',
|
||||||
|
'tr',
|
||||||
|
'del',
|
||||||
|
'img',
|
||||||
|
'svg',
|
||||||
|
'path',
|
||||||
|
'input',
|
||||||
|
],
|
||||||
|
allowedAttributes: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MarkdownProps {
|
interface MarkdownProps {
|
||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Markdown
|
export default Markdown
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
import { MessageColor } from '@utils/Constants'
|
import { MessageColor } from '@utils/Constants'
|
||||||
|
|
||||||
const Message = ({ type, children }:MessageProps):JSX.Element => {
|
const Message = ({ type, children }: MessageProps): JSX.Element => {
|
||||||
return <div className={`${MessageColor[type]} px-6 py-4 rounded-md text-base mx-auto w-full text-left`}>
|
return (
|
||||||
{children}
|
<div
|
||||||
</div>
|
className={`${MessageColor[type]} px-6 py-4 rounded-md text-base mx-auto w-full text-left`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageProps{
|
interface MessageProps {
|
||||||
type?: 'success' | 'error' | 'warning' | 'info'
|
type?: 'success' | 'error' | 'warning' | 'info'
|
||||||
children: JSX.Element | JSX.Element[] | string
|
children: JSX.Element | JSX.Element[] | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Message
|
export default Message
|
||||||
|
|||||||
@ -1,35 +1,40 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { Modal as ReactModal} from 'react-responsive-modal'
|
import { Modal as ReactModal } from 'react-responsive-modal'
|
||||||
import 'react-responsive-modal/styles.css'
|
import 'react-responsive-modal/styles.css'
|
||||||
|
|
||||||
const Modal = ({ children, isOpen, onClose, dark, header }:ModalProps):JSX.Element => {
|
const Modal = ({ children, isOpen, onClose, dark, header }: ModalProps): JSX.Element => {
|
||||||
return <ReactModal open={isOpen} onClose={onClose} center animationDuration={100} classNames={{
|
return (
|
||||||
modal: 'bg-discord-dark'
|
<ReactModal
|
||||||
}}
|
open={isOpen}
|
||||||
showCloseIcon={false}
|
onClose={onClose}
|
||||||
styles={{
|
center
|
||||||
modal: {
|
animationDuration={100}
|
||||||
borderRadius: '10px',
|
classNames={{
|
||||||
background: dark ? '#2C2F33' : '#fbfbfb',
|
modal: 'bg-discord-dark',
|
||||||
color: dark ? 'white' : 'black'
|
}}
|
||||||
}
|
showCloseIcon={false}
|
||||||
}}
|
styles={{
|
||||||
>
|
modal: {
|
||||||
<h2 className='text-lg font-black uppercase'>{header}</h2>
|
borderRadius: '10px',
|
||||||
<div className='pt-4 relative'>
|
background: dark ? '#2C2F33' : '#fbfbfb',
|
||||||
<div>
|
color: dark ? 'white' : 'black',
|
||||||
{children}
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 className='text-lg font-black uppercase'>{header}</h2>
|
||||||
|
<div className='relative pt-4'>
|
||||||
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ReactModal>
|
||||||
</ReactModal>
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
dark: boolean
|
dark: boolean
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
header?: string
|
header?: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
onClose(): void
|
onClose(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Modal
|
export default Modal
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
const Notice = ({ header, desc }:NoticeProps) => {
|
const Notice = ({ header, desc }: NoticeProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='py-48 px-10 mx-auto my-auto h-screen text-center'>
|
<div className='mx-auto my-auto px-10 py-48 h-screen text-center'>
|
||||||
<h1 className='text-4xl font-bold'>KOREANBOTS</h1>
|
<h1 className='text-4xl font-bold'>KOREANBOTS</h1>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<h1 className='text-3xl font-bold mb-10'>{header}</h1>
|
<h1 className='mb-10 text-3xl font-bold'>{header}</h1>
|
||||||
|
|
||||||
<h2 className='text-lg font-semibold'>{desc}</h2>
|
<h2 className='text-lg font-semibold'>{desc}</h2>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
@ -18,4 +18,4 @@ export default Notice
|
|||||||
interface NoticeProps {
|
interface NoticeProps {
|
||||||
header: string
|
header: string
|
||||||
desc: string
|
desc: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,26 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import DiscordAvatar from '@components/DiscordAvatar'
|
import DiscordAvatar from '@components/DiscordAvatar'
|
||||||
|
|
||||||
const Owner = ({ id, username, tag }:OwnerProps):JSX.Element => {
|
const Owner = ({ id, username, tag }: OwnerProps): JSX.Element => {
|
||||||
return <Link href={`/users/${id}`}>
|
return (
|
||||||
<a className='text-base bg-little-white dark:bg-discord-black text-black dark:text-gray-400 rounded flex hover:bg-little-white-hover dark:hover:bg-discord-dark-hover cursor-pointer px-4 py-4 mb-1'>
|
<Link href={`/users/${id}`}>
|
||||||
<div className='rounded-full h-8 w-8 flex-shrink-0 mr-3 mt-1 overflow-hidden shadow-inner relative'>
|
<a className='dark:hover:bg-discord-dark-hover flex mb-1 px-4 py-4 text-black dark:text-gray-400 text-base dark:bg-discord-black bg-little-white hover:bg-little-white-hover rounded cursor-pointer'>
|
||||||
<DiscordAvatar userID={id} className='absolute inset-0 z-negative w-full h-full'/>
|
<div className='relative flex-shrink-0 mr-3 mt-1 w-8 h-8 rounded-full shadow-inner overflow-hidden'>
|
||||||
</div>
|
<DiscordAvatar userID={id} className='z-negative absolute inset-0 w-full h-full' />
|
||||||
<div className='flex-1 leading-snug w-0'>
|
</div>
|
||||||
<h4 className='whitespace-nowrap'>{username}
|
<div className='flex-1 w-0 leading-snug'>
|
||||||
</h4><span className='text-sm text-gray-600'>#{tag}</span>
|
<h4 className='whitespace-nowrap'>{username}</h4>
|
||||||
</div>
|
<span className='text-gray-600 text-sm'>#{tag}</span>
|
||||||
</a>
|
</div>
|
||||||
</Link>
|
</a>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Owner
|
export default Owner
|
||||||
|
|
||||||
interface OwnerProps {
|
interface OwnerProps {
|
||||||
id: string
|
id: string
|
||||||
tag: string
|
tag: string
|
||||||
username: string
|
username: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,82 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
const Paginator = ({ currentPage, totalPage, pathname }:PaginatorProps):JSX.Element => {
|
const Paginator = ({ currentPage, totalPage, pathname }: PaginatorProps): JSX.Element => {
|
||||||
let pages = []
|
let pages = []
|
||||||
if(currentPage < 4) pages = [ 1, totalPage < 2 ? null : 2, totalPage < 3 ? null : 3, totalPage < 4 ? null : 4, totalPage < 5 ? null : 5 ]
|
if (currentPage < 4)
|
||||||
else if(currentPage > totalPage - 3) pages = [ totalPage - 4 < 1 ? null : totalPage - 4, totalPage - 3 < 1 ? null : totalPage - 3, totalPage - 2 < 1 ? null : totalPage - 2, totalPage - 1 < 1 ? null : totalPage - 1, totalPage ]
|
pages = [
|
||||||
else pages = [ currentPage - 2 < 1 ? null : currentPage - 2, currentPage - 1 < 1 ? null : currentPage - 1, currentPage, currentPage + 1 > totalPage ? null : currentPage + 1, currentPage + 2 > totalPage ? null : currentPage + 2 ]
|
1,
|
||||||
|
totalPage < 2 ? null : 2,
|
||||||
|
totalPage < 3 ? null : 3,
|
||||||
|
totalPage < 4 ? null : 4,
|
||||||
|
totalPage < 5 ? null : 5,
|
||||||
|
]
|
||||||
|
else if (currentPage > totalPage - 3)
|
||||||
|
pages = [
|
||||||
|
totalPage - 4 < 1 ? null : totalPage - 4,
|
||||||
|
totalPage - 3 < 1 ? null : totalPage - 3,
|
||||||
|
totalPage - 2 < 1 ? null : totalPage - 2,
|
||||||
|
totalPage - 1 < 1 ? null : totalPage - 1,
|
||||||
|
totalPage,
|
||||||
|
]
|
||||||
|
else
|
||||||
|
pages = [
|
||||||
|
currentPage - 2 < 1 ? null : currentPage - 2,
|
||||||
|
currentPage - 1 < 1 ? null : currentPage - 1,
|
||||||
|
currentPage,
|
||||||
|
currentPage + 1 > totalPage ? null : currentPage + 1,
|
||||||
|
currentPage + 2 > totalPage ? null : currentPage + 2,
|
||||||
|
]
|
||||||
pages = pages.filter(el => el)
|
pages = pages.filter(el => el)
|
||||||
return <div className='flex flex-col items-center py-4 text-center justify-center'>
|
return (
|
||||||
<div className='flex'>
|
<div className='flex flex-col items-center justify-center py-4 text-center'>
|
||||||
<Link href={{ pathname, query: { page: currentPage - 1} }}>
|
<div className='flex'>
|
||||||
<a className={`${currentPage === 1 ? 'invisible' : ''} h-12 w-12 mr-1 flex justify-center items-center rounded-full transition duration-150 ease-in bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover cursor-pointer text-center`}>
|
<Link href={{ pathname, query: { page: currentPage - 1 } }}>
|
||||||
<i className='fas fa-chevron-left'></i>
|
<a
|
||||||
</a>
|
className={`${
|
||||||
</Link>
|
currentPage === 1 ? 'invisible' : ''
|
||||||
{
|
} h-12 w-12 mr-1 flex justify-center items-center rounded-full transition duration-150 ease-in bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover cursor-pointer text-center`}
|
||||||
pages.map((el, i) => <Link key={i} href={{ pathname, query: { page: el } }}>
|
>
|
||||||
<a className={`w-12 flex justify-center items-center cursor-pointer leading-5 transition duration-150 ease-in ${i===0 && i===pages.length-1 ? 'rounded-full' : i===0 ? 'rounded-l-full' : i===pages.length-1 ? 'rounded-r-full' : ''} ${currentPage === el ? 'bg-gray-300 dark:bg-discord-dark-hover' : 'bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover'}`}>{el}</a>
|
<i className='fas fa-chevron-left'></i>
|
||||||
</Link>)
|
</a>
|
||||||
}
|
</Link>
|
||||||
<Link href={{ pathname, query: { page: currentPage + 1} }}>
|
{pages.map((el, i) => (
|
||||||
<a className={`${currentPage === totalPage ? 'invisible' : '' } h-12 w-12 ml-1 flex justify-center items-center rounded-full transition duration-150 ease-in bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover cursor-pointer text-center`}>
|
<Link key={i} href={{ pathname, query: { page: el } }}>
|
||||||
<i className='fas fa-chevron-right'></i>
|
<a
|
||||||
</a>
|
className={`w-12 flex justify-center items-center cursor-pointer leading-5 transition duration-150 ease-in ${
|
||||||
</Link>
|
i === 0 && i === pages.length - 1
|
||||||
|
? 'rounded-full'
|
||||||
|
: i === 0
|
||||||
|
? 'rounded-l-full'
|
||||||
|
: i === pages.length - 1
|
||||||
|
? 'rounded-r-full'
|
||||||
|
: ''
|
||||||
|
} ${
|
||||||
|
currentPage === el
|
||||||
|
? 'bg-gray-300 dark:bg-discord-dark-hover'
|
||||||
|
: 'bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{el}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<Link href={{ pathname, query: { page: currentPage + 1 } }}>
|
||||||
|
<a
|
||||||
|
className={`${
|
||||||
|
currentPage === totalPage ? 'invisible' : ''
|
||||||
|
} h-12 w-12 ml-1 flex justify-center items-center rounded-full transition duration-150 ease-in bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover cursor-pointer text-center`}
|
||||||
|
>
|
||||||
|
<i className='fas fa-chevron-right'></i>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaginatorProps {
|
interface PaginatorProps {
|
||||||
pathname: string
|
pathname: string
|
||||||
currentPage: number
|
currentPage: number
|
||||||
totalPage: number
|
totalPage: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Paginator
|
export default Paginator
|
||||||
|
|||||||
@ -10,21 +10,26 @@ import DiscordAvatar from '@components/DiscordAvatar'
|
|||||||
|
|
||||||
const Search = (): JSX.Element => {
|
const Search = (): JSX.Element => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [ query, setQuery ] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [ data, setData ] = useState<ResponseProps<BotList>>(null)
|
const [data, setData] = useState<ResponseProps<BotList>>(null)
|
||||||
const [ loading, setLoading ] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [ abortControl, setAbortControl ] = useState(new AbortController())
|
const [abortControl, setAbortControl] = useState(new AbortController())
|
||||||
const [ hidden, setHidden ] = useState(true)
|
const [hidden, setHidden] = useState(true)
|
||||||
const SearchResults = async (value: string) => {
|
const SearchResults = async (value: string) => {
|
||||||
setQuery(value)
|
setQuery(value)
|
||||||
try { abortControl.abort() } catch { return null }
|
try {
|
||||||
|
abortControl.abort()
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
setAbortControl(controller)
|
setAbortControl(controller)
|
||||||
if(value.length > 2) setLoading(true)
|
if (value.length > 2) setLoading(true)
|
||||||
const res = await Fetch<BotList>(`/search/bots?q=${encodeURIComponent(value)}`, { signal: controller.signal })
|
const res = await Fetch<BotList>(`/search/bots?q=${encodeURIComponent(value)}`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
setData(res)
|
setData(res)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
@ -32,39 +37,84 @@ const Search = (): JSX.Element => {
|
|||||||
redirectTo(router, `/search/?q=${encodeURIComponent(query)}`)
|
redirectTo(router, `/search/?q=${encodeURIComponent(query)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return (
|
||||||
<div onFocus={() => setHidden(false)} onBlur={() => setTimeout(() => setHidden(true), 80)} className='relative w-full mt-5 text-black bg-white dark:text-gray-100 dark:bg-very-black flex rounded-lg z-10'>
|
<div>
|
||||||
<input maxLength={50} className='bg-transparent flex-grow outline-none border-none shadow border-0 py-3 px-7 pr-20 h-16 text-xl' placeholder='검색...' value={query} onChange={(e)=> {
|
<div
|
||||||
SearchResults(e.target.value)
|
onFocus={() => setHidden(false)}
|
||||||
}} onKeyDown={(e) => {
|
onBlur={() => setTimeout(() => setHidden(true), 80)}
|
||||||
if(e.key === 'Enter') return onSubmit()
|
className='relative z-10 flex mt-5 w-full text-black dark:text-gray-100 dark:bg-very-black bg-white rounded-lg'
|
||||||
}} />
|
>
|
||||||
<button className='outline-none cusor-pointer absolute right-0 top-0 mt-5 mr-5' onClick={onSubmit}>
|
<input
|
||||||
<i className='text-gray-600 hover:text-gray-700 text-2xl fas fa-search' />
|
maxLength={50}
|
||||||
</button>
|
className='flex-grow pr-20 px-7 py-3 h-16 text-xl bg-transparent border-0 border-none outline-none shadow'
|
||||||
</div>
|
placeholder='검색...'
|
||||||
<div className={`relative ${hidden ? 'hidden' : 'block'}`}>
|
value={query}
|
||||||
<div className='absolute rounded shadow-md my-2 pin-t pin-l text-black bg-white dark:text-gray-100 dark:bg-very-black h-60 md:h-80 overflow-y-scroll w-full'>
|
onChange={e => {
|
||||||
<ul>
|
SearchResults(e.target.value)
|
||||||
{
|
}}
|
||||||
data && data.code === 200 && data.data ? data.data.data.length === 0 ? <li className='px-3 py-3.5'>검색 결과가 없습니다.</li> :
|
onKeyDown={e => {
|
||||||
data.data.data.map(el => <Link key={el.id} href={makeBotURL(el)}>
|
if (e.key === 'Enter') return onSubmit()
|
||||||
<li className='px-3 py-2 flex h-15 cursor-pointer'>
|
}}
|
||||||
<DiscordAvatar className='w-12 h-12 mt-1' size={128} userID={el.id} />
|
/>
|
||||||
<div className='ml-2'>
|
<button
|
||||||
<h1 className='text-lg text-black dark:text-gray-100'>{el.name}</h1>
|
className='cusor-pointer absolute right-0 top-0 mr-5 mt-5 outline-none'
|
||||||
<p className='text-sm text-gray-400'>
|
onClick={onSubmit}
|
||||||
{el.intro}
|
>
|
||||||
</p>
|
<i className='fas fa-search text-gray-600 hover:text-gray-700 text-2xl' />
|
||||||
</div>
|
</button>
|
||||||
</li>
|
</div>
|
||||||
</Link>) : loading ? <li className='px-3 py-3.5'>검색중입니다...</li> : <li className='px-3 py-3.5'>{query && data ? data.message?.includes('문법') ? <>검색 문법이 잘못되었습니다.<br/><a className='text-blue-500 hover:text-blue-400' href='https://docs.koreanbots.dev/bots/usage/search' target='_blank' rel='noreferrer' >더 알아보기</a></> : data.errors && data.errors[0] || data.message : query.length < 3 ? '최소 2글자 이상 입력해주세요.' : '검색어를 입력해주세요.'}</li>
|
<div className={`relative ${hidden ? 'hidden' : 'block'}`}>
|
||||||
}
|
<div className='pin-t pin-l absolute my-2 w-full h-60 text-black dark:text-gray-100 dark:bg-very-black bg-white rounded shadow-md overflow-y-scroll md:h-80'>
|
||||||
|
<ul>
|
||||||
</ul>
|
{data && data.code === 200 && data.data ? (
|
||||||
|
data.data.data.length === 0 ? (
|
||||||
|
<li className='px-3 py-3.5'>검색 결과가 없습니다.</li>
|
||||||
|
) : (
|
||||||
|
data.data.data.map(el => (
|
||||||
|
<Link key={el.id} href={makeBotURL(el)}>
|
||||||
|
<li className='h-15 flex px-3 py-2 cursor-pointer'>
|
||||||
|
<DiscordAvatar className='mt-1 w-12 h-12' size={128} userID={el.id} />
|
||||||
|
<div className='ml-2'>
|
||||||
|
<h1 className='text-black dark:text-gray-100 text-lg'>{el.name}</h1>
|
||||||
|
<p className='text-gray-400 text-sm'>{el.intro}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</Link>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
) : loading ? (
|
||||||
|
<li className='px-3 py-3.5'>검색중입니다...</li>
|
||||||
|
) : (
|
||||||
|
<li className='px-3 py-3.5'>
|
||||||
|
{query && data ? (
|
||||||
|
data.message?.includes('문법') ? (
|
||||||
|
<>
|
||||||
|
검색 문법이 잘못되었습니다.
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
className='hover:text-blue-400 text-blue-500'
|
||||||
|
href='https://docs.koreanbots.dev/bots/usage/search'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
더 알아보기
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
(data.errors && data.errors[0]) || data.message
|
||||||
|
)
|
||||||
|
) : query.length < 3 ? (
|
||||||
|
'최소 2글자 이상 입력해주세요.'
|
||||||
|
) : (
|
||||||
|
'검색어를 입력해주세요.'
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Search
|
export default Search
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
const Segment = ({ children, className='' }:SegmentProps): JSX.Element => {
|
const Segment = ({ children, className = '' }: SegmentProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={`py-3 px-7 text-black dark:text-white dark:bg-discord-black bg-little-white rounded-sm ${className}`}>
|
<div
|
||||||
|
className={`py-3 px-7 text-black dark:text-white dark:bg-discord-black bg-little-white rounded-sm ${className}`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,34 +5,43 @@ import Tag from '@components/Tag'
|
|||||||
import { SubmittedBot } from '@types'
|
import { SubmittedBot } from '@types'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
const SubmittedBotCard = ({ href, submit }:SubmittedBotProps):JSX.Element => {
|
const SubmittedBotCard = ({ href, submit }: SubmittedBotProps): JSX.Element => {
|
||||||
return <Link href={href}>
|
return (
|
||||||
<a className='relative mx-auto w-full h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl px-4 py-5 transform hover:-translate-y-1 transition duration-100 ease-in'>
|
<Link href={href}>
|
||||||
<div className='h-18'>
|
<a className='relative mx-auto px-4 py-5 w-full h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl transform hover:-translate-y-1 transition duration-100 ease-in'>
|
||||||
<div className='flex'>
|
<div className='h-18'>
|
||||||
<div className='flex-grow w-full'>
|
<div className='flex'>
|
||||||
<h2 className='text-lg'>{submit.id}</h2>
|
<div className='flex-grow w-full'>
|
||||||
</div>
|
<h2 className='text-lg'>{submit.id}</h2>
|
||||||
<div className='grid grid-cols-1 px-4 w-2/5 h-0 absolute right-0'>
|
</div>
|
||||||
<Tag
|
<div className='absolute right-0 grid grid-cols-1 px-4 w-2/5 h-0'>
|
||||||
text={
|
<Tag
|
||||||
<>
|
text={
|
||||||
<i className={`fas fa-circle text-${[Status.offline, Status.online, Status.dnd][submit.state]?.color}`} />
|
<>
|
||||||
{' '}{['대기중', '승인됨', '거부됨'][submit.state]}
|
<i
|
||||||
</>
|
className={`fas fa-circle text-${
|
||||||
}
|
[Status.offline, Status.online, Status.dnd][submit.state]?.color
|
||||||
dark
|
}`}
|
||||||
/>
|
/>{' '}
|
||||||
|
{['대기중', '승인됨', '거부됨'][submit.state]}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
dark
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className='mt-1.5 w-full h-6 text-left text-gray-400 text-sm font-medium truncate'>
|
||||||
|
{submit.intro.slice(0, 25)}
|
||||||
|
{submit.intro.length > 25 && '...'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-left truncate text-gray-400 text-sm font-medium mt-1.5 h-6 w-full'>{submit.intro.slice(0, 25)}{submit.intro.length > 25 && '...'}</p>
|
</a>
|
||||||
</div>
|
</Link>
|
||||||
</a>
|
)
|
||||||
</Link>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubmittedBotProps {
|
interface SubmittedBotProps {
|
||||||
href: string
|
href: string
|
||||||
submit: SubmittedBot
|
submit: SubmittedBot
|
||||||
}
|
}
|
||||||
export default SubmittedBotCard
|
export default SubmittedBotCard
|
||||||
|
|||||||
@ -13,30 +13,13 @@ const Tag = ({
|
|||||||
bigger = false,
|
bigger = false,
|
||||||
...props
|
...props
|
||||||
}: LabelProps): JSX.Element => {
|
}: LabelProps): JSX.Element => {
|
||||||
return href ? newTab ? (
|
return href ? (
|
||||||
<a
|
newTab ? (
|
||||||
href={href}
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'
|
|
||||||
className={`${className ?? ''} text-center text-base ${
|
|
||||||
dark
|
|
||||||
? blurple
|
|
||||||
? 'bg-discord-blurple text-white'
|
|
||||||
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
|
|
||||||
: github
|
|
||||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
|
||||||
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
|
||||||
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
|
||||||
circular ? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}` : `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
|
||||||
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
) : (
|
|
||||||
<Link href={href}>
|
|
||||||
<a
|
<a
|
||||||
className={`${className ?? ''} text-center text-base ${
|
href={href}
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
className={`${className ?? ''} text-center text-base ${
|
||||||
dark
|
dark
|
||||||
? blurple
|
? blurple
|
||||||
? 'bg-discord-blurple text-white'
|
? 'bg-discord-blurple text-white'
|
||||||
@ -44,13 +27,37 @@ const Tag = ({
|
|||||||
: github
|
: github
|
||||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||||
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
||||||
} ${!blurple && !github ? 'text-black dark:text-gray-400' : 'hover:bg-little-white-hover'} ${
|
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
||||||
circular ? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}` : `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
circular
|
||||||
|
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
||||||
|
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
||||||
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}
|
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
) : (
|
||||||
|
<Link href={href}>
|
||||||
|
<a
|
||||||
|
className={`${className ?? ''} text-center text-base ${
|
||||||
|
dark
|
||||||
|
? blurple
|
||||||
|
? 'bg-discord-blurple text-white'
|
||||||
|
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
|
||||||
|
: github
|
||||||
|
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||||
|
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
||||||
|
} ${
|
||||||
|
!blurple && !github ? 'text-black dark:text-gray-400' : 'hover:bg-little-white-hover'
|
||||||
|
} ${
|
||||||
|
circular
|
||||||
|
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
||||||
|
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
||||||
|
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
{...props}
|
{...props}
|
||||||
@ -60,10 +67,20 @@ const Tag = ({
|
|||||||
? 'font-bg bg-discord-blurple text-white'
|
? 'font-bg bg-discord-blurple text-white'
|
||||||
: github
|
: github
|
||||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||||
: `bg-little-white-hover dark:bg-very-black ${props.onClick ? 'hover:bg-little-white dark:hover:bg-discord-dark-hover transition duration-100 ease-in' : '' }`
|
: `bg-little-white-hover dark:bg-very-black ${
|
||||||
: `bg-little-white dark:bg-discord-black ${props.onClick ? 'hover:bg-little-white-hover dark:hover:bg-discord-dark-hover transition duration-100 ease-in' : '' }`
|
props.onClick
|
||||||
|
? 'hover:bg-little-white dark:hover:bg-discord-dark-hover transition duration-100 ease-in'
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
: `bg-little-white dark:bg-discord-black ${
|
||||||
|
props.onClick
|
||||||
|
? 'hover:bg-little-white-hover dark:hover:bg-discord-dark-hover transition duration-100 ease-in'
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
||||||
circular ? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}` : `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
circular
|
||||||
|
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
||||||
|
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
||||||
} mr-1 mb-${marginBottom}`}
|
} mr-1 mb-${marginBottom}`}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
|
|||||||
@ -1,13 +1,28 @@
|
|||||||
const Toggle = ({ checked, onChange }:ToggleProps):JSX.Element => {
|
const Toggle = ({ checked, onChange }: ToggleProps): JSX.Element => {
|
||||||
return <button className='relative inline-block w-10 mr-2 align-middle select-none outline-none' onClick={onChange} onKeyPress={onChange}>
|
return (
|
||||||
<input type='checkbox' checked={checked} className='absolute block w-6 h-6 rounded-full bg-white border-4 border-transparent appearance-none cursor-pointer outline-none checked:right-0' readOnly />
|
<button
|
||||||
<span className={`block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer ${checked ? 'bg-koreanbots-blue' : ''}`}></span>
|
className='relative inline-block align-middle mr-2 w-10 outline-none select-none'
|
||||||
</button>
|
onClick={onChange}
|
||||||
|
onKeyPress={onChange}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={checked}
|
||||||
|
className='absolute checked:right-0 block w-6 h-6 bg-white border-4 border-transparent rounded-full outline-none appearance-none cursor-pointer'
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer ${
|
||||||
|
checked ? 'bg-koreanbots-blue' : ''
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToggleProps {
|
interface ToggleProps {
|
||||||
checked: boolean,
|
checked: boolean
|
||||||
onChange(): void
|
onChange(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Toggle
|
export default Toggle
|
||||||
|
|||||||
@ -1,43 +1,115 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
const Tooltip = ({ href, size='small', children, direction='center', text }:TooltipProps):JSX.Element => {
|
const Tooltip = ({
|
||||||
return href ? <Link href={href}>
|
href,
|
||||||
|
size = 'small',
|
||||||
|
children,
|
||||||
|
direction = 'center',
|
||||||
|
text,
|
||||||
|
}: TooltipProps): JSX.Element => {
|
||||||
|
return href ? (
|
||||||
|
<Link href={href}>
|
||||||
|
<a className='inline'>
|
||||||
|
<div className='relative inline py-3'>
|
||||||
|
<div className='group relative inline-block text-center cursor-pointer'>
|
||||||
|
{children}
|
||||||
|
<div
|
||||||
|
className={`opacity-0 ${
|
||||||
|
size === 'small' ? 'w-44' : 'w-60'
|
||||||
|
} bg-black text-white text-center text-xs rounded-lg py-2 px-3 absolute z-10 group-hover:opacity-100 bottom-full -left-4 pointer-events-none`}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
{direction === 'left' ? (
|
||||||
|
<svg
|
||||||
|
className='absolute left-5 top-full mr-3 h-2 text-black'
|
||||||
|
x='0px'
|
||||||
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
) : direction === 'center' ? (
|
||||||
|
<svg
|
||||||
|
className='absolute left-0 top-full w-full h-2 text-black'
|
||||||
|
x='0px'
|
||||||
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
className='absolute right-5 top-full mr-3 h-2 text-black'
|
||||||
|
x='0px'
|
||||||
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
<a className='inline'>
|
<a className='inline'>
|
||||||
<div className='relative py-3 inline'>
|
<div className='relative inline py-3'>
|
||||||
<div className='group cursor-pointer relative inline-block text-center'>{children}
|
<div className='group relative inline-block text-center cursor-pointer'>
|
||||||
<div className={`opacity-0 ${size==='small' ? 'w-44' : 'w-60'} bg-black text-white text-center text-xs rounded-lg py-2 px-3 absolute z-10 group-hover:opacity-100 bottom-full -left-4 pointer-events-none`}>
|
{children}
|
||||||
|
<div
|
||||||
|
className={`opacity-0 ${
|
||||||
|
size === 'small' ? 'w-44' : 'w-60'
|
||||||
|
} bg-black text-white text-center text-xs rounded-lg py-2 px-3 absolute z-10 group-hover:opacity-100 bottom-full -left-4 pointer-events-none`}
|
||||||
|
>
|
||||||
{text}
|
{text}
|
||||||
{
|
{direction === 'left' ? (
|
||||||
direction === 'left' ? <svg className='absolute text-black h-2 left-5 mr-3 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
<svg
|
||||||
: direction === 'center' ? <svg className='absolute text-black h-2 w-full left-0 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
className='absolute left-5 top-full mr-3 h-2 text-black'
|
||||||
: <svg className='absolute text-black h-2 right-5 mr-3 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
x='0px'
|
||||||
}
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
) : direction === 'center' ? (
|
||||||
|
<svg
|
||||||
|
className='absolute left-0 top-full w-full h-2 text-black'
|
||||||
|
x='0px'
|
||||||
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
className='absolute right-5 top-full mr-3 h-2 text-black'
|
||||||
|
x='0px'
|
||||||
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link> : <a className='inline'>
|
)
|
||||||
<div className='relative py-3 inline'>
|
|
||||||
<div className='group cursor-pointer relative inline-block text-center'>{children}
|
|
||||||
<div className={`opacity-0 ${size==='small' ? 'w-44' : 'w-60'} bg-black text-white text-center text-xs rounded-lg py-2 px-3 absolute z-10 group-hover:opacity-100 bottom-full -left-4 pointer-events-none`}>
|
|
||||||
{text}
|
|
||||||
{
|
|
||||||
direction === 'left' ? <svg className='absolute text-black h-2 left-5 mr-3 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
|
||||||
: direction === 'center' ? <svg className='absolute text-black h-2 w-full left-0 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
|
||||||
: <svg className='absolute text-black h-2 right-5 mr-3 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TooltipProps {
|
interface TooltipProps {
|
||||||
href?: string
|
href?: string
|
||||||
size?: 'small' | 'large'
|
size?: 'small' | 'large'
|
||||||
direction?: 'left' | 'center' | 'right'
|
direction?: 'left' | 'center' | 'right'
|
||||||
text: string
|
text: string
|
||||||
children: JSX.Element | JSX.Element[]
|
children: JSX.Element | JSX.Element[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Tooltip
|
export default Tooltip
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
version: "3"
|
version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
@ -24,4 +24,4 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 500M
|
memory: 500M
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
apps : [{
|
apps: [
|
||||||
name: 'koreanbots',
|
{
|
||||||
script: 'npm start',
|
name: 'koreanbots',
|
||||||
env: {
|
script: 'npm start',
|
||||||
NODE_ENV: 'development',
|
env: {
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
},
|
||||||
|
env_production: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
env_production: {
|
],
|
||||||
NODE_ENV: 'production',
|
}
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,6 @@ module.exports = {
|
|||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'@types': '<rootDir>/types'
|
'@types': '<rootDir>/types',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
170
package.json
170
package.json
@ -1,86 +1,88 @@
|
|||||||
{
|
{
|
||||||
"name": "client-next",
|
"name": "client-next",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start | (sleep 1; wget http://localhost:3000/api -O /dev/null)",
|
"start": "next start | (sleep 1; wget http://localhost:3000/api -O /dev/null)",
|
||||||
"lint": "eslint --ext ts,tsx .",
|
"lint": "eslint --ext ts,tsx .",
|
||||||
"lint:fix": "eslint --ext ts,tsx . --fix",
|
"prettier": "prettier --write **/*",
|
||||||
"test": "jest",
|
"lint:fix": "eslint --ext ts,tsx . --fix",
|
||||||
"docker": "docker-compose up -d --build"
|
"test": "jest",
|
||||||
},
|
"docker": "docker-compose up -d --build",
|
||||||
"dependencies": {
|
"postinstall": "husky install"
|
||||||
"@fortawesome/fontawesome-free": "5.15.2",
|
},
|
||||||
"@sentry/browser": "6.2.0",
|
"dependencies": {
|
||||||
"@sentry/integrations": "6.2.0",
|
"@fortawesome/fontawesome-free": "5.15.2",
|
||||||
"@sentry/node": "6.2.0",
|
"@sentry/browser": "6.2.0",
|
||||||
"@sentry/webpack-plugin": "1.14.1",
|
"@sentry/integrations": "6.2.0",
|
||||||
"autoprefixer": "10.2.4",
|
"@sentry/node": "6.2.0",
|
||||||
"badgen": "3.2.2",
|
"@sentry/webpack-plugin": "1.14.1",
|
||||||
"cookie": "0.4.1",
|
"autoprefixer": "10.2.4",
|
||||||
"core-js": "3.9.0",
|
"badgen": "3.2.2",
|
||||||
"csrf": "3.1.0",
|
"cookie": "0.4.1",
|
||||||
"dataloader": "2.0.0",
|
"core-js": "3.9.0",
|
||||||
"dayjs": "1.10.4",
|
"csrf": "3.1.0",
|
||||||
"discord.js": "12.5.1",
|
"dataloader": "2.0.0",
|
||||||
"emoji-mart": "3.0.1",
|
"dayjs": "1.10.4",
|
||||||
"formik": "2.2.6",
|
"discord.js": "12.5.1",
|
||||||
"generate-license-file": "1.1.0",
|
"emoji-mart": "3.0.1",
|
||||||
"josa": "3.0.1",
|
"formik": "2.2.6",
|
||||||
"jsonwebtoken": "8.5.1",
|
"generate-license-file": "1.1.0",
|
||||||
"knex": "0.21.18",
|
"josa": "3.0.1",
|
||||||
"mysql": "2.18.1",
|
"jsonwebtoken": "8.5.1",
|
||||||
"next": "10.0.6",
|
"knex": "0.21.18",
|
||||||
"next-connect": "0.10.0",
|
"mysql": "2.18.1",
|
||||||
"next-session": "3.4.0",
|
"next": "10.0.6",
|
||||||
"node-emoji": "1.10.0",
|
"next-connect": "0.10.0",
|
||||||
"postcss": "8.2.6",
|
"next-session": "3.4.0",
|
||||||
"postcss-preset-env": "6.7.0",
|
"node-emoji": "1.10.0",
|
||||||
"react": "17.0.1",
|
"postcss": "8.2.6",
|
||||||
"react-dom": "17.0.1",
|
"postcss-preset-env": "6.7.0",
|
||||||
"react-responsive-modal": "6.0.1",
|
"react": "17.0.1",
|
||||||
"react-select": "4.1.0",
|
"react-dom": "17.0.1",
|
||||||
"react-showdown": "2.1.0",
|
"react-responsive-modal": "6.0.1",
|
||||||
"react-sortable-hoc": "1.11.0",
|
"react-select": "4.1.0",
|
||||||
"react-use-clipboard": "1.0.7",
|
"react-showdown": "2.1.0",
|
||||||
"sanitize-html": "2.3.2",
|
"react-sortable-hoc": "1.11.0",
|
||||||
"tailwindcss": "2.0.3",
|
"react-use-clipboard": "1.0.7",
|
||||||
"tlru": "1.0.2",
|
"sanitize-html": "2.3.2",
|
||||||
"twemoji": "13.0.1",
|
"tailwindcss": "2.0.3",
|
||||||
"url-regex-safe": "2.0.2",
|
"tlru": "1.0.2",
|
||||||
"yup": "0.32.9",
|
"twemoji": "13.0.1",
|
||||||
"yup-locales-ko": "1.0.2"
|
"url-regex-safe": "2.0.2",
|
||||||
},
|
"yup": "0.32.9",
|
||||||
"devDependencies": {
|
"yup-locales-ko": "1.0.2"
|
||||||
"@types/cookie": "0.4.0",
|
},
|
||||||
"@types/core-js": "2.5.4",
|
"devDependencies": {
|
||||||
"@types/emoji-mart": "3.0.4",
|
"@types/cookie": "0.4.0",
|
||||||
"@types/jest": "26.0.20",
|
"@types/core-js": "2.5.4",
|
||||||
"@types/josa": "3.0.2",
|
"@types/emoji-mart": "3.0.4",
|
||||||
"@types/jsonwebtoken": "8.5.0",
|
"@types/jest": "26.0.20",
|
||||||
"@types/node": "14.14.31",
|
"@types/josa": "3.0.2",
|
||||||
"@types/node-emoji": "1.8.1",
|
"@types/jsonwebtoken": "8.5.0",
|
||||||
"@types/node-fetch": "2.5.8",
|
"@types/node": "14.14.31",
|
||||||
"@types/react": "17.0.2",
|
"@types/node-emoji": "1.8.1",
|
||||||
"@types/react-select": "4.0.13",
|
"@types/node-fetch": "2.5.8",
|
||||||
"@types/sanitize-html": "1.27.1",
|
"@types/react": "17.0.2",
|
||||||
"@types/twemoji": "12.1.1",
|
"@types/react-select": "4.0.13",
|
||||||
"@types/url-regex-safe": "1.0.0",
|
"@types/sanitize-html": "1.27.1",
|
||||||
"@typescript-eslint/eslint-plugin": "4.15.2",
|
"@types/twemoji": "12.1.1",
|
||||||
"@typescript-eslint/parser": "4.15.2",
|
"@types/url-regex-safe": "1.0.0",
|
||||||
"eslint": "7.20.0",
|
"@typescript-eslint/eslint-plugin": "4.15.2",
|
||||||
"eslint-config-prettier": "8.1.0",
|
"@typescript-eslint/parser": "4.15.2",
|
||||||
"eslint-plugin-jsx-a11y": "6.4.1",
|
"eslint": "7.20.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-config-prettier": "8.1.0",
|
||||||
"eslint-plugin-react": "7.22.0",
|
"eslint-plugin-jsx-a11y": "6.4.1",
|
||||||
"eslint-plugin-react-hooks": "4.2.0",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"jest": "26.6.3",
|
"eslint-plugin-react": "7.22.0",
|
||||||
"prettier": "2.2.1",
|
"eslint-plugin-react-hooks": "4.2.0",
|
||||||
"prettier-plugin-tailwind": "2.2.9",
|
"jest": "26.6.3",
|
||||||
"ts-jest": "26.5.2",
|
"prettier": "2.2.1",
|
||||||
"typescript": "4.2.2"
|
"prettier-plugin-tailwind": "2.2.9",
|
||||||
},
|
"ts-jest": "26.5.2",
|
||||||
"license": "AGPL-3.0"
|
"typescript": "4.2.2"
|
||||||
|
},
|
||||||
|
"license": "AGPL-3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,15 +2,25 @@ import { NextPage } from 'next'
|
|||||||
import { ErrorText } from '@utils/Constants'
|
import { ErrorText } from '@utils/Constants'
|
||||||
|
|
||||||
const NotFound: NextPage = () => {
|
const NotFound: NextPage = () => {
|
||||||
return <div className='h-screen flex md:flex-col items-center justify-center' style={{ background: 'url("https://cdn.discordapp.com/attachments/745844596176715806/799149423505440768/1590927393326.jpg")' }}>
|
return (
|
||||||
<div className='text-center'>
|
<div
|
||||||
<h1 className='w-inline-block text-4xl font-bold align-top border-none md:border-r md:border-solid md:border-current m-0 md:mr-10 py-10 md:pr-10'>404</h1>
|
className='flex items-center justify-center h-screen md:flex-col'
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'url("https://cdn.discordapp.com/attachments/745844596176715806/799149423505440768/1590927393326.jpg")',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='text-center'>
|
||||||
|
<h1 className='w-inline-block align-top m-0 py-10 text-4xl font-bold border-none md:mr-10 md:pr-10 md:border-r md:border-solid md:border-current'>
|
||||||
|
404
|
||||||
|
</h1>
|
||||||
|
|
||||||
<h2 className='inline-block text-2xl md:text-4xl font-semibold align-top m-0 py-10'>
|
<h2 className='inline-block align-top m-0 py-10 text-2xl font-semibold md:text-4xl'>
|
||||||
{ ErrorText[404] }
|
{ErrorText[404]}
|
||||||
</h2>
|
</h2>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotFound
|
export default NotFound
|
||||||
|
|||||||
@ -10,8 +10,10 @@ class MyDocument extends Document {
|
|||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel='stylesheet'
|
<link
|
||||||
href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/default.min.css'/>
|
rel='stylesheet'
|
||||||
|
href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/default.min.css'
|
||||||
|
/>
|
||||||
<script src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js'></script>
|
<script src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js'></script>
|
||||||
<script
|
<script
|
||||||
data-ad-client='ca-pub-4856582423981759'
|
data-ad-client='ca-pub-4856582423981759'
|
||||||
@ -38,7 +40,7 @@ class MyDocument extends Document {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<body className='h-full overflow-x-hidden text-black dark:text-gray-100 dark:bg-discord-dark bg-white'>
|
<body className='h-full text-black dark:text-gray-100 dark:bg-discord-dark bg-white overflow-x-hidden'>
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const NotFound = RequestHandler()
|
const NotFound = RequestHandler().all(async (_req, res) => {
|
||||||
.all(async(_req, res) => {
|
return ResponseWrapper(res, { code: 404, message: '요청하신 URL에 페이지가 존재하지 않습니다.' })
|
||||||
return ResponseWrapper(res, { code: 404, message: '요청하신 URL에 페이지가 존재하지 않습니다.' })
|
})
|
||||||
})
|
|
||||||
|
|
||||||
export default NotFound
|
export default NotFound
|
||||||
|
|||||||
@ -3,7 +3,11 @@ import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
|||||||
|
|
||||||
const RateLimit: NextApiHandler = (_req: NextApiRequest, res: NextApiResponse) => {
|
const RateLimit: NextApiHandler = (_req: NextApiRequest, res: NextApiResponse) => {
|
||||||
res.statusCode = 429
|
res.statusCode = 429
|
||||||
return ResponseWrapper(res, { code: 429, message: '지정된 시간에 너무 많은 요청을 보냈습니다. 잠시 뒤에 시도해주세요.', errors: ['지정된 시간에 너무 많은 요청을 보냈습니다. 잠시 뒤에 시도해주세요.'] })
|
return ResponseWrapper(res, {
|
||||||
|
code: 429,
|
||||||
|
message: '지정된 시간에 너무 많은 요청을 보냈습니다. 잠시 뒤에 시도해주세요.',
|
||||||
|
errors: ['지정된 시간에 너무 많은 요청을 보냈습니다. 잠시 뒤에 시도해주세요.'],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RateLimit
|
export default RateLimit
|
||||||
|
|||||||
@ -11,51 +11,62 @@ import { update } from '@utils/Query'
|
|||||||
import { verify } from '@utils/Jwt'
|
import { verify } from '@utils/Jwt'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Callback = RequestHandler()
|
const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
.get(async(req: ApiRequest, res) => {
|
const validate = await OauthCallbackSchema.validate(req.query)
|
||||||
const validate = await OauthCallbackSchema.validate(req.query).then(r=> r).catch((e) => {
|
.then(r => r)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if(!validate) return
|
|
||||||
|
|
||||||
res.statusCode = 200
|
|
||||||
const token:DiscordTokenInfo = await fetch(DiscordEnpoints.Token, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData({
|
|
||||||
client_id: process.env.DISCORD_CLIENT_ID,
|
|
||||||
redirect_uri: process.env.KOREANBOTS_URL + '/api/auth/discord/callback',
|
|
||||||
client_secret: process.env.DISCORD_CLIENT_SECRET,
|
|
||||||
scope: process.env.DISCORD_SCOPE,
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
code: req.query.code
|
|
||||||
|
|
||||||
}),
|
if (!validate) return
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
|
||||||
}
|
|
||||||
}).then(r=> r.json())
|
|
||||||
if(token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
|
|
||||||
|
|
||||||
const user:DiscordUserInfo = await fetch(DiscordEnpoints.Me, {
|
res.statusCode = 200
|
||||||
method: 'GET',
|
const token: DiscordTokenInfo = await fetch(DiscordEnpoints.Token, {
|
||||||
headers: {
|
method: 'POST',
|
||||||
Authorization: `${token.token_type} ${token.access_token}`
|
body: formData({
|
||||||
}
|
client_id: process.env.DISCORD_CLIENT_ID,
|
||||||
}).then(r => r.json())
|
redirect_uri: process.env.KOREANBOTS_URL + '/api/auth/discord/callback',
|
||||||
|
client_secret: process.env.DISCORD_CLIENT_SECRET,
|
||||||
|
scope: process.env.DISCORD_SCOPE,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: req.query.code,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
}).then(r => r.json())
|
||||||
|
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
|
||||||
|
|
||||||
const userToken = await update.assignToken({ id: user.id, access_token: token.access_token, expires_in: token.expires_in, refresh_token: token.refresh_token, email: user.email, username: user.username, discriminator: user.discriminator })
|
const user: DiscordUserInfo = await fetch(DiscordEnpoints.Me, {
|
||||||
const info = verify(userToken)
|
method: 'GET',
|
||||||
res.setHeader('set-cookie', serialize('token', userToken, {
|
headers: {
|
||||||
|
Authorization: `${token.token_type} ${token.access_token}`,
|
||||||
|
},
|
||||||
|
}).then(r => r.json())
|
||||||
|
|
||||||
|
const userToken = await update.assignToken({
|
||||||
|
id: user.id,
|
||||||
|
access_token: token.access_token,
|
||||||
|
expires_in: token.expires_in,
|
||||||
|
refresh_token: token.refresh_token,
|
||||||
|
email: user.email,
|
||||||
|
username: user.username,
|
||||||
|
discriminator: user.discriminator,
|
||||||
|
})
|
||||||
|
const info = verify(userToken)
|
||||||
|
res.setHeader(
|
||||||
|
'set-cookie',
|
||||||
|
serialize('token', userToken, {
|
||||||
expires: new Date(info.exp * 1000),
|
expires: new Date(info.exp * 1000),
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
path: '/'
|
path: '/',
|
||||||
}))
|
})
|
||||||
res.redirect(301, '/callback/discord')
|
)
|
||||||
})
|
res.redirect(301, '/callback/discord')
|
||||||
|
})
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@ -2,9 +2,11 @@ import { NextApiRequest, NextApiResponse } from 'next'
|
|||||||
import { generateOauthURL } from '@utils/Tools'
|
import { generateOauthURL } from '@utils/Tools'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Discord = RequestHandler()
|
const Discord = RequestHandler().get(async (_req: NextApiRequest, res: NextApiResponse) => {
|
||||||
.get(async (_req: NextApiRequest, res: NextApiResponse) => {
|
res.redirect(
|
||||||
res.redirect(301, generateOauthURL('discord', process.env.DISCORD_CLIENT_ID, process.env.DISCORD_SCOPE))
|
301,
|
||||||
})
|
generateOauthURL('discord', process.env.DISCORD_CLIENT_ID, process.env.DISCORD_SCOPE)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
export default Discord
|
export default Discord
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import { serialize } from 'cookie'
|
import { serialize } from 'cookie'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
|
const Logout = RequestHandler().get(async (req, res) => {
|
||||||
const Logout = RequestHandler()
|
res.setHeader('Cache-control', 'no-cache')
|
||||||
.get(async(req, res) => {
|
res.setHeader(
|
||||||
res.setHeader('Cache-control', 'no-cache')
|
'set-cookie',
|
||||||
res.setHeader('set-cookie', serialize('token', '', {
|
serialize('token', '', {
|
||||||
maxAge: -1,
|
maxAge: -1,
|
||||||
path: '/'
|
path: '/',
|
||||||
}))
|
})
|
||||||
res.redirect(301, '/')
|
)
|
||||||
})
|
res.redirect(301, '/')
|
||||||
export default Logout
|
})
|
||||||
|
export default Logout
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Deprecated = RequestHandler()
|
const Deprecated = RequestHandler().get(async (_req, res) => {
|
||||||
.get(async (_req, res) => {
|
return ResponseWrapper(res, {
|
||||||
return ResponseWrapper(res, {
|
code: 406,
|
||||||
code: 406,
|
message: '해당 API 버전은 지원 종료되었습니다.',
|
||||||
message: '해당 API 버전은 지원 종료되었습니다.',
|
version: 1,
|
||||||
version: 1,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
export default Deprecated
|
export default Deprecated
|
||||||
|
|||||||
@ -8,25 +8,25 @@ import RequestHandler from '@utils/RequestHandler'
|
|||||||
|
|
||||||
import { User } from '@types'
|
import { User } from '@types'
|
||||||
|
|
||||||
const BotApplications = RequestHandler()
|
const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
|
||||||
.patch(async (req: ApiRequest, res) => {
|
const user = await get.Authorization(req.cookies.token)
|
||||||
const user = await get.Authorization(req.cookies.token)
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
if(!user) return ResponseWrapper(res, { code: 401 })
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
if (!csrfValidated) return
|
||||||
if(!csrfValidated) return
|
const validated = await DeveloperBotSchema.validate(req.body, { abortEarly: false })
|
||||||
const validated = await DeveloperBotSchema.validate(req.body, { abortEarly: false }).then(el => el).catch(e => {
|
.then(el => el)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if(!validated) return
|
if (!validated) return
|
||||||
const bot = await get.bot.load(req.query.id)
|
const bot = await get.bot.load(req.query.id)
|
||||||
if(!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||||
if(!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
||||||
await update.updateBotApplication(req.query.id, { webhook: validated.webhook || null })
|
await update.updateBotApplication(req.query.id, { webhook: validated.webhook || null })
|
||||||
return ResponseWrapper(res, { code: 200 })
|
return ResponseWrapper(res, { code: 200 })
|
||||||
|
})
|
||||||
})
|
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
body: DeveloperBot
|
body: DeveloperBot
|
||||||
@ -35,4 +35,4 @@ interface ApiRequest extends NextApiRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BotApplications
|
export default BotApplications
|
||||||
|
|||||||
@ -8,31 +8,32 @@ import RequestHandler from '@utils/RequestHandler'
|
|||||||
|
|
||||||
import { User } from '@types'
|
import { User } from '@types'
|
||||||
|
|
||||||
const ResetApplication = RequestHandler()
|
const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
|
||||||
.post(async (req: ApiRequest, res) => {
|
const user = await get.Authorization(req.cookies.token)
|
||||||
const user = await get.Authorization(req.cookies.token)
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
if(!user) return ResponseWrapper(res, { code: 401 })
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
if (!csrfValidated) return
|
||||||
if(!csrfValidated) return
|
const validated = await ResetBotTokenSchema.validate(req.body, { abortEarly: false })
|
||||||
const validated = await ResetBotTokenSchema.validate(req.body, { abortEarly: false }).then(el => el).catch(e => {
|
.then(el => el)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if(!validated) return
|
if (!validated) return
|
||||||
const bot = await get.bot.load(req.query.id)
|
const bot = await get.bot.load(req.query.id)
|
||||||
if(!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||||
if(!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
||||||
const d = await update.resetBotToken(req.query.id, validated.token)
|
const d = await update.resetBotToken(req.query.id, validated.token)
|
||||||
if(!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
|
if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
|
||||||
return ResponseWrapper(res, { code: 200, data: { token: d }})
|
return ResponseWrapper(res, { code: 200, data: { token: d } })
|
||||||
})
|
})
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
body: ResetBotToken
|
body: ResetBotToken
|
||||||
query: {
|
query: {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResetApplication
|
export default ResetApplication
|
||||||
|
|||||||
@ -7,29 +7,56 @@ import { AddBotSubmit, AddBotSubmitSchema } from '@utils/Yup'
|
|||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Bots = RequestHandler()
|
const Bots = RequestHandler()
|
||||||
.get(async(req: GetApiRequest, res) => {
|
.get(async (req: GetApiRequest, res) => {
|
||||||
const bot = await get.bot.load(req.query.id)
|
const bot = await get.bot.load(req.query.id)
|
||||||
if(!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||||
else return ResponseWrapper(res, { code: 200, data: bot })
|
else return ResponseWrapper(res, { code: 200, data: bot })
|
||||||
})
|
})
|
||||||
.post(async (req: PostApiRequest, res) => {
|
.post(async (req: PostApiRequest, res) => {
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if(!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if(!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
|
|
||||||
const validated = await AddBotSubmitSchema.validate(req.body, { abortEarly: false }).then(el => el).catch(e => {
|
const validated = await AddBotSubmitSchema.validate(req.body, { abortEarly: false })
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
.then(el => el)
|
||||||
return null
|
.catch(e => {
|
||||||
})
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
if(!validated) return
|
if (!validated) return
|
||||||
if(validated.id !== req.query.id) return ResponseWrapper(res, { code: 400, errors: ['요청 주소와 Body의 정보가 다릅니다.'] })
|
if (validated.id !== req.query.id)
|
||||||
|
return ResponseWrapper(res, { code: 400, errors: ['요청 주소와 Body의 정보가 다릅니다.'] })
|
||||||
const result = await put.submitBot(user, validated)
|
const result = await put.submitBot(user, validated)
|
||||||
if(result === 1) return ResponseWrapper(res, { code: 403, message: '이미 대기중인 봇이 있습니다.', errors: ['한 번에 최대 2개의 봇까지만 신청하실 수 있습니다.\n다른 봇들의 심사가 완료된 뒤에 신청해주세요.'] })
|
if (result === 1)
|
||||||
else if(result === 2) return ResponseWrapper(res, { code: 406, message: '해당 봇은 이미 심사중이거나 이미 등록되어있습니다.', errors: ['해당 아이디의 봇은 이미 심사중이거나 등록되어있습니다. 본인 소유의 봇이고 신청하신 적이 없으시다면 문의해주세요.'] })
|
return ResponseWrapper(res, {
|
||||||
else if(result === 3) return ResponseWrapper(res, { code: 404, message: '올바르지 않은 봇 아이디입니다.', errors: ['해당 아이디의 봇은 존재하지 않습니다. 다시 확인해주세요.'] })
|
code: 403,
|
||||||
else if(result === 4) return ResponseWrapper(res, { code: 403, message: '디스코드 서버에 참가해주세요.' , errors: ['봇 신청하시기 위해서는 공식 디스코드 서버에 참가해주셔야합니다.'] })
|
message: '이미 대기중인 봇이 있습니다.',
|
||||||
|
errors: [
|
||||||
|
'한 번에 최대 2개의 봇까지만 신청하실 수 있습니다.\n다른 봇들의 심사가 완료된 뒤에 신청해주세요.',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
else if (result === 2)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 406,
|
||||||
|
message: '해당 봇은 이미 심사중이거나 이미 등록되어있습니다.',
|
||||||
|
errors: [
|
||||||
|
'해당 아이디의 봇은 이미 심사중이거나 등록되어있습니다. 본인 소유의 봇이고 신청하신 적이 없으시다면 문의해주세요.',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
else if (result === 3)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 404,
|
||||||
|
message: '올바르지 않은 봇 아이디입니다.',
|
||||||
|
errors: ['해당 아이디의 봇은 존재하지 않습니다. 다시 확인해주세요.'],
|
||||||
|
})
|
||||||
|
else if (result === 4)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 403,
|
||||||
|
message: '디스코드 서버에 참가해주세요.',
|
||||||
|
errors: ['봇 신청하시기 위해서는 공식 디스코드 서버에 참가해주셔야합니다.'],
|
||||||
|
})
|
||||||
return ResponseWrapper(res, { code: 200, data: result })
|
return ResponseWrapper(res, { code: 200, data: result })
|
||||||
})
|
})
|
||||||
.patch(async (req, res) => {
|
.patch(async (req, res) => {
|
||||||
@ -49,4 +76,4 @@ interface PostApiRequest extends GetApiRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Bots
|
export default Bots
|
||||||
|
|||||||
@ -7,28 +7,32 @@ import { SearchQuerySchema } from '@utils/Yup'
|
|||||||
|
|
||||||
import { BotList } from '@types'
|
import { BotList } from '@types'
|
||||||
|
|
||||||
const SearchBots = RequestHandler()
|
const SearchBots = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
|
||||||
.get(async (req: ApiRequest, res: NextApiResponse) => {
|
const validated = await SearchQuerySchema.validate({ q: req.query.q, page: req.query.page })
|
||||||
const validated = await SearchQuerySchema.validate({ q: req.query.q, page: req.query.page }).then(el => el).catch(e => {
|
.then(el => el)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
})
|
})
|
||||||
if(!validated) return
|
if (!validated) return
|
||||||
|
|
||||||
let result: BotList
|
let result: BotList
|
||||||
try {
|
try {
|
||||||
result = await get.list.search.load(JSON.stringify({ page: validated.page, query: validated.q }))
|
result = await get.list.search.load(
|
||||||
} catch {
|
JSON.stringify({ page: validated.page, query: validated.q })
|
||||||
return ResponseWrapper(res, { code: 400, message: '검색 문법이 잘못되었습니다.' })
|
)
|
||||||
}
|
} catch {
|
||||||
if(result.totalPage < validated.page || result.currentPage !== validated.page) return ResponseWrapper(res, { code: 404, message: '검색 결과가 없습니다.' })
|
return ResponseWrapper(res, { code: 400, message: '검색 문법이 잘못되었습니다.' })
|
||||||
else ResponseWrapper<BotList>(res, { code: 200, data: result })
|
}
|
||||||
})
|
if (result.totalPage < validated.page || result.currentPage !== validated.page)
|
||||||
|
return ResponseWrapper(res, { code: 404, message: '검색 결과가 없습니다.' })
|
||||||
|
else ResponseWrapper<BotList>(res, { code: 200, data: result })
|
||||||
|
})
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
query: {
|
query: {
|
||||||
q: string
|
q: string
|
||||||
page: string
|
page: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SearchBots
|
export default SearchBots
|
||||||
|
|||||||
@ -4,13 +4,12 @@ import { get } from '@utils/Query'
|
|||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Users = RequestHandler()
|
const Users = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
.get(async(req: ApiRequest, res) => {
|
console.log(req.query)
|
||||||
console.log(req.query)
|
const user = await get.user.load(req.query?.id)
|
||||||
const user = await get.user.load(req.query?.id)
|
if (!user) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 유저 입니다.' })
|
||||||
if(!user) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 유저 입니다.' })
|
else return ResponseWrapper(res, { code: 200, data: user })
|
||||||
else return ResponseWrapper(res, { code: 200, data: user })
|
})
|
||||||
})
|
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
query: {
|
query: {
|
||||||
@ -18,4 +17,4 @@ interface ApiRequest extends NextApiRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Users
|
export default Users
|
||||||
|
|||||||
@ -8,41 +8,49 @@ import { get } from '@utils/Query'
|
|||||||
import { BotBadgeType, DiscordEnpoints } from '@utils/Constants'
|
import { BotBadgeType, DiscordEnpoints } from '@utils/Constants'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Widget= RequestHandler()
|
const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
|
||||||
.get(async(req: ApiRequest, res: NextApiResponse) => {
|
const { id: param, type, style = 'flat', scale = 1, icon = true } = req.query
|
||||||
const { id: param, type, style='flat', scale=1, icon=true } = req.query
|
const splitted = param.split('.')
|
||||||
const splitted = param.split('.')
|
|
||||||
|
|
||||||
const validated = await WidgetOptionsSchema.validate({
|
const validated = await WidgetOptionsSchema.validate({
|
||||||
id: splitted.slice(0, splitted.length - 1).join('.'),
|
id: splitted.slice(0, splitted.length - 1).join('.'),
|
||||||
ext: splitted[splitted.length - 1],
|
ext: splitted[splitted.length - 1],
|
||||||
style,
|
style,
|
||||||
type,
|
type,
|
||||||
scale,
|
scale,
|
||||||
icon
|
icon,
|
||||||
}).then(el=> el).catch(e=> {
|
})
|
||||||
|
.then(el => el)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if(!validated) return
|
if (!validated) return
|
||||||
|
|
||||||
const data = await get.bot.load(validated.id)
|
|
||||||
|
|
||||||
if(!data) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
|
||||||
const userImage = !data.avatar ? null : await get.images.user.load(DiscordEnpoints.CDN.user(data.id, data.avatar, { format: 'png', size: 128 }))
|
|
||||||
const img = userImage || await get.images.user.load(DiscordEnpoints.CDN.default(data.tag, { format: 'png', size: 128 }))
|
|
||||||
res.setHeader('content-type', 'image/svg+xml; charset=utf-8')
|
|
||||||
const badgeData = {
|
|
||||||
...BotBadgeType(data)[type],
|
|
||||||
style: validated.style,
|
|
||||||
scale: validated.scale,
|
|
||||||
icon: validated.icon ? `data:image/png;base64,${img.toString('base64')}` : null
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send(badgen(badgeData))
|
const data = await get.bot.load(validated.id)
|
||||||
|
|
||||||
})
|
if (!data) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||||
|
const userImage = !data.avatar
|
||||||
|
? null
|
||||||
|
: await get.images.user.load(
|
||||||
|
DiscordEnpoints.CDN.user(data.id, data.avatar, { format: 'png', size: 128 })
|
||||||
|
)
|
||||||
|
const img =
|
||||||
|
userImage ||
|
||||||
|
(await get.images.user.load(
|
||||||
|
DiscordEnpoints.CDN.default(data.tag, { format: 'png', size: 128 })
|
||||||
|
))
|
||||||
|
res.setHeader('content-type', 'image/svg+xml; charset=utf-8')
|
||||||
|
const badgeData = {
|
||||||
|
...BotBadgeType(data)[type],
|
||||||
|
style: validated.style,
|
||||||
|
scale: validated.scale,
|
||||||
|
icon: validated.icon ? `data:image/png;base64,${img.toString('base64')}` : null,
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(badgen(badgeData))
|
||||||
|
})
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
query: {
|
query: {
|
||||||
@ -54,4 +62,4 @@ interface ApiRequest extends NextApiRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Widget
|
export default Widget
|
||||||
|
|||||||
@ -21,7 +21,7 @@ const Segment = dynamic(() => import('@components/Segment'))
|
|||||||
const SEO = dynamic(() => import('@components/SEO'))
|
const SEO = dynamic(() => import('@components/SEO'))
|
||||||
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
||||||
|
|
||||||
const VoteBot: NextPage<VoteBotProps> = ({ data, user, csrfToken }) => {
|
const VoteBot: NextPage<VoteBotProps> = ({ data, csrfToken }) => {
|
||||||
console.log(csrfToken)
|
console.log(csrfToken)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
if(!data?.id) return <NotFound />
|
if(!data?.id) return <NotFound />
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { NextPage } from 'next'
|
import { NextPage } from 'next'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
const Reserved:NextPage = () => {
|
const Reserved: NextPage = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
router.push('/bots/iu')
|
router.push('/bots/iu')
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Reserved
|
export default Reserved
|
||||||
|
|||||||
@ -7,4 +7,4 @@ const Developers: NextPage = () => {
|
|||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Developers
|
export default Developers
|
||||||
|
|||||||
@ -14,4 +14,4 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": ["meta: dependencies"],
|
"labels": ["meta: dependencies"],
|
||||||
"reviewers": ["team:koreanbots-devs"],
|
"reviewers": ["team:koreanbots-devs"],
|
||||||
"schedule": [
|
"schedule": ["before 8am"],
|
||||||
"before 8am"
|
"extends": ["config:base"],
|
||||||
],
|
"timezone": "Asia/Seoul"
|
||||||
"extends": [
|
|
||||||
"config:base"
|
|
||||||
],
|
|
||||||
"timezone": "Asia/Seoul"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,9 @@ test('checking Permission', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('check CDN URL', () => {
|
test('check CDN URL', () => {
|
||||||
expect(DiscordEnpoints.CDN.user('000000000000000000', 'abcdefghijklm', { format: 'jpg', size: 1024 })).toBe('https://cdn.discordapp.com/avatars/000000000000000000/abcdefghijklm.jpg?size=1024')
|
expect(
|
||||||
|
DiscordEnpoints.CDN.user('000000000000000000', 'abcdefghijklm', { format: 'jpg', size: 1024 })
|
||||||
|
).toBe('https://cdn.discordapp.com/avatars/000000000000000000/abcdefghijklm.jpg?size=1024')
|
||||||
})
|
})
|
||||||
|
|
||||||
export { }
|
export {}
|
||||||
|
|||||||
@ -1,36 +1,25 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@components/*": ["components/*"],
|
"@components/*": ["components/*"],
|
||||||
"@utils/*": ["utils/*"],
|
"@utils/*": ["utils/*"],
|
||||||
"@types": ["types/index.ts"]
|
"@types": ["types/index.ts"]
|
||||||
|
},
|
||||||
},
|
"target": "es5",
|
||||||
"target": "es5",
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"lib": [
|
"allowJs": true,
|
||||||
"dom",
|
"skipLibCheck": true,
|
||||||
"dom.iterable",
|
"strict": false,
|
||||||
"esnext"
|
"forceConsistentCasingInFileNames": true,
|
||||||
],
|
"noEmit": true,
|
||||||
"allowJs": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"module": "esnext",
|
||||||
"strict": false,
|
"moduleResolution": "node",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"resolveJsonModule": true,
|
||||||
"noEmit": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"jsx": "preserve"
|
||||||
"module": "esnext",
|
},
|
||||||
"moduleResolution": "node",
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"resolveJsonModule": true,
|
"exclude": ["node_modules"]
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"next-env.d.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
types/global.d.ts
vendored
2
types/global.d.ts
vendored
@ -3,4 +3,4 @@ declare module 'yup' {
|
|||||||
class ArraySchema extends Yup.array {
|
class ArraySchema extends Yup.array {
|
||||||
unique(format?: string): this
|
unique(format?: string): this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export enum UserFlags {
|
|||||||
general = 0 << 0,
|
general = 0 << 0,
|
||||||
staff = 1 << 0,
|
staff = 1 << 0,
|
||||||
bughunter = 1 << 1,
|
bughunter = 1 << 1,
|
||||||
premium = 1 << 2
|
premium = 1 << 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BotFlags {
|
export enum BotFlags {
|
||||||
@ -56,23 +56,23 @@ export enum BotFlags {
|
|||||||
partnered = 1 << 3,
|
partnered = 1 << 3,
|
||||||
verifed = 1 << 4,
|
verifed = 1 << 4,
|
||||||
premium = 1 << 5,
|
premium = 1 << 5,
|
||||||
hackerthon = 1 << 6
|
hackerthon = 1 << 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DiscordUserFlags {
|
export enum DiscordUserFlags {
|
||||||
DISCORD_EMPLOYEE = 1 << 0,
|
DISCORD_EMPLOYEE = 1 << 0,
|
||||||
DISCORD_PARTNER = 1 << 1,
|
DISCORD_PARTNER = 1 << 1,
|
||||||
HYPESQUAD_EVENTS = 1 << 2,
|
HYPESQUAD_EVENTS = 1 << 2,
|
||||||
BUGHUNTER_LEVEL_1 = 1 << 3,
|
BUGHUNTER_LEVEL_1 = 1 << 3,
|
||||||
HOUSE_BRAVERY = 1 << 6,
|
HOUSE_BRAVERY = 1 << 6,
|
||||||
HOUSE_BRILLIANCE = 1 << 7,
|
HOUSE_BRILLIANCE = 1 << 7,
|
||||||
HOUSE_BALANCE = 1 << 8,
|
HOUSE_BALANCE = 1 << 8,
|
||||||
EARLY_SUPPORTER = 1 << 9,
|
EARLY_SUPPORTER = 1 << 9,
|
||||||
TEAM_USER = 1 << 10,
|
TEAM_USER = 1 << 10,
|
||||||
SYSTEM = 1 << 12,
|
SYSTEM = 1 << 12,
|
||||||
BUGHUNTER_LEVEL_2 = 1 << 14,
|
BUGHUNTER_LEVEL_2 = 1 << 14,
|
||||||
VERIFIED_BOT = 1 << 16,
|
VERIFIED_BOT = 1 << 16,
|
||||||
VERIFIED_DEVELOPER = 1 << 17
|
VERIFIED_DEVELOPER = 1 << 17,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BotList {
|
export interface BotList {
|
||||||
@ -97,7 +97,6 @@ export interface SubmittedBot {
|
|||||||
discord: string | null
|
discord: string | null
|
||||||
state: number
|
state: number
|
||||||
reason: string | null
|
reason: string | null
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiscordTokenInfo {
|
export interface DiscordTokenInfo {
|
||||||
@ -215,7 +214,7 @@ export enum DiscordImageType {
|
|||||||
EMOJI = 'emoji',
|
EMOJI = 'emoji',
|
||||||
GUILD = 'guild',
|
GUILD = 'guild',
|
||||||
USER = 'user',
|
USER = 'user',
|
||||||
FALLBACK = 'default'
|
FALLBACK = 'default',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CsrfContext extends NextPageContext {
|
export interface CsrfContext extends NextPageContext {
|
||||||
@ -226,7 +225,7 @@ export interface CsrfRequestMessage extends IncomingMessage {
|
|||||||
csrfToken(): string
|
csrfToken(): string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResponseProps<T=Data> {
|
export interface ResponseProps<T = Data> {
|
||||||
code?: number
|
code?: number
|
||||||
message?: string
|
message?: string
|
||||||
version?: number
|
version?: number
|
||||||
@ -234,6 +233,6 @@ export interface ResponseProps<T=Data> {
|
|||||||
errors?: string[]
|
errors?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Data<T=unknown> {
|
interface Data<T = unknown> {
|
||||||
[key: string]: T
|
[key: string]: T
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,20 +9,23 @@ const csrfKey = '_csrf'
|
|||||||
|
|
||||||
const Token = new csrf()
|
const Token = new csrf()
|
||||||
|
|
||||||
export const tokenCreate = ():string => Token.create(process.env.CSRF_SECRET)
|
export const tokenCreate = (): string => Token.create(process.env.CSRF_SECRET)
|
||||||
|
|
||||||
export const tokenVerify = (token: string):boolean => Token.verify(process.env.CSRF_SECRET, token)
|
export const tokenVerify = (token: string): boolean => Token.verify(process.env.CSRF_SECRET, token)
|
||||||
|
|
||||||
export const getToken = (req: IncomingMessage, res: ServerResponse) => {
|
export const getToken = (req: IncomingMessage, res: ServerResponse) => {
|
||||||
const parsed = parse(req.headers.cookie || '')
|
const parsed = parse(req.headers.cookie || '')
|
||||||
let key:string = parsed[csrfKey]
|
let key: string = parsed[csrfKey]
|
||||||
if(!key || !tokenVerify(key)) {
|
if (!key || !tokenVerify(key)) {
|
||||||
key = tokenCreate()
|
key = tokenCreate()
|
||||||
res.setHeader('set-cookie', serialize(csrfKey, key, {
|
res.setHeader(
|
||||||
expires: new Date(+new Date() + 24 * 60 * 60 * 1000),
|
'set-cookie',
|
||||||
httpOnly: true,
|
serialize(csrfKey, key, {
|
||||||
path: '/'
|
expires: new Date(+new Date() + 24 * 60 * 60 * 1000),
|
||||||
}))
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return key
|
return key
|
||||||
@ -30,8 +33,8 @@ export const getToken = (req: IncomingMessage, res: ServerResponse) => {
|
|||||||
|
|
||||||
export const checkToken = (req: NextApiRequest, res: NextApiResponse, token: string): boolean => {
|
export const checkToken = (req: NextApiRequest, res: NextApiResponse, token: string): boolean => {
|
||||||
const parsed = parse(req.headers.cookie || '')
|
const parsed = parse(req.headers.cookie || '')
|
||||||
if(parsed[csrfKey] !== token || !tokenVerify(token)) {
|
if (parsed[csrfKey] !== token || !tokenVerify(token)) {
|
||||||
ResponseWrapper(res, { code: 400, message: 'CSRF 검증 에러 (페이지를 새로고침해주세요)' })
|
ResponseWrapper(res, { code: 400, message: 'CSRF 검증 에러 (페이지를 새로고침해주세요)' })
|
||||||
return false
|
return false
|
||||||
} else return true
|
} else return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,4 +12,4 @@ DiscordBot.on('ready', async () => {
|
|||||||
DiscordBot.login(process.env.DISCORD_TOKEN)
|
DiscordBot.login(process.env.DISCORD_TOKEN)
|
||||||
|
|
||||||
const getMainGuild = () => DiscordBot.guilds.cache.get(guildID)
|
const getMainGuild = () => DiscordBot.guilds.cache.get(guildID)
|
||||||
export { DiscordBot, getMainGuild }
|
export { DiscordBot, getMainGuild }
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { ResponseProps } from '@types'
|
import { ResponseProps } from '@types'
|
||||||
import { KoreanbotsEndPoints } from './Constants'
|
import { KoreanbotsEndPoints } from './Constants'
|
||||||
|
|
||||||
const Fetch = async <T>(endpoint: string, options?: RequestInit):Promise<ResponseProps<T>> => {
|
const Fetch = async <T>(endpoint: string, options?: RequestInit): Promise<ResponseProps<T>> => {
|
||||||
const url = KoreanbotsEndPoints.baseAPI + ( endpoint.startsWith('/') ? endpoint : '/' + endpoint)
|
const url = KoreanbotsEndPoints.baseAPI + (endpoint.startsWith('/') ? endpoint : '/' + endpoint)
|
||||||
|
|
||||||
const res = await fetch(url, { method: 'GET', headers: { 'content-type': 'application/json', ...options.headers }, ...options })
|
const res = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'content-type': 'application/json', ...options.headers },
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
let json = {}
|
let json = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -13,8 +17,8 @@ const Fetch = async <T>(endpoint: string, options?: RequestInit):Promise<Respons
|
|||||||
} catch {
|
} catch {
|
||||||
json = { code: 500, message: 'Internal Server Error' }
|
json = { code: 500, message: 'Internal Server Error' }
|
||||||
}
|
}
|
||||||
|
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Fetch
|
export default Fetch
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default knex({
|
|||||||
user: process.env.MYSQL_USER || 'root',
|
user: process.env.MYSQL_USER || 'root',
|
||||||
password: process.env.MYSQL_PASSWORD,
|
password: process.env.MYSQL_PASSWORD,
|
||||||
database: process.env.MYSQL_DATABASE || 'discordbots',
|
database: process.env.MYSQL_DATABASE || 'discordbots',
|
||||||
charset: 'utf8mb4'
|
charset: 'utf8mb4',
|
||||||
},
|
},
|
||||||
debug: process.env.NODE_ENV === 'development',
|
debug: process.env.NODE_ENV === 'development',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -7,10 +7,10 @@ const Logger = {
|
|||||||
},
|
},
|
||||||
error: function(message: string) {
|
error: function(message: string) {
|
||||||
print('ERROR', message, genStyle('red', 'white'))
|
print('ERROR', message, genStyle('red', 'white'))
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function genStyle(bg: string, text='black') {
|
function genStyle(bg: string, text = 'black') {
|
||||||
return `color:${text};background:${bg};padding:1px 3px;border-radius:2px;margin-right:5px;`
|
return `color:${text};background:${bg};padding:1px 3px;border-radius:2px;margin-right:5px;`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
import { NextApiResponse } from 'next'
|
import { NextApiResponse } from 'next'
|
||||||
import ResponseWrapper from './ResponseWrapper'
|
import ResponseWrapper from './ResponseWrapper'
|
||||||
|
|
||||||
export default function RateLimitHandler(
|
export default function RateLimitHandler(res: NextApiResponse, ratelimit: RateLimit) {
|
||||||
res: NextApiResponse, ratelimit: RateLimit
|
|
||||||
) {
|
|
||||||
res.setHeader('x-ratelimit-limit', ratelimit.limit)
|
res.setHeader('x-ratelimit-limit', ratelimit.limit)
|
||||||
res.setHeader('x-ratelimit-remaining', 600 - (ratelimit.used > ratelimit.limit ? ratelimit.limit : ratelimit.used))
|
res.setHeader(
|
||||||
|
'x-ratelimit-remaining',
|
||||||
|
600 - (ratelimit.used > ratelimit.limit ? ratelimit.limit : ratelimit.used)
|
||||||
|
)
|
||||||
res.setHeader('x-ratelimit-reset', Math.round(ratelimit.reset / 1000))
|
res.setHeader('x-ratelimit-reset', Math.round(ratelimit.reset / 1000))
|
||||||
if(ratelimit.limit < ratelimit.used) {
|
if (ratelimit.limit < ratelimit.used) {
|
||||||
if(ratelimit.onLimitExceed) ratelimit.onLimitExceed(res)
|
if (ratelimit.onLimitExceed) ratelimit.onLimitExceed(res)
|
||||||
else ResponseWrapper(res, { code: 429 })
|
else ResponseWrapper(res, { code: 429 })
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface RateLimit {
|
interface RateLimit {
|
||||||
used: number
|
used: number
|
||||||
limit: number
|
limit: number
|
||||||
reset: number,
|
reset: number
|
||||||
onLimitExceed: (res: NextApiResponse)=> void | Promise<void>
|
onLimitExceed: (res: NextApiResponse) => void | Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,8 @@ export const Prefix = /^[^\s]/
|
|||||||
export const HTTPProtocol = /^https?:\/\/.*?/
|
export const HTTPProtocol = /^https?:\/\/.*?/
|
||||||
export const Url = urlRegex({ strict: true })
|
export const Url = urlRegex({ strict: true })
|
||||||
|
|
||||||
export const Emoji = '(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])'
|
export const Emoji =
|
||||||
|
'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])'
|
||||||
export const Heading = '<h\\d id="(.+?)">(.*?)<\\/h(\\d)>'
|
export const Heading = '<h\\d id="(.+?)">(.*?)<\\/h(\\d)>'
|
||||||
export const EmojiSyntax = ':(\\w+):'
|
export const EmojiSyntax = ':(\\w+):'
|
||||||
export const ImageTag = /<img\s[^>]*?alt\s*=\s*['"]([^'"]*?)['"][^>]*?>/
|
export const ImageTag = /<img\s[^>]*?alt\s*=\s*['"]([^'"]*?)['"][^>]*?>/
|
||||||
|
|||||||
@ -2,14 +2,15 @@ import { NextApiRequest, NextApiResponse } from 'next'
|
|||||||
import nc from 'next-connect'
|
import nc from 'next-connect'
|
||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
|
|
||||||
const RequestHandler = () => nc<NextApiRequest, NextApiResponse>({
|
const RequestHandler = () =>
|
||||||
onNoMatch(_req, res) {
|
nc<NextApiRequest, NextApiResponse>({
|
||||||
return ResponseWrapper(res, { code: 405 })
|
onNoMatch(_req, res) {
|
||||||
},
|
return ResponseWrapper(res, { code: 405 })
|
||||||
onError(err, _req, res) {
|
},
|
||||||
console.error(err)
|
onError(err, _req, res) {
|
||||||
return ResponseWrapper(res, { code: 500 })
|
console.error(err)
|
||||||
}
|
return ResponseWrapper(res, { code: 500 })
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export default RequestHandler
|
export default RequestHandler
|
||||||
|
|||||||
@ -2,14 +2,19 @@ import http from 'http'
|
|||||||
import { NextApiResponse } from 'next'
|
import { NextApiResponse } from 'next'
|
||||||
import { ResponseProps } from '@types'
|
import { ResponseProps } from '@types'
|
||||||
import { ErrorText } from './Constants'
|
import { ErrorText } from './Constants'
|
||||||
export default function ResponseWrapper<T=unknown>(
|
export default function ResponseWrapper<T = unknown>(
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
{ code=200, message, version = 2, data, errors }: ResponseProps<T>
|
{ code = 200, message, version = 2, data, errors }: ResponseProps<T>
|
||||||
) {
|
) {
|
||||||
if (!code) throw new Error('`code` is required.')
|
if (!code) throw new Error('`code` is required.')
|
||||||
if (!http.STATUS_CODES[code]) throw new Error('Invalid http code.')
|
if (!http.STATUS_CODES[code]) throw new Error('Invalid http code.')
|
||||||
res.statusCode = code
|
res.statusCode = code
|
||||||
res.setHeader('Access-Control-Allow-Origin', process.env.KOREANBOTS_URL)
|
res.setHeader('Access-Control-Allow-Origin', process.env.KOREANBOTS_URL)
|
||||||
res.json({ code, data, errors, version, ...(message || !data ? { message: message || ErrorText[code] || http.STATUS_CODES[code] } : {}) })
|
res.json({
|
||||||
|
code,
|
||||||
|
data,
|
||||||
|
errors,
|
||||||
|
version,
|
||||||
|
...(message || !data ? { message: message || ErrorText[code] || http.STATUS_CODES[code] } : {}),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
188
utils/Yup.ts
188
utils/Yup.ts
@ -1,4 +1,3 @@
|
|||||||
import { TokenExpiredError } from 'jsonwebtoken'
|
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import YupKorean from 'yup-locales-ko'
|
import YupKorean from 'yup-locales-ko'
|
||||||
import { ListType } from '../types'
|
import { ListType } from '../types'
|
||||||
@ -8,17 +7,23 @@ import { HTTPProtocol, ID, Prefix, Url, Vanity } from './Regex'
|
|||||||
Yup.setLocale(YupKorean)
|
Yup.setLocale(YupKorean)
|
||||||
Yup.addMethod(Yup.array, 'unique', function(message, mapper = a => a) {
|
Yup.addMethod(Yup.array, 'unique', function(message, mapper = a => a) {
|
||||||
return this.test('unique', message || 'array must be unique', function(list) {
|
return this.test('unique', message || 'array must be unique', function(list) {
|
||||||
return list.length === new Set(list.map(mapper)).size
|
return list.length === new Set(list.map(mapper)).size
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export const botListArgumentSchema: Yup.SchemaOf<botListArgument> = Yup.object({
|
export const botListArgumentSchema: Yup.SchemaOf<botListArgument> = Yup.object({
|
||||||
type: Yup.mixed().oneOf(['VOTE', 'TRUSTED', 'NEW', 'PARTNERED', 'CATEGORY', 'SEARCH']).required(),
|
type: Yup.mixed()
|
||||||
page: Yup.number().positive().integer().notRequired().default(1),
|
.oneOf(['VOTE', 'TRUSTED', 'NEW', 'PARTNERED', 'CATEGORY', 'SEARCH'])
|
||||||
query: Yup.string().notRequired()
|
.required(),
|
||||||
|
page: Yup.number()
|
||||||
|
.positive()
|
||||||
|
.integer()
|
||||||
|
.notRequired()
|
||||||
|
.default(1),
|
||||||
|
query: Yup.string().notRequired(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface botListArgument {
|
export interface botListArgument {
|
||||||
type: ListType
|
type: ListType
|
||||||
page?: number
|
page?: number
|
||||||
query?: string
|
query?: string
|
||||||
@ -26,8 +31,12 @@ export interface botListArgument {
|
|||||||
|
|
||||||
export const ImageOptionsSchema: Yup.SchemaOf<ImageOptions> = Yup.object({
|
export const ImageOptionsSchema: Yup.SchemaOf<ImageOptions> = Yup.object({
|
||||||
id: Yup.string().required(),
|
id: Yup.string().required(),
|
||||||
ext: Yup.mixed<ext>().oneOf(['webp', 'png', 'gif']).required(),
|
ext: Yup.mixed<ext>()
|
||||||
size: Yup.mixed<ImageSize>().oneOf(['128', '256', '512']).required()
|
.oneOf(['webp', 'png', 'gif'])
|
||||||
|
.required(),
|
||||||
|
size: Yup.mixed<ImageSize>()
|
||||||
|
.oneOf(['128', '256', '512'])
|
||||||
|
.required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
interface ImageOptions {
|
interface ImageOptions {
|
||||||
@ -41,11 +50,21 @@ type ImageSize = '128' | '256' | '512'
|
|||||||
|
|
||||||
export const WidgetOptionsSchema: Yup.SchemaOf<WidgetOptions> = Yup.object({
|
export const WidgetOptionsSchema: Yup.SchemaOf<WidgetOptions> = Yup.object({
|
||||||
id: Yup.string().required(),
|
id: Yup.string().required(),
|
||||||
ext: Yup.mixed<widgetExt>().oneOf(['svg']).required(),
|
ext: Yup.mixed<widgetExt>()
|
||||||
type: Yup.mixed<widgetType>().oneOf(['votes', 'servers', 'status']).required(),
|
.oneOf(['svg'])
|
||||||
scale: Yup.number().positive().min(0.5).max(3).required(),
|
.required(),
|
||||||
style: Yup.mixed<'flat'|'classic'>().oneOf(['flat', 'classic']).default('flat'),
|
type: Yup.mixed<widgetType>()
|
||||||
icon: Yup.boolean().default(true)
|
.oneOf(['votes', 'servers', 'status'])
|
||||||
|
.required(),
|
||||||
|
scale: Yup.number()
|
||||||
|
.positive()
|
||||||
|
.min(0.5)
|
||||||
|
.max(3)
|
||||||
|
.required(),
|
||||||
|
style: Yup.mixed<'flat' | 'classic'>()
|
||||||
|
.oneOf(['flat', 'classic'])
|
||||||
|
.default('flat'),
|
||||||
|
icon: Yup.boolean().default(true),
|
||||||
})
|
})
|
||||||
|
|
||||||
interface WidgetOptions {
|
interface WidgetOptions {
|
||||||
@ -60,15 +79,20 @@ interface WidgetOptions {
|
|||||||
type widgetType = 'votes' | 'servers' | 'status'
|
type widgetType = 'votes' | 'servers' | 'status'
|
||||||
type widgetExt = 'svg'
|
type widgetExt = 'svg'
|
||||||
|
|
||||||
export const PageCount = Yup.number().integer().positive().required()
|
export const PageCount = Yup.number()
|
||||||
|
.integer()
|
||||||
|
.positive()
|
||||||
|
.required()
|
||||||
|
|
||||||
export const OauthCallbackSchema: Yup.SchemaOf<OauthCallback> = Yup.object({
|
export const OauthCallbackSchema: Yup.SchemaOf<OauthCallback> = Yup.object({
|
||||||
code: Yup.string().required()
|
code: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const botCategoryListArgumentSchema: Yup.SchemaOf<botCategoryListArgument> = Yup.object({
|
export const botCategoryListArgumentSchema: Yup.SchemaOf<botCategoryListArgument> = Yup.object({
|
||||||
page: PageCount,
|
page: PageCount,
|
||||||
category: Yup.mixed().oneOf(categories).required()
|
category: Yup.mixed()
|
||||||
|
.oneOf(categories)
|
||||||
|
.required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
interface botCategoryListArgument {
|
interface botCategoryListArgument {
|
||||||
@ -81,8 +105,17 @@ interface OauthCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SearchQuerySchema: Yup.SchemaOf<SearchQuery> = Yup.object({
|
export const SearchQuerySchema: Yup.SchemaOf<SearchQuery> = Yup.object({
|
||||||
q: Yup.string().min(2, '최소 2글자 이상 입력해주세요.').max(50).required('검색어를 입력해주세요.').label('검색어'),
|
q: Yup.string()
|
||||||
page: Yup.number().positive().integer().notRequired().default(1).label('페이지')
|
.min(2, '최소 2글자 이상 입력해주세요.')
|
||||||
|
.max(50)
|
||||||
|
.required('검색어를 입력해주세요.')
|
||||||
|
.label('검색어'),
|
||||||
|
page: Yup.number()
|
||||||
|
.positive()
|
||||||
|
.integer()
|
||||||
|
.notRequired()
|
||||||
|
.default(1)
|
||||||
|
.label('페이지'),
|
||||||
})
|
})
|
||||||
|
|
||||||
interface SearchQuery {
|
interface SearchQuery {
|
||||||
@ -91,18 +124,49 @@ interface SearchQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AddBotSubmitSchema: Yup.SchemaOf<AddBotSubmit> = Yup.object({
|
export const AddBotSubmitSchema: Yup.SchemaOf<AddBotSubmit> = Yup.object({
|
||||||
agree: Yup.boolean().oneOf([true], '상단의 체크박스를 클릭해주세요.').required('상단의 체크박스를 클릭해주세요.'),
|
agree: Yup.boolean()
|
||||||
id: Yup.string().matches(ID, '올바른 봇 ID를 입력해주세요.').required('봇 ID는 필수 항목입니다.'),
|
.oneOf([true], '상단의 체크박스를 클릭해주세요.')
|
||||||
prefix: Yup.string().matches(Prefix, '접두사는 띄어쓰기로 시작할 수 없습니다.').min(1, '접두사는 최소 1자여야합니다.').max(32, '접두사는 최대 32자까지만 가능합니다.').required('접두사는 필수 항목입니다.'),
|
.required('상단의 체크박스를 클릭해주세요.'),
|
||||||
library: Yup.string().oneOf(library).required('라이브러리는 필수 항목입니다.'),
|
id: Yup.string()
|
||||||
website: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 웹사이트 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
.matches(ID, '올바른 봇 ID를 입력해주세요.')
|
||||||
url: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 초대링크 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
.required('봇 ID는 필수 항목입니다.'),
|
||||||
git: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 깃 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
prefix: Yup.string()
|
||||||
discord: Yup.string().matches(Vanity, '디스코드 초대코드 형식을 지켜주세요.').min(2, '지원 디스코드는 최소 2자여야합니다.').max(32, '지원 디스코드는 최대 32자까지만 가능합니다.'),
|
.matches(Prefix, '접두사는 띄어쓰기로 시작할 수 없습니다.')
|
||||||
category: Yup.array(Yup.string().oneOf(categories)).min(1, '최소 한 개의 카테고리를 선택해주세요.').unique('카테고리는 중복될 수 없습니다.').required('카테고리는 필수 항목입니다.'),
|
.min(1, '접두사는 최소 1자여야합니다.')
|
||||||
intro: Yup.string().min(2, '봇 소개는 최소 2자여야합니다.').max(60, '봇 소개는 최대 60자여야합니다.').required('봇 소개는 필수 항목입니다.'),
|
.max(32, '접두사는 최대 32자까지만 가능합니다.')
|
||||||
desc: Yup.string().min(100, '봇 설명은 최소 100자여야합니다.').max(1500, '봇 설명은 최대 1500자여야합니다.').required('봇 설명은 필수 항목입니다.'),
|
.required('접두사는 필수 항목입니다.'),
|
||||||
_csrf: Yup.string().required()
|
library: Yup.string()
|
||||||
|
.oneOf(library)
|
||||||
|
.required('라이브러리는 필수 항목입니다.'),
|
||||||
|
website: Yup.string()
|
||||||
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 웹사이트 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
url: Yup.string()
|
||||||
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 초대링크 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
git: Yup.string()
|
||||||
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 깃 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
discord: Yup.string()
|
||||||
|
.matches(Vanity, '디스코드 초대코드 형식을 지켜주세요.')
|
||||||
|
.min(2, '지원 디스코드는 최소 2자여야합니다.')
|
||||||
|
.max(32, '지원 디스코드는 최대 32자까지만 가능합니다.'),
|
||||||
|
category: Yup.array(Yup.string().oneOf(categories))
|
||||||
|
.min(1, '최소 한 개의 카테고리를 선택해주세요.')
|
||||||
|
.unique('카테고리는 중복될 수 없습니다.')
|
||||||
|
.required('카테고리는 필수 항목입니다.'),
|
||||||
|
intro: Yup.string()
|
||||||
|
.min(2, '봇 소개는 최소 2자여야합니다.')
|
||||||
|
.max(60, '봇 소개는 최대 60자여야합니다.')
|
||||||
|
.required('봇 소개는 필수 항목입니다.'),
|
||||||
|
desc: Yup.string()
|
||||||
|
.min(100, '봇 설명은 최소 100자여야합니다.')
|
||||||
|
.max(1500, '봇 설명은 최대 1500자여야합니다.')
|
||||||
|
.required('봇 설명은 필수 항목입니다.'),
|
||||||
|
_csrf: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface AddBotSubmit {
|
export interface AddBotSubmit {
|
||||||
@ -121,32 +185,66 @@ export interface AddBotSubmit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ManageBotSchema = Yup.object({
|
export const ManageBotSchema = Yup.object({
|
||||||
prefix: Yup.string().matches(Prefix, '접두사는 띄어쓰기로 시작할 수 없습니다.').min(1, '접두사는 최소 1자여야합니다.').max(32, '접두사는 최대 32자까지만 가능합니다.').required('접두사는 필수 항목입니다.'),
|
prefix: Yup.string()
|
||||||
library: Yup.string().oneOf(library).required('라이브러리는 필수 항목입니다.'),
|
.matches(Prefix, '접두사는 띄어쓰기로 시작할 수 없습니다.')
|
||||||
website: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 웹사이트 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
.min(1, '접두사는 최소 1자여야합니다.')
|
||||||
url: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 초대링크 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
.max(32, '접두사는 최대 32자까지만 가능합니다.')
|
||||||
git: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 깃 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
.required('접두사는 필수 항목입니다.'),
|
||||||
discord: Yup.string().matches(Vanity, '디스코드 초대코드 형식을 지켜주세요.').min(2, '지원 디스코드는 최소 2자여야합니다.').max(32, '지원 디스코드는 최대 32자까지만 가능합니다.'),
|
library: Yup.string()
|
||||||
category: Yup.array(Yup.string().oneOf(categories)).min(1, '최소 한 개의 카테고리를 선택해주세요.').unique('카테고리는 중복될 수 없습니다.').required('카테고리는 필수 항목입니다.'),
|
.oneOf(library)
|
||||||
intro: Yup.string().min(2, '봇 소개는 최소 2자여야합니다.').max(60, '봇 소개는 최대 60자여야합니다.').required('봇 소개는 필수 항목입니다.'),
|
.required('라이브러리는 필수 항목입니다.'),
|
||||||
desc: Yup.string().min(100, '봇 설명은 최소 100자여야합니다.').max(1500, '봇 설명은 최대 1500자여야합니다.').required('봇 설명은 필수 항목입니다.'),
|
website: Yup.string()
|
||||||
owners: Yup.array(Yup.string()).min(1, '최소 한 명의 소유자는 입력해주세요.').max(10, '소유자는 최대 10명까지만 가능합니다.').unique('소유자 아이디는 중복될 수 없습니다.').required('소유자는 필수 항목입니다.'),
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
_csrf: Yup.string().required()
|
.matches(Url, '올바른 웹사이트 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
url: Yup.string()
|
||||||
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 초대링크 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
git: Yup.string()
|
||||||
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 깃 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
discord: Yup.string()
|
||||||
|
.matches(Vanity, '디스코드 초대코드 형식을 지켜주세요.')
|
||||||
|
.min(2, '지원 디스코드는 최소 2자여야합니다.')
|
||||||
|
.max(32, '지원 디스코드는 최대 32자까지만 가능합니다.'),
|
||||||
|
category: Yup.array(Yup.string().oneOf(categories))
|
||||||
|
.min(1, '최소 한 개의 카테고리를 선택해주세요.')
|
||||||
|
.unique('카테고리는 중복될 수 없습니다.')
|
||||||
|
.required('카테고리는 필수 항목입니다.'),
|
||||||
|
intro: Yup.string()
|
||||||
|
.min(2, '봇 소개는 최소 2자여야합니다.')
|
||||||
|
.max(60, '봇 소개는 최대 60자여야합니다.')
|
||||||
|
.required('봇 소개는 필수 항목입니다.'),
|
||||||
|
desc: Yup.string()
|
||||||
|
.min(100, '봇 설명은 최소 100자여야합니다.')
|
||||||
|
.max(1500, '봇 설명은 최대 1500자여야합니다.')
|
||||||
|
.required('봇 설명은 필수 항목입니다.'),
|
||||||
|
owners: Yup.array(Yup.string())
|
||||||
|
.min(1, '최소 한 명의 소유자는 입력해주세요.')
|
||||||
|
.max(10, '소유자는 최대 10명까지만 가능합니다.')
|
||||||
|
.unique('소유자 아이디는 중복될 수 없습니다.')
|
||||||
|
.required('소유자는 필수 항목입니다.'),
|
||||||
|
_csrf: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const DeveloperBotSchema: Yup.SchemaOf<DeveloperBot> = Yup.object({
|
export const DeveloperBotSchema: Yup.SchemaOf<DeveloperBot> = Yup.object({
|
||||||
webhook: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 웹훅 URL을 입력해주세요.').max(150, 'URL은 최대 150자까지만 가능합니다.'),
|
webhook: Yup.string()
|
||||||
_csrf: Yup.string().required()
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 웹훅 URL을 입력해주세요.')
|
||||||
|
.max(150, 'URL은 최대 150자까지만 가능합니다.'),
|
||||||
|
_csrf: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface DeveloperBot {
|
export interface DeveloperBot {
|
||||||
webhook: string | null
|
webhook: string | null
|
||||||
_csrf: string
|
_csrf: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResetBotTokenSchema = Yup.object({
|
export const ResetBotTokenSchema = Yup.object({
|
||||||
token: Yup.string().required(),
|
token: Yup.string().required(),
|
||||||
_csrf: Yup.string().required()
|
_csrf: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface ResetBotToken {
|
export interface ResetBotToken {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { RefObject, useEffect } from 'react'
|
import { RefObject, useEffect } from 'react'
|
||||||
|
|
||||||
const useOutsideClick = (ref: RefObject<HTMLElement>, callback: () => void) => {
|
const useOutsideClick = (ref: RefObject<HTMLElement>, callback: () => void) => {
|
||||||
const handleClick = (e) => {
|
const handleClick = e => {
|
||||||
if (ref.current && !ref.current.contains(e.target)) {
|
if (ref.current && !ref.current.contains(e.target)) {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
@ -16,4 +16,4 @@ const useOutsideClick = (ref: RefObject<HTMLElement>, callback: () => void) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useOutsideClick
|
export default useOutsideClick
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user