mirror of
https://github.com/koreanbots/core.git
synced 2025-12-15 22: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,
|
node: true,
|
||||||
es6: true,
|
es6: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
es2021: true
|
es2021: true,
|
||||||
},
|
},
|
||||||
ignorePatterns: ['node_modules/*', '.next/*', '.out/*', '!.prettierrc.js'],
|
ignorePatterns: ['node_modules/*', '.next/*', '.out/*', '!.prettierrc.js'],
|
||||||
extends: [
|
extends: [
|
||||||
@ -12,20 +12,17 @@ module.exports = {
|
|||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
'plugin:jsx-a11y/recommended'
|
'plugin:jsx-a11y/recommended',
|
||||||
],
|
],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true
|
jsx: true,
|
||||||
},
|
},
|
||||||
ecmaVersion: 12,
|
ecmaVersion: 12,
|
||||||
sourceType: 'module'
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: ['react', '@typescript-eslint'],
|
||||||
'react',
|
|
||||||
'@typescript-eslint'
|
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
'jsx-quotes': ['error', 'prefer-single'],
|
'jsx-quotes': ['error', 'prefer-single'],
|
||||||
'react/no-unescaped-entities': 'off',
|
'react/no-unescaped-entities': 'off',
|
||||||
@ -36,17 +33,8 @@ module.exports = {
|
|||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': ['warn'],
|
'@typescript-eslint/no-unused-vars': ['warn'],
|
||||||
indent: [
|
indent: ['error', 'tab'],
|
||||||
'error',
|
quotes: ['error', 'single'],
|
||||||
'tab'
|
semi: ['error', 'never'],
|
||||||
],
|
},
|
||||||
quotes: [
|
|
||||||
'error',
|
|
||||||
'single'
|
|
||||||
],
|
|
||||||
semi: [
|
|
||||||
'error',
|
|
||||||
'never'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
14
.github/ISSUE_TEMPLATE/bug.md
vendored
14
.github/ISSUE_TEMPLATE/bug.md
vendored
@ -1,32 +1,38 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
name: 🐛 버그 제보
|
name: 🐛 버그 제보
|
||||||
about: 버그를 제보해주세요!
|
about: 버그를 제보해주세요!
|
||||||
title: "[버그] "
|
title: '[버그] '
|
||||||
labels: 'bug'
|
labels: 'bug'
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
---## 재현방법
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 재현방법
|
|
||||||
> 어떻게하면 발생시킬 수 있나요?
|
> 어떻게하면 발생시킬 수 있나요?
|
||||||
|
|
||||||
## 예상되는 정상적인 동작
|
## 예상되는 정상적인 동작
|
||||||
|
|
||||||
> 정상이라면 어떻게 되야하나요?
|
> 정상이라면 어떻게 되야하나요?
|
||||||
|
|
||||||
## 발생한 문제
|
## 발생한 문제
|
||||||
|
|
||||||
> 어떤 문제가 발생하나요?
|
> 어떤 문제가 발생하나요?
|
||||||
|
|
||||||
## 클라이언트 버전
|
## 클라이언트 버전
|
||||||
|
|
||||||
> 클라이언트 버전을 알려주세요!
|
> 클라이언트 버전을 알려주세요!
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
클라이언트 버전을 가져오실 줄 모르신다면 아래 링크를 참고해주세요
|
클라이언트 버전을 가져오실 줄 모르신다면 아래 링크를 참고해주세요
|
||||||
|
|
||||||
https://github.com/koreanbots/docs/blob/master/version.md
|
https://github.com/koreanbots/docs/blob/master/version.md
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 사양
|
## 사양
|
||||||
|
|
||||||
> OS 정보와 브라우저의 버전을 알려주세요!
|
> OS 정보와 브라우저의 버전을 알려주세요!
|
||||||
|
|
||||||
## 확인
|
## 확인
|
||||||
|
|
||||||
- [ ] 중복되는 이슈는 없나요?
|
- [ ] 중복되는 이슈는 없나요?
|
||||||
- [ ] 해당 버그를 다시 발생시킬 수 있나요?
|
- [ ] 해당 버그를 다시 발생시킬 수 있나요?
|
||||||
|
|
||||||
|
|||||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.next/
|
||||||
|
node_modules/
|
||||||
25
app.css
25
app.css
@ -13,13 +13,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Uni Sans Heavy CAPS";
|
font-family: 'Uni Sans Heavy CAPS';
|
||||||
src: url("/logofont.otf");
|
src: url('/logofont.otf');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logofont {
|
.logofont {
|
||||||
font-family: "Uni Sans Heavy CAPS";
|
font-family: 'Uni Sans Heavy CAPS';
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-dropdown {
|
.animation-dropdown {
|
||||||
@ -36,7 +36,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
width: 20px
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html * ::-webkit-scrollbar {
|
html * ::-webkit-scrollbar {
|
||||||
@ -49,16 +49,16 @@ html * ::-webkit-scrollbar-thumb {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
-webkit-transition: color .2s ease;
|
-webkit-transition: color 0.2s ease;
|
||||||
transition: color .2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
html .dark * ::-webkit-scrollbar-thumb {
|
html .dark * ::-webkit-scrollbar-thumb {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #202225;
|
background: #202225;
|
||||||
-webkit-transition: color .2s ease;
|
-webkit-transition: color 0.2s ease;
|
||||||
transition: color .2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
html * ::-webkit-scrollbar-track {
|
html * ::-webkit-scrollbar-track {
|
||||||
@ -73,7 +73,9 @@ html .dark * ::-webkit-scrollbar-track {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .__multi-value, .dark .__multi-value__label, .dark .__multi-value__remove {
|
.dark .__multi-value,
|
||||||
|
.dark .__multi-value__label,
|
||||||
|
.dark .__multi-value__remove {
|
||||||
background: #2e3338 !important;
|
background: #2e3338 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +96,7 @@ button {
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-image: url("https://unpkg.com/emoji-datasource-twitter@5.0.1/img/twitter/sheets-256/64.png");
|
background-image: url('https://unpkg.com/emoji-datasource-twitter@5.0.1/img/twitter/sheets-256/64.png');
|
||||||
background-size: 5700% 5700%;
|
background-size: 5700% 5700%;
|
||||||
background-position: 53.5714% 62.5%;
|
background-position: 53.5714% 62.5%;
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
@ -108,6 +110,7 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-mart-category-list > *, .emoji-mart-emoji > span {
|
.emoji-mart-category-list > *,
|
||||||
|
.emoji-mart-emoji > span {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -10,24 +10,36 @@ const Advertisement = ({ size='short' }:AdvertisementProps): JSX.Element => {
|
|||||||
Logger.debug('Ads Pushed')
|
Logger.debug('Ads Pushed')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div className={`z-0 mx-auto w-full text-center text-white ${process.env.NODE_ENV === 'production' ? '' : 'py-12 bg-gray-700'}`} style={size === 'short' ? { height: '90px' } : { height: '330px'}}>
|
return (
|
||||||
{
|
<div
|
||||||
process.env.NODE_ENV === 'production' ? <ins
|
className={`z-0 mx-auto w-full text-center text-white ${
|
||||||
|
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'
|
className='adsbygoogle mb-5 w-full'
|
||||||
style={{ display: 'inline-block', height: '90px' }}
|
style={{ display: 'inline-block', height: '90px' }}
|
||||||
data-ad-client='ca-pub-4856582423981759'
|
data-ad-client='ca-pub-4856582423981759'
|
||||||
data-ad-slot='3250141451'
|
data-ad-slot='3250141451'
|
||||||
data-adtest='on'
|
data-adtest='on'
|
||||||
data-full-width-responsive='true'
|
data-full-width-responsive='true'
|
||||||
></ins> : 'Advertisement'
|
></ins>
|
||||||
}</div>
|
) : (
|
||||||
|
'Advertisement'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window { adsbygoogle: {
|
interface Window {
|
||||||
|
adsbygoogle: {
|
||||||
loaded?: boolean
|
loaded?: boolean
|
||||||
push(obj: unknown): void
|
push(obj: unknown): void
|
||||||
} }
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdvertisementProps {
|
interface AdvertisementProps {
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import Link from 'next/link'
|
|||||||
import DiscordAvatar from './DiscordAvatar'
|
import DiscordAvatar from './DiscordAvatar'
|
||||||
|
|
||||||
const Application = ({ type, id, name }: ApplicationProps): JSX.Element => {
|
const Application = ({ type, id, name }: ApplicationProps): JSX.Element => {
|
||||||
return <Link href={`/developers/applications/${type+'s'}/${id}`}>
|
return (
|
||||||
<div className='relative py-4 px-2 bg-little-white dark:bg-discord-black text-center transform hover:-translate-y-1 transition duration-100 ease-in cursor-pointer rounded-lg'>
|
<Link href={`/developers/applications/${type + 's'}/${id}`}>
|
||||||
<DiscordAvatar userID={id} className='w-full rounded-xl px-2' />
|
<div className='relative px-2 py-4 text-center dark:bg-discord-black bg-little-white rounded-lg cursor-pointer transform hover:-translate-y-1 transition duration-100 ease-in'>
|
||||||
<h2 className='text-xl font-medium pt-2 whitespace-nowrap truncate'>{name}</h2>
|
<DiscordAvatar userID={id} className='px-2 w-full rounded-xl' />
|
||||||
|
<h2 className='pt-2 whitespace-nowrap text-xl font-medium truncate'>{name}</h2>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApplicationProps {
|
interface ApplicationProps {
|
||||||
|
|||||||
@ -14,13 +14,28 @@ const BotCard = ({ manage=false, bot }: BotProps): JSX.Element => {
|
|||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='h-full'>
|
<div className='h-full'>
|
||||||
<div className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl' style={checkBotFlag(bot.flags, 'trusted') && bot.banner ? { background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${bot.banner}") center top / cover no-repeat`, color: 'white' } : {}}>
|
<div
|
||||||
|
className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl'
|
||||||
|
style={
|
||||||
|
checkBotFlag(bot.flags, 'trusted') && bot.banner
|
||||||
|
? {
|
||||||
|
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${bot.banner}") center top / cover no-repeat`,
|
||||||
|
color: 'white',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
<Link href={makeBotURL(bot)}>
|
<Link href={makeBotURL(bot)}>
|
||||||
<a className='cursor-pointer'>
|
<a className='cursor-pointer'>
|
||||||
<div className='flex h-44'>
|
<div className='flex h-44'>
|
||||||
<div className='w-2/3'>
|
<div className='w-2/3'>
|
||||||
<div className='flex justify-start'>
|
<div className='flex justify-start'>
|
||||||
<DiscordAvatar size={128} userID={bot.id} alt='Avatar' className='rounded-full absolute -left-2 -top-8 mx-auto w-32 h-32 bg-white'/>
|
<DiscordAvatar
|
||||||
|
size={128}
|
||||||
|
userID={bot.id}
|
||||||
|
alt='Avatar'
|
||||||
|
className='absolute -left-2 -top-8 mx-auto w-32 h-32 bg-white rounded-full'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-28 px-4'>
|
<div className='mt-28 px-4'>
|
||||||
@ -28,9 +43,7 @@ const BotCard = ({ manage=false, bot }: BotProps): JSX.Element => {
|
|||||||
<i className={`fas fa-circle text-${Status[bot.status]?.color}`} />
|
<i className={`fas fa-circle text-${Status[bot.status]?.color}`} />
|
||||||
{Status[bot.status]?.text}
|
{Status[bot.status]?.text}
|
||||||
</h2>
|
</h2>
|
||||||
<h1 className='mb-3 text-left text-2xl font-bold truncate'>
|
<h1 className='mb-3 text-left text-2xl font-bold truncate'>{bot.name}</h1>
|
||||||
{bot.name}
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid grid-cols-1 pr-5 py-5 w-1/3 h-0'>
|
<div className='grid grid-cols-1 pr-5 py-5 w-1/3 h-0'>
|
||||||
@ -42,43 +55,53 @@ const BotCard = ({ manage=false, bot }: BotProps): JSX.Element => {
|
|||||||
}
|
}
|
||||||
dark
|
dark
|
||||||
/>
|
/>
|
||||||
<Tag blurple text={bot.servers ? <>{formatNumber(bot.servers)} 서버</> : 'N/A'} dark />
|
<Tag
|
||||||
|
blurple
|
||||||
|
text={bot.servers ? <>{formatNumber(bot.servers)} 서버</> : 'N/A'}
|
||||||
|
dark
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className='px-4 text-left text-gray-400 text-sm font-medium mb-10 h-6'>{bot.intro}</p>
|
<p className='mb-10 px-4 h-6 text-left text-gray-400 text-sm font-medium'>
|
||||||
<div className='category px-2 flex flex-wrap'>
|
{bot.intro}
|
||||||
|
</p>
|
||||||
|
<div className='category flex flex-wrap px-2'>
|
||||||
{bot.category.slice(0, 3).map(el => (
|
{bot.category.slice(0, 3).map(el => (
|
||||||
<Tag key={el} text={el} href={`/categories/${el}`} dark />
|
<Tag key={el} text={el} href={`/categories/${el}`} dark />
|
||||||
))} {
|
))}{' '}
|
||||||
bot.category.length > 3 && <Tag text={`+${bot.category.length - 3}`} dark />
|
{bot.category.length > 3 && <Tag text={`+${bot.category.length - 3}`} dark />}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className='flex justify-evenly'>
|
<div className='flex justify-evenly'>
|
||||||
<Link
|
<Link href={makeBotURL(bot)}>
|
||||||
href={makeBotURL(bot)}
|
<a className='py-3 w-full text-center text-koreanbots-blue hover:text-white text-sm font-bold hover:bg-koreanbots-blue rounded-bl-2xl hover:shadow-lg transition duration-100 ease-in'>
|
||||||
>
|
|
||||||
<a className='rounded-bl-2xl py-3 w-full text-center text-koreanbots-blue hover:text-white text-sm font-bold hover:bg-koreanbots-blue hover:shadow-lg transition duration-100 ease-in'>
|
|
||||||
보기
|
보기
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{
|
{manage ? (
|
||||||
manage ? <Link href={`/manage/${bot.id}`}>
|
<Link href={`/manage/${bot.id}`}>
|
||||||
<a
|
<a className='py-3 w-full text-center text-green-500 hover:text-white text-sm font-bold hover:bg-green-500 rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'>
|
||||||
className='rounded-br-2xl py-3 w-full text-center text-green-500 hover:text-white text-sm font-bold hover:bg-green-500 hover:shadow-lg transition duration-100 ease-in'
|
|
||||||
>
|
|
||||||
관리하기
|
관리하기
|
||||||
</a>
|
</a>
|
||||||
</Link> : <Link href={bot.url || `https://discordapp.com/oauth2/authorize?client_id=${bot.id}&scope=bot&permissions=0`}>
|
</Link>
|
||||||
<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'
|
<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>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,16 +1,41 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
const Button = ({ type='button', className, children, href, onClick }: ButtonProps):JSX.Element => {
|
const Button = ({
|
||||||
return href ? <Link href={href}>
|
type = 'button',
|
||||||
<a className={`cursor-pointer rounded-md px-4 py-2 m-1 transition duration-300 ease select-none outline-none foucs:outline-none ${className ?? 'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}>
|
className,
|
||||||
|
children,
|
||||||
|
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}
|
{children}
|
||||||
</a>
|
</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'}`}>
|
</Link>
|
||||||
{ children }
|
) : onClick ? (
|
||||||
</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'}`}>
|
<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}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
className={`cursor-pointer rounded-md px-4 py-2 m-0.5 transition duration-300 ease select-none outline-none foucs:outline-none ${className ??
|
||||||
|
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
const ColorCard = ({ header, first, second, className }: ColorCardProps): JSX.Element => {
|
const ColorCard = ({ header, first, second, className }: ColorCardProps): JSX.Element => {
|
||||||
return <div className={`rounded-lg p-10 ${className} shadow-lg`}>
|
return (
|
||||||
|
<div className={`rounded-lg p-10 ${className} shadow-lg`}>
|
||||||
<h2 className='text-2xl font-bold'>{header}</h2>
|
<h2 className='text-2xl font-bold'>{header}</h2>
|
||||||
<p className='opacity-80'>
|
<p className='opacity-80'>
|
||||||
{first} <br />
|
{first} <br />
|
||||||
{second}
|
{second}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColorCardProps {
|
interface ColorCardProps {
|
||||||
|
|||||||
@ -8,9 +8,9 @@ const Container = ({
|
|||||||
}: ContainerProps): JSX.Element => {
|
}: ContainerProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${ignoreColor ? '' : 'text-black dark:text-gray-100'} ${
|
||||||
ignoreColor ? '' : 'text-black dark:text-gray-100'
|
paddingTop ? 'pt-20' : ''
|
||||||
} ${paddingTop ? 'pt-20' : ''}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`container mx-auto px-4 ${className}`}>{children}</div>
|
<div className={`container mx-auto px-4 ${className}`}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,10 @@ import SEO from './SEO'
|
|||||||
const Docs = ({ title, header, description, subheader, children }: DocsProps): JSX.Element => {
|
const Docs = ({ title, header, description, subheader, children }: DocsProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SEO title={typeof header === 'string' ? header : title} description={description || subheader} />
|
<SEO
|
||||||
|
title={typeof header === 'string' ? header : title}
|
||||||
|
description={description || subheader}
|
||||||
|
/>
|
||||||
<div className='dark:bg-discord-black bg-discord-blurple'>
|
<div className='dark:bg-discord-black bg-discord-blurple'>
|
||||||
<Container className='pb-10 pt-20' ignoreColor>
|
<Container className='pb-10 pt-20' ignoreColor>
|
||||||
<h1 className='mt-10 text-center text-gray-100 text-4xl font-bold sm:text-left'>
|
<h1 className='mt-10 text-center text-gray-100 text-4xl font-bold sm:text-left'>
|
||||||
@ -21,7 +24,7 @@ const Docs = ({ title, header, description, subheader, children }: DocsProps): J
|
|||||||
</div>
|
</div>
|
||||||
<Wave
|
<Wave
|
||||||
color='currentColor'
|
color='currentColor'
|
||||||
className='dark:text-discord-black text-discord-blurple dark:bg-discord-dark bg-white hidden md:block'
|
className='hidden dark:text-discord-black text-discord-blurple dark:bg-discord-dark bg-white md:block'
|
||||||
/>
|
/>
|
||||||
<Container>
|
<Container>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
|
|||||||
@ -8,7 +8,11 @@ import { Theme } from '@types'
|
|||||||
const Footer = ({ color, theme, setTheme }: FooterProps): JSX.Element => {
|
const Footer = ({ color, theme, setTheme }: FooterProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className='releative z-30'>
|
<div className='releative z-30'>
|
||||||
<Wave color='currentColor' className={`${color ?? 'dark:text-discord-dark text-white bg-discord-black'} hidden md:block`} />
|
<Wave
|
||||||
|
color='currentColor'
|
||||||
|
className={`${color ??
|
||||||
|
'dark:text-discord-dark text-white bg-discord-black'} hidden md:block`}
|
||||||
|
/>
|
||||||
<div className='bottom-0 text-white bg-discord-black'>
|
<div className='bottom-0 text-white bg-discord-black'>
|
||||||
<Container className='pb-20 pt-10 w-11/12 lg:flex lg:pt-0 lg:w-4/5' ignoreColor>
|
<Container className='pb-20 pt-10 w-11/12 lg:flex lg:pt-0 lg:w-4/5' ignoreColor>
|
||||||
<div className='w-full md:w-2/5'>
|
<div className='w-full md:w-2/5'>
|
||||||
@ -28,8 +32,8 @@ const Footer = ({ color, theme, setTheme }:FooterProps): JSX.Element => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-grow grid grid-cols-2 md:grid-cols-7 gap-2'>
|
<div className='grid flex-grow gap-2 grid-cols-2 md:grid-cols-7'>
|
||||||
<div className='mb-2 col-span-2'>
|
<div className='col-span-2 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>한국 디스코드봇 리스트</h2>
|
<h2 className='text-koreanbots-blue text-base font-bold'>한국 디스코드봇 리스트</h2>
|
||||||
<ul className='text-sm'>
|
<ul className='text-sm'>
|
||||||
<li>
|
<li>
|
||||||
@ -44,7 +48,7 @@ const Footer = ({ color, theme, setTheme }:FooterProps): JSX.Element => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-2 col-span-1'>
|
<div className='col-span-1 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>커뮤니티</h2>
|
<h2 className='text-koreanbots-blue text-base font-bold'>커뮤니티</h2>
|
||||||
<ul className='text-sm'>
|
<ul className='text-sm'>
|
||||||
<li>
|
<li>
|
||||||
@ -59,7 +63,7 @@ const Footer = ({ color, theme, setTheme }:FooterProps): JSX.Element => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-2 col-span-1'>
|
<div className='col-span-1 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>정책</h2>
|
<h2 className='text-koreanbots-blue text-base font-bold'>정책</h2>
|
||||||
<ul className='text-sm'>
|
<ul className='text-sm'>
|
||||||
<li>
|
<li>
|
||||||
@ -79,19 +83,21 @@ const Footer = ({ color, theme, setTheme }:FooterProps): JSX.Element => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-2 col-span-2'>
|
<div className='col-span-2 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>기타</h2>
|
<h2 className='text-koreanbots-blue text-base font-bold'>기타</h2>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<a className='hover:text-gray-300 mr-2'>다크모드</a>
|
<a className='mr-2 hover:text-gray-300'>다크모드</a>
|
||||||
<Toggle checked={theme === 'dark'} onChange={() => {
|
<Toggle
|
||||||
|
checked={theme === 'dark'}
|
||||||
|
onChange={() => {
|
||||||
const t = theme === 'dark' ? 'light' : 'dark'
|
const t = theme === 'dark' ? 'light' : 'dark'
|
||||||
setTheme(t)
|
setTheme(t)
|
||||||
localStorage.setItem('theme', t)
|
localStorage.setItem('theme', t)
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Field } from 'formik'
|
import { Field } from 'formik'
|
||||||
|
|
||||||
const CheckBox = ({ name, ...props }: CheckBoxProps): JSX.Element => {
|
const CheckBox = ({ name, ...props }: CheckBoxProps): JSX.Element => {
|
||||||
return <Field type='checkbox' name={name} className='mr-1 h-4 w-4 rounded' {...props}/>
|
return <Field type='checkbox' name={name} className='mr-1 w-4 h-4 rounded' {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CheckBoxProps {
|
interface CheckBoxProps {
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { Field } from 'formik'
|
import { Field } from 'formik'
|
||||||
|
|
||||||
const Input = ({ name, placeholder }: InputProps): JSX.Element => {
|
const Input = ({ name, placeholder }: InputProps): JSX.Element => {
|
||||||
return <Field name={name} className='border border-grey-light dark:border-transparent text-black dark:bg-very-black dark:text-white w-full h-10 rounded px-3 relative outline-none' placeholder={placeholder}/>
|
return (
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
className='border-grey-light relative px-3 w-full h-10 text-black dark:text-white dark:bg-very-black border dark:border-transparent rounded outline-none'
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputProps {
|
interface InputProps {
|
||||||
|
|||||||
@ -1,22 +1,37 @@
|
|||||||
const Label = ({ For, children, label, labelDesc, error=null, grid=true, short=false, required=false }:LabelProps):JSX.Element => {
|
const Label = ({
|
||||||
return <>
|
For,
|
||||||
<label className={grid ? 'grid grid-cols-1 xl:grid-cols-4 gap-2 my-4' : 'inline-flex items-center'} htmlFor={For}>
|
children,
|
||||||
{
|
label,
|
||||||
label && <div className='col-span-1 text-sm'>
|
labelDesc,
|
||||||
<h3 className='text-lg font-bold text-discord-blurple'>{label}
|
error = null,
|
||||||
{
|
grid = true,
|
||||||
required && <span className='text-base font-semibold align-text-top text-red-500'> *</span>
|
short = false,
|
||||||
}
|
required = false,
|
||||||
|
}: 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>
|
</h3>
|
||||||
{labelDesc}
|
{labelDesc}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
<div className={short ? 'col-span-1' : 'col-span-3'}>
|
<div className={short ? 'col-span-1' : 'col-span-3'}>
|
||||||
{children}
|
{children}
|
||||||
<div className='text-red-500 text-xs font-light mt-1'>{error}</div>
|
<div className='mt-1 text-red-500 text-xs font-light'>{error}</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LabelProps {
|
interface LabelProps {
|
||||||
|
|||||||
@ -1,16 +1,31 @@
|
|||||||
import ReactSelect from 'react-select'
|
import ReactSelect from 'react-select'
|
||||||
|
|
||||||
const Select = ({ placeholder, options, handleChange, handleTouch }: SelectProps): JSX.Element => {
|
const Select = ({ placeholder, options, handleChange, handleTouch }: SelectProps): JSX.Element => {
|
||||||
return <ReactSelect styles={{
|
return (
|
||||||
control: (provided) => {
|
<ReactSelect
|
||||||
|
styles={{
|
||||||
|
control: provided => {
|
||||||
return { ...provided, border: 'none' }
|
return { ...provided, border: 'none' }
|
||||||
},
|
},
|
||||||
option: (provided) => {
|
option: provided => {
|
||||||
return { ...provided, cursor: 'pointer', ':hover': {
|
return {
|
||||||
opacity: '0.7'
|
...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={() => '검색 결과가 없습니다.'}/>
|
},
|
||||||
|
}}
|
||||||
|
className='border-grey-light border dark:border-transparent rounded'
|
||||||
|
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white '
|
||||||
|
placeholder={placeholder || '선택해주세요.'}
|
||||||
|
options={options}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleTouch}
|
||||||
|
noOptionsMessage={() => '검색 결과가 없습니다.'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectProps {
|
interface SelectProps {
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
const Loader = ({ text, visible = true }: LoaderProps): JSX.Element => {
|
const Loader = ({ text, visible = true }: LoaderProps): JSX.Element => {
|
||||||
return <div className={`${visible ? '' : 'hidden '}w-full h-full fixed block top-0 left-0 bg-gray-500 bg-opacity-75 z-50 dark:text-black`}>
|
return (
|
||||||
<h1 className='text-2xl font-semibold opacity-100 top-1/2 my-0 mx-auto block relative text-center'>
|
<div
|
||||||
|
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}
|
{text}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoaderProps {
|
interface LoaderProps {
|
||||||
|
|||||||
@ -4,21 +4,102 @@ import sanitizeHtml from 'sanitize-html'
|
|||||||
import Emoji from 'node-emoji'
|
import Emoji from 'node-emoji'
|
||||||
|
|
||||||
const Markdown = ({ text }: MarkdownProps): JSX.Element => {
|
const Markdown = ({ text }: MarkdownProps): JSX.Element => {
|
||||||
return <div className='w-full markdown-body'>
|
return (
|
||||||
<MarkdownView markdown={Emoji.emojify(text)} extensions={[ twemoji, customEmoji, anchorHeader ]} options={{ openLinksInNewWindow: true, underline: true, omitExtraWLInCodeBlocks: true, literalMidWordUnderscores: true, simplifiedAutoLink: true, tables: true, strikethrough: true, smoothLivePreview: true, tasklists: true, ghCompatibleHeaderId: true, encodeEmails: true }} sanitizeHtml={(html)=> sanitizeHtml(html, {
|
<div className='markdown-body w-full'>
|
||||||
|
<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: [
|
allowedTags: [
|
||||||
'addr', 'address', 'article', 'aside', 'h1', 'h2', 'h3', 'h4',
|
'addr',
|
||||||
'h5', 'h6', 'section', 'blockquote', 'dd', 'div',
|
'address',
|
||||||
'dl', 'dt', 'hr', 'li', 'ol', 'p', 'pre',
|
'article',
|
||||||
'ul', 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn',
|
'aside',
|
||||||
'em', 'i', 'kbd', 'mark', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp',
|
'h1',
|
||||||
'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr', 'caption',
|
'h2',
|
||||||
'col', 'colgroup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'del',
|
'h3',
|
||||||
'img', 'svg', 'path', 'input'
|
'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
|
allowedAttributes: false,
|
||||||
})} />
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MarkdownProps {
|
interface MarkdownProps {
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { MessageColor } from '@utils/Constants'
|
import { MessageColor } from '@utils/Constants'
|
||||||
|
|
||||||
const Message = ({ type, children }: MessageProps): JSX.Element => {
|
const Message = ({ type, children }: MessageProps): JSX.Element => {
|
||||||
return <div className={`${MessageColor[type]} px-6 py-4 rounded-md text-base mx-auto w-full text-left`}>
|
return (
|
||||||
|
<div
|
||||||
|
className={`${MessageColor[type]} px-6 py-4 rounded-md text-base mx-auto w-full text-left`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
|
|||||||
@ -3,25 +3,30 @@ import { Modal as ReactModal} from 'react-responsive-modal'
|
|||||||
import 'react-responsive-modal/styles.css'
|
import 'react-responsive-modal/styles.css'
|
||||||
|
|
||||||
const Modal = ({ children, isOpen, onClose, dark, header }: ModalProps): JSX.Element => {
|
const Modal = ({ children, isOpen, onClose, dark, header }: ModalProps): JSX.Element => {
|
||||||
return <ReactModal open={isOpen} onClose={onClose} center animationDuration={100} classNames={{
|
return (
|
||||||
modal: 'bg-discord-dark'
|
<ReactModal
|
||||||
|
open={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
center
|
||||||
|
animationDuration={100}
|
||||||
|
classNames={{
|
||||||
|
modal: 'bg-discord-dark',
|
||||||
}}
|
}}
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
styles={{
|
styles={{
|
||||||
modal: {
|
modal: {
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
background: dark ? '#2C2F33' : '#fbfbfb',
|
background: dark ? '#2C2F33' : '#fbfbfb',
|
||||||
color: dark ? 'white' : 'black'
|
color: dark ? 'white' : 'black',
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2 className='text-lg font-black uppercase'>{header}</h2>
|
<h2 className='text-lg font-black uppercase'>{header}</h2>
|
||||||
<div className='pt-4 relative'>
|
<div className='relative pt-4'>
|
||||||
<div>
|
<div>{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ReactModal>
|
</ReactModal>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
const Notice = ({ header, desc }: NoticeProps) => {
|
const Notice = ({ header, desc }: NoticeProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='py-48 px-10 mx-auto my-auto h-screen text-center'>
|
<div className='mx-auto my-auto px-10 py-48 h-screen text-center'>
|
||||||
<h1 className='text-4xl font-bold'>KOREANBOTS</h1>
|
<h1 className='text-4xl font-bold'>KOREANBOTS</h1>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<h1 className='text-3xl font-bold mb-10'>{header}</h1>
|
<h1 className='mb-10 text-3xl font-bold'>{header}</h1>
|
||||||
|
|
||||||
<h2 className='text-lg font-semibold'>{desc}</h2>
|
<h2 className='text-lg font-semibold'>{desc}</h2>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@ -2,17 +2,19 @@ import Link from 'next/link'
|
|||||||
import DiscordAvatar from '@components/DiscordAvatar'
|
import DiscordAvatar from '@components/DiscordAvatar'
|
||||||
|
|
||||||
const Owner = ({ id, username, tag }: OwnerProps): JSX.Element => {
|
const Owner = ({ id, username, tag }: OwnerProps): JSX.Element => {
|
||||||
return <Link href={`/users/${id}`}>
|
return (
|
||||||
<a className='text-base bg-little-white dark:bg-discord-black text-black dark:text-gray-400 rounded flex hover:bg-little-white-hover dark:hover:bg-discord-dark-hover cursor-pointer px-4 py-4 mb-1'>
|
<Link href={`/users/${id}`}>
|
||||||
<div className='rounded-full h-8 w-8 flex-shrink-0 mr-3 mt-1 overflow-hidden shadow-inner relative'>
|
<a className='dark:hover:bg-discord-dark-hover flex mb-1 px-4 py-4 text-black dark:text-gray-400 text-base dark:bg-discord-black bg-little-white hover:bg-little-white-hover rounded cursor-pointer'>
|
||||||
<DiscordAvatar userID={id} className='absolute inset-0 z-negative w-full h-full'/>
|
<div className='relative flex-shrink-0 mr-3 mt-1 w-8 h-8 rounded-full shadow-inner overflow-hidden'>
|
||||||
|
<DiscordAvatar userID={id} className='z-negative absolute inset-0 w-full h-full' />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-1 leading-snug w-0'>
|
<div className='flex-1 w-0 leading-snug'>
|
||||||
<h4 className='whitespace-nowrap'>{username}
|
<h4 className='whitespace-nowrap'>{username}</h4>
|
||||||
</h4><span className='text-sm text-gray-600'>#{tag}</span>
|
<span className='text-gray-600 text-sm'>#{tag}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Owner
|
export default Owner
|
||||||
|
|||||||
@ -1,30 +1,76 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
const Paginator = ({ currentPage, totalPage, pathname }: PaginatorProps): JSX.Element => {
|
const Paginator = ({ currentPage, totalPage, pathname }: PaginatorProps): JSX.Element => {
|
||||||
let pages = []
|
let pages = []
|
||||||
if(currentPage < 4) pages = [ 1, totalPage < 2 ? null : 2, totalPage < 3 ? null : 3, totalPage < 4 ? null : 4, totalPage < 5 ? null : 5 ]
|
if (currentPage < 4)
|
||||||
else if(currentPage > totalPage - 3) pages = [ totalPage - 4 < 1 ? null : totalPage - 4, totalPage - 3 < 1 ? null : totalPage - 3, totalPage - 2 < 1 ? null : totalPage - 2, totalPage - 1 < 1 ? null : totalPage - 1, totalPage ]
|
pages = [
|
||||||
else pages = [ currentPage - 2 < 1 ? null : currentPage - 2, currentPage - 1 < 1 ? null : currentPage - 1, currentPage, currentPage + 1 > totalPage ? null : currentPage + 1, currentPage + 2 > totalPage ? null : currentPage + 2 ]
|
1,
|
||||||
|
totalPage < 2 ? null : 2,
|
||||||
|
totalPage < 3 ? null : 3,
|
||||||
|
totalPage < 4 ? null : 4,
|
||||||
|
totalPage < 5 ? null : 5,
|
||||||
|
]
|
||||||
|
else if (currentPage > totalPage - 3)
|
||||||
|
pages = [
|
||||||
|
totalPage - 4 < 1 ? null : totalPage - 4,
|
||||||
|
totalPage - 3 < 1 ? null : totalPage - 3,
|
||||||
|
totalPage - 2 < 1 ? null : totalPage - 2,
|
||||||
|
totalPage - 1 < 1 ? null : totalPage - 1,
|
||||||
|
totalPage,
|
||||||
|
]
|
||||||
|
else
|
||||||
|
pages = [
|
||||||
|
currentPage - 2 < 1 ? null : currentPage - 2,
|
||||||
|
currentPage - 1 < 1 ? null : currentPage - 1,
|
||||||
|
currentPage,
|
||||||
|
currentPage + 1 > totalPage ? null : currentPage + 1,
|
||||||
|
currentPage + 2 > totalPage ? null : currentPage + 2,
|
||||||
|
]
|
||||||
pages = pages.filter(el => el)
|
pages = pages.filter(el => el)
|
||||||
return <div className='flex flex-col items-center py-4 text-center justify-center'>
|
return (
|
||||||
|
<div className='flex flex-col items-center justify-center py-4 text-center'>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<Link href={{ pathname, query: { page: currentPage - 1 } }}>
|
<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`}>
|
<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>
|
<i className='fas fa-chevron-left'></i>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{
|
{pages.map((el, i) => (
|
||||||
pages.map((el, i) => <Link key={i} href={{ pathname, query: { page: el } }}>
|
<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>
|
<a
|
||||||
</Link>)
|
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 } }}>
|
<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`}>
|
<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>
|
<i className='fas fa-chevron-right'></i>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaginatorProps {
|
interface PaginatorProps {
|
||||||
|
|||||||
@ -17,14 +17,19 @@ const Search = (): JSX.Element => {
|
|||||||
const [hidden, setHidden] = useState(true)
|
const [hidden, setHidden] = useState(true)
|
||||||
const SearchResults = async (value: string) => {
|
const SearchResults = async (value: string) => {
|
||||||
setQuery(value)
|
setQuery(value)
|
||||||
try { abortControl.abort() } catch { return null }
|
try {
|
||||||
|
abortControl.abort()
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
setAbortControl(controller)
|
setAbortControl(controller)
|
||||||
if (value.length > 2) setLoading(true)
|
if (value.length > 2) setLoading(true)
|
||||||
const res = await Fetch<BotList>(`/search/bots?q=${encodeURIComponent(value)}`, { signal: controller.signal })
|
const res = await Fetch<BotList>(`/search/bots?q=${encodeURIComponent(value)}`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
setData(res)
|
setData(res)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
@ -32,39 +37,84 @@ const Search = (): JSX.Element => {
|
|||||||
redirectTo(router, `/search/?q=${encodeURIComponent(query)}`)
|
redirectTo(router, `/search/?q=${encodeURIComponent(query)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return (
|
||||||
<div onFocus={() => setHidden(false)} onBlur={() => setTimeout(() => setHidden(true), 80)} className='relative w-full mt-5 text-black bg-white dark:text-gray-100 dark:bg-very-black flex rounded-lg z-10'>
|
<div>
|
||||||
<input maxLength={50} className='bg-transparent flex-grow outline-none border-none shadow border-0 py-3 px-7 pr-20 h-16 text-xl' placeholder='검색...' value={query} onChange={(e)=> {
|
<div
|
||||||
|
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)
|
SearchResults(e.target.value)
|
||||||
}} onKeyDown={(e) => {
|
}}
|
||||||
|
onKeyDown={e => {
|
||||||
if (e.key === 'Enter') return onSubmit()
|
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
|
||||||
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={`relative ${hidden ? 'hidden' : 'block'}`}>
|
<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'>
|
<div className='pin-t pin-l absolute my-2 w-full h-60 text-black dark:text-gray-100 dark:bg-very-black bg-white rounded shadow-md overflow-y-scroll md:h-80'>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{data && data.code === 200 && data.data ? (
|
||||||
data && data.code === 200 && data.data ? data.data.data.length === 0 ? <li className='px-3 py-3.5'>검색 결과가 없습니다.</li> :
|
data.data.data.length === 0 ? (
|
||||||
data.data.data.map(el => <Link key={el.id} href={makeBotURL(el)}>
|
<li className='px-3 py-3.5'>검색 결과가 없습니다.</li>
|
||||||
<li className='px-3 py-2 flex h-15 cursor-pointer'>
|
) : (
|
||||||
<DiscordAvatar className='w-12 h-12 mt-1' size={128} userID={el.id} />
|
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'>
|
<div className='ml-2'>
|
||||||
<h1 className='text-lg text-black dark:text-gray-100'>{el.name}</h1>
|
<h1 className='text-black dark:text-gray-100 text-lg'>{el.name}</h1>
|
||||||
<p className='text-sm text-gray-400'>
|
<p className='text-gray-400 text-sm'>{el.intro}</p>
|
||||||
{el.intro}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</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>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Search
|
export default Search
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
const Segment = ({ children, className = '' }: SegmentProps): JSX.Element => {
|
const Segment = ({ children, className = '' }: SegmentProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={`py-3 px-7 text-black dark:text-white dark:bg-discord-black bg-little-white rounded-sm ${className}`}>
|
<div
|
||||||
|
className={`py-3 px-7 text-black dark:text-white dark:bg-discord-black bg-little-white rounded-sm ${className}`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,29 +6,38 @@ import { SubmittedBot } from '@types'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
const SubmittedBotCard = ({ href, submit }: SubmittedBotProps): JSX.Element => {
|
const SubmittedBotCard = ({ href, submit }: SubmittedBotProps): JSX.Element => {
|
||||||
return <Link href={href}>
|
return (
|
||||||
<a className='relative mx-auto w-full h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl px-4 py-5 transform hover:-translate-y-1 transition duration-100 ease-in'>
|
<Link href={href}>
|
||||||
|
<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='h-18'>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<div className='flex-grow w-full'>
|
<div className='flex-grow w-full'>
|
||||||
<h2 className='text-lg'>{submit.id}</h2>
|
<h2 className='text-lg'>{submit.id}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid grid-cols-1 px-4 w-2/5 h-0 absolute right-0'>
|
<div className='absolute right-0 grid grid-cols-1 px-4 w-2/5 h-0'>
|
||||||
<Tag
|
<Tag
|
||||||
text={
|
text={
|
||||||
<>
|
<>
|
||||||
<i className={`fas fa-circle text-${[Status.offline, Status.online, Status.dnd][submit.state]?.color}`} />
|
<i
|
||||||
{' '}{['대기중', '승인됨', '거부됨'][submit.state]}
|
className={`fas fa-circle text-${
|
||||||
|
[Status.offline, Status.online, Status.dnd][submit.state]?.color
|
||||||
|
}`}
|
||||||
|
/>{' '}
|
||||||
|
{['대기중', '승인됨', '거부됨'][submit.state]}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
dark
|
dark
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-left truncate text-gray-400 text-sm font-medium mt-1.5 h-6 w-full'>{submit.intro.slice(0, 25)}{submit.intro.length > 25 && '...'}</p>
|
<p className='mt-1.5 w-full h-6 text-left text-gray-400 text-sm font-medium truncate'>
|
||||||
|
{submit.intro.slice(0, 25)}
|
||||||
|
{submit.intro.length > 25 && '...'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubmittedBotProps {
|
interface SubmittedBotProps {
|
||||||
|
|||||||
@ -13,7 +13,8 @@ const Tag = ({
|
|||||||
bigger = false,
|
bigger = false,
|
||||||
...props
|
...props
|
||||||
}: LabelProps): JSX.Element => {
|
}: LabelProps): JSX.Element => {
|
||||||
return href ? newTab ? (
|
return href ? (
|
||||||
|
newTab ? (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
@ -27,12 +28,13 @@ const Tag = ({
|
|||||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||||
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
||||||
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
||||||
circular ? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}` : `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
circular
|
||||||
|
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
||||||
|
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
||||||
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}
|
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<Link href={href}>
|
<Link href={href}>
|
||||||
<a
|
<a
|
||||||
@ -44,13 +46,18 @@ const Tag = ({
|
|||||||
: github
|
: github
|
||||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||||
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
||||||
} ${!blurple && !github ? 'text-black dark:text-gray-400' : 'hover:bg-little-white-hover'} ${
|
} ${
|
||||||
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' : '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`}
|
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
{...props}
|
{...props}
|
||||||
@ -60,10 +67,20 @@ const Tag = ({
|
|||||||
? 'font-bg bg-discord-blurple text-white'
|
? 'font-bg bg-discord-blurple text-white'
|
||||||
: github
|
: github
|
||||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||||
: `bg-little-white-hover dark:bg-very-black ${props.onClick ? 'hover:bg-little-white dark:hover:bg-discord-dark-hover transition duration-100 ease-in' : '' }`
|
: `bg-little-white-hover dark:bg-very-black ${
|
||||||
: `bg-little-white dark:bg-discord-black ${props.onClick ? 'hover:bg-little-white-hover dark:hover:bg-discord-dark-hover transition duration-100 ease-in' : '' }`
|
props.onClick
|
||||||
|
? 'hover:bg-little-white dark:hover:bg-discord-dark-hover transition duration-100 ease-in'
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
: `bg-little-white dark:bg-discord-black ${
|
||||||
|
props.onClick
|
||||||
|
? 'hover:bg-little-white-hover dark:hover:bg-discord-dark-hover transition duration-100 ease-in'
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
||||||
circular ? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}` : `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
circular
|
||||||
|
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
||||||
|
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
||||||
} mr-1 mb-${marginBottom}`}
|
} mr-1 mb-${marginBottom}`}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
|
|||||||
@ -1,12 +1,27 @@
|
|||||||
const Toggle = ({ checked, onChange }: ToggleProps): JSX.Element => {
|
const Toggle = ({ checked, onChange }: ToggleProps): JSX.Element => {
|
||||||
return <button className='relative inline-block w-10 mr-2 align-middle select-none outline-none' onClick={onChange} onKeyPress={onChange}>
|
return (
|
||||||
<input type='checkbox' checked={checked} className='absolute block w-6 h-6 rounded-full bg-white border-4 border-transparent appearance-none cursor-pointer outline-none checked:right-0' readOnly />
|
<button
|
||||||
<span className={`block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer ${checked ? 'bg-koreanbots-blue' : ''}`}></span>
|
className='relative inline-block align-middle mr-2 w-10 outline-none select-none'
|
||||||
|
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>
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToggleProps {
|
interface ToggleProps {
|
||||||
checked: boolean,
|
checked: boolean
|
||||||
onChange(): void
|
onChange(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +1,107 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
const Tooltip = ({ href, size='small', children, direction='center', text }:TooltipProps):JSX.Element => {
|
const Tooltip = ({
|
||||||
return href ? <Link href={href}>
|
href,
|
||||||
|
size = 'small',
|
||||||
|
children,
|
||||||
|
direction = 'center',
|
||||||
|
text,
|
||||||
|
}: TooltipProps): JSX.Element => {
|
||||||
|
return href ? (
|
||||||
|
<Link href={href}>
|
||||||
<a className='inline'>
|
<a className='inline'>
|
||||||
<div className='relative py-3 inline'>
|
<div className='relative inline py-3'>
|
||||||
<div className='group cursor-pointer relative inline-block text-center'>{children}
|
<div className='group relative inline-block text-center cursor-pointer'>
|
||||||
<div className={`opacity-0 ${size==='small' ? 'w-44' : 'w-60'} bg-black text-white text-center text-xs rounded-lg py-2 px-3 absolute z-10 group-hover:opacity-100 bottom-full -left-4 pointer-events-none`}>
|
{children}
|
||||||
|
<div
|
||||||
|
className={`opacity-0 ${
|
||||||
|
size === 'small' ? 'w-44' : 'w-60'
|
||||||
|
} bg-black text-white text-center text-xs rounded-lg py-2 px-3 absolute z-10 group-hover:opacity-100 bottom-full -left-4 pointer-events-none`}
|
||||||
|
>
|
||||||
{text}
|
{text}
|
||||||
{
|
{direction === 'left' ? (
|
||||||
direction === 'left' ? <svg className='absolute text-black h-2 left-5 mr-3 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
<svg
|
||||||
: direction === 'center' ? <svg className='absolute text-black h-2 w-full left-0 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
className='absolute left-5 top-full mr-3 h-2 text-black'
|
||||||
: <svg className='absolute text-black h-2 right-5 mr-3 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
x='0px'
|
||||||
}
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
) : direction === 'center' ? (
|
||||||
|
<svg
|
||||||
|
className='absolute left-0 top-full w-full h-2 text-black'
|
||||||
|
x='0px'
|
||||||
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
className='absolute right-5 top-full mr-3 h-2 text-black'
|
||||||
|
x='0px'
|
||||||
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link> : <a className='inline'>
|
</Link>
|
||||||
<div className='relative py-3 inline'>
|
) : (
|
||||||
<div className='group cursor-pointer relative inline-block text-center'>{children}
|
<a className='inline'>
|
||||||
<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}
|
{text}
|
||||||
{
|
{direction === 'left' ? (
|
||||||
direction === 'left' ? <svg className='absolute text-black h-2 left-5 mr-3 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
<svg
|
||||||
: direction === 'center' ? <svg className='absolute text-black h-2 w-full left-0 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
className='absolute left-5 top-full mr-3 h-2 text-black'
|
||||||
: <svg className='absolute text-black h-2 right-5 mr-3 top-full' x='0px' y='0px' viewBox='0 0 255 255' xmlSpace='preserve'><polygon className='fill-current' points='0,0 127.5,127.5 255,0'/></svg>
|
x='0px'
|
||||||
}
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
) : direction === 'center' ? (
|
||||||
|
<svg
|
||||||
|
className='absolute left-0 top-full w-full h-2 text-black'
|
||||||
|
x='0px'
|
||||||
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
className='absolute right-5 top-full mr-3 h-2 text-black'
|
||||||
|
x='0px'
|
||||||
|
y='0px'
|
||||||
|
viewBox='0 0 255 255'
|
||||||
|
xmlSpace='preserve'
|
||||||
|
>
|
||||||
|
<polygon className='fill-current' points='0,0 127.5,127.5 255,0' />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TooltipProps {
|
interface TooltipProps {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
version: "3"
|
version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
apps : [{
|
apps: [
|
||||||
|
{
|
||||||
name: 'koreanbots',
|
name: 'koreanbots',
|
||||||
script: 'npm start',
|
script: 'npm start',
|
||||||
env: {
|
env: {
|
||||||
@ -8,6 +8,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
env_production: {
|
env_production: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
}
|
},
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
.dark .markdown-body {
|
.dark .markdown-body {
|
||||||
color: #ffffff
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
.markdown-body .octicon {
|
.markdown-body .octicon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -75,7 +75,8 @@
|
|||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #24292e;
|
color: #24292e;
|
||||||
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif,
|
||||||
|
Apple Color Emoji, Segoe UI Emoji;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
@ -105,7 +106,7 @@
|
|||||||
|
|
||||||
.markdown-body h1 {
|
.markdown-body h1 {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
margin: .67em 0;
|
margin: 0.67em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body img {
|
.markdown-body img {
|
||||||
@ -135,7 +136,7 @@
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body [type=checkbox] {
|
.markdown-body [type='checkbox'] {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@ -178,13 +179,13 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px solid hsla(0,0%,100%,.1);
|
border-bottom: 1px solid hsla(0, 0%, 100%, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body hr:after,
|
.markdown-body hr:after,
|
||||||
.markdown-body hr:before {
|
.markdown-body hr:before {
|
||||||
display: table;
|
display: table;
|
||||||
content: "";
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body hr:after {
|
.markdown-body hr:after {
|
||||||
@ -294,20 +295,19 @@
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
list-style-type: decimal
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ol ol,
|
.markdown-body ol ol,
|
||||||
.markdown-body ul ol
|
.markdown-body ul ol .markdown-body ol {
|
||||||
.markdown-body ol {
|
list-style-type: decimal;
|
||||||
list-style-type: decimal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ol ol ol,
|
.markdown-body ol ol ol,
|
||||||
.markdown-body ol ul ol,
|
.markdown-body ol ul ol,
|
||||||
.markdown-body ul ol ol,
|
.markdown-body ul ol ol,
|
||||||
.markdown-body ul ul ol {
|
.markdown-body ul ul ol {
|
||||||
list-style-type: decimal
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body dd {
|
.markdown-body dd {
|
||||||
@ -621,11 +621,10 @@
|
|||||||
border-bottom-color: #30363d;
|
border-bottom-color: #30363d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.markdown-body:after,
|
.markdown-body:after,
|
||||||
.markdown-body:before {
|
.markdown-body:before {
|
||||||
display: table;
|
display: table;
|
||||||
content: "";
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body:after {
|
.markdown-body:after {
|
||||||
@ -658,7 +657,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body hr {
|
.markdown-body hr {
|
||||||
height: .25em;
|
height: 0.25em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 24px 0;
|
margin: 24px 0;
|
||||||
background-color: #e1e4e8;
|
background-color: #e1e4e8;
|
||||||
@ -666,19 +665,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-body hr {
|
.dark .markdown-body hr {
|
||||||
background-color: hsla(0,0%,100%,.1);
|
background-color: hsla(0, 0%, 100%, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body blockquote {
|
.markdown-body blockquote {
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
border-left: .25em solid #dfe2e5;
|
border-left: 0.25em solid #dfe2e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-body blockquote {
|
.dark .markdown-body blockquote {
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
color: #6a737d;
|
color: #6a737d;
|
||||||
border-left: .25em solid #3b434b;
|
border-left: 0.25em solid #3b434b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body blockquote > :first-child {
|
.markdown-body blockquote > :first-child {
|
||||||
@ -707,14 +706,14 @@
|
|||||||
|
|
||||||
.markdown-body h1,
|
.markdown-body h1,
|
||||||
.markdown-body h2 {
|
.markdown-body h2 {
|
||||||
padding-bottom: .3em;
|
padding-bottom: 0.3em;
|
||||||
border-bottom: 1px solid #eaecef;
|
border-bottom: 1px solid #eaecef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-body h1,
|
.dark .markdown-body h1,
|
||||||
.dark .markdown-body h2 {
|
.dark .markdown-body h2 {
|
||||||
padding-bottom: .3em;
|
padding-bottom: 0.3em;
|
||||||
border-bottom: 1px solid hsla(0,0%,100%,.1);
|
border-bottom: 1px solid hsla(0, 0%, 100%, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h2 {
|
.markdown-body h2 {
|
||||||
@ -730,11 +729,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h5 {
|
.markdown-body h5 {
|
||||||
font-size: .875em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h6 {
|
.markdown-body h6 {
|
||||||
font-size: .85em;
|
font-size: 0.85em;
|
||||||
color: #6a737d;
|
color: #6a737d;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -764,7 +763,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body li + li {
|
.markdown-body li + li {
|
||||||
margin-top: .25em;
|
margin-top: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body dl {
|
.markdown-body dl {
|
||||||
@ -834,25 +833,24 @@
|
|||||||
box-sizing: initial;
|
box-sizing: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-body img[align='right'] {
|
||||||
.markdown-body img[align=right] {
|
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body img[align=left] {
|
.markdown-body img[align='left'] {
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body code {
|
.markdown-body code {
|
||||||
padding: .2em .4em;
|
padding: 0.2em 0.4em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
background-color: rgba(27,31,35,.05);
|
background-color: rgba(27, 31, 35, 0.05);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-body code {
|
.dark .markdown-body code {
|
||||||
padding: .2em .4em;
|
padding: 0.2em 0.4em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
background-color: #2c3035;
|
background-color: #2c3035;
|
||||||
@ -863,7 +861,8 @@
|
|||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body pre>code, .dark .markdown-body pre>code {
|
.markdown-body pre > code,
|
||||||
|
.dark .markdown-body pre > code {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
@ -944,7 +943,7 @@
|
|||||||
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
|
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: rgba(27,31,35,.3);
|
color: rgba(27, 31, 35, 0.3);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
@ -956,7 +955,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .blob-num:hover {
|
.markdown-body .blob-num:hover {
|
||||||
color: rgba(27,31,35,.6);
|
color: rgba(27, 31, 35, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .blob-num:before {
|
.markdown-body .blob-num:before {
|
||||||
@ -986,62 +985,62 @@
|
|||||||
background: #ffea7f;
|
background: #ffea7f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="1"] {
|
.markdown-body .tab-size[data-tab-size='1'] {
|
||||||
-moz-tab-size: 1;
|
-moz-tab-size: 1;
|
||||||
tab-size: 1;
|
tab-size: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="2"] {
|
.markdown-body .tab-size[data-tab-size='2'] {
|
||||||
-moz-tab-size: 2;
|
-moz-tab-size: 2;
|
||||||
tab-size: 2;
|
tab-size: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="3"] {
|
.markdown-body .tab-size[data-tab-size='3'] {
|
||||||
-moz-tab-size: 3;
|
-moz-tab-size: 3;
|
||||||
tab-size: 3;
|
tab-size: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="4"] {
|
.markdown-body .tab-size[data-tab-size='4'] {
|
||||||
-moz-tab-size: 4;
|
-moz-tab-size: 4;
|
||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="5"] {
|
.markdown-body .tab-size[data-tab-size='5'] {
|
||||||
-moz-tab-size: 5;
|
-moz-tab-size: 5;
|
||||||
tab-size: 5;
|
tab-size: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="6"] {
|
.markdown-body .tab-size[data-tab-size='6'] {
|
||||||
-moz-tab-size: 6;
|
-moz-tab-size: 6;
|
||||||
tab-size: 6;
|
tab-size: 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="7"] {
|
.markdown-body .tab-size[data-tab-size='7'] {
|
||||||
-moz-tab-size: 7;
|
-moz-tab-size: 7;
|
||||||
tab-size: 7;
|
tab-size: 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="8"] {
|
.markdown-body .tab-size[data-tab-size='8'] {
|
||||||
-moz-tab-size: 8;
|
-moz-tab-size: 8;
|
||||||
tab-size: 8;
|
tab-size: 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="9"] {
|
.markdown-body .tab-size[data-tab-size='9'] {
|
||||||
-moz-tab-size: 9;
|
-moz-tab-size: 9;
|
||||||
tab-size: 9;
|
tab-size: 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="10"] {
|
.markdown-body .tab-size[data-tab-size='10'] {
|
||||||
-moz-tab-size: 10;
|
-moz-tab-size: 10;
|
||||||
tab-size: 10;
|
tab-size: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="11"] {
|
.markdown-body .tab-size[data-tab-size='11'] {
|
||||||
-moz-tab-size: 11;
|
-moz-tab-size: 11;
|
||||||
tab-size: 11;
|
tab-size: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .tab-size[data-tab-size="12"] {
|
.markdown-body .tab-size[data-tab-size='12'] {
|
||||||
-moz-tab-size: 12;
|
-moz-tab-size: 12;
|
||||||
tab-size: 12;
|
tab-size: 12;
|
||||||
}
|
}
|
||||||
@ -1055,12 +1054,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .task-list-item input {
|
.markdown-body .task-list-item input {
|
||||||
margin: 0 .2em .25em -1.6em;
|
margin: 0 0.2em 0.25em -1.6em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body input:not([type=checkbox]) {
|
.markdown-body input:not([type='checkbox']) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,6 @@ module.exports = {
|
|||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'@types': '<rootDir>/types'
|
'@types': '<rootDir>/types',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
@ -7,9 +7,11 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start | (sleep 1; wget http://localhost:3000/api -O /dev/null)",
|
"start": "next start | (sleep 1; wget http://localhost:3000/api -O /dev/null)",
|
||||||
"lint": "eslint --ext ts,tsx .",
|
"lint": "eslint --ext ts,tsx .",
|
||||||
|
"prettier": "prettier --write **/*",
|
||||||
"lint:fix": "eslint --ext ts,tsx . --fix",
|
"lint:fix": "eslint --ext ts,tsx . --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"docker": "docker-compose up -d --build"
|
"docker": "docker-compose up -d --build",
|
||||||
|
"postinstall": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "5.15.2",
|
"@fortawesome/fontawesome-free": "5.15.2",
|
||||||
|
|||||||
@ -2,15 +2,25 @@ import { NextPage } from 'next'
|
|||||||
import { ErrorText } from '@utils/Constants'
|
import { ErrorText } from '@utils/Constants'
|
||||||
|
|
||||||
const NotFound: NextPage = () => {
|
const NotFound: NextPage = () => {
|
||||||
return <div className='h-screen flex md:flex-col items-center justify-center' style={{ background: 'url("https://cdn.discordapp.com/attachments/745844596176715806/799149423505440768/1590927393326.jpg")' }}>
|
return (
|
||||||
|
<div
|
||||||
|
className='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'>
|
<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>
|
<h1 className='w-inline-block align-top m-0 py-10 text-4xl font-bold border-none md:mr-10 md:pr-10 md:border-r md:border-solid md:border-current'>
|
||||||
|
404
|
||||||
|
</h1>
|
||||||
|
|
||||||
<h2 className='inline-block text-2xl md:text-4xl font-semibold align-top m-0 py-10'>
|
<h2 className='inline-block align-top m-0 py-10 text-2xl font-semibold md:text-4xl'>
|
||||||
{ErrorText[404]}
|
{ErrorText[404]}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotFound
|
export default NotFound
|
||||||
|
|||||||
@ -10,8 +10,10 @@ class MyDocument extends Document {
|
|||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel='stylesheet'
|
<link
|
||||||
href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/default.min.css'/>
|
rel='stylesheet'
|
||||||
|
href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/default.min.css'
|
||||||
|
/>
|
||||||
<script src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js'></script>
|
<script src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js'></script>
|
||||||
<script
|
<script
|
||||||
data-ad-client='ca-pub-4856582423981759'
|
data-ad-client='ca-pub-4856582423981759'
|
||||||
@ -38,7 +40,7 @@ class MyDocument extends Document {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<body className='h-full overflow-x-hidden text-black dark:text-gray-100 dark:bg-discord-dark bg-white'>
|
<body className='h-full text-black dark:text-gray-100 dark:bg-discord-dark bg-white overflow-x-hidden'>
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const NotFound = RequestHandler()
|
const NotFound = RequestHandler().all(async (_req, res) => {
|
||||||
.all(async(_req, res) => {
|
|
||||||
return ResponseWrapper(res, { code: 404, message: '요청하신 URL에 페이지가 존재하지 않습니다.' })
|
return ResponseWrapper(res, { code: 404, message: '요청하신 URL에 페이지가 존재하지 않습니다.' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,11 @@ import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
|||||||
|
|
||||||
const RateLimit: NextApiHandler = (_req: NextApiRequest, res: NextApiResponse) => {
|
const RateLimit: NextApiHandler = (_req: NextApiRequest, res: NextApiResponse) => {
|
||||||
res.statusCode = 429
|
res.statusCode = 429
|
||||||
return ResponseWrapper(res, { code: 429, message: '지정된 시간에 너무 많은 요청을 보냈습니다. 잠시 뒤에 시도해주세요.', errors: ['지정된 시간에 너무 많은 요청을 보냈습니다. 잠시 뒤에 시도해주세요.'] })
|
return ResponseWrapper(res, {
|
||||||
|
code: 429,
|
||||||
|
message: '지정된 시간에 너무 많은 요청을 보냈습니다. 잠시 뒤에 시도해주세요.',
|
||||||
|
errors: ['지정된 시간에 너무 많은 요청을 보냈습니다. 잠시 뒤에 시도해주세요.'],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RateLimit
|
export default RateLimit
|
||||||
|
|||||||
@ -11,9 +11,10 @@ import { update } from '@utils/Query'
|
|||||||
import { verify } from '@utils/Jwt'
|
import { verify } from '@utils/Jwt'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Callback = RequestHandler()
|
const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
.get(async(req: ApiRequest, res) => {
|
const validate = await OauthCallbackSchema.validate(req.query)
|
||||||
const validate = await OauthCallbackSchema.validate(req.query).then(r=> r).catch((e) => {
|
.then(r => r)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -29,31 +30,41 @@ const Callback = RequestHandler()
|
|||||||
client_secret: process.env.DISCORD_CLIENT_SECRET,
|
client_secret: process.env.DISCORD_CLIENT_SECRET,
|
||||||
scope: process.env.DISCORD_SCOPE,
|
scope: process.env.DISCORD_SCOPE,
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code: req.query.code
|
code: req.query.code,
|
||||||
|
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
}
|
},
|
||||||
}).then(r => r.json())
|
}).then(r => r.json())
|
||||||
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
|
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
|
||||||
|
|
||||||
const user: DiscordUserInfo = await fetch(DiscordEnpoints.Me, {
|
const user: DiscordUserInfo = await fetch(DiscordEnpoints.Me, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `${token.token_type} ${token.access_token}`
|
Authorization: `${token.token_type} ${token.access_token}`,
|
||||||
}
|
},
|
||||||
}).then(r => r.json())
|
}).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 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)
|
const info = verify(userToken)
|
||||||
res.setHeader('set-cookie', serialize('token', userToken, {
|
res.setHeader(
|
||||||
|
'set-cookie',
|
||||||
|
serialize('token', userToken, {
|
||||||
expires: new Date(info.exp * 1000),
|
expires: new Date(info.exp * 1000),
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
path: '/'
|
path: '/',
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
res.redirect(301, '/callback/discord')
|
res.redirect(301, '/callback/discord')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,11 @@ import { NextApiRequest, NextApiResponse } from 'next'
|
|||||||
import { generateOauthURL } from '@utils/Tools'
|
import { generateOauthURL } from '@utils/Tools'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Discord = RequestHandler()
|
const Discord = RequestHandler().get(async (_req: NextApiRequest, res: NextApiResponse) => {
|
||||||
.get(async (_req: NextApiRequest, res: NextApiResponse) => {
|
res.redirect(
|
||||||
res.redirect(301, generateOauthURL('discord', process.env.DISCORD_CLIENT_ID, process.env.DISCORD_SCOPE))
|
301,
|
||||||
|
generateOauthURL('discord', process.env.DISCORD_CLIENT_ID, process.env.DISCORD_SCOPE)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default Discord
|
export default Discord
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import { serialize } from 'cookie'
|
import { serialize } from 'cookie'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
|
const Logout = RequestHandler().get(async (req, res) => {
|
||||||
const Logout = RequestHandler()
|
|
||||||
.get(async(req, res) => {
|
|
||||||
res.setHeader('Cache-control', 'no-cache')
|
res.setHeader('Cache-control', 'no-cache')
|
||||||
res.setHeader('set-cookie', serialize('token', '', {
|
res.setHeader(
|
||||||
|
'set-cookie',
|
||||||
|
serialize('token', '', {
|
||||||
maxAge: -1,
|
maxAge: -1,
|
||||||
path: '/'
|
path: '/',
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
res.redirect(301, '/')
|
res.redirect(301, '/')
|
||||||
})
|
})
|
||||||
export default Logout
|
export default Logout
|
||||||
@ -1,8 +1,7 @@
|
|||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Deprecated = RequestHandler()
|
const Deprecated = RequestHandler().get(async (_req, res) => {
|
||||||
.get(async (_req, res) => {
|
|
||||||
return ResponseWrapper(res, {
|
return ResponseWrapper(res, {
|
||||||
code: 406,
|
code: 406,
|
||||||
message: '해당 API 버전은 지원 종료되었습니다.',
|
message: '해당 API 버전은 지원 종료되었습니다.',
|
||||||
|
|||||||
@ -8,13 +8,14 @@ import RequestHandler from '@utils/RequestHandler'
|
|||||||
|
|
||||||
import { User } from '@types'
|
import { User } from '@types'
|
||||||
|
|
||||||
const BotApplications = RequestHandler()
|
const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
|
||||||
.patch(async (req: ApiRequest, res) => {
|
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
const validated = await DeveloperBotSchema.validate(req.body, { abortEarly: false }).then(el => el).catch(e => {
|
const validated = await DeveloperBotSchema.validate(req.body, { abortEarly: false })
|
||||||
|
.then(el => el)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -25,7 +26,6 @@ const BotApplications = RequestHandler()
|
|||||||
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
||||||
await update.updateBotApplication(req.query.id, { webhook: validated.webhook || null })
|
await update.updateBotApplication(req.query.id, { webhook: validated.webhook || null })
|
||||||
return ResponseWrapper(res, { code: 200 })
|
return ResponseWrapper(res, { code: 200 })
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
|
|||||||
@ -8,13 +8,14 @@ import RequestHandler from '@utils/RequestHandler'
|
|||||||
|
|
||||||
import { User } from '@types'
|
import { User } from '@types'
|
||||||
|
|
||||||
const ResetApplication = RequestHandler()
|
const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
|
||||||
.post(async (req: ApiRequest, res) => {
|
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
const validated = await ResetBotTokenSchema.validate(req.body, { abortEarly: false }).then(el => el).catch(e => {
|
const validated = await ResetBotTokenSchema.validate(req.body, { abortEarly: false })
|
||||||
|
.then(el => el)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@ -18,18 +18,45 @@ const Bots = RequestHandler()
|
|||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
|
|
||||||
const validated = await AddBotSubmitSchema.validate(req.body, { abortEarly: false }).then(el => el).catch(e => {
|
const validated = await AddBotSubmitSchema.validate(req.body, { abortEarly: false })
|
||||||
|
.then(el => el)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
if(validated.id !== req.query.id) return ResponseWrapper(res, { code: 400, errors: ['요청 주소와 Body의 정보가 다릅니다.'] })
|
if (validated.id !== req.query.id)
|
||||||
|
return ResponseWrapper(res, { code: 400, errors: ['요청 주소와 Body의 정보가 다릅니다.'] })
|
||||||
const result = await put.submitBot(user, validated)
|
const result = await put.submitBot(user, validated)
|
||||||
if(result === 1) return ResponseWrapper(res, { code: 403, message: '이미 대기중인 봇이 있습니다.', errors: ['한 번에 최대 2개의 봇까지만 신청하실 수 있습니다.\n다른 봇들의 심사가 완료된 뒤에 신청해주세요.'] })
|
if (result === 1)
|
||||||
else if(result === 2) return ResponseWrapper(res, { code: 406, message: '해당 봇은 이미 심사중이거나 이미 등록되어있습니다.', errors: ['해당 아이디의 봇은 이미 심사중이거나 등록되어있습니다. 본인 소유의 봇이고 신청하신 적이 없으시다면 문의해주세요.'] })
|
return ResponseWrapper(res, {
|
||||||
else if(result === 3) return ResponseWrapper(res, { code: 404, message: '올바르지 않은 봇 아이디입니다.', errors: ['해당 아이디의 봇은 존재하지 않습니다. 다시 확인해주세요.'] })
|
code: 403,
|
||||||
else if(result === 4) return ResponseWrapper(res, { code: 403, message: '디스코드 서버에 참가해주세요.' , errors: ['봇 신청하시기 위해서는 공식 디스코드 서버에 참가해주셔야합니다.'] })
|
message: '이미 대기중인 봇이 있습니다.',
|
||||||
|
errors: [
|
||||||
|
'한 번에 최대 2개의 봇까지만 신청하실 수 있습니다.\n다른 봇들의 심사가 완료된 뒤에 신청해주세요.',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
else if (result === 2)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 406,
|
||||||
|
message: '해당 봇은 이미 심사중이거나 이미 등록되어있습니다.',
|
||||||
|
errors: [
|
||||||
|
'해당 아이디의 봇은 이미 심사중이거나 등록되어있습니다. 본인 소유의 봇이고 신청하신 적이 없으시다면 문의해주세요.',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
else if (result === 3)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 404,
|
||||||
|
message: '올바르지 않은 봇 아이디입니다.',
|
||||||
|
errors: ['해당 아이디의 봇은 존재하지 않습니다. 다시 확인해주세요.'],
|
||||||
|
})
|
||||||
|
else if (result === 4)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 403,
|
||||||
|
message: '디스코드 서버에 참가해주세요.',
|
||||||
|
errors: ['봇 신청하시기 위해서는 공식 디스코드 서버에 참가해주셔야합니다.'],
|
||||||
|
})
|
||||||
return ResponseWrapper(res, { code: 200, data: result })
|
return ResponseWrapper(res, { code: 200, data: result })
|
||||||
})
|
})
|
||||||
.patch(async (req, res) => {
|
.patch(async (req, res) => {
|
||||||
|
|||||||
@ -7,20 +7,24 @@ import { SearchQuerySchema } from '@utils/Yup'
|
|||||||
|
|
||||||
import { BotList } from '@types'
|
import { BotList } from '@types'
|
||||||
|
|
||||||
const SearchBots = RequestHandler()
|
const SearchBots = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
|
||||||
.get(async (req: ApiRequest, res: NextApiResponse) => {
|
const validated = await SearchQuerySchema.validate({ q: req.query.q, page: req.query.page })
|
||||||
const validated = await SearchQuerySchema.validate({ q: req.query.q, page: req.query.page }).then(el => el).catch(e => {
|
.then(el => el)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
})
|
})
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
|
|
||||||
let result: BotList
|
let result: BotList
|
||||||
try {
|
try {
|
||||||
result = await get.list.search.load(JSON.stringify({ page: validated.page, query: validated.q }))
|
result = await get.list.search.load(
|
||||||
|
JSON.stringify({ page: validated.page, query: validated.q })
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return ResponseWrapper(res, { code: 400, message: '검색 문법이 잘못되었습니다.' })
|
return ResponseWrapper(res, { code: 400, message: '검색 문법이 잘못되었습니다.' })
|
||||||
}
|
}
|
||||||
if(result.totalPage < validated.page || result.currentPage !== validated.page) return ResponseWrapper(res, { code: 404, message: '검색 결과가 없습니다.' })
|
if (result.totalPage < validated.page || result.currentPage !== validated.page)
|
||||||
|
return ResponseWrapper(res, { code: 404, message: '검색 결과가 없습니다.' })
|
||||||
else ResponseWrapper<BotList>(res, { code: 200, data: result })
|
else ResponseWrapper<BotList>(res, { code: 200, data: result })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,7 @@ import { get } from '@utils/Query'
|
|||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Users = RequestHandler()
|
const Users = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
.get(async(req: ApiRequest, res) => {
|
|
||||||
console.log(req.query)
|
console.log(req.query)
|
||||||
const user = await get.user.load(req.query?.id)
|
const user = await get.user.load(req.query?.id)
|
||||||
if (!user) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 유저 입니다.' })
|
if (!user) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 유저 입니다.' })
|
||||||
|
|||||||
@ -8,8 +8,7 @@ import { get } from '@utils/Query'
|
|||||||
import { BotBadgeType, DiscordEnpoints } from '@utils/Constants'
|
import { BotBadgeType, DiscordEnpoints } from '@utils/Constants'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|
||||||
const Widget= RequestHandler()
|
const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
|
||||||
.get(async(req: ApiRequest, res: NextApiResponse) => {
|
|
||||||
const { id: param, type, style = 'flat', scale = 1, icon = true } = req.query
|
const { id: param, type, style = 'flat', scale = 1, icon = true } = req.query
|
||||||
const splitted = param.split('.')
|
const splitted = param.split('.')
|
||||||
|
|
||||||
@ -19,8 +18,10 @@ const Widget= RequestHandler()
|
|||||||
style,
|
style,
|
||||||
type,
|
type,
|
||||||
scale,
|
scale,
|
||||||
icon
|
icon,
|
||||||
}).then(el=> el).catch(e=> {
|
})
|
||||||
|
.then(el => el)
|
||||||
|
.catch(e => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -30,18 +31,25 @@ const Widget= RequestHandler()
|
|||||||
const data = await get.bot.load(validated.id)
|
const data = await get.bot.load(validated.id)
|
||||||
|
|
||||||
if (!data) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
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 userImage = !data.avatar
|
||||||
const img = userImage || await get.images.user.load(DiscordEnpoints.CDN.default(data.tag, { format: 'png', size: 128 }))
|
? 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')
|
res.setHeader('content-type', 'image/svg+xml; charset=utf-8')
|
||||||
const badgeData = {
|
const badgeData = {
|
||||||
...BotBadgeType(data)[type],
|
...BotBadgeType(data)[type],
|
||||||
style: validated.style,
|
style: validated.style,
|
||||||
scale: validated.scale,
|
scale: validated.scale,
|
||||||
icon: validated.icon ? `data:image/png;base64,${img.toString('base64')}` : null
|
icon: validated.icon ? `data:image/png;base64,${img.toString('base64')}` : null,
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send(badgen(badgeData))
|
res.send(badgen(badgeData))
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ const Segment = dynamic(() => import('@components/Segment'))
|
|||||||
const SEO = dynamic(() => import('@components/SEO'))
|
const SEO = dynamic(() => import('@components/SEO'))
|
||||||
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
||||||
|
|
||||||
const VoteBot: NextPage<VoteBotProps> = ({ data, user, csrfToken }) => {
|
const VoteBot: NextPage<VoteBotProps> = ({ data, csrfToken }) => {
|
||||||
console.log(csrfToken)
|
console.log(csrfToken)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
if(!data?.id) return <NotFound />
|
if(!data?.id) return <NotFound />
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": ["meta: dependencies"],
|
"labels": ["meta: dependencies"],
|
||||||
"reviewers": ["team:koreanbots-devs"],
|
"reviewers": ["team:koreanbots-devs"],
|
||||||
"schedule": [
|
"schedule": ["before 8am"],
|
||||||
"before 8am"
|
"extends": ["config:base"],
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"config:base"
|
|
||||||
],
|
|
||||||
"timezone": "Asia/Seoul"
|
"timezone": "Asia/Seoul"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,9 @@ test('checking Permission', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('check CDN URL', () => {
|
test('check CDN URL', () => {
|
||||||
expect(DiscordEnpoints.CDN.user('000000000000000000', 'abcdefghijklm', { format: 'jpg', size: 1024 })).toBe('https://cdn.discordapp.com/avatars/000000000000000000/abcdefghijklm.jpg?size=1024')
|
expect(
|
||||||
|
DiscordEnpoints.CDN.user('000000000000000000', 'abcdefghijklm', { format: 'jpg', size: 1024 })
|
||||||
|
).toBe('https://cdn.discordapp.com/avatars/000000000000000000/abcdefghijklm.jpg?size=1024')
|
||||||
})
|
})
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
@ -5,14 +5,9 @@
|
|||||||
"@components/*": ["components/*"],
|
"@components/*": ["components/*"],
|
||||||
"@utils/*": ["utils/*"],
|
"@utils/*": ["utils/*"],
|
||||||
"@types": ["types/index.ts"]
|
"@types": ["types/index.ts"]
|
||||||
|
|
||||||
},
|
},
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
@ -25,12 +20,6 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve"
|
"jsx": "preserve"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"next-env.d.ts",
|
"exclude": ["node_modules"]
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export enum UserFlags {
|
|||||||
general = 0 << 0,
|
general = 0 << 0,
|
||||||
staff = 1 << 0,
|
staff = 1 << 0,
|
||||||
bughunter = 1 << 1,
|
bughunter = 1 << 1,
|
||||||
premium = 1 << 2
|
premium = 1 << 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BotFlags {
|
export enum BotFlags {
|
||||||
@ -56,7 +56,7 @@ export enum BotFlags {
|
|||||||
partnered = 1 << 3,
|
partnered = 1 << 3,
|
||||||
verifed = 1 << 4,
|
verifed = 1 << 4,
|
||||||
premium = 1 << 5,
|
premium = 1 << 5,
|
||||||
hackerthon = 1 << 6
|
hackerthon = 1 << 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DiscordUserFlags {
|
export enum DiscordUserFlags {
|
||||||
@ -72,7 +72,7 @@ export enum DiscordUserFlags {
|
|||||||
SYSTEM = 1 << 12,
|
SYSTEM = 1 << 12,
|
||||||
BUGHUNTER_LEVEL_2 = 1 << 14,
|
BUGHUNTER_LEVEL_2 = 1 << 14,
|
||||||
VERIFIED_BOT = 1 << 16,
|
VERIFIED_BOT = 1 << 16,
|
||||||
VERIFIED_DEVELOPER = 1 << 17
|
VERIFIED_DEVELOPER = 1 << 17,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BotList {
|
export interface BotList {
|
||||||
@ -97,7 +97,6 @@ export interface SubmittedBot {
|
|||||||
discord: string | null
|
discord: string | null
|
||||||
state: number
|
state: number
|
||||||
reason: string | null
|
reason: string | null
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiscordTokenInfo {
|
export interface DiscordTokenInfo {
|
||||||
@ -215,7 +214,7 @@ export enum DiscordImageType {
|
|||||||
EMOJI = 'emoji',
|
EMOJI = 'emoji',
|
||||||
GUILD = 'guild',
|
GUILD = 'guild',
|
||||||
USER = 'user',
|
USER = 'user',
|
||||||
FALLBACK = 'default'
|
FALLBACK = 'default',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CsrfContext extends NextPageContext {
|
export interface CsrfContext extends NextPageContext {
|
||||||
|
|||||||
@ -18,11 +18,14 @@ export const getToken = (req: IncomingMessage, res: ServerResponse) => {
|
|||||||
let key: string = parsed[csrfKey]
|
let key: string = parsed[csrfKey]
|
||||||
if (!key || !tokenVerify(key)) {
|
if (!key || !tokenVerify(key)) {
|
||||||
key = tokenCreate()
|
key = tokenCreate()
|
||||||
res.setHeader('set-cookie', serialize(csrfKey, key, {
|
res.setHeader(
|
||||||
|
'set-cookie',
|
||||||
|
serialize(csrfKey, key, {
|
||||||
expires: new Date(+new Date() + 24 * 60 * 60 * 1000),
|
expires: new Date(+new Date() + 24 * 60 * 60 * 1000),
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: '/'
|
path: '/',
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return key
|
return key
|
||||||
|
|||||||
@ -4,7 +4,11 @@ import { KoreanbotsEndPoints } from './Constants'
|
|||||||
const Fetch = async <T>(endpoint: string, options?: RequestInit): Promise<ResponseProps<T>> => {
|
const Fetch = async <T>(endpoint: string, options?: RequestInit): Promise<ResponseProps<T>> => {
|
||||||
const url = KoreanbotsEndPoints.baseAPI + (endpoint.startsWith('/') ? endpoint : '/' + endpoint)
|
const url = KoreanbotsEndPoints.baseAPI + (endpoint.startsWith('/') ? endpoint : '/' + endpoint)
|
||||||
|
|
||||||
const res = await fetch(url, { method: 'GET', headers: { 'content-type': 'application/json', ...options.headers }, ...options })
|
const res = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'content-type': 'application/json', ...options.headers },
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
let json = {}
|
let json = {}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default knex({
|
|||||||
user: process.env.MYSQL_USER || 'root',
|
user: process.env.MYSQL_USER || 'root',
|
||||||
password: process.env.MYSQL_PASSWORD,
|
password: process.env.MYSQL_PASSWORD,
|
||||||
database: process.env.MYSQL_DATABASE || 'discordbots',
|
database: process.env.MYSQL_DATABASE || 'discordbots',
|
||||||
charset: 'utf8mb4'
|
charset: 'utf8mb4',
|
||||||
},
|
},
|
||||||
debug: process.env.NODE_ENV === 'development',
|
debug: process.env.NODE_ENV === 'development',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const Logger = {
|
|||||||
},
|
},
|
||||||
error: function(message: string) {
|
error: function(message: string) {
|
||||||
print('ERROR', message, genStyle('red', 'white'))
|
print('ERROR', message, genStyle('red', 'white'))
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function genStyle(bg: string, text = 'black') {
|
function genStyle(bg: string, text = 'black') {
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { NextApiResponse } from 'next'
|
import { NextApiResponse } from 'next'
|
||||||
import ResponseWrapper from './ResponseWrapper'
|
import ResponseWrapper from './ResponseWrapper'
|
||||||
|
|
||||||
export default function RateLimitHandler(
|
export default function RateLimitHandler(res: NextApiResponse, ratelimit: RateLimit) {
|
||||||
res: NextApiResponse, ratelimit: RateLimit
|
|
||||||
) {
|
|
||||||
res.setHeader('x-ratelimit-limit', ratelimit.limit)
|
res.setHeader('x-ratelimit-limit', ratelimit.limit)
|
||||||
res.setHeader('x-ratelimit-remaining', 600 - (ratelimit.used > ratelimit.limit ? ratelimit.limit : ratelimit.used))
|
res.setHeader(
|
||||||
|
'x-ratelimit-remaining',
|
||||||
|
600 - (ratelimit.used > ratelimit.limit ? ratelimit.limit : ratelimit.used)
|
||||||
|
)
|
||||||
res.setHeader('x-ratelimit-reset', Math.round(ratelimit.reset / 1000))
|
res.setHeader('x-ratelimit-reset', Math.round(ratelimit.reset / 1000))
|
||||||
if (ratelimit.limit < ratelimit.used) {
|
if (ratelimit.limit < ratelimit.used) {
|
||||||
if (ratelimit.onLimitExceed) ratelimit.onLimitExceed(res)
|
if (ratelimit.onLimitExceed) ratelimit.onLimitExceed(res)
|
||||||
@ -14,10 +15,9 @@ export default function RateLimitHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface RateLimit {
|
interface RateLimit {
|
||||||
used: number
|
used: number
|
||||||
limit: number
|
limit: number
|
||||||
reset: number,
|
reset: number
|
||||||
onLimitExceed: (res: NextApiResponse) => void | Promise<void>
|
onLimitExceed: (res: NextApiResponse) => void | Promise<void>
|
||||||
}
|
}
|
||||||
@ -6,7 +6,8 @@ export const Prefix = /^[^\s]/
|
|||||||
export const HTTPProtocol = /^https?:\/\/.*?/
|
export const HTTPProtocol = /^https?:\/\/.*?/
|
||||||
export const Url = urlRegex({ strict: true })
|
export const Url = urlRegex({ strict: true })
|
||||||
|
|
||||||
export const Emoji = '(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])'
|
export const Emoji =
|
||||||
|
'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])'
|
||||||
export const Heading = '<h\\d id="(.+?)">(.*?)<\\/h(\\d)>'
|
export const Heading = '<h\\d id="(.+?)">(.*?)<\\/h(\\d)>'
|
||||||
export const EmojiSyntax = ':(\\w+):'
|
export const EmojiSyntax = ':(\\w+):'
|
||||||
export const ImageTag = /<img\s[^>]*?alt\s*=\s*['"]([^'"]*?)['"][^>]*?>/
|
export const ImageTag = /<img\s[^>]*?alt\s*=\s*['"]([^'"]*?)['"][^>]*?>/
|
||||||
|
|||||||
@ -2,14 +2,15 @@ import { NextApiRequest, NextApiResponse } from 'next'
|
|||||||
import nc from 'next-connect'
|
import nc from 'next-connect'
|
||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
|
|
||||||
const RequestHandler = () => nc<NextApiRequest, NextApiResponse>({
|
const RequestHandler = () =>
|
||||||
|
nc<NextApiRequest, NextApiResponse>({
|
||||||
onNoMatch(_req, res) {
|
onNoMatch(_req, res) {
|
||||||
return ResponseWrapper(res, { code: 405 })
|
return ResponseWrapper(res, { code: 405 })
|
||||||
},
|
},
|
||||||
onError(err, _req, res) {
|
onError(err, _req, res) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
return ResponseWrapper(res, { code: 500 })
|
return ResponseWrapper(res, { code: 500 })
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default RequestHandler
|
export default RequestHandler
|
||||||
@ -10,6 +10,11 @@ export default function ResponseWrapper<T=unknown>(
|
|||||||
if (!http.STATUS_CODES[code]) throw new Error('Invalid http code.')
|
if (!http.STATUS_CODES[code]) throw new Error('Invalid http code.')
|
||||||
res.statusCode = code
|
res.statusCode = code
|
||||||
res.setHeader('Access-Control-Allow-Origin', process.env.KOREANBOTS_URL)
|
res.setHeader('Access-Control-Allow-Origin', process.env.KOREANBOTS_URL)
|
||||||
res.json({ code, data, errors, version, ...(message || !data ? { message: message || ErrorText[code] || http.STATUS_CODES[code] } : {}) })
|
res.json({
|
||||||
|
code,
|
||||||
|
data,
|
||||||
|
errors,
|
||||||
|
version,
|
||||||
|
...(message || !data ? { message: message || ErrorText[code] || http.STATUS_CODES[code] } : {}),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
182
utils/Yup.ts
182
utils/Yup.ts
@ -1,4 +1,3 @@
|
|||||||
import { TokenExpiredError } from 'jsonwebtoken'
|
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import YupKorean from 'yup-locales-ko'
|
import YupKorean from 'yup-locales-ko'
|
||||||
import { ListType } from '../types'
|
import { ListType } from '../types'
|
||||||
@ -13,9 +12,15 @@ Yup.addMethod(Yup.array, 'unique', function(message, mapper = a => a) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const botListArgumentSchema: Yup.SchemaOf<botListArgument> = Yup.object({
|
export const botListArgumentSchema: Yup.SchemaOf<botListArgument> = Yup.object({
|
||||||
type: Yup.mixed().oneOf(['VOTE', 'TRUSTED', 'NEW', 'PARTNERED', 'CATEGORY', 'SEARCH']).required(),
|
type: Yup.mixed()
|
||||||
page: Yup.number().positive().integer().notRequired().default(1),
|
.oneOf(['VOTE', 'TRUSTED', 'NEW', 'PARTNERED', 'CATEGORY', 'SEARCH'])
|
||||||
query: Yup.string().notRequired()
|
.required(),
|
||||||
|
page: Yup.number()
|
||||||
|
.positive()
|
||||||
|
.integer()
|
||||||
|
.notRequired()
|
||||||
|
.default(1),
|
||||||
|
query: Yup.string().notRequired(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface botListArgument {
|
export interface botListArgument {
|
||||||
@ -26,8 +31,12 @@ export interface botListArgument {
|
|||||||
|
|
||||||
export const ImageOptionsSchema: Yup.SchemaOf<ImageOptions> = Yup.object({
|
export const ImageOptionsSchema: Yup.SchemaOf<ImageOptions> = Yup.object({
|
||||||
id: Yup.string().required(),
|
id: Yup.string().required(),
|
||||||
ext: Yup.mixed<ext>().oneOf(['webp', 'png', 'gif']).required(),
|
ext: Yup.mixed<ext>()
|
||||||
size: Yup.mixed<ImageSize>().oneOf(['128', '256', '512']).required()
|
.oneOf(['webp', 'png', 'gif'])
|
||||||
|
.required(),
|
||||||
|
size: Yup.mixed<ImageSize>()
|
||||||
|
.oneOf(['128', '256', '512'])
|
||||||
|
.required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
interface ImageOptions {
|
interface ImageOptions {
|
||||||
@ -41,11 +50,21 @@ type ImageSize = '128' | '256' | '512'
|
|||||||
|
|
||||||
export const WidgetOptionsSchema: Yup.SchemaOf<WidgetOptions> = Yup.object({
|
export const WidgetOptionsSchema: Yup.SchemaOf<WidgetOptions> = Yup.object({
|
||||||
id: Yup.string().required(),
|
id: Yup.string().required(),
|
||||||
ext: Yup.mixed<widgetExt>().oneOf(['svg']).required(),
|
ext: Yup.mixed<widgetExt>()
|
||||||
type: Yup.mixed<widgetType>().oneOf(['votes', 'servers', 'status']).required(),
|
.oneOf(['svg'])
|
||||||
scale: Yup.number().positive().min(0.5).max(3).required(),
|
.required(),
|
||||||
style: Yup.mixed<'flat'|'classic'>().oneOf(['flat', 'classic']).default('flat'),
|
type: Yup.mixed<widgetType>()
|
||||||
icon: Yup.boolean().default(true)
|
.oneOf(['votes', 'servers', 'status'])
|
||||||
|
.required(),
|
||||||
|
scale: Yup.number()
|
||||||
|
.positive()
|
||||||
|
.min(0.5)
|
||||||
|
.max(3)
|
||||||
|
.required(),
|
||||||
|
style: Yup.mixed<'flat' | 'classic'>()
|
||||||
|
.oneOf(['flat', 'classic'])
|
||||||
|
.default('flat'),
|
||||||
|
icon: Yup.boolean().default(true),
|
||||||
})
|
})
|
||||||
|
|
||||||
interface WidgetOptions {
|
interface WidgetOptions {
|
||||||
@ -60,15 +79,20 @@ interface WidgetOptions {
|
|||||||
type widgetType = 'votes' | 'servers' | 'status'
|
type widgetType = 'votes' | 'servers' | 'status'
|
||||||
type widgetExt = 'svg'
|
type widgetExt = 'svg'
|
||||||
|
|
||||||
export const PageCount = Yup.number().integer().positive().required()
|
export const PageCount = Yup.number()
|
||||||
|
.integer()
|
||||||
|
.positive()
|
||||||
|
.required()
|
||||||
|
|
||||||
export const OauthCallbackSchema: Yup.SchemaOf<OauthCallback> = Yup.object({
|
export const OauthCallbackSchema: Yup.SchemaOf<OauthCallback> = Yup.object({
|
||||||
code: Yup.string().required()
|
code: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const botCategoryListArgumentSchema: Yup.SchemaOf<botCategoryListArgument> = Yup.object({
|
export const botCategoryListArgumentSchema: Yup.SchemaOf<botCategoryListArgument> = Yup.object({
|
||||||
page: PageCount,
|
page: PageCount,
|
||||||
category: Yup.mixed().oneOf(categories).required()
|
category: Yup.mixed()
|
||||||
|
.oneOf(categories)
|
||||||
|
.required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
interface botCategoryListArgument {
|
interface botCategoryListArgument {
|
||||||
@ -81,8 +105,17 @@ interface OauthCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SearchQuerySchema: Yup.SchemaOf<SearchQuery> = Yup.object({
|
export const SearchQuerySchema: Yup.SchemaOf<SearchQuery> = Yup.object({
|
||||||
q: Yup.string().min(2, '최소 2글자 이상 입력해주세요.').max(50).required('검색어를 입력해주세요.').label('검색어'),
|
q: Yup.string()
|
||||||
page: Yup.number().positive().integer().notRequired().default(1).label('페이지')
|
.min(2, '최소 2글자 이상 입력해주세요.')
|
||||||
|
.max(50)
|
||||||
|
.required('검색어를 입력해주세요.')
|
||||||
|
.label('검색어'),
|
||||||
|
page: Yup.number()
|
||||||
|
.positive()
|
||||||
|
.integer()
|
||||||
|
.notRequired()
|
||||||
|
.default(1)
|
||||||
|
.label('페이지'),
|
||||||
})
|
})
|
||||||
|
|
||||||
interface SearchQuery {
|
interface SearchQuery {
|
||||||
@ -91,18 +124,49 @@ interface SearchQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AddBotSubmitSchema: Yup.SchemaOf<AddBotSubmit> = Yup.object({
|
export const AddBotSubmitSchema: Yup.SchemaOf<AddBotSubmit> = Yup.object({
|
||||||
agree: Yup.boolean().oneOf([true], '상단의 체크박스를 클릭해주세요.').required('상단의 체크박스를 클릭해주세요.'),
|
agree: Yup.boolean()
|
||||||
id: Yup.string().matches(ID, '올바른 봇 ID를 입력해주세요.').required('봇 ID는 필수 항목입니다.'),
|
.oneOf([true], '상단의 체크박스를 클릭해주세요.')
|
||||||
prefix: Yup.string().matches(Prefix, '접두사는 띄어쓰기로 시작할 수 없습니다.').min(1, '접두사는 최소 1자여야합니다.').max(32, '접두사는 최대 32자까지만 가능합니다.').required('접두사는 필수 항목입니다.'),
|
.required('상단의 체크박스를 클릭해주세요.'),
|
||||||
library: Yup.string().oneOf(library).required('라이브러리는 필수 항목입니다.'),
|
id: Yup.string()
|
||||||
website: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 웹사이트 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
.matches(ID, '올바른 봇 ID를 입력해주세요.')
|
||||||
url: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 초대링크 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
.required('봇 ID는 필수 항목입니다.'),
|
||||||
git: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 깃 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
prefix: Yup.string()
|
||||||
discord: Yup.string().matches(Vanity, '디스코드 초대코드 형식을 지켜주세요.').min(2, '지원 디스코드는 최소 2자여야합니다.').max(32, '지원 디스코드는 최대 32자까지만 가능합니다.'),
|
.matches(Prefix, '접두사는 띄어쓰기로 시작할 수 없습니다.')
|
||||||
category: Yup.array(Yup.string().oneOf(categories)).min(1, '최소 한 개의 카테고리를 선택해주세요.').unique('카테고리는 중복될 수 없습니다.').required('카테고리는 필수 항목입니다.'),
|
.min(1, '접두사는 최소 1자여야합니다.')
|
||||||
intro: Yup.string().min(2, '봇 소개는 최소 2자여야합니다.').max(60, '봇 소개는 최대 60자여야합니다.').required('봇 소개는 필수 항목입니다.'),
|
.max(32, '접두사는 최대 32자까지만 가능합니다.')
|
||||||
desc: Yup.string().min(100, '봇 설명은 최소 100자여야합니다.').max(1500, '봇 설명은 최대 1500자여야합니다.').required('봇 설명은 필수 항목입니다.'),
|
.required('접두사는 필수 항목입니다.'),
|
||||||
_csrf: Yup.string().required()
|
library: Yup.string()
|
||||||
|
.oneOf(library)
|
||||||
|
.required('라이브러리는 필수 항목입니다.'),
|
||||||
|
website: Yup.string()
|
||||||
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 웹사이트 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
url: Yup.string()
|
||||||
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 초대링크 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
git: Yup.string()
|
||||||
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 깃 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
discord: Yup.string()
|
||||||
|
.matches(Vanity, '디스코드 초대코드 형식을 지켜주세요.')
|
||||||
|
.min(2, '지원 디스코드는 최소 2자여야합니다.')
|
||||||
|
.max(32, '지원 디스코드는 최대 32자까지만 가능합니다.'),
|
||||||
|
category: Yup.array(Yup.string().oneOf(categories))
|
||||||
|
.min(1, '최소 한 개의 카테고리를 선택해주세요.')
|
||||||
|
.unique('카테고리는 중복될 수 없습니다.')
|
||||||
|
.required('카테고리는 필수 항목입니다.'),
|
||||||
|
intro: Yup.string()
|
||||||
|
.min(2, '봇 소개는 최소 2자여야합니다.')
|
||||||
|
.max(60, '봇 소개는 최대 60자여야합니다.')
|
||||||
|
.required('봇 소개는 필수 항목입니다.'),
|
||||||
|
desc: Yup.string()
|
||||||
|
.min(100, '봇 설명은 최소 100자여야합니다.')
|
||||||
|
.max(1500, '봇 설명은 최대 1500자여야합니다.')
|
||||||
|
.required('봇 설명은 필수 항목입니다.'),
|
||||||
|
_csrf: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface AddBotSubmit {
|
export interface AddBotSubmit {
|
||||||
@ -121,22 +185,56 @@ export interface AddBotSubmit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ManageBotSchema = Yup.object({
|
export const ManageBotSchema = Yup.object({
|
||||||
prefix: Yup.string().matches(Prefix, '접두사는 띄어쓰기로 시작할 수 없습니다.').min(1, '접두사는 최소 1자여야합니다.').max(32, '접두사는 최대 32자까지만 가능합니다.').required('접두사는 필수 항목입니다.'),
|
prefix: Yup.string()
|
||||||
library: Yup.string().oneOf(library).required('라이브러리는 필수 항목입니다.'),
|
.matches(Prefix, '접두사는 띄어쓰기로 시작할 수 없습니다.')
|
||||||
website: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 웹사이트 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
.min(1, '접두사는 최소 1자여야합니다.')
|
||||||
url: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 초대링크 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
.max(32, '접두사는 최대 32자까지만 가능합니다.')
|
||||||
git: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 깃 URL을 입력해주세요.').max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
.required('접두사는 필수 항목입니다.'),
|
||||||
discord: Yup.string().matches(Vanity, '디스코드 초대코드 형식을 지켜주세요.').min(2, '지원 디스코드는 최소 2자여야합니다.').max(32, '지원 디스코드는 최대 32자까지만 가능합니다.'),
|
library: Yup.string()
|
||||||
category: Yup.array(Yup.string().oneOf(categories)).min(1, '최소 한 개의 카테고리를 선택해주세요.').unique('카테고리는 중복될 수 없습니다.').required('카테고리는 필수 항목입니다.'),
|
.oneOf(library)
|
||||||
intro: Yup.string().min(2, '봇 소개는 최소 2자여야합니다.').max(60, '봇 소개는 최대 60자여야합니다.').required('봇 소개는 필수 항목입니다.'),
|
.required('라이브러리는 필수 항목입니다.'),
|
||||||
desc: Yup.string().min(100, '봇 설명은 최소 100자여야합니다.').max(1500, '봇 설명은 최대 1500자여야합니다.').required('봇 설명은 필수 항목입니다.'),
|
website: Yup.string()
|
||||||
owners: Yup.array(Yup.string()).min(1, '최소 한 명의 소유자는 입력해주세요.').max(10, '소유자는 최대 10명까지만 가능합니다.').unique('소유자 아이디는 중복될 수 없습니다.').required('소유자는 필수 항목입니다.'),
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
_csrf: Yup.string().required()
|
.matches(Url, '올바른 웹사이트 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
url: Yup.string()
|
||||||
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 초대링크 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
git: Yup.string()
|
||||||
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 깃 URL을 입력해주세요.')
|
||||||
|
.max(64, 'URL은 최대 64자까지만 가능합니다.'),
|
||||||
|
discord: Yup.string()
|
||||||
|
.matches(Vanity, '디스코드 초대코드 형식을 지켜주세요.')
|
||||||
|
.min(2, '지원 디스코드는 최소 2자여야합니다.')
|
||||||
|
.max(32, '지원 디스코드는 최대 32자까지만 가능합니다.'),
|
||||||
|
category: Yup.array(Yup.string().oneOf(categories))
|
||||||
|
.min(1, '최소 한 개의 카테고리를 선택해주세요.')
|
||||||
|
.unique('카테고리는 중복될 수 없습니다.')
|
||||||
|
.required('카테고리는 필수 항목입니다.'),
|
||||||
|
intro: Yup.string()
|
||||||
|
.min(2, '봇 소개는 최소 2자여야합니다.')
|
||||||
|
.max(60, '봇 소개는 최대 60자여야합니다.')
|
||||||
|
.required('봇 소개는 필수 항목입니다.'),
|
||||||
|
desc: Yup.string()
|
||||||
|
.min(100, '봇 설명은 최소 100자여야합니다.')
|
||||||
|
.max(1500, '봇 설명은 최대 1500자여야합니다.')
|
||||||
|
.required('봇 설명은 필수 항목입니다.'),
|
||||||
|
owners: Yup.array(Yup.string())
|
||||||
|
.min(1, '최소 한 명의 소유자는 입력해주세요.')
|
||||||
|
.max(10, '소유자는 최대 10명까지만 가능합니다.')
|
||||||
|
.unique('소유자 아이디는 중복될 수 없습니다.')
|
||||||
|
.required('소유자는 필수 항목입니다.'),
|
||||||
|
_csrf: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const DeveloperBotSchema: Yup.SchemaOf<DeveloperBot> = Yup.object({
|
export const DeveloperBotSchema: Yup.SchemaOf<DeveloperBot> = Yup.object({
|
||||||
webhook: Yup.string().matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.').matches(Url, '올바른 웹훅 URL을 입력해주세요.').max(150, 'URL은 최대 150자까지만 가능합니다.'),
|
webhook: Yup.string()
|
||||||
_csrf: Yup.string().required()
|
.matches(HTTPProtocol, 'http:// 또는 https:// 로 시작해야합니다.')
|
||||||
|
.matches(Url, '올바른 웹훅 URL을 입력해주세요.')
|
||||||
|
.max(150, 'URL은 최대 150자까지만 가능합니다.'),
|
||||||
|
_csrf: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface DeveloperBot {
|
export interface DeveloperBot {
|
||||||
@ -146,7 +244,7 @@ export interface DeveloperBot {
|
|||||||
|
|
||||||
export const ResetBotTokenSchema = Yup.object({
|
export const ResetBotTokenSchema = Yup.object({
|
||||||
token: Yup.string().required(),
|
token: Yup.string().required(),
|
||||||
_csrf: Yup.string().required()
|
_csrf: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface ResetBotToken {
|
export interface ResetBotToken {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { RefObject, useEffect } from 'react'
|
import { RefObject, useEffect } from 'react'
|
||||||
|
|
||||||
const useOutsideClick = (ref: RefObject<HTMLElement>, callback: () => void) => {
|
const useOutsideClick = (ref: RefObject<HTMLElement>, callback: () => void) => {
|
||||||
const handleClick = (e) => {
|
const handleClick = e => {
|
||||||
if (ref.current && !ref.current.contains(e.target)) {
|
if (ref.current && !ref.current.contains(e.target)) {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user