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