chore: apply prettier (#637)

* chore: apply prettier

* chore: edit ready comment

* chore: move ts comment
This commit is contained in:
SKINMAKER 2023-11-29 22:04:33 +09:00 committed by GitHub
parent a324106f9f
commit b421d1ab64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
152 changed files with 8356 additions and 5120 deletions

View File

@ -1,16 +1,21 @@
import AdSense from 'react-adsense' import AdSense from 'react-adsense'
const Advertisement: React.FC<AdvertisementProps> = ({ size = 'short' }) => { const Advertisement: React.FC<AdvertisementProps> = ({ size = 'short' }) => {
return <div className='py-5'> return (
<div className='py-5'>
<div <div
className={`z-0 mx-auto w-full text-center text-white ${ className={`z-0 mx-auto w-full text-center text-white ${
process.env.NODE_ENV === 'production' ? '' : 'py-12 bg-gray-700' process.env.NODE_ENV === 'production' ? '' : 'bg-gray-700 py-12'
}`} }`}
style={size === 'short' ? { height: '90px' } : { height: '330px' }} style={size === 'short' ? { height: '90px' } : { height: '330px' }}
> >
{process.env.NODE_ENV === 'production' ? ( {process.env.NODE_ENV === 'production' ? (
<AdSense.Google <AdSense.Google
style={{ display: 'inline-block', width: '100%', height: size === 'short' ? '90px' : '330px'}} style={{
display: 'inline-block',
width: '100%',
height: size === 'short' ? '90px' : '330px',
}}
client='ca-pub-4856582423981759' client='ca-pub-4856582423981759'
slot='3250141451' slot='3250141451'
format='' format=''
@ -20,6 +25,7 @@ const Advertisement: React.FC<AdvertisementProps> = ({ size = 'short' }) => {
)} )}
</div> </div>
</div> </div>
)
} }
declare global { declare global {

View File

@ -7,17 +7,16 @@ const ServerIcon = dynamic(() => import('@components/ServerIcon'))
const Application: React.FC<ApplicationProps> = ({ type, id, name }) => { const Application: React.FC<ApplicationProps> = ({ type, id, name }) => {
return ( return (
<Link href={`/developers/applications/${type + 's'}/${id}`} legacyBehavior> <Link href={`/developers/applications/${type + 's'}/${id}`} legacyBehavior>
<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'> <div className='relative transform cursor-pointer rounded-lg bg-little-white px-2 py-4 text-center transition duration-100 ease-in hover:-translate-y-1 dark:bg-discord-black'>
{ {type === 'bot' ? (
type === 'bot' ? <DiscordAvatar userID={id} className='w-full rounded-xl px-2' />
<DiscordAvatar userID={id} className='px-2 w-full rounded-xl' /> : ) : (
<ServerIcon id={id} className='px-2 w-full rounded-xl' /> <ServerIcon id={id} className='w-full rounded-xl px-2' />
} )}
<h2 className='pt-2 whitespace-nowrap text-xl font-medium truncate'>{name}</h2> <h2 className='truncate whitespace-nowrap pt-2 text-xl font-medium'>{name}</h2>
</div> </div>
</Link> </Link>
) )
} }
interface ApplicationProps { interface ApplicationProps {

View File

@ -11,12 +11,12 @@ const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => { const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
return ( return (
<div className='min-w-0 container mb-16 transform hover:-translate-y-1 transition duration-100 ease-in cursor-pointer'> <div className='container mb-16 min-w-0 transform cursor-pointer transition duration-100 ease-in hover:-translate-y-1'>
<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 <div
className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl' className='relative mx-auto h-full rounded-2xl bg-little-white text-black shadow-xl dark:bg-discord-black dark:text-white'
style={ style={
checkBotFlag(bot.flags, 'trusted') && bot.banner checkBotFlag(bot.flags, 'trusted') && bot.banner
? { ? {
@ -30,15 +30,15 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
<div> <div>
<div className='flex flex-col'> <div className='flex flex-col'>
<div className='flex'> <div className='flex'>
<div className='w-3/5 flex justify-start'> <div className='flex w-3/5 justify-start'>
<DiscordAvatar <DiscordAvatar
size={128} size={128}
userID={bot.id} userID={bot.id}
alt='Avatar' alt='Avatar'
className='absolute -left-2 -top-8 mx-auto w-32 h-32 bg-white rounded-full' className='absolute -left-2 -top-8 mx-auto h-32 w-32 rounded-full bg-white'
/> />
</div> </div>
<div className='grid grid-cols-1 pr-5 pt-5 w-2/5'> <div className='grid w-2/5 grid-cols-1 pr-5 pt-5'>
<Tag <Tag
text={ text={
<> <>
@ -55,19 +55,19 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
</div> </div>
</div> </div>
</div> </div>
<div className='mt-3 px-4 h-16'> <div className='mt-3 h-16 px-4'>
<h2 className='px-1 text-sm'> <h2 className='px-1 text-sm'>
<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-xl sm:text-2xl font-bold truncate'>{bot.name}</h1> <h1 className='mb-3 truncate text-left text-xl font-bold sm:text-2xl'>
{bot.name}
</h1>
</div> </div>
<p className='mb-10 px-4 h-6 text-left text-gray-400 text-sm'> <p className='mb-10 h-6 px-4 text-left text-sm text-gray-400'>{bot.intro}</p>
{bot.intro}
</p>
<div> <div>
<div className='category flex flex-wrap px-2'> <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={`/bots/categories/${el}`} dark /> <Tag key={el} text={el} href={`/bots/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 />}
@ -80,35 +80,31 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
<div className='flex justify-evenly'> <div className='flex justify-evenly'>
<Link <Link
href={makeBotURL(bot)} href={makeBotURL(bot)}
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'> className='w-full rounded-bl-2xl py-3 text-center text-sm font-bold text-koreanbots-blue transition duration-100 ease-in hover:bg-koreanbots-blue hover:text-white hover:shadow-lg'
>
</Link> </Link>
{manage ? ( {manage ? (
<Link <Link
href={`/bots/${bot.id}/edit`} href={`/bots/${bot.id}/edit`}
className='py-3 w-full text-center text-emerald-500 hover:text-white text-sm font-bold hover:bg-emerald-500 rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'> className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-emerald-500 transition duration-100 ease-in hover:bg-emerald-500 hover:text-white hover:shadow-lg'
</Link>
) : bot.state !== 'ok' ? <a
className='py-3 w-full text-center text-discord-blurple text-sm font-bold rounded-br-2xl hover:shadow-lg transition duration-100 ease-in opacity-50 cursor-default select-none'
> >
</Link>
) : bot.state !== 'ok' ? (
<a className='w-full cursor-default select-none rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple opacity-50 transition duration-100 ease-in hover:shadow-lg'>
</a> : </a>
) : (
<a <a
href={ href={makeBotURL(bot) + '/invite'}
makeBotURL(bot) + '/invite'
}
rel='noopener noreferrer' rel='noopener noreferrer'
target='_blank' 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' className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple transition duration-100 ease-in hover:bg-discord-blurple hover:text-white hover:shadow-lg'
> >
</a> </a>
} )}
</div> </div>
</div> </div>
</div> </div>
@ -117,7 +113,6 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
</div> </div>
</div> </div>
) )
} }
interface BotCardProps { interface BotCardProps {

View File

@ -9,30 +9,38 @@ const Button: React.FC<ButtonProps> = ({
disabled = false, disabled = false,
onClick, onClick,
}) => { }) => {
return href ? <Link return href ? (
<Link
href={!disabled && href} href={!disabled && href}
className={`cursor-pointer rounded-md px-4 py-2 transition duration-300 ease select-none outline-none foucs:outline-none mr-1.5 ${className ?? className={`ease foucs:outline-none mr-1.5 cursor-pointer select-none rounded-md px-4 py-2 outline-none transition duration-300 ${
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}> className ??
'bg-discord-blurple text-white hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover'
{children} }`}
</Link>
: onClick ? <button
type={disabled ? 'button' : type}
onClick={disabled ? null : onClick}
className={`cursor-pointer rounded-md px-4 py-2 transition duration-300 ease select-none outline-none foucs:outline-none mr-1.5 ${className ??
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}
> >
{children} {children}
</button> </Link>
: ) : onClick ? (
<button <button
type={disabled ? 'button' : type} type={disabled ? 'button' : type}
className={`cursor-pointer rounded-md px-4 py-2 transition duration-300 ease select-none outline-none foucs:outline-none mr-1.5 ${className ?? onClick={disabled ? null : onClick}
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`} className={`ease foucs:outline-none mr-1.5 cursor-pointer select-none rounded-md px-4 py-2 outline-none transition duration-300 ${
className ??
'bg-discord-blurple text-white hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover'
}`}
> >
{children} {children}
</button> </button>
) : (
<button
type={disabled ? 'button' : type}
className={`ease foucs:outline-none mr-1.5 cursor-pointer select-none rounded-md px-4 py-2 outline-none transition duration-300 ${
className ??
'bg-discord-blurple text-white hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover'
}`}
>
{children}
</button>
)
} }
interface ButtonProps { interface ButtonProps {

View File

@ -1,9 +1,14 @@
import { Ref } from 'react' import { Ref } from 'react'
import HCaptcha from '@hcaptcha/react-hcaptcha' import HCaptcha from '@hcaptcha/react-hcaptcha'
const Captcha: React.FC<CaptchaProps> = ({ dark, onVerify }) => { const Captcha: React.FC<CaptchaProps> = ({ dark, onVerify }) => {
return <HCaptcha sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITEKEY} theme={dark ? 'dark' : 'light'} onVerify={onVerify}/> return (
<HCaptcha
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITEKEY}
theme={dark ? 'dark' : 'light'}
onVerify={onVerify}
/>
)
} }
interface CaptchaProps { interface CaptchaProps {

View File

@ -9,98 +9,161 @@ import { NextSeo } from 'next-seo'
const Container = dynamic(() => import('@components/Container')) const Container = dynamic(() => import('@components/Container'))
const Divider = dynamic(() => import('@components/Divider')) const Divider = dynamic(() => import('@components/Divider'))
const DeveloperLayout: React.FC<DeveloperLayout> = ({ children, enabled, docs, currentDoc }:DeveloperLayout) => { const DeveloperLayout: React.FC<DeveloperLayout> = ({
children,
enabled,
docs,
currentDoc,
}: DeveloperLayout) => {
const [navbarEnabled, setNavbarOpen] = useState(false) const [navbarEnabled, setNavbarOpen] = useState(false)
return ( return (
<div className='flex min-h-screen'> <div className='min-h-screen flex'>
<NextSeo title='한디리 개발자' description='한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.' openGraph={{ <NextSeo
title='한디리 개발자'
description='한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.'
openGraph={{
title: '한디리 개발자', title: '한디리 개발자',
description:'한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.' description: '한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.',
}} /> }}
<div className='block lg:hidden h-screen relative'> />
<div className='w-18 pt-20 px-2 h-full text-center bg-little-white dark:bg-discord-black fixed'> <div className='relative block h-screen lg:hidden'>
<div className='w-18 fixed h-full bg-little-white px-2 pt-20 text-center dark:bg-discord-black'>
<ul className='text-gray-600 dark:text-gray-300'> <ul className='text-gray-600 dark:text-gray-300'>
<li className={`cursor-pointer py-2 px-4 mb-2 rounded-md ${enabled === 'applications' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}> <li
<Link href='/developers/applications' legacyBehavior><i className='fas fa-robot'/></Link> className={`mb-2 cursor-pointer rounded-md px-4 py-2 ${
enabled === 'applications'
? 'bg-discord-blurple text-white'
: 'hover:text-gray-500 dark:hover:text-white'
}`}
>
<Link href='/developers/applications' legacyBehavior>
<i className='fas fa-robot' />
</Link>
</li> </li>
<li className={`cursor-pointer py-2 px-4 my-2 rounded-md ${enabled === 'docs' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}> <li
<Link href='/developers/docs' legacyBehavior><i className='fas fa-book'/></Link> className={`my-2 cursor-pointer rounded-md px-4 py-2 ${
enabled === 'docs'
? 'bg-discord-blurple text-white'
: 'hover:text-gray-500 dark:hover:text-white'
}`}
>
<Link href='/developers/docs' legacyBehavior>
<i className='fas fa-book' />
</Link>
</li> </li>
{ {enabled === 'docs' && (
enabled === 'docs' && <> <>
<Divider /> <Divider />
<li className='cursor-pointer py-2 px-4 my-2 rounded-md hover:text-gray-500 dark:hover:text-white' onKeyDown={() => setNavbarOpen(true)} onClick={() => setNavbarOpen(true)}> <li
className='my-2 cursor-pointer rounded-md px-4 py-2 hover:text-gray-500 dark:hover:text-white'
onKeyDown={() => setNavbarOpen(true)}
onClick={() => setNavbarOpen(true)}
>
<i className='fas fa-bars' /> <i className='fas fa-bars' />
</li></> </li>
} </>
)}
</ul> </ul>
</div> </div>
</div> </div>
<div className={`${navbarEnabled ? 'block' : 'hidden'} lg:block relative`}> <div className={`${navbarEnabled ? 'block' : 'hidden'} relative lg:block`}>
<div className='bg-little-white dark:bg-discord-black pt-20 px-6 fixed h-screen w-screen lg:w-60 overflow-y-auto'> <div className='fixed h-screen w-screen overflow-y-auto bg-little-white px-6 pt-20 dark:bg-discord-black lg:w-60'>
<ul className='text-base text-gray-600 dark:text-gray-300 mb-6 hidden lg:block'> <ul className='mb-6 hidden text-base text-gray-600 dark:text-gray-300 lg:block'>
<li className='cursor-pointer py-2 px-4 rounded-md hover:text-gray-500 dark:hover:text-white lg:hidden' onKeyDown={() => setNavbarOpen(false)} onClick={() => setNavbarOpen(false)}></li> <li
className='cursor-pointer rounded-md px-4 py-2 hover:text-gray-500 dark:hover:text-white lg:hidden'
onKeyDown={() => setNavbarOpen(false)}
onClick={() => setNavbarOpen(false)}
>
</li>
<Divider className='lg:hidden' /> <Divider className='lg:hidden' />
<Link href='/developers/applications' legacyBehavior> <Link href='/developers/applications' legacyBehavior>
<li className={`cursor-pointer py-2 px-4 rounded-md ${enabled === 'applications' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}> <li
className={`cursor-pointer rounded-md px-4 py-2 ${
enabled === 'applications'
? 'bg-discord-blurple text-white'
: 'hover:text-gray-500 dark:hover:text-white'
}`}
>
</li> </li>
</Link> </Link>
<Link href='/developers/docs' legacyBehavior> <Link href='/developers/docs' legacyBehavior>
<li className={`cursor-pointer py-2 px-4 rounded-md ${enabled === 'docs' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}> <li
className={`cursor-pointer rounded-md px-4 py-2 ${
enabled === 'docs'
? 'bg-discord-blurple text-white'
: 'hover:text-gray-500 dark:hover:text-white'
}`}
>
</li> </li>
</Link> </Link>
</ul> </ul>
{ {enabled === 'docs' && (
enabled === 'docs' && <> <>
<Divider className='hidden lg:block' /> <Divider className='hidden lg:block' />
<ul className='text-sm text-gray-600 dark:text-gray-300 px-0.5 lg:mt-6'> <ul className='px-0.5 text-sm text-gray-600 dark:text-gray-300 lg:mt-6'>
<li onClick={() => setNavbarOpen(false)} className='lg:hidden cursor-pointer py-1 px-4 rounded-md mb-2'> <li
onClick={() => setNavbarOpen(false)}
className='mb-2 cursor-pointer rounded-md px-4 py-1 lg:hidden'
>
<i className='fas fa-times' /> <i className='fas fa-times' />
</li> </li>
<Divider className='lg:hidden' /> <Divider className='lg:hidden' />
{ {docs?.map((el) => {
docs?.map(el => { if (el.list)
if(el.list) return ( return (
<div key={el.name} className='mt-2'> <div key={el.name} className='mt-2'>
<span className='text-gray-600 dark:text-gray-100 font-bold mb-1'>{el.name}</span> <span className='mb-1 font-bold text-gray-600 dark:text-gray-100'>
<ul className='text-sm py-3'> {el.name}
{ </span>
el.list.map(e => <ul className='py-3 text-sm'>
{el.list.map((e) => (
<Link <Link
key={e.name} key={e.name}
href={`/developers/docs/${el.name}/${e.name}`} href={`/developers/docs/${el.name}/${e.name}`}
legacyBehavior> legacyBehavior
<li onClick={() => setNavbarOpen(false)} className={`cursor-pointer px-4 py-2 rounded-md ${currentDoc === e.name ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}> >
<li
onClick={() => setNavbarOpen(false)}
className={`cursor-pointer rounded-md px-4 py-2 ${
currentDoc === e.name
? 'bg-discord-blurple text-white'
: 'hover:text-gray-500 dark:hover:text-white'
}`}
>
{e.name} {e.name}
</li> </li>
</Link> </Link>
) ))}
}
</ul> </ul>
</div> </div>
) )
return ( return (
<Link key={el.name} href={`/developers/docs/${el.name}`} legacyBehavior> <Link key={el.name} href={`/developers/docs/${el.name}`} legacyBehavior>
<li onClick={() => setNavbarOpen(false)} className={`cursor-pointer py-2 px-4 rounded-md ${currentDoc === el.name ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}> <li
onClick={() => setNavbarOpen(false)}
className={`cursor-pointer rounded-md px-4 py-2 ${
currentDoc === el.name
? 'bg-discord-blurple text-white'
: 'hover:text-gray-500 dark:hover:text-white'
}`}
>
{el.name} {el.name}
</li> </li>
</Link> </Link>
) )
}) })}
}
</ul> </ul>
</> </>
} )}
</div> </div>
</div> </div>
<div className='w-full py-28 lg:pl-60 pl-16'> <div className='w-full py-28 pl-16 lg:pl-60'>
<Container> <Container>{children}</Container>
{children}
</Container>
</div> </div>
</div> </div>
) )

View File

@ -4,13 +4,20 @@ import { KoreanbotsEndPoints } from '@utils/Constants'
const Image = dynamic(() => import('@components/Image')) const Image = dynamic(() => import('@components/Image'))
const DiscordAvatar: React.FC<DiscordAvatarProps> = props => { const DiscordAvatar: React.FC<DiscordAvatarProps> = (props) => {
return <Image return (
<Image
{...props} {...props}
src={KoreanbotsEndPoints.CDN.avatar(props.userID, { format: 'webp', size: props.size ?? 256})} src={KoreanbotsEndPoints.CDN.avatar(props.userID, {
fallbackSrc={KoreanbotsEndPoints.CDN.avatar(props.userID, { format: 'png', size: props.size ?? 256})} format: 'webp',
size: props.size ?? 256,
})}
fallbackSrc={KoreanbotsEndPoints.CDN.avatar(props.userID, {
format: 'png',
size: props.size ?? 256,
})}
/> />
)
} }
interface DiscordAvatarProps { interface DiscordAvatarProps {

View File

@ -8,26 +8,28 @@ const Docs: React.FC<DocsProps> = ({ title, header, description, subheader, chil
const d = description || subheader const d = description || subheader
return ( return (
<> <>
<NextSeo title={t} description={d} <NextSeo
title={t}
description={d}
openGraph={{ openGraph={{
title: t, title: t,
description: d description: d,
}} }}
/> />
<div className='dark:bg-discord-black bg-discord-blurple z-20'> <div className='z-20 bg-discord-blurple dark:bg-discord-black'>
<Container className='py-20' ignoreColor> <Container className='py-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-4xl font-bold text-gray-100 sm:text-left'>
{header} {header}
</h1> </h1>
<h2 className='mt-5 text-center text-gray-200 text-xl font-medium sm:text-left'> <h2 className='mt-5 text-center text-xl font-medium text-gray-200 sm:text-left'>
{description} {description}
</h2> </h2>
<h2 className='mt-5 text-center text-gray-200 text-xl font-medium sm:text-left'> <h2 className='mt-5 text-center text-xl font-medium text-gray-200 sm:text-left'>
{subheader} {subheader}
</h2> </h2>
</Container> </Container>
</div> </div>
<Container className='pt-10 pb-20'> <Container className='pb-20 pt-10'>
<div>{children}</div> <div>{children}</div>
</Container> </Container>
</> </>

View File

@ -9,16 +9,16 @@ const Toggle = dynamic(() => import('@components/Toggle'))
const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => { const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
return ( return (
<div className='releative z-30'> <div className='releative z-30'>
<div className='bottom-0 text-white bg-discord-black py-24'> <div className='bottom-0 bg-discord-black py-24 text-white'>
<Container className='w-11/12 lg:flex lg:pt-0 lg:w-4/5' ignoreColor> <Container className='w-11/12 lg:flex lg:w-4/5 lg:pt-0' ignoreColor>
<div className='w-full lg:w-2/5'> <div className='w-full lg:w-2/5'>
<h1 className='text-koreanbots-blue text-2xl font-bold'> .</h1> <h1 className='text-2xl font-bold text-koreanbots-blue'>
.
</h1>
<span className='text-base'>2020-2023 , All rights reserved.</span> <span className='text-base'>2020-2023 , All rights reserved.</span>
<div className='text-2xl flex space-x-1'> <div className='flex space-x-1 text-2xl'>
<Link href='/discord'> <Link href='/discord'>
<i className='fab fa-discord inline-block w-full' /> <i className='fab fa-discord inline-block w-full' />
</Link> </Link>
<a href='https://github.com/koreanbots'> <a href='https://github.com/koreanbots'>
<i className='fab fa-github inline-block w-full' /> <i className='fab fa-github inline-block w-full' />
@ -28,9 +28,9 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
</a> </a>
</div> </div>
</div> </div>
<div className='grid grow gap-2 grid-cols-2 md:grid-cols-7'> <div className='grid grow grid-cols-2 gap-2 md:grid-cols-7'>
<div className='col-span-2 mb-2'> <div className='col-span-2 mb-2'>
<h2 className='text-koreanbots-blue text-base font-bold'> </h2> <h2 className='text-base font-bold text-koreanbots-blue'> </h2>
<ul className='text-sm'> <ul className='text-sm'>
<li> <li>
<Link href='/about' className='hover:text-gray-300'> <Link href='/about' className='hover:text-gray-300'>
@ -50,7 +50,7 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
</ul> </ul>
</div> </div>
<div className='col-span-2 mb-2'> <div className='col-span-2 mb-2'>
<h2 className='text-koreanbots-blue text-base font-bold'></h2> <h2 className='text-base font-bold text-koreanbots-blue'></h2>
<ul className='text-sm'> <ul className='text-sm'>
<li> <li>
<Link href='/tos' className='hover:text-gray-300'> <Link href='/tos' className='hover:text-gray-300'>
@ -75,7 +75,7 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
</ul> </ul>
</div> </div>
<div className='col-span-1 mb-2'> <div className='col-span-1 mb-2'>
<h2 className='text-koreanbots-blue text-base font-bold'></h2> <h2 className='text-base font-bold text-koreanbots-blue'></h2>
<ul className='text-sm'> <ul className='text-sm'>
{/* <li> {/* <li>
<Link href='/partners'> <Link href='/partners'>
@ -90,7 +90,7 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
</ul> </ul>
</div> </div>
<div className='col-span-2 mb-2'> <div className='col-span-2 mb-2'>
<h2 className='text-koreanbots-blue text-base font-bold'></h2> <h2 className='text-base font-bold text-koreanbots-blue'></h2>
<div className='flex'> <div className='flex'>
<a className='mr-2 hover:text-gray-300'></a> <a className='mr-2 hover:text-gray-300'></a>
<Toggle <Toggle

View File

@ -8,19 +8,21 @@ const Button = dynamic(() => import('@components/Button'))
const Forbidden: React.FC = () => { const Forbidden: React.FC = () => {
const router = useRouter() const router = useRouter()
return <> return (
<>
<NextSeo title='권한이 없습니다' /> <NextSeo title='권한이 없습니다' />
<div className='flex items-center justify-center h-screen select-none'> <div className='flex h-screen select-none items-center justify-center'>
<div className='container mx-auto px-20 md:text-left text-center'> <div className='container mx-auto px-20 text-center md:text-left'>
<h1 className='text-8xl font-semibold'>403</h1> <h1 className='text-8xl font-semibold'>403</h1>
<h2 className='text-2xl font-semibold py-2'> <h2 className='py-2 text-2xl font-semibold'>{ErrorText[403]}</h2>
{ErrorText[403]}
</h2>
<Button onClick={router.back}> </Button> <Button onClick={router.back}> </Button>
<p className='text-gray-400 text-sm mt-2'> . .</p> <p className='mt-2 text-sm text-gray-400'>
. .
</p>
</div> </div>
</div> </div>
</> </>
)
} }
export default Forbidden export default Forbidden

View File

@ -1,7 +1,14 @@
import { Field } from 'formik' import { Field } from 'formik'
const CheckBox: React.FC<CheckBoxProps> = ({ name, ...props }) => { const CheckBox: React.FC<CheckBoxProps> = ({ name, ...props }) => {
return <Field type='checkbox' name={name} className='form-checkbox text-koreanbots-blue bg-gray-300 h-4 w-4 rounded' {...props} /> return (
<Field
type='checkbox'
name={name}
className='form-checkbox h-4 w-4 rounded bg-gray-300 text-koreanbots-blue'
{...props}
/>
)
} }
interface CheckBoxProps { interface CheckBoxProps {

View File

@ -1,12 +1,16 @@
import { Field } from 'formik' import { Field } from 'formik'
const Input: React.FC<InputProps> = ({ name, placeholder, ...props }) => { const Input: React.FC<InputProps> = ({ name, placeholder, ...props }) => {
return <Field return (
<Field
{...props} {...props}
name={name} 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'} className={
'border-grey-light relative h-10 w-full rounded border px-3 text-black outline-none dark:border-transparent dark:bg-very-black dark:text-white'
}
placeholder={placeholder} placeholder={placeholder}
/> />
)
} }
interface InputProps { interface InputProps {

View File

@ -6,18 +6,19 @@ const Label: React.FC<LabelProps> = ({
error = null, error = null,
grid = true, grid = true,
short = false, short = false,
required = false required = false,
}) => { }) => {
return <label return (
className={grid ? 'grid grid-cols-1 xl:grid-cols-4 gap-2 my-4' : 'inline-flex items-center'} <label
className={grid ? 'my-4 grid grid-cols-1 gap-2 xl:grid-cols-4' : 'inline-flex items-center'}
htmlFor={For} htmlFor={For}
> >
{label && ( {label && (
<div className='col-span-1 text-sm'> <div className='col-span-1 text-sm'>
<h3 className='text-koreanbots-blue text-lg font-bold'> <h3 className='text-lg font-bold text-koreanbots-blue'>
{label} {label}
{required && ( {required && (
<span className='align-text-top text-red-500 text-base font-semibold'> *</span> <span className='align-text-top text-base font-semibold text-red-500'> *</span>
)} )}
</h3> </h3>
{labelDesc} {labelDesc}
@ -25,9 +26,10 @@ const Label: React.FC<LabelProps> = ({
)} )}
<div className={short ? 'col-span-1' : 'col-span-3'}> <div className={short ? 'col-span-1' : 'col-span-3'}>
{children} {children}
<div className='mt-1 text-red-500 text-xs font-light'>{error}</div> <div className='mt-1 text-xs font-light text-red-500'>{error}</div>
</div> </div>
</label> </label>
)
} }
interface LabelProps { interface LabelProps {

View File

@ -1,12 +1,19 @@
import ReactSelect from 'react-select' import ReactSelect from 'react-select'
const Select: React.FC<SelectProps> = ({ placeholder, options, handleChange, handleTouch, value }) => { const Select: React.FC<SelectProps> = ({
return <ReactSelect placeholder,
options,
handleChange,
handleTouch,
value,
}) => {
return (
<ReactSelect
styles={{ styles={{
control: provided => { control: (provided) => {
return { ...provided, border: 'none' } return { ...provided, border: 'none' }
}, },
option: provided => { option: (provided) => {
return { return {
...provided, ...provided,
cursor: 'pointer', cursor: 'pointer',
@ -15,20 +22,20 @@ const Select: React.FC<SelectProps> = ({ placeholder, options, handleChange, han
}, },
} }
}, },
placeholder: provided => { placeholder: (provided) => {
return { return {
...provided, ...provided,
position: 'absolute' position: 'absolute',
} }
}, },
singleValue: provided => { singleValue: (provided) => {
return { return {
...provided, ...provided,
position: 'absolute' position: 'absolute',
}
} }
},
}} }}
className='border-grey-light border dark:border-transparent rounded' className='border-grey-light rounded border dark:border-transparent'
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white ' classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white '
placeholder={placeholder || '선택해주세요.'} placeholder={placeholder || '선택해주세요.'}
options={options} options={options}
@ -37,6 +44,7 @@ const Select: React.FC<SelectProps> = ({ placeholder, options, handleChange, han
noOptionsMessage={() => '검색 결과가 없습니다.'} noOptionsMessage={() => '검색 결과가 없습니다.'}
defaultValue={value} defaultValue={value}
/> />
)
} }
interface SelectProps { interface SelectProps {

View File

@ -1,9 +1,5 @@
import React, { MouseEventHandler } from 'react' import React, { MouseEventHandler } from 'react'
import ReactSelect, { import ReactSelect, { components, MultiValueProps, MultiValueRemoveProps } from 'react-select'
components,
MultiValueProps,
MultiValueRemoveProps,
} from 'react-select'
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core' import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core'
import { restrictToParentElement } from '@dnd-kit/modifiers' import { restrictToParentElement } from '@dnd-kit/modifiers'
import { import {
@ -20,8 +16,9 @@ const MultiValue = (props: MultiValueProps<Option>) => {
e.stopPropagation() e.stopPropagation()
} }
const innerProps = { ...props.innerProps, onMouseDown } const innerProps = { ...props.innerProps, onMouseDown }
const { attributes, listeners, setNodeRef, transform, transition } = const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
useSortable({ id: props.data.value }) id: props.data.value,
})
const style = { const style = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
@ -46,16 +43,30 @@ const MultiValueRemove = (props: MultiValueRemoveProps<Option>) => {
) )
} }
const Select: React.FC<SelectProps> = ({ placeholder, options, values, setValues, handleChange, handleTouch }) => { const Select: React.FC<SelectProps> = ({
placeholder,
options,
values,
setValues,
handleChange,
handleTouch,
}) => {
const onSortEnd = (event: DragEndEvent) => { const onSortEnd = (event: DragEndEvent) => {
const { active, over } = event const { active, over } = event
const newValue = arrayMove(values, values.findIndex(i => i === active.id), values.findIndex(i => i === over.id)) const newValue = arrayMove(
values,
values.findIndex((i) => i === active.id),
values.findIndex((i) => i === over.id)
)
setValues(newValue) setValues(newValue)
} }
return <DndContext modifiers={[restrictToParentElement]} onDragEnd={onSortEnd} collisionDetection={closestCenter}> return (
<SortableContext <DndContext
items={values} modifiers={[restrictToParentElement]}
strategy={horizontalListSortingStrategy}> onDragEnd={onSortEnd}
collisionDetection={closestCenter}
>
<SortableContext items={values} strategy={horizontalListSortingStrategy}>
<ReactSelect <ReactSelect
styles={{ styles={{
placeholder: (provided) => { placeholder: (provided) => {
@ -65,20 +76,24 @@ const Select: React.FC<SelectProps> = ({ placeholder, options, values, setValues
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',
},
} }
},
}} }}
isMulti isMulti
className='border border-grey-light dark:border-transparent rounded' className='border-grey-light rounded border dark:border-transparent'
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white cursor-pointer ' classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white cursor-pointer '
placeholder={placeholder || '선택해주세요.'} placeholder={placeholder || '선택해주세요.'}
options={options} options={options}
onChange={handleChange} onChange={handleChange}
onBlur={handleTouch} onBlur={handleTouch}
noOptionsMessage={() => '검색 결과가 없습니다.'} noOptionsMessage={() => '검색 결과가 없습니다.'}
value={values.map(el => ({ label: el, value: el}))} value={values.map((el) => ({ label: el, value: el }))}
components={{ components={{
MultiValue, MultiValue,
MultiValueRemove, MultiValueRemove,
@ -87,6 +102,7 @@ const Select: React.FC<SelectProps> = ({ placeholder, options, values, setValues
/> />
</SortableContext> </SortableContext>
</DndContext> </DndContext>
)
} }
interface SelectProps { interface SelectProps {

View File

@ -8,26 +8,45 @@ import useOutsideClick from '@utils/useOutsideClick'
import 'emoji-mart/css/emoji-mart.css' import 'emoji-mart/css/emoji-mart.css'
const TextArea: React.FC<TextAreaProps> = ({
name,
const TextArea: React.FC<TextAreaProps> = ({ name, placeholder, theme='auto', max, setValue, value }) => { placeholder,
theme = 'auto',
max,
setValue,
value,
}) => {
const ref = useRef() const ref = useRef()
const [emojiPickerHidden, setEmojiPickerHidden] = useState(true) const [emojiPickerHidden, setEmojiPickerHidden] = useState(true)
useOutsideClick(ref, () => { useOutsideClick(ref, () => {
setEmojiPickerHidden(true) setEmojiPickerHidden(true)
}) })
return <div className='border border-grey-light dark:border-transparent h-96 text-black dark:bg-very-black dark:text-white rounded px-4 py-3 inline-block relative w-full'> return (
<Field as='textarea' name={name} className='dark:border-transparent text-black dark:bg-very-black dark:text-white w-full relative h-full resize-none outline-none' placeholder={placeholder} /> <div className='border-grey-light relative inline-block h-96 w-full rounded border px-4 py-3 text-black dark:border-transparent dark:bg-very-black dark:text-white'>
<Field
as='textarea'
name={name}
className='relative h-full w-full resize-none text-black outline-none dark:border-transparent dark:bg-very-black dark:text-white'
placeholder={placeholder}
/>
<div ref={ref}> <div ref={ref}>
<div className='absolute bottom-12 left-10 z-30'> <div className='absolute bottom-12 left-10 z-30'>
{ {!emojiPickerHidden && (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
!emojiPickerHidden && <Picker title='선택해주세요' emoji='sunglasses' set='twitter' enableFrequentEmojiSort theme={theme} showSkinTones={false} onSelect={(e) => { <Picker
title='선택해주세요'
emoji='sunglasses'
set='twitter'
enableFrequentEmojiSort
theme={theme}
showSkinTones={false}
onSelect={(e) => {
setEmojiPickerHidden(true) setEmojiPickerHidden(true)
setValue(value + ' ' + ((e as { native: string }).native || e.colons)) setValue(value + ' ' + ((e as { native: string }).native || e.colons))
}} i18n={{ }}
i18n={{
search: '검색', search: '검색',
notfound: '검색 결과가 없습니다.', notfound: '검색 결과가 없습니다.',
categories: { categories: {
@ -41,21 +60,32 @@ const TextArea: React.FC<TextAreaProps> = ({ name, placeholder, theme='auto', ma
objects: '사물', objects: '사물',
symbols: '기호', symbols: '기호',
flags: '국기', flags: '국기',
custom: '커스텀' custom: '커스텀',
} },
}} custom={KoreanbotsEmoji}/> }}
} custom={KoreanbotsEmoji}
/>
)}
</div> </div>
<div className='absolute bottom-2 left-4 hidden sm:block'> <div className='absolute bottom-2 left-4 hidden sm:block'>
<div className='emoji-selector-button outline-none' onClick={() => setEmojiPickerHidden(false)} onKeyPress={() => setEmojiPickerHidden(false)} role='button' tabIndex={0} /> <div
className='emoji-selector-button outline-none'
onClick={() => setEmojiPickerHidden(false)}
onKeyPress={() => setEmojiPickerHidden(false)}
role='button'
tabIndex={0}
/>
</div> </div>
{ {max && (
max && <span className={`absolute bottom-2 right-4 ${max < value.length ? ' text-red-400' : ''}`}> <span
className={`absolute bottom-2 right-4 ${max < value.length ? ' text-red-400' : ''}`}
>
{max - value.length} {max - value.length}
</span> </span>
} )}
</div> </div>
</div> </div>
)
} }
interface TextAreaProps { interface TextAreaProps {
@ -68,4 +98,3 @@ interface TextAreaProps {
} }
export default TextArea export default TextArea

View File

@ -1,7 +1,12 @@
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo' import { NextSeo } from 'next-seo'
import { botCategories, botCategoryIcon, serverCategories, serverCategoryIcon } from '@utils/Constants' import {
botCategories,
botCategoryIcon,
serverCategories,
serverCategoryIcon,
} from '@utils/Constants'
const Container = dynamic(() => import('@components/Container')) const Container = dynamic(() => import('@components/Container'))
const Tag = dynamic(() => import('@components/Tag')) const Tag = dynamic(() => import('@components/Tag'))
@ -9,57 +14,135 @@ const Search = dynamic(()=> import('@components/Search'))
const Hero: React.FC<HeroProps> = ({ type = 'all', header, description }) => { const Hero: React.FC<HeroProps> = ({ type = 'all', header, description }) => {
const link = `/${type}/categories` const link = `/${type}/categories`
return <> return (
<NextSeo title={header} description={description} openGraph={{ <>
<NextSeo
title={header}
description={description}
openGraph={{
title: header, title: header,
description description,
}} /> }}
<div className='dark:bg-discord-black bg-discord-blurple text-gray-100 md:p-0 mb-8'> />
<Container className='pt-24 pb-16 md:pb-20' ignoreColor> <div className='mb-8 bg-discord-blurple text-gray-100 dark:bg-discord-black md:p-0'>
<h1 className='hidden md:block text-left text-3xl font-bold'> <Container className='pb-16 pt-24 md:pb-20' ignoreColor>
<h1 className='hidden text-left text-3xl font-bold md:block'>
{header && `${header} - `} {header && `${header} - `}
</h1> </h1>
<h1 className='md:hidden text-center text-3xl font-semibold'> <h1 className='text-center text-3xl font-semibold md:hidden'>
{ header && <span className='text-4xl'>{header}<br/></span>} {header && (
<span className='text-4xl'>
{header}
<br />
</span>
)}
</h1> </h1>
<p className='text-center sm:text-left text-xl font-base mt-2'>{description || `${type !== 'all' ? '다양한 ' : ''}국내 디스코드${{ all: '의 모든 것을', bots: ' 봇들을', servers: ' 서버들을' }[type]} 한 곳에서 확인하세요!`}</p> <p className='font-base mt-2 text-center text-xl sm:text-left'>
{description ||
`${type !== 'all' ? '다양한 ' : ''}국내 디스코드${
{ all: '의 모든 것을', bots: ' 봇들을', servers: ' 서버들을' }[type]
} !`}
</p>
<Search /> <Search />
<div className='flex flex-wrap mt-5'> <div className='mt-5 flex flex-wrap'>
{ {type === 'all' ? (
type === 'all' ? <> <>
<Tag text={ <Tag
text={
<> <>
<i className='fas fa-robot text-koreanbots-blue' /> <i className='fas fa-robot text-koreanbots-blue' />
</> </>
} dark bigger href='/bots' /> }
<Tag text={ dark
bigger
href='/bots'
/>
<Tag
text={
<> <>
<i className='fas fa-users text-koreanbots-blue' /> <i className='fas fa-users text-koreanbots-blue' />
</> </>
} dark bigger href='/servers' />
{
botCategories.slice(0, 2).map(t => <Tag key={t} text={<><i className={botCategoryIcon[t]} /> {t} </>} dark bigger href={`/bots/categories/${t}`} />)
} }
dark
{ bigger
serverCategories.slice(0, 2).map(t => <Tag key={t} text={<><i className={serverCategoryIcon[t]} /> {t} </>} dark bigger href={`/servers/categories/${t}`} />) href='/servers'
} />
</>: <> {botCategories.slice(0, 2).map((t) => (
<Tag key='list' text={<> <Tag
<i className='fas fa-heart text-red-600'/> key={t}
</>} dark bigger href={type === 'bots' ? '/bots/list/votes' : '/servers/list/votes'} /> text={
{ (type === 'bots' ? botCategories : serverCategories).slice(0, 4).map(t=> <Tag key={t} text={<> <>
<i className={(type === 'bots' ? botCategoryIcon : serverCategoryIcon)[t]} /> {t} <i className={botCategoryIcon[t]} /> {t}
</>} dark bigger href={`${link}/${t}`} />) }
<Tag key='tag' text={<>
<i className='fas fa-tag'/>
</>} dark bigger href={link} />
</> </>
} }
dark
bigger
href={`/bots/categories/${t}`}
/>
))}
{serverCategories.slice(0, 2).map((t) => (
<Tag
key={t}
text={
<>
<i className={serverCategoryIcon[t]} /> {t}
</>
}
dark
bigger
href={`/servers/categories/${t}`}
/>
))}
</>
) : (
<>
<Tag
key='list'
text={
<>
<i className='fas fa-heart text-red-600' />
</>
}
dark
bigger
href={type === 'bots' ? '/bots/list/votes' : '/servers/list/votes'}
/>
{(type === 'bots' ? botCategories : serverCategories).slice(0, 4).map((t) => (
<Tag
key={t}
text={
<>
<i
className={(type === 'bots' ? botCategoryIcon : serverCategoryIcon)[t]}
/>{' '}
{t}
</>
}
dark
bigger
href={`${link}/${t}`}
/>
))}
<Tag
key='tag'
text={
<>
<i className='fas fa-tag' />
</>
}
dark
bigger
href={link}
/>
</>
)}
</div> </div>
</Container> </Container>
</div> </div>
</> </>
)
} }
interface HeroProps { interface HeroProps {

View File

@ -2,7 +2,7 @@ import { SyntheticEvent, useEffect, useState } from 'react'
import { supportsWebP } from '@utils/Tools' import { supportsWebP } from '@utils/Tools'
import Logger from '@utils/Logger' import Logger from '@utils/Logger'
const BaseImage: React.FC<ImageProps> = props => { const BaseImage: React.FC<ImageProps> = (props) => {
const fallback = '/img/default.png' const fallback = '/img/default.png'
const [webpUnavailable, setWebpUnavailable] = useState<boolean>() const [webpUnavailable, setWebpUnavailable] = useState<boolean>()
@ -10,37 +10,38 @@ const BaseImage: React.FC<ImageProps> = props => {
setWebpUnavailable(localStorage.webp === 'false') setWebpUnavailable(localStorage.webp === 'false')
}, []) }, [])
return <img return (
<img
alt={props.alt ?? 'Image'} alt={props.alt ?? 'Image'}
loading='lazy' loading='lazy'
className={props.className} className={props.className}
src={ src={(webpUnavailable && props.fallbackSrc) || props.src}
webpUnavailable && props.fallbackSrc || props.src
}
onError={(e: SyntheticEvent<HTMLImageElement, ImageEvent>) => { onError={(e: SyntheticEvent<HTMLImageElement, ImageEvent>) => {
if (webpUnavailable) { if (webpUnavailable) {
(e.target as ImageTarget).onerror = (event) => { ;(e.target as ImageTarget).onerror = (event) => {
// All Fails // All Fails
(event.target as ImageTarget).onerror = () => { Logger.warn('FALLBACK IMAGE LOAD FAIL') } ;(event.target as ImageTarget).onerror = () => {
(event.target as ImageTarget).src = fallback Logger.warn('FALLBACK IMAGE LOAD FAIL')
} }
;(event.target as ImageTarget).src = fallback
} }
else if (props.fallbackSrc) { } else if (props.fallbackSrc) {
(e.target as ImageTarget).onerror = (event) => { ;(e.target as ImageTarget).onerror = (event) => {
// All Fails // All Fails
(event.target as ImageTarget).onerror = () => { Logger.warn('FALLBACK IMAGE LOAD FAIL') } ;(event.target as ImageTarget).onerror = () => {
(event.target as ImageTarget).src = fallback Logger.warn('FALLBACK IMAGE LOAD FAIL')
}
;(event.target as ImageTarget).src = fallback
} }
// Webp Load Fail // Webp Load Fail
(e.target as ImageTarget).src = props.fallbackSrc ;(e.target as ImageTarget).src = props.fallbackSrc
if (!supportsWebP()) localStorage.setItem('webp', 'false') if (!supportsWebP()) localStorage.setItem('webp', 'false')
} } else {
else { ;(e.target as ImageTarget).src = fallback
(e.target as ImageTarget).src = fallback
} }
}} }}
/> />
)
} }
interface ImageProps { interface ImageProps {

View File

@ -3,9 +3,9 @@ const Loader: React.FC<LoaderProps> = ({ text, visible = true }) => {
<div <div
className={`${ className={`${
visible ? '' : 'hidden ' visible ? '' : 'hidden '
} w-full h-full fixed block top-0 left-0 bg-gray-500 bg-opacity-75 z-50 dark:text-black`} } fixed left-0 top-0 z-50 block h-full w-full bg-gray-500 bg-opacity-75 dark:text-black`}
> >
<h1 className='relative top-1/2 block mx-auto my-0 text-center text-2xl font-semibold opacity-100'> <h1 className='relative top-1/2 mx-auto my-0 block text-center text-2xl font-semibold opacity-100'>
{text} {text}
</h1> </h1>
</div> </div>

View File

@ -9,9 +9,7 @@ const Login: React.FC<React.PropsWithChildren> = ({ children }) => {
localStorage.redirectTo = window.location.href localStorage.redirectTo = window.location.href
redirectTo(router, 'login') redirectTo(router, 'login')
}) })
return <> return <>{children}</>
{children}
</>
} }
export default Login export default Login

View File

@ -1,34 +1,60 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/no-static-element-interactions */
import Link from 'next/link' import Link from 'next/link'
const LongButton: React.FC<LongButtonProps> = ({ children, newTab=false, href, onClick, center=false }) => { const LongButton: React.FC<LongButtonProps> = ({
children,
newTab = false,
href,
onClick,
center = false,
}) => {
if (href) { if (href) {
if(newTab) return <a href={href} rel='noopener noreferrer' if (newTab)
target='_blank'> return (
<div className={`${center ? 'justify-center ': '' }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`}> <a href={href} rel='noopener noreferrer' target='_blank'>
<div
className={`${
center ? 'justify-center ' : ''
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
>
{children} {children}
</div> </div>
</a> </a>
else return ( )
else
return (
<Link <Link
href={href} href={href}
className={`${center ? 'justify-center ': '' }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`}> className={`${
center ? 'justify-center ' : ''
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
>
{children} {children}
</Link> </Link>
) )
} }
if(onClick) return <div onKeyPress={onClick} onClick={onClick} className={`${center ? 'justify-center ': '' }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`}> if (onClick)
return (
<div
onKeyPress={onClick}
onClick={onClick}
className={`${
center ? 'justify-center ' : ''
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
>
{children} {children}
</div> </div>
)
return <a className={`${center ? 'justify-center ': '' }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`}> return (
<a
className={`${
center ? 'justify-center ' : ''
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
>
{children} {children}
</a> </a>
)
} }
export default LongButton export default LongButton

View File

@ -5,7 +5,12 @@ import * as Emoji from 'node-emoji'
import { anchorHeader, customEmoji, twemoji } from '@utils/Tools' import { anchorHeader, customEmoji, twemoji } from '@utils/Tools'
const Markdown: React.FC<MarkdownProps> = ({ text, options={}, allowedTag=[], components={} }) => { const Markdown: React.FC<MarkdownProps> = ({
text,
options = {},
allowedTag = [],
components = {},
}) => {
return ( return (
<div className='markdown-body w-full'> <div className='markdown-body w-full'>
<MarkdownView <MarkdownView
@ -23,10 +28,10 @@ const Markdown: React.FC<MarkdownProps> = ({ text, options={}, allowedTag=[], co
tasklists: true, tasklists: true,
ghCompatibleHeaderId: true, ghCompatibleHeaderId: true,
encodeEmails: true, encodeEmails: true,
...options ...options,
}} }}
components={components} components={components}
sanitizeHtml={html => sanitizeHtml={(html) =>
sanitizeHtml(html, { sanitizeHtml(html, {
allowedTags: [ allowedTags: [
'addr', 'addr',
@ -98,16 +103,16 @@ const Markdown: React.FC<MarkdownProps> = ({ text, options={}, allowedTag=[], co
'svg', 'svg',
'path', 'path',
'input', 'input',
...allowedTag ...allowedTag,
], ],
allowedAttributes: false, allowedAttributes: false,
allowedClasses: { allowedClasses: {
'*': ['align-middle'], '*': ['align-middle'],
a: ['anchor', 'mr-1'], a: ['anchor', 'mr-1'],
svg: ['octicon-link'], svg: ['octicon-link'],
img: ['emoji', 'special'] img: ['emoji', 'special'],
}, },
allowedStyles: {} allowedStyles: {},
}) })
} }
/> />

View File

@ -4,7 +4,7 @@ import Markdown from './Markdown'
const Message: React.FC<MessageProps> = ({ type, children }) => { const Message: React.FC<MessageProps> = ({ type, children }) => {
return ( return (
<div <div
className={`${MessageColor[type]} px-6 py-4 rounded-md text-base mx-auto w-full text-left`} className={`${MessageColor[type]} mx-auto w-full rounded-md px-6 py-4 text-left text-base`}
> >
{typeof children === 'string' ? <Markdown text={children} /> : children} {typeof children === 'string' ? <Markdown text={children} /> : children}
</div> </div>

View File

@ -2,7 +2,15 @@ import { ReactNode } from 'react'
import { Modal as ReactModal } from 'react-responsive-modal' import { Modal as ReactModal } from 'react-responsive-modal'
import 'react-responsive-modal/styles.css' import 'react-responsive-modal/styles.css'
const Modal: React.FC<ModalProps> = ({ children, isOpen, onClose, closeIcon=false, dark, header, full=false }) => { const Modal: React.FC<ModalProps> = ({
children,
isOpen,
onClose,
closeIcon = false,
dark,
header,
full = false,
}) => {
return ( return (
<ReactModal <ReactModal
open={isOpen} open={isOpen}
@ -12,13 +20,13 @@ const Modal: React.FC<ModalProps> = ({ children, isOpen, onClose, closeIcon=fals
showCloseIcon={closeIcon} showCloseIcon={closeIcon}
styles={{ styles={{
closeButton: { closeButton: {
color: dark ? 'white' : 'black' color: dark ? 'white' : 'black',
}, },
modal: { modal: {
borderRadius: '10px', borderRadius: '10px',
background: dark ? '#2C2F33' : '#fbfbfb', background: dark ? '#2C2F33' : '#fbfbfb',
color: dark ? 'white' : 'black', color: dark ? 'white' : 'black',
width: full ? '90%' : 'inherit' width: full ? '90%' : 'inherit',
}, },
}} }}
> >

View File

@ -4,25 +4,38 @@ const Button = dynamic(() => import('@components/Button'))
const Container = dynamic(() => import('@components/Container')) const Container = dynamic(() => import('@components/Container'))
const NSFW: React.FC<NSFWProps> = ({ onClick, onDisableClick }) => { const NSFW: React.FC<NSFWProps> = ({ onClick, onDisableClick }) => {
return <Container> return (
<div className='flex items-center h-screen select-none'> <Container>
<div className='flex h-screen select-none items-center'>
<div className='px-10'> <div className='px-10'>
<h1 className='text-2xl font-bold flex'> <h1 className='flex text-2xl font-bold'>
<img draggable='false' alt='⚠' src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/26a0.svg' className='emoji mr-2 w-8' /> <img
19 .</h1> draggable='false'
<p className='text-lg mb-3'>?</p> alt='⚠'
src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/26a0.svg'
className='emoji mr-2 w-8'
/>
19 .
</h1>
<p className='mb-3 text-lg'>?</p>
<Button onClick={onClick}> <Button onClick={onClick}>
<i className='fas fa-arrow-right' /> <i className='fas fa-arrow-right' />
</Button> </Button>
<div className='mt-1'> <div className='mt-1'>
<button className='text-blue-500 hover:text-blue-600' onClick={() => { <button
className='text-blue-500 hover:text-blue-600'
onClick={() => {
onClick() onClick()
onDisableClick() onDisableClick()
}}> .</button> }}
>
.
</button>
</div> </div>
</div> </div>
</div> </div>
</Container> </Container>
)
} }
interface NSFWProps { interface NSFWProps {

View File

@ -20,7 +20,11 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
const [mobileAddDropdownOpen, setMobileAddDropdownOpen] = useState<boolean>(false) const [mobileAddDropdownOpen, setMobileAddDropdownOpen] = useState<boolean>(false)
const router = useRouter() const router = useRouter()
const logged = userCache?.id && userCache.version === 2 const logged = userCache?.id && userCache.version === 2
const type: Nullable<'bot'|'server'> = router.pathname.startsWith('/bots') ? 'bot' : router.pathname.startsWith('/servers') ? 'server' : null const type: Nullable<'bot' | 'server'> = router.pathname.startsWith('/bots')
? 'bot'
: router.pathname.startsWith('/servers')
? 'server'
: null
const dev = router.pathname.startsWith('/developers') const dev = router.pathname.startsWith('/developers')
useEffect(() => { useEffect(() => {
@ -28,107 +32,127 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
if (localStorage.userCache) { if (localStorage.userCache) {
setUserCache(token ? JSON.parse(localStorage.userCache) : null) setUserCache(token ? JSON.parse(localStorage.userCache) : null)
} }
Fetch<User>('/users/@me').then(data => { Fetch<User>('/users/@me').then((data) => {
if (data.code !== 200) return if (data.code !== 200) return
setUserCache(JSON.parse(localStorage.userCache = JSON.stringify({ setUserCache(
JSON.parse(
(localStorage.userCache = JSON.stringify({
id: data.data.id, id: data.data.id,
username: data.data.globalName, username: data.data.globalName,
tag: data.data.tag, tag: data.data.tag,
version: 2 version: 2,
}))) }))
)
)
}) })
} catch { } catch {
setUserCache(null) setUserCache(null)
} }
}, [token]) }, [token])
return <> return (
<nav className='fixed z-40 top-0 flex flex-wrap items-center justify-between px-2 py-3 w-full text-gray-100 dark:bg-discord-black bg-discord-blurple lg:absolute'> <>
<div className='container flex flex-wrap items-center justify-between mx-auto px-4'> <nav className='fixed top-0 z-40 flex w-full flex-wrap items-center justify-between bg-discord-blurple px-2 py-3 text-gray-100 dark:bg-discord-black lg:absolute'>
<div className='relative flex justify-between w-full lg:justify-start lg:w-auto'> <div className='container mx-auto flex flex-wrap items-center justify-between px-4'>
<div className='relative flex w-full justify-between lg:w-auto lg:justify-start'>
<Link <Link
href={dev ? '/developers' : '/'} href={dev ? '/developers' : '/'}
className={`${dev ? 'dark:text-koreanbots-blue ' : ''}logofont text-large whitespace-no-wrap inline-block mr-4 py-2 hover:text-gray-300 font-semibold leading-relaxed uppercase sm:text-2xl`}> className={`${
dev ? 'dark:text-koreanbots-blue ' : ''
{ dev ? <><i className='fas fa-tools mr-1'/> DEVELOPERS</> : 'KOREANLIST'} }logofont text-large whitespace-no-wrap mr-4 inline-block py-2 font-semibold uppercase leading-relaxed hover:text-gray-300 sm:text-2xl`}
>
{dev ? (
<>
<i className='fas fa-tools mr-1' /> DEVELOPERS
</>
) : (
'KOREANLIST'
)}
</Link> </Link>
<button <button
className='block px-3 py-1 dark:text-gray-200 text-xl leading-none bg-transparent border border-solid border-transparent rounded outline-none focus:outline-none cursor-pointer lg:hidden' className='block cursor-pointer rounded border border-solid border-transparent bg-transparent px-3 py-1 text-xl leading-none outline-none focus:outline-none dark:text-gray-200 lg:hidden'
type='button' type='button'
onClick={() => setNavbarOpen(!navbarOpen)} onClick={() => setNavbarOpen(!navbarOpen)}
> >
<i className={`fas ${!navbarOpen ? 'fa-bars' : 'fa-times'}`}></i> <i className={`fas ${!navbarOpen ? 'fa-bars' : 'fa-times'}`}></i>
</button> </button>
<ul className='hidden lg:flex flex-col list-none lg:flex-row lg:ml-auto'> <ul className='hidden list-none flex-col lg:ml-auto lg:flex lg:flex-row'>
<li className='flex items-center'> <li className='flex items-center'>
<Link <Link
href={dev ? '/' : '/developers'} href={dev ? '/' : '/developers'}
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'> className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
>
{dev ? '홈' : '개발자'} {dev ? '홈' : '개발자'}
</Link> </Link>
</li> </li>
{ {type !== 'bot' && (
type !== 'bot' && <li className='flex items-center'> <li className='flex items-center'>
<Link <Link
href='/bots' href='/bots'
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'> className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
>
</Link> </Link>
</li> </li>
} )}
{ {type !== 'server' && (
type !== 'server' && <li className='flex items-center'> <li className='flex items-center'>
<Link <Link
href='/servers' href='/servers'
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'> className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
>
</Link> </Link>
</li> </li>
} )}
<li className='flex items-center'> <li className='flex items-center'>
<Link <Link
href='/discord' href='/discord'
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'> className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
>
</Link> </Link>
</li> </li>
<li className='flex items-center'> <li className='flex items-center'>
<Link <Link
href='/about' href='/about'
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'> className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
>
</Link> </Link>
</li> </li>
<li className='flex items-center' onFocus={() => setAddDropdownOpen(true)} onMouseOver={() => setAddDropdownOpen(true)} onMouseOut={() => setAddDropdownOpen(false)} onBlur={() => setAddDropdownOpen(false)}> <li
<span className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100 cursor-pointer'> className='flex items-center'
onFocus={() => setAddDropdownOpen(true)}
onMouseOver={() => setAddDropdownOpen(true)}
onMouseOut={() => setAddDropdownOpen(false)}
onBlur={() => setAddDropdownOpen(false)}
>
<span className='flex w-full cursor-pointer items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'>
</span> </span>
<div className={`rounded shadow-md absolute mt-11 top-0 w-40 bg-white text-black dark:bg-very-black dark:text-gray-300 text-sm ${addDropdownOpen ? 'block' : 'hidden'}`}> <div
className={`absolute top-0 mt-11 w-40 rounded bg-white text-sm text-black shadow-md dark:bg-very-black dark:text-gray-300 ${
addDropdownOpen ? 'block' : 'hidden'
}`}
>
<ul className='relative'> <ul className='relative'>
<li> <li>
<Link <Link
href='/addbot' href='/addbot'
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-t'> className='block rounded-t px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
>
<i className='fas fa-robot' /> <i className='fas fa-robot' />
</Link> </Link>
</li> </li>
<li> <li>
<Link <Link
href='/addserver' href='/addserver'
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-b'> className='block rounded-b px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
<i className='fas fa-users' /> >
<i className='fas fa-users' />
</Link> </Link>
</li> </li>
</ul> </ul>
@ -137,61 +161,87 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
</ul> </ul>
</div> </div>
<div className='hidden grow items-center bg-white lg:flex lg:bg-transparent lg:shadow-none'> <div className='hidden grow items-center bg-white lg:flex lg:bg-transparent lg:shadow-none'>
<ul className='flex flex-col list-none lg:flex-row lg:ml-auto'> <ul className='flex list-none flex-col lg:ml-auto lg:flex-row'>
<li className='flex items-center outline-none' onFocus={() => setDropdownOpen(true)} onMouseOver={() => setDropdownOpen(true)} onMouseOut={() => setDropdownOpen(false)} onBlur={() => setDropdownOpen(false)}> <li
{ className='flex items-center outline-none'
logged ? onFocus={() => setDropdownOpen(true)}
onMouseOver={() => setDropdownOpen(true)}
onMouseOut={() => setDropdownOpen(false)}
onBlur={() => setDropdownOpen(false)}
>
{logged ? (
<> <>
<a <a className='flex w-full cursor-pointer items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'>
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100 cursor-pointer'> <DiscordAvatar
<DiscordAvatar userID={userCache.id} className='w-8 h-8 rounded-full mr-1.5' size={128}/> userID={userCache.id}
{userCache.username} <i className='ml-2 fas fa-sort-down' /> className='mr-1.5 h-8 w-8 rounded-full'
size={128}
/>
{userCache.username} <i className='fas fa-sort-down ml-2' />
</a> </a>
<div className={`rounded shadow-md absolute mt-14 top-0 w-48 bg-white text-black dark:bg-very-black dark:text-gray-300 text-sm ${dropdownOpen ? 'block' : 'hidden'}`}> <div
className={`absolute top-0 mt-14 w-48 rounded bg-white text-sm text-black shadow-md dark:bg-very-black dark:text-gray-300 ${
dropdownOpen ? 'block' : 'hidden'
}`}
>
<ul className='relative'> <ul className='relative'>
<li> <li>
<Link <Link
href={`/users/${userCache.id}`} href={`/users/${userCache.id}`}
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-t'> className='block rounded-t px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
<i className='fas fa-user' /> >
<i className='fas fa-user' />
</Link> </Link>
</li> </li>
<li> <li>
<Link <Link
href='/panel' href='/panel'
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover'> className='block px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
<i className='fas fa-cogs' /> >
<i className='fas fa-cogs' />
</Link> </Link>
</li> </li>
{/* <li><hr className='border-t mx-2'/></li> */} {/* <li><hr className='border-t mx-2'/></li> */}
<li> <li>
<a onKeyPress={() => { <a
onKeyPress={() => {
localStorage.removeItem('userCache') localStorage.removeItem('userCache')
redirectTo(router, 'logout') redirectTo(router, 'logout')
} }}
} onClick={() => { onClick={() => {
localStorage.removeItem('userCache') localStorage.removeItem('userCache')
redirectTo(router, 'logout') redirectTo(router, 'logout')
}} className='px-4 py-2 block text-red-500 hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-b cursor-pointer'><i className='fas fa-sign-out-alt' /> </a> }}
className='block cursor-pointer rounded-b px-4 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
>
<i className='fas fa-sign-out-alt' />
</a>
</li> </li>
</ul> </ul>
</div> </div>
</> : </>
<a tabIndex={0} onClick={()=> { ) : (
<a
tabIndex={0}
onClick={() => {
localStorage.redirectTo = window.location.href localStorage.redirectTo = window.location.href
setNavbarOpen(false) setNavbarOpen(false)
redirectTo(router, 'login') redirectTo(router, 'login')
}} className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100 cursor-pointer outline-none'> }}
className='flex w-full cursor-pointer items-center px-3 py-4 text-sm font-semibold text-gray-700 outline-none hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
>
</a> </a>
} )}
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</nav> </nav>
<div <div
className={`z-30 w-full h-full fixed bg-discord-blurple dark:bg-discord-black mt-8 sm:mt-0 lg:hidden overflow-y-scroll lg:scroll-none ${ className={`lg:scroll-none fixed z-30 mt-8 h-full w-full overflow-y-scroll bg-discord-blurple dark:bg-discord-black sm:mt-0 lg:hidden ${
navbarOpen ? 'block' : 'hidden' navbarOpen ? 'block' : 'hidden'
}`} }`}
> >
@ -199,57 +249,48 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
<Link <Link
href={dev ? '/' : '/developers'} href={dev ? '/' : '/developers'}
onClick={() => setNavbarOpen(false)} onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'> className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
{ {dev ? <i className='fas fa-home' /> : <i className='fas fa-tools' />}
dev ? <i className='fas fa-home' /> : <i className='fas fa-tools' /> <span className='px-2 font-medium'>{dev ? '홈' : '개발자'}</span>
}
<span className='px-2 font-medium'>
{dev ? '홈' : '개발자'}
</span>
</Link> </Link>
{ {type !== 'bot' && (
type !== 'bot' && <Link <Link
href='/bots' href='/bots'
onClick={() => setNavbarOpen(false)} onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'> className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='fas fa-robot' /> <i className='fas fa-robot' />
<span className='px-2 font-medium'> </span> <span className='px-2 font-medium'> </span>
</Link> </Link>
} )}
{ {type !== 'server' && (
type !== 'server' && <Link <Link
href='/servers' href='/servers'
onClick={() => setNavbarOpen(false)} onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'> className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='fas fa-users' /> <i className='fas fa-users' />
<span className='px-2 font-medium'> </span> <span className='px-2 font-medium'> </span>
</Link> </Link>
} )}
<Link <Link
href='/discord' href='/discord'
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
onClick={() => setNavbarOpen(false)} onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'> className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='fab fa-discord' /> <i className='fab fa-discord' />
<span className='px-2 font-medium'> </span> <span className='px-2 font-medium'> </span>
</Link> </Link>
<Link <Link
href='/about' href='/about'
onClick={() => setNavbarOpen(false)} onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'> className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='fas fa-layer-group' /> <i className='fas fa-layer-group' />
<span className='px-2 font-medium'></span> <span className='px-2 font-medium'></span>
</Link> </Link>
<a <a
onClick={() => { onClick={() => {
@ -260,69 +301,74 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
<i className='fas fa-plus' /> <i className='fas fa-plus' />
<span className='px-2 font-medium'></span> <span className='px-2 font-medium'></span>
</a> </a>
<div className={mobileAddDropdownOpen ? 'px-4 flex flex-col' : 'px-4 hidden'}> <div className={mobileAddDropdownOpen ? 'flex flex-col px-4' : 'hidden px-4'}>
<Link <Link
href='/addbot' href='/addbot'
onClick={() => setNavbarOpen(false)} onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'> className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='fas fa-robot' /> <i className='fas fa-robot' />
<span className='px-2 font-medium'> </span> <span className='px-2 font-medium'> </span>
</Link> </Link>
<Link <Link
href='/addserver' href='/addserver'
onClick={() => setNavbarOpen(false)} onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'> className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='fas fa-users' /> <i className='fas fa-users' />
<span className='px-2 font-medium'> </span> <span className='px-2 font-medium'> </span>
</Link> </Link>
</div> </div>
</nav> </nav>
<div className='my-10'> <div className='my-10'>
{ {logged ? (
logged ? <> <>
<Link <Link
href={`/users/${userCache.id}`} href={`/users/${userCache.id}`}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300' className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
onClick={() => setNavbarOpen(!navbarOpen)}> onClick={() => setNavbarOpen(!navbarOpen)}
>
<i className='far fa-user' /> <i className='far fa-user' />
<span className='px-2 font-medium'>{userCache.username}</span> <span className='px-2 font-medium'>{userCache.username}</span>
</Link> </Link>
<Link <Link
href='/panel' href='/panel'
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300' className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
onClick={() => setNavbarOpen(!navbarOpen)}> onClick={() => setNavbarOpen(!navbarOpen)}
>
<i className='fas fa-cogs' /> <i className='fas fa-cogs' />
<span className='px-2 font-medium'></span> <span className='px-2 font-medium'></span>
</Link> </Link>
<a onClick={()=> { <a
onClick={() => {
setNavbarOpen(!navbarOpen) setNavbarOpen(!navbarOpen)
localStorage.removeItem('userCache') localStorage.removeItem('userCache')
redirectTo(router, 'logout') redirectTo(router, 'logout')
}} className='flex items-center px-8 py-2 text-red-500 hover:text-red-400'> }}
className='flex items-center px-8 py-2 text-red-500 hover:text-red-400'
>
<i className='fas fa-sign-out-alt' /> <i className='fas fa-sign-out-alt' />
<span className='px-2 font-medium'></span> <span className='px-2 font-medium'></span>
</a> </a>
</> : <a onClick={() => { </>
) : (
<a
onClick={() => {
localStorage.redirectTo = window.location.href localStorage.redirectTo = window.location.href
setNavbarOpen(false) setNavbarOpen(false)
redirectTo(router, 'login') redirectTo(router, 'login')
}} className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'> }}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='far fa-user' /> <i className='far fa-user' />
<span className='px-2 font-medium'></span> <span className='px-2 font-medium'></span>
</a> </a>
} )}
</div> </div>
</div> </div>
</> </>
)
} }
interface NavbarProps { interface NavbarProps {

View File

@ -1,6 +1,6 @@
const Notice: React.FC<NoticeProps> = ({ header, desc }) => { const Notice: React.FC<NoticeProps> = ({ header, desc }) => {
return ( return (
<div className='mx-auto my-auto px-10 py-48 h-screen text-center'> <div className='mx-auto my-auto h-screen px-10 py-48 text-center'>
<h1 className='text-4xl font-bold'>KOREANBOTS</h1> <h1 className='text-4xl font-bold'>KOREANBOTS</h1>
<br /> <br />
<div> <div>

View File

@ -3,19 +3,21 @@ import DiscordAvatar from '@components/DiscordAvatar'
const Owner: React.FC<OwnerProps> = ({ id, globalName, username, tag, crown = false }) => { const Owner: React.FC<OwnerProps> = ({ id, globalName, username, tag, crown = false }) => {
return ( return (
(<Link <Link
href={`/users/${id}`} href={`/users/${id}`}
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'> className='mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-base text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover'
>
<div className='relative shrink-0 mr-3 mt-1 w-8 h-8 rounded-full shadow-inner overflow-hidden'> <div className='relative mr-3 mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full shadow-inner'>
<DiscordAvatar userID={id} className='z-negative absolute inset-0 w-full h-full' /> <DiscordAvatar userID={id} className='z-negative absolute inset-0 h-full w-full' />
</div> </div>
<div className='flex-1 w-0 leading-snug'> <div className='w-0 flex-1 leading-snug'>
<h4 className='whitespace-nowrap truncate'>{ crown && <i className='fas fa-crown text-amber-300 text-xs' /> }{tag === '0' ? globalName : username}</h4> <h4 className='truncate whitespace-nowrap'>
<span className='text-gray-600 text-sm'>{tag === '0' ? '@' + username : '#' + tag}</span> {crown && <i className='fas fa-crown text-xs text-amber-300' />}
{tag === '0' ? globalName : username}
</h4>
<span className='text-sm text-gray-600'>{tag === '0' ? '@' + username : '#' + tag}</span>
</div> </div>
</Link>
</Link>)
) )
} }

View File

@ -1,5 +1,10 @@
import Link from 'next/link' import Link from 'next/link'
const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname, searchParams }) => { const Paginator: React.FC<PaginatorProps> = ({
currentPage,
totalPage,
pathname,
searchParams,
}) => {
let pages = [] let pages = []
if (currentPage < 4) if (currentPage < 4)
pages = [ pages = [
@ -25,7 +30,7 @@ const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname,
currentPage + 1 > totalPage ? null : currentPage + 1, currentPage + 1 > totalPage ? null : currentPage + 1,
currentPage + 2 > totalPage ? null : currentPage + 2, currentPage + 2 > totalPage ? null : currentPage + 2,
] ]
pages = pages.filter(el => el) pages = pages.filter((el) => el)
return ( return (
<div className='flex flex-col items-center justify-center py-4 text-center'> <div className='flex flex-col items-center justify-center py-4 text-center'>
<div className='flex'> <div className='flex'>
@ -33,16 +38,15 @@ const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname,
href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage - 1 } }} href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage - 1 } }}
className={`${ className={`${
currentPage === 1 ? 'invisible' : '' 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`}> } mr-1 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-gray-200 text-center transition duration-150 ease-in hover:bg-gray-300 dark:bg-discord-black dark:hover:bg-discord-dark-hover`}
>
<i className='fas fa-chevron-left'></i> <i className='fas fa-chevron-left'></i>
</Link> </Link>
{pages.map((el, i) => ( {pages.map((el, i) => (
(<Link <Link
key={i} key={i}
href={{ pathname, hash: 'list', query: { ...searchParams, page: el } }} href={{ pathname, hash: 'list', query: { ...searchParams, page: el } }}
className={`w-12 flex justify-center items-center cursor-pointer leading-5 transition duration-150 ease-in ${ className={`flex w-12 cursor-pointer items-center justify-center leading-5 transition duration-150 ease-in ${
i === 0 && i === pages.length - 1 i === 0 && i === pages.length - 1
? 'rounded-full' ? 'rounded-full'
: i === 0 : i === 0
@ -53,21 +57,19 @@ const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname,
} ${ } ${
currentPage === el currentPage === el
? 'bg-gray-300 dark:bg-discord-dark-hover' ? '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' : 'bg-gray-200 hover:bg-gray-300 dark:bg-discord-black dark:hover:bg-discord-dark-hover'
}`}> }`}
>
{el} {el}
</Link>
</Link>)
))} ))}
<Link <Link
href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage + 1 } }} href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage + 1 } }}
className={`${ className={`${
currentPage === totalPage ? 'invisible' : '' 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`}> } ml-1 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-gray-200 text-center transition duration-150 ease-in hover:bg-gray-300 dark:bg-discord-black dark:hover:bg-discord-dark-hover`}
>
<i className='fas fa-chevron-right'></i> <i className='fas fa-chevron-right'></i>
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -1,6 +1,9 @@
import { ReactNode } from 'react' import { ReactNode } from 'react'
const PlatformDisplay: React.FC<PlatformDisplayProps> = ({ osx, children }:PlatformDisplayProps) => { const PlatformDisplay: React.FC<PlatformDisplayProps> = ({
osx,
children,
}: PlatformDisplayProps) => {
const isOSX = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) const isOSX = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)
return <>{isOSX ? osx ?? children : children}</> return <>{isOSX ? osx ?? children : children}</>
} }

View File

@ -12,14 +12,16 @@ const Redirect: React.FC<RedirectProps> = ({ to, text=true, children }) => {
useEffect(() => { useEffect(() => {
redirectTo(router, to) redirectTo(router, to)
}) })
if(children) return <> if (children) return <>{children}</>
{children} return (
</> <Container paddingTop>
return <Container paddingTop>
<div> <div>
<a href={to} className='text-blue-400'>{text && '자동으로 리다이렉트되지 않는다면 클릭하세요.'}</a> <a href={to} className='text-blue-400'>
{text && '자동으로 리다이렉트되지 않는다면 클릭하세요.'}
</a>
</div> </div>
</Container> </Container>
)
} }
interface RedirectProps { interface RedirectProps {

View File

@ -5,64 +5,113 @@ import { FormikErrors, FormikTouched } from 'formik'
const Button = dynamic(() => import('@components/Button')) const Button = dynamic(() => import('@components/Button'))
const TextArea = dynamic(() => import('@components/Form/TextArea')) const TextArea = dynamic(() => import('@components/Form/TextArea'))
export const Check: FC<{ checked: boolean, text: string }> = ({ checked, text }) => <> export const Check: FC<{ checked: boolean; text: string }> = ({ checked, text }) => (
{checked && <i className='text-emerald-400 fas fa-check-circle mr-1' />} <>
{checked && <i className='fas fa-check-circle mr-1 text-emerald-400' />}
{text} {text}
</> </>
)
export const SubmitButton: FC = () => <div className='text-right'> export const SubmitButton: FC = () => (
<div className='text-right'>
<Button type='submit'></Button> <Button type='submit'></Button>
</div> </div>
)
export const TextField: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => <> export const TextField: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => (
<TextArea name='description' placeholder='최대한 자세하게 설명해주세요!' value={values.description} setValue={(value) => setFieldValue('description', value)} /> <>
<div className='mt-1 text-red-500 text-xs font-light'>{errors.description && touched.description ? errors.description : null}</div> <TextArea
name='description'
placeholder='최대한 자세하게 설명해주세요!'
value={values.description}
setValue={(value) => setFieldValue('description', value)}
/>
<div className='mt-1 text-xs font-light text-red-500'>
{errors.description && touched.description ? errors.description : null}
</div>
<SubmitButton /> <SubmitButton />
</> </>
)
export const DMCA: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => { export const DMCA: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => {
const [isOwner, setOwner] = useState(null) const [isOwner, setOwner] = useState(null)
const [contacted, setContacted] = useState(null) const [contacted, setContacted] = useState(null)
return <div> return (
<h3 className='font-bold my-2'> ?</h3> <div>
<h3 className='my-2 font-bold'> ?</h3>
<Button onClick={() => setOwner(true)}> <Button onClick={() => setOwner(true)}>
<Check checked={isOwner} text='권리자 본인 혹은 대리인입니다.' /> <Check checked={isOwner} text='권리자 본인 혹은 대리인입니다.' />
</Button> </Button>
<Button onClick={() => setOwner(false)}> <Button onClick={() => setOwner(false)}>
<Check checked={isOwner === false} text='권리자가 아닙니다.' /> <Check checked={isOwner === false} text='권리자가 아닙니다.' />
</Button> </Button>
{ {isOwner === true ? (
isOwner === true ? <> <>
<h3 className='font-bold my-2'> ?</h3> <h3 className='my-2 font-bold'>
?
</h3>
<Button onClick={() => setContacted(true)}> <Button onClick={() => setContacted(true)}>
<Check checked={contacted} text='최대한 연락을 시도하였지만 개선되지 않았습니다.' /> <Check checked={contacted} text='최대한 연락을 시도하였지만 개선되지 않았습니다.' />
</Button> </Button>
<Button onClick={() => setContacted(false)}> <Button onClick={() => setContacted(false)}>
<Check checked={contacted === false} text='아니요, 아직 연락하지 않았습니다.' /> <Check checked={contacted === false} text='아니요, 아직 연락하지 않았습니다.' />
</Button> </Button>
{ {contacted ? (
contacted ? <div> <div>
<h3 className='font-bold mt-2'></h3> <h3 className='mt-2 font-bold'></h3>
<p className='text-gray-400 text-sm mb-1'> .</p> <p className='mb-1 text-sm text-gray-400'> .</p>
<ul className='text-gray-400 text-sm mb-1 list-disc list-inside'> <ul className='mb-1 list-inside list-disc text-sm text-gray-400'>
<li> ( )</li> <li>
(
)
</li>
<li> ( , )</li> <li> ( , )</li>
</ul> </ul>
<p className='text-gray-400 text-sm mb-1'> <a className='text-blue-400' target='_blank' rel='noreferrer' href={`mailto:dmca@koreanbots.dev?subject=${encodeURI('[DMCA] 추가 컨텐츠')}&body=${encodeURI('디스코드 태그:')}`}>dmca@koreanbots.dev</a> , .</p> <p className='mb-1 text-sm text-gray-400'>
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} /> {' '}
<a
className='text-blue-400'
target='_blank'
rel='noreferrer'
href={`mailto:dmca@koreanbots.dev?subject=${encodeURI(
'[DMCA] 추가 컨텐츠'
)}&body=${encodeURI('디스코드 태그:')}`}
>
dmca@koreanbots.dev
</a>
, .
</p>
<TextField
values={values}
errors={errors}
touched={touched}
setFieldValue={setFieldValue}
/>
</div> </div>
: contacted === false ? <> ) : contacted === false ? (
<h2 className='font-bold mt-4 text-xl'> .</h2> <>
<p> , .</p> <h2 className='mt-4 text-xl font-bold'> .</h2>
</> : '' <p>
} ,
.
</p>
</> </>
: isOwner === false ? <> ) : (
<h2 className='font-bold mt-4 text-xl'>, .</h2> ''
)}
</>
) : isOwner === false ? (
<>
<h2 className='mt-4 text-xl font-bold'>
, .
</h2>
<p> , !</p> <p> , !</p>
</> : '' </>
} ) : (
''
)}
</div> </div>
)
} }
interface ReportValues { interface ReportValues {

View File

@ -1,7 +1,9 @@
const ResponsiveGrid: React.FC<React.PropsWithChildren> = ({ children }) => { const ResponsiveGrid: React.FC<React.PropsWithChildren> = ({ children }) => {
return <div className='grid gap-x-4 grid-rows-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mt-10 -mb-10'> return (
<div className='-mb-10 mt-10 grid grid-rows-1 gap-x-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
{children} {children}
</div> </div>
)
} }
export default ResponsiveGrid export default ResponsiveGrid

View File

@ -58,19 +58,25 @@ const Search: React.FC = () => {
if (!localStorage.recentSearch) localStorage.recentSearch = '[]' if (!localStorage.recentSearch) localStorage.recentSearch = '[]'
try { try {
const d = JSON.parse(localStorage.recentSearch).reverse() const d = JSON.parse(localStorage.recentSearch).reverse()
if(d.findIndex(n => n.value === query) !== -1) d.splice(d.findIndex(n => n.value === query), 1) if (d.findIndex((n) => n.value === query) !== -1)
d.splice(
d.findIndex((n) => n.value === query),
1
)
d.push({ d.push({
value: query, value: query,
date: Date.now() date: Date.now(),
}) })
d.reverse() d.reverse()
setRecentSearch(d.slice(0, 10)) setRecentSearch(d.slice(0, 10))
localStorage.recentSearch = JSON.stringify(d.slice(0, 10)) localStorage.recentSearch = JSON.stringify(d.slice(0, 10))
} catch { } catch {
setRecentSearch([{ setRecentSearch([
{
value: query, value: query,
date: Date.now() date: Date.now(),
}]) },
])
localStorage.recentSearch = JSON.stringify(recentSearch) localStorage.recentSearch = JSON.stringify(recentSearch)
} finally { } finally {
redirectTo(router, `/search/?q=${encodeURIComponent(query)}`) redirectTo(router, `/search/?q=${encodeURIComponent(query)}`)
@ -79,19 +85,17 @@ const Search: React.FC = () => {
return ( return (
<div onFocus={() => setHidden(false)} ref={ref}> <div onFocus={() => setHidden(false)} ref={ref}>
<div <div className='relative z-10 mt-5 flex w-full rounded-lg bg-white text-black dark:bg-very-black dark:text-gray-100'>
className='relative z-10 flex mt-5 w-full text-black dark:text-gray-100 dark:bg-very-black bg-white rounded-lg'
>
<input <input
type='search' type='search'
maxLength={50} maxLength={50}
className='grow pr-20 px-7 py-3 h-16 text-xl bg-transparent border-0 border-none outline-none shadow' className='h-16 grow border-0 border-none bg-transparent px-7 py-3 pr-20 text-xl shadow outline-none'
placeholder='검색...' placeholder='검색...'
value={query} value={query}
onChange={e => { onChange={(e) => {
SearchResults(e.target.value) SearchResults(e.target.value)
}} }}
onKeyDown={e => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
onSubmit() onSubmit()
} }
@ -101,61 +105,64 @@ const Search: React.FC = () => {
className='cusor-pointer absolute right-0 top-0 mr-5 mt-5 outline-none' className='cusor-pointer absolute right-0 top-0 mr-5 mt-5 outline-none'
onClick={onSubmit} onClick={onSubmit}
> >
<i className='fas fa-search text-gray-600 hover:text-gray-700 text-2xl' /> <i className='fas fa-search text-2xl text-gray-600 hover:text-gray-700' />
</button> </button>
</div> </div>
<div className={`relative ${hidden ? 'hidden' : 'block'} z-50`}> <div className={`relative ${hidden ? 'hidden' : 'block'} z-50`}>
<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'> <div className='pin-t pin-l absolute my-2 h-60 w-full overflow-y-scroll rounded bg-white text-black shadow-md dark:bg-very-black dark:text-gray-100 md:h-80'>
<ul> <ul>
{(data && data.code === 200) ? ( {data && data.code === 200 ? (
<div className='grid lg:grid-cols-2'> <div className='grid lg:grid-cols-2'>
<ul> <ul>
<li className='px-3 py-3.5 font-bold'></li> <li className='px-3 py-3.5 font-bold'></li>
{ {data.data.bots.length === 0 ? (
data.data.bots.length === 0 ? <li className='px-3 py-3.5'> .</li>
<li className='px-3 py-3.5'> .</li> : ) : (
data.data.bots.map(el => ( data.data.bots.map((el) => (
<Link key={el.id} href={makeBotURL(el)} legacyBehavior> <Link key={el.id} href={makeBotURL(el)} legacyBehavior>
<li className='h-15 flex px-3 py-2 cursor-pointer'> <li className='h-15 flex cursor-pointer px-3 py-2'>
<DiscordAvatar className='mt-1 w-12 h-12' size={128} userID={el.id} /> <DiscordAvatar className='mt-1 h-12 w-12' size={128} userID={el.id} />
<div className='ml-2'> <div className='ml-2'>
<h1 className='text-black dark:text-gray-100 text-lg'>{el.name}</h1> <h1 className='text-lg text-black dark:text-gray-100'>{el.name}</h1>
<p className='text-gray-400 text-sm'>{el.intro}</p> <p className='text-sm text-gray-400'>{el.intro}</p>
</div> </div>
</li> </li>
</Link> </Link>
)) ))
} )}
</ul> </ul>
<ul> <ul>
<li className='px-3 py-3.5 font-bold'></li> <li className='px-3 py-3.5 font-bold'></li>
{ {data.data.servers.length === 0 ? (
data.data.servers.length === 0 ? <li className='px-3 py-3.5'> .</li>
<li className='px-3 py-3.5'> .</li> : ) : (
data.data.servers.map(el => ( data.data.servers.map((el) => (
<Link key={el.id} href={makeServerURL(el)} legacyBehavior> <Link key={el.id} href={makeServerURL(el)} legacyBehavior>
<li className='h-15 flex px-3 py-2 cursor-pointer'> <li className='h-15 flex cursor-pointer px-3 py-2'>
<ServerIcon className='mt-1 w-12 h-12' size={128} id={el.id} /> <ServerIcon className='mt-1 h-12 w-12' size={128} id={el.id} />
<div className='ml-2'> <div className='ml-2'>
<h1 className='text-black dark:text-gray-100 text-lg'>{el.name}</h1> <h1 className='text-lg text-black dark:text-gray-100'>{el.name}</h1>
<p className='text-gray-400 text-sm'>{el.intro}</p> <p className='text-sm text-gray-400'>{el.intro}</p>
</div> </div>
</li> </li>
</Link> </Link>
)) ))
} )}
</ul> </ul>
</div> </div>
) : loading ? <ul> ) : loading ? (
<ul>
<li className='px-3 py-3.5'>...</li> <li className='px-3 py-3.5'>...</li>
</ul> : <ul> </ul>
) : (
<ul>
{query && data ? ( {query && data ? (
data.message?.includes('문법') ? ( data.message?.includes('문법') ? (
<li className='px-3 py-3.5'> <li className='px-3 py-3.5'>
. .
<br /> <br />
<a <a
className='hover:text-blue-400 text-blue-500' className='text-blue-500 hover:text-blue-400'
href='https://docs.koreanbots.dev/bots/usage/search' href='https://docs.koreanbots.dev/bots/usage/search'
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
@ -163,41 +170,51 @@ const Search: React.FC = () => {
</a> </a>
</li> </li>
) : <li className='px-3 py-3.5'>{(data.errors && data.errors[0]) || data.message || '검색중입니다...'}</li> ) : (
) : query.length === 0 ? !recentSearch || !Array.isArray(recentSearch) || recentSearch.length === 0? <li className='px-3 py-3.5'> .</li> <li className='px-3 py-3.5'>
: <> {(data.errors && data.errors[0]) || data.message || '검색중입니다...'}
<li className='h-15 px-3 py-2 cursor-pointer font-semibold'> </li>
)
) : query.length === 0 ? (
!recentSearch || !Array.isArray(recentSearch) || recentSearch.length === 0 ? (
<li className='px-3 py-3.5'> .</li>
) : (
<>
<li className='h-15 cursor-pointer px-3 py-2 font-semibold'>
<button className='absolute right-0 pr-10 text-sm text-red-500 hover:opacity-90' onClick={() => { <button
className='absolute right-0 pr-10 text-sm text-red-500 hover:opacity-90'
onClick={() => {
setRecentSearch([]) setRecentSearch([])
localStorage.recentSearch = '[]' localStorage.recentSearch = '[]'
}}> }}
>
</button> </button>
</li> </li>
{ {recentSearch.slice(0, 10).map((el, n) => (
recentSearch.slice(0, 10).map((el, n) => (
<Link <Link
key={n} key={n}
href={`/search?q=${encodeURIComponent(el?.value)}`} href={`/search?q=${encodeURIComponent(el?.value)}`}
legacyBehavior> legacyBehavior
<li className='h-15 px-3 py-2 cursor-pointer'> >
<li className='h-15 cursor-pointer px-3 py-2'>
<i className='fas fa-history' /> {el?.value} <i className='fas fa-history' /> {el?.value}
<span className='absolute right-0 pr-10 text-gray-400 text-sm'> <span className='absolute right-0 pr-10 text-sm text-gray-400'>
{Day(el?.date).format('MM.DD.')} {Day(el?.date).format('MM.DD.')}
</span> </span>
</li> </li>
</Link> </Link>
)) ))}
} </>
</> : )
query.length < 3 ? ( ) : query.length < 3 ? (
'최소 2글자 이상 입력해주세요.' '최소 2글자 이상 입력해주세요.'
) : ( ) : (
'검색어를 입력해주세요.' '검색어를 입력해주세요.'
)} )}
</ul> </ul>
} )}
</ul> </ul>
</div> </div>
</div> </div>
@ -208,6 +225,6 @@ const Search: React.FC = () => {
export default Search export default Search
interface ListAll { interface ListAll {
bots: Bot[], bots: Bot[]
servers: Server[] servers: Server[]
} }

View File

@ -3,7 +3,7 @@ import { ReactNode } from 'react'
const Segment: React.FC<SegmentProps> = ({ children, className = '' }) => { const Segment: React.FC<SegmentProps> = ({ children, className = '' }) => {
return ( return (
<div <div
className={`py-3 px-7 text-black dark:text-white dark:bg-discord-black bg-little-white rounded ${className}`} className={`rounded bg-little-white px-7 py-3 text-black dark:bg-discord-black dark:text-white ${className}`}
> >
{children} {children}
</div> </div>

View File

@ -10,14 +10,22 @@ const Tag = dynamic(() => import('@components/Tag'))
const ServerIcon = dynamic(() => import('@components/ServerIcon')) const ServerIcon = dynamic(() => import('@components/ServerIcon'))
const ServerCard: React.FC<BotCardProps> = ({ type, server }) => { const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
const newServerLink = server.data ? `/addserver/${server.id}` : `${DiscordEnpoints.InviteApplication(DSKR_BOT_ID, {}, 'bot', null, server.id)}&disable_guild_select=true` const newServerLink = server.data
? `/addserver/${server.id}`
: `${DiscordEnpoints.InviteApplication(
DSKR_BOT_ID,
{},
'bot',
null,
server.id
)}&disable_guild_select=true`
return ( return (
<div className='min-w-0 container mb-16 transform hover:-translate-y-1 transition duration-100 ease-in cursor-pointer'> <div className='container mb-16 min-w-0 transform cursor-pointer transition duration-100 ease-in hover:-translate-y-1'>
<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 <div
className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl' className='relative mx-auto h-full rounded-2xl bg-little-white text-black shadow-xl dark:bg-discord-black dark:text-white'
style={ style={
checkServerFlag(server.flags, 'trusted') && server.banner checkServerFlag(server.flags, 'trusted') && server.banner
? { ? {
@ -27,26 +35,25 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
: {} : {}
} }
> >
<Link <Link href={type !== 'add' ? makeServerURL(server) : newServerLink} legacyBehavior>
href={type !== 'add' ? makeServerURL(server) : newServerLink}
legacyBehavior>
<div> <div>
<div className='flex flex-col'> <div className='flex flex-col'>
<div className='flex'> <div className='flex'>
<div className='w-3/5 flex justify-start'> <div className='flex w-3/5 justify-start'>
<ServerIcon <ServerIcon
size={128} size={128}
id={server.id} id={server.id}
hash={type === 'add' && server.icon} hash={type === 'add' && server.icon}
alt='Icon' alt='Icon'
className='absolute -left-2 -top-8 mx-auto w-32 h-32 bg-white rounded-full' className='absolute -left-2 -top-8 mx-auto h-32 w-32 rounded-full bg-white'
/> />
</div> </div>
<div className='grid grid-cols-1 pr-5 pt-5 w-2/5'> <div className='grid w-2/5 grid-cols-1 pr-5 pt-5'>
<Tag <Tag
text={ text={
<> <>
<i className='fas fa-heart text-red-600' /> {formatNumber(server.votes)} <i className='fas fa-heart text-red-600' />{' '}
{formatNumber(server.votes)}
</> </>
} }
dark dark
@ -58,26 +65,38 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
/> />
</div> </div>
</div> </div>
<div className='mt-3 px-4 h-16'> <div className='mt-3 h-16 px-4'>
<h2 className={`px-1 text-sm ${server.state !== 'unreachable' ? ' invisible' : ''}`}> <h2
<i className='fas fa-ban text-red-600' /> className={`px-1 text-sm ${
server.state !== 'unreachable' ? ' invisible' : ''
}`}
>
<i className='fas fa-ban text-red-600' />
</h2> </h2>
<h1 className='mb-3 text-left text-xl sm:text-2xl font-bold truncate'>{server.name}</h1> <h1 className='mb-3 truncate text-left text-xl font-bold sm:text-2xl'>
{server.name}
</h1>
</div> </div>
</div> </div>
<p className='mb-10 px-4 h-6 text-left text-gray-400 text-sm font'> <p className='font mb-10 h-6 px-4 text-left text-sm text-gray-400'>
{type === 'add' ? {type === 'add'
server.data ? '지금 바로 서버를 등록할 수 있습니다.' : '봇을 초대해야 서버를 등록할 수 있습니다.' ? server.data
: server.intro ? '지금 바로 서버를 등록할 수 있습니다.'
} : '봇을 초대해야 서버를 등록할 수 있습니다.'
: server.intro}
</p> </p>
<div> <div>
<div className='category flex flex-wrap px-2'> <div className='category flex flex-wrap px-2'>
{server.category?.slice(0, 3).map(el => ( {server.category
?.slice(0, 3)
.map((el) => (
<Tag key={el} text={el} href={`/servers/categories/${el}`} dark /> <Tag key={el} text={el} href={`/servers/categories/${el}`} dark />
))}{' '} ))}{' '}
{server.category?.length > 3 && <Tag text={`+${server.category.length - 3}`} dark />} {server.category?.length > 3 && (
<Tag text={`+${server.category.length - 3}`} dark />
)}
</div> </div>
</div> </div>
</div> </div>
@ -85,59 +104,55 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
<Divider /> <Divider />
<div className='w-full'> <div className='w-full'>
<div className='flex justify-evenly'> <div className='flex justify-evenly'>
{ {type === 'add' ? (
type === 'add' ? server.data ? (
server.data ? <Link <Link
href={newServerLink} href={newServerLink}
className='py-3 w-full text-center text-emerald-500 hover:text-white text-sm font-bold hover:bg-emerald-500 rounded-b-2xl hover:shadow-lg transition duration-100 ease-in'> className='w-full rounded-b-2xl py-3 text-center text-sm font-bold text-emerald-500 transition duration-100 ease-in hover:bg-emerald-500 hover:text-white hover:shadow-lg'
>
</Link> : <Link
href={newServerLink}
className='py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple rounded-b-2xl hover:shadow-lg transition duration-100 ease-in'
rel='noopener noreferrer'
target='_blank'>
</Link> </Link>
: ) : (
<Link
href={newServerLink}
className='w-full rounded-b-2xl py-3 text-center text-sm font-bold text-discord-blurple transition duration-100 ease-in hover:bg-discord-blurple hover:text-white hover:shadow-lg'
rel='noopener noreferrer'
target='_blank'
>
</Link>
)
) : (
<> <>
<Link <Link
href={makeServerURL(server)} href={makeServerURL(server)}
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'> className='w-full rounded-bl-2xl py-3 text-center text-sm font-bold text-koreanbots-blue transition duration-100 ease-in hover:bg-koreanbots-blue hover:text-white hover:shadow-lg'
>
</Link> </Link>
{type === 'manage' ? ( {type === 'manage' ? (
<Link <Link
href={`/servers/${server.id}/edit`} href={`/servers/${server.id}/edit`}
className='py-3 w-full text-center text-emerald-500 hover:text-white text-sm font-bold hover:bg-emerald-500 rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'> className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-emerald-500 transition duration-100 ease-in hover:bg-emerald-500 hover:text-white hover:shadow-lg'
</Link>
) : !['ok', 'unreachable'].includes(server.state) ? <a
className='py-3 w-full text-center text-discord-blurple text-sm font-bold rounded-br-2xl hover:shadow-lg transition duration-100 ease-in opacity-50 cursor-default select-none'
> >
</Link>
) : !['ok', 'unreachable'].includes(server.state) ? (
<a className='w-full cursor-default select-none rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple opacity-50 transition duration-100 ease-in hover:shadow-lg'>
</a> : </a>
) : (
<a <a
href={ href={makeServerURL(server) + '/join'}
makeServerURL(server) + '/join'
}
rel='noopener noreferrer' rel='noopener noreferrer'
target='_blank' 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' className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple transition duration-100 ease-in hover:bg-discord-blurple hover:text-white hover:shadow-lg'
> >
</a> </a>
} )}
</> </>
)}
}
</div> </div>
</div> </div>
</div> </div>
@ -146,27 +161,25 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
</div> </div>
</div> </div>
) )
} }
interface BotCardProps { interface BotCardProps {
type: 'list' | 'manage' | 'add' type: 'list' | 'manage' | 'add'
server: { server: {
id: string, id: string
name: string, name: string
intro?: string intro?: string
desc?: string, desc?: string
flags?: number flags?: number
state?: ServerState state?: ServerState
icon: string | null, icon: string | null
banner?: string | null, banner?: string | null
bg?: string | null, bg?: string | null
vanity?: string | null vanity?: string | null
category?: string[] category?: string[]
votes?: number | null votes?: number | null
members?: number | null, members?: number | null
data?: ServerData data?: ServerData
} }
} }

View File

@ -5,13 +5,22 @@ import { DiscordEnpoints, KoreanbotsEndPoints } from '@utils/Constants'
const Image = dynamic(() => import('@components/Image')) const Image = dynamic(() => import('@components/Image'))
const ServerIcon: React.FC<ServerIconProps> = ({ id, size, className, alt, hash }) => { const ServerIcon: React.FC<ServerIconProps> = ({ id, size, className, alt, hash }) => {
return <Image return (
<Image
className={className} className={className}
alt={alt} alt={alt}
src={hash ? DiscordEnpoints.CDN.guild(id, hash, { format: 'webp', size: size ?? 256 }) : KoreanbotsEndPoints.CDN.icon(id, { format: 'webp', size: size ?? 256})} src={
fallbackSrc={hash ? DiscordEnpoints.CDN.guild(id, hash, { format: 'png', size: size ?? 256 }) : KoreanbotsEndPoints.CDN.icon(id, { format: 'png', size: size ?? 256})} hash
? DiscordEnpoints.CDN.guild(id, hash, { format: 'webp', size: size ?? 256 })
: KoreanbotsEndPoints.CDN.icon(id, { format: 'webp', size: size ?? 256 })
}
fallbackSrc={
hash
? DiscordEnpoints.CDN.guild(id, hash, { format: 'png', size: size ?? 256 })
: KoreanbotsEndPoints.CDN.icon(id, { format: 'png', size: size ?? 256 })
}
/> />
)
} }
interface ServerIconProps { interface ServerIconProps {

View File

@ -7,23 +7,23 @@ import Link from 'next/link'
const SubmittedBotCard: React.FC<SubmittedBotProps> = ({ href, submit }) => { const SubmittedBotCard: React.FC<SubmittedBotProps> = ({ href, submit }) => {
return ( return (
(<Link <Link
href={href} href={href}
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'> className='relative mx-auto h-full w-full transform rounded-2xl bg-little-white px-4 py-5 text-black shadow-xl transition duration-100 ease-in hover:-translate-y-1 dark:bg-discord-black dark:text-white'
>
<div className='h-18'> <div className='h-18'>
<div className='flex'> <div className='flex'>
<div className='grow w-full'> <div className='w-full grow'>
<h2 className='text-lg'>{submit.id}</h2> <h2 className='text-lg'>{submit.id}</h2>
</div> </div>
<div className='absolute right-0 grid grid-cols-1 px-4 w-2/5 h-0'> <div className='absolute right-0 grid h-0 w-2/5 grid-cols-1 px-4'>
<Tag <Tag
text={ text={
<> <>
<i <i
className={`fas fa-circle text-${ className={`fas fa-circle text-${[Status.offline, Status.online, Status.dnd][
[Status.offline, Status.online, Status.dnd][submit.state]?.color submit.state
}`} ]?.color}`}
/>{' '} />{' '}
{['대기중', '승인됨', '거부됨'][submit.state]} {['대기중', '승인됨', '거부됨'][submit.state]}
</> </>
@ -32,13 +32,12 @@ const SubmittedBotCard: React.FC<SubmittedBotProps> = ({ href, submit }) => {
/> />
</div> </div>
</div> </div>
<p className='mt-1.5 w-full h-6 text-left text-gray-400 text-sm font-medium truncate'> <p className='mt-1.5 h-6 w-full truncate text-left text-sm font-medium text-gray-400'>
{submit.intro.slice(0, 25)} {submit.intro.slice(0, 25)}
{submit.intro.length > 25 && '...'} {submit.intro.length > 25 && '...'}
</p> </p>
</div> </div>
</Link>
</Link>)
) )
} }

View File

@ -27,17 +27,17 @@ const Tag: React.FC<LabelProps> = ({
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black' : 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
: 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 hover:bg-little-white-hover dark:bg-discord-black'
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${ } ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
circular circular
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}` ? `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'}` : `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} transition duration-100 ease-in dark:hover:bg-discord-dark-hover`}
> >
{text} {text}
</a> </a>
) : ( ) : (
(<Link <Link
href={href} href={href}
className={`${className ?? ''} text-center text-base ${ className={`${className ?? ''} text-center text-base ${
dark dark
@ -46,18 +46,17 @@ const Tag: React.FC<LabelProps> = ({
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black' : 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
: 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 hover:bg-little-white-hover dark:bg-discord-black'
} ${ } ${
!blurple && !github ? 'text-black dark:text-gray-400' : 'hover:bg-little-white-hover' !blurple && !github ? 'text-black dark:text-gray-400' : 'hover:bg-little-white-hover'
} ${ } ${
circular circular
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}` ? `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'}` : `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} transition duration-100 ease-in dark:hover:bg-discord-dark-hover`}
>
{text} {text}
</Link>
</Link>)
) )
) : ( ) : (
<a <a
@ -70,12 +69,12 @@ const Tag: React.FC<LabelProps> = ({
? '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 ${ : `bg-little-white-hover dark:bg-very-black ${
props.onClick props.onClick
? 'hover:bg-little-white dark:hover:bg-discord-dark-hover transition duration-100 ease-in' ? 'transition duration-100 ease-in hover:bg-little-white dark:hover:bg-discord-dark-hover'
: '' : ''
}` }`
: `bg-little-white dark:bg-discord-black ${ : `bg-little-white dark:bg-discord-black ${
props.onClick props.onClick
? 'hover:bg-little-white-hover dark:hover:bg-discord-dark-hover transition duration-100 ease-in' ? 'transition duration-100 ease-in hover:bg-little-white-hover dark:hover:bg-discord-dark-hover'
: '' : ''
}` }`
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${ } ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${

View File

@ -1,18 +1,18 @@
const Toggle: React.FC<ToggleProps> = ({ checked, onChange }: ToggleProps) => { const Toggle: React.FC<ToggleProps> = ({ checked, onChange }: ToggleProps) => {
return ( return (
<button <button
className='relative inline-block align-middle mr-2 w-10 outline-none select-none' className='relative mr-2 inline-block w-10 select-none align-middle outline-none'
onClick={onChange} onClick={onChange}
onKeyPress={onChange} onKeyPress={onChange}
> >
<input <input
type='checkbox' type='checkbox'
checked={checked} 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' className='absolute block h-6 w-6 cursor-pointer appearance-none rounded-full border-4 border-transparent bg-white outline-none checked:right-0'
readOnly readOnly
/> />
<span <span
className={`block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer ${ className={`block h-6 cursor-pointer overflow-hidden rounded-full bg-gray-300 ${
checked ? 'bg-koreanbots-blue' : '' checked ? 'bg-koreanbots-blue' : ''
}`} }`}
></span> ></span>

View File

@ -8,15 +8,14 @@ const Tooltip: React.FC<TooltipProps> = ({
text, text,
}) => { }) => {
return href ? ( return href ? (
(<Link href={href} className='inline'> <Link href={href} className='inline'>
<div className='relative inline py-3'> <div className='relative inline py-3'>
<div className='group relative inline-block text-center cursor-pointer'> <div className='group relative inline-block cursor-pointer text-center'>
{children} {children}
<div <div
className={`opacity-0 ${ className={`opacity-0 ${
size === 'small' ? 'w-44' : 'w-60' 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`} } pointer-events-none absolute -left-4 bottom-full z-10 rounded-lg bg-black px-3 py-2 text-center text-xs text-white group-hover:opacity-100`}
> >
{text} {text}
{direction === 'left' ? ( {direction === 'left' ? (
@ -31,7 +30,7 @@ const Tooltip: React.FC<TooltipProps> = ({
</svg> </svg>
) : direction === 'center' ? ( ) : direction === 'center' ? (
<svg <svg
className='absolute left-0 top-full w-full h-2 text-black' className='absolute left-0 top-full h-2 w-full text-black'
x='0px' x='0px'
y='0px' y='0px'
viewBox='0 0 255 255' viewBox='0 0 255 255'
@ -53,17 +52,16 @@ const Tooltip: React.FC<TooltipProps> = ({
</div> </div>
</div> </div>
</div> </div>
</Link>
</Link>)
) : ( ) : (
<a className='inline'> <a className='inline'>
<div className='relative inline py-3'> <div className='relative inline py-3'>
<div className='group relative inline-block text-center cursor-pointer'> <div className='group relative inline-block cursor-pointer text-center'>
{children} {children}
<div <div
className={`opacity-0 ${ className={`opacity-0 ${
size === 'small' ? 'w-44' : 'w-60' 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`} } pointer-events-none absolute -left-4 bottom-full z-10 rounded-lg bg-black px-3 py-2 text-center text-xs text-white group-hover:opacity-100`}
> >
{text} {text}
{direction === 'left' ? ( {direction === 'left' ? (
@ -78,7 +76,7 @@ const Tooltip: React.FC<TooltipProps> = ({
</svg> </svg>
) : direction === 'center' ? ( ) : direction === 'center' ? (
<svg <svg
className='absolute left-0 top-full w-full h-2 text-black' className='absolute left-0 top-full h-2 w-full text-black'
x='0px' x='0px'
y='0px' y='0px'
viewBox='0 0 255 255' viewBox='0 0 255 255'

View File

@ -3,20 +3,19 @@ import { ErrorText } from '@utils/Constants'
const NotFound: NextPage<{ message?: string }> = ({ message }) => { const NotFound: NextPage<{ message?: string }> = ({ message }) => {
return ( return (
<div <div className='flex h-screen select-none items-center justify-center text-center'>
className='flex items-center justify-center h-screen select-none text-center'
>
<div> <div>
<div className='flex flex-row justify-center text-9xl'> <div className='flex flex-row justify-center text-9xl'>
4 4
<img alt='robot' src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f916.svg' className='w-24 mx-6 md:mx-12 rounded-full' /> <img
alt='robot'
src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f916.svg'
className='mx-6 w-24 rounded-full md:mx-12'
/>
4 4
</div> </div>
<h2 className='text-2xl font-semibold'> <h2 className='text-2xl font-semibold'>{message || ErrorText[404]}</h2>
{message || ErrorText[404]}
</h2>
</div> </div>
</div> </div>
) )
} }

View File

@ -44,7 +44,11 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
'%c' + 'KOREANBOTS', '%c' + 'KOREANBOTS',
'color: #3366FF; -webkit-text-stroke: 2px black; font-size: 72px; font-weight: bold;' 'color: #3366FF; -webkit-text-stroke: 2px black; font-size: 72px; font-weight: bold;'
) )
Logger.debug(`[BUILD INFO] Tag: ${parseDockerhubTag(process.env.NEXT_PUBLIC_TAG)}, Version: v${Package.version}, Hash: ${process.env.NEXT_PUBLIC_SOURCE_COMMIT}`) Logger.debug(
`[BUILD INFO] Tag: ${parseDockerhubTag(process.env.NEXT_PUBLIC_TAG)}, Version: v${
Package.version
}, Hash: ${process.env.NEXT_PUBLIC_SOURCE_COMMIT}`
)
console.log( console.log(
'%c' + '이곳에 코드를 붙여넣으면 공격자에게 엑세스 토큰을 넘겨줄 수 있습니다!!', '%c' + '이곳에 코드를 붙여넣으면 공격자에게 엑세스 토큰을 넘겨줄 수 있습니다!!',
'color: #ff0000; font-size: 20px; font-weight: bold;' 'color: #ff0000; font-size: 20px; font-weight: bold;'
@ -52,16 +56,29 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
if (!localStorage.theme) { if (!localStorage.theme) {
Logger.debug(`[THEME] ${systemTheme().toUpperCase()} THEME DETECTED`) Logger.debug(`[THEME] ${systemTheme().toUpperCase()} THEME DETECTED`)
setTheme(systemTheme()) setTheme(systemTheme())
} } else setTheme(localStorage.theme)
else setTheme(localStorage.theme)
setStandalone(handlePWA()) setStandalone(handlePWA())
const script = document.querySelector('script[src*=googlesyndication]') const script = document.querySelector('script[src*=googlesyndication]')
if (script) script.addEventListener('error', () => {ReactGA.ga('send', 'event', 'adblock', 'adblock_' + (navigator.userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i) ? 'mobile' : 'pc'))}) if (script)
script.addEventListener('error', () => {
ReactGA.ga(
'send',
'event',
'adblock',
'adblock_' +
(navigator.userAgent.match(
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
)
? 'mobile'
: 'pc')
)
})
}, []) }, [])
return <div className={theme}> return (
<div className={theme}>
<DefaultSeo <DefaultSeo
titleTemplate='%s - 한국 디스코드 리스트' titleTemplate='%s - 한국 디스코드 리스트'
defaultTitle={TITLE} defaultTitle={TITLE}
@ -77,14 +94,14 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
url: '/logo.png', url: '/logo.png',
width: 300, width: 300,
height: 300, height: 300,
alt: 'Logo' alt: 'Logo',
} },
] ],
}} }}
twitter={{ twitter={{
site: '@koreanbots', site: '@koreanbots',
handle: '@koreanbots', handle: '@koreanbots',
cardType: 'summary' cardType: 'summary',
}} }}
/> />
<Head> <Head>
@ -92,7 +109,10 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
<meta charSet='utf-8' /> <meta charSet='utf-8' />
<meta httpEquiv='X-UA-Compatible' content='IE=edge' /> <meta httpEquiv='X-UA-Compatible' content='IE=edge' />
<meta name='keywords' content='Korea, Korean, Discord, Bot, 디스코드봇, 한디리' /> <meta name='keywords' content='Korea, Korean, Discord, Bot, 디스코드봇, 한디리' />
<meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' /> <meta
name='viewport'
content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no'
/>
{/* Android */} {/* Android */}
<meta name='theme-color' content={THEME_COLOR} /> <meta name='theme-color' content={THEME_COLOR} />
@ -125,43 +145,45 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
<meta name='layoutmode' content='fitscreen' /> <meta name='layoutmode' content='fitscreen' />
<meta name='imagemode' content='force' /> <meta name='imagemode' content='force' />
<meta name='screen-orientation' content='portrait' /> <meta name='screen-orientation' content='portrait' />
</Head> </Head>
<Navbar token={cookie.token} /> <Navbar token={cookie.token} />
<div className='iu-is-the-best min-h-screen text-black dark:text-gray-100 dark:bg-discord-dark bg-white'> <div className='iu-is-the-best min-h-screen bg-white text-black dark:bg-discord-dark dark:text-gray-100'>
<Component {...pageProps} err={err} theme={theme} setTheme={setTheme} pwa={standalone} /> <Component {...pageProps} err={err} theme={theme} setTheme={setTheme} pwa={standalone} />
</div> </div>
{ {!router.pathname.startsWith('/developers') && <Footer theme={theme} setTheme={setTheme} />}
!(router.pathname.startsWith('/developers')) && <Footer theme={theme} setTheme={setTheme} /> <Modal
} full
<Modal full isOpen={shortcutModal} onClose={() => setShortcutModal(false)} dark={theme === 'dark'} header='단축키 안내'> isOpen={shortcutModal}
<div className='px-3 h-80'> onClose={() => setShortcutModal(false)}
dark={theme === 'dark'}
header='단축키 안내'
>
<div className='h-80 px-3'>
<h3 className='text-md font-semibold'></h3> <h3 className='text-md font-semibold'></h3>
<ul> <ul>
<li className='pt-2'> <li className='pt-2'>
<h4 className='text-gray-500 dark:text-gray-400 text-xs'> </h4> <h4 className='text-xs text-gray-500 dark:text-gray-400'> </h4>
<kbd> <kbd>
<PlatformDisplay osx='CMD'> <PlatformDisplay osx='CMD'>Ctrl</PlatformDisplay>
Ctrl </kbd>{' '}
</PlatformDisplay> <kbd>/</kbd>
</kbd> <kbd>/</kbd>
</li> </li>
<li className='pt-2'> <li className='pt-2'>
<h4 className='text-gray-500 dark:text-gray-400 text-xs'> </h4> <h4 className='text-xs text-gray-500 dark:text-gray-400'> </h4>
<kbd> <kbd>
<PlatformDisplay osx='CMD'> <PlatformDisplay osx='CMD'>Ctrl</PlatformDisplay>
Ctrl
</PlatformDisplay>
</kbd> </kbd>
<kbd>Shift</kbd> <kbd>D</kbd> <kbd>Shift</kbd> <kbd>D</kbd>
</li> </li>
</ul> </ul>
</div> </div>
</Modal> </Modal>
<GlobalHotKeys keyMap={shortcutKeyMap} handlers={{ <GlobalHotKeys
keyMap={shortcutKeyMap}
handlers={{
SHORTCUT_HELP: (event) => { SHORTCUT_HELP: (event) => {
event.preventDefault() event.preventDefault()
setShortcutModal(value => !value) setShortcutModal((value) => !value)
return return
}, },
CHANGE_THEME: (event) => { CHANGE_THEME: (event) => {
@ -170,9 +192,11 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
setTheme(overwrite) setTheme(overwrite)
localStorage.setItem('theme', overwrite) localStorage.setItem('theme', overwrite)
return false return false
} },
}} /> }}
/>
</div> </div>
)
} }
KoreanbotsApp.getInitialProps = async (appCtx: AppContext) => { KoreanbotsApp.getInitialProps = async (appCtx: AppContext) => {
@ -180,7 +204,7 @@ KoreanbotsApp.getInitialProps = async (appCtx: AppContext) => {
const parsed = parseCookie(appCtx.ctx.req) const parsed = parseCookie(appCtx.ctx.req)
return { return {
...appProps, ...appProps,
cookie: parsed cookie: parsed,
} }
} }

View File

@ -13,7 +13,12 @@ class MyDocument extends Document {
<Head> <Head>
{/* LINK */} {/* LINK */}
<link rel='manifest' href='/manifest.json' /> <link rel='manifest' href='/manifest.json' />
<link rel='search' type='application/opensearchdescription+xml' title={TITLE} href='/opensearch.xml' /> <link
rel='search'
type='application/opensearchdescription+xml'
title={TITLE}
href='/opensearch.xml'
/>
<link <link
rel='stylesheet' rel='stylesheet'
href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.6.0/styles/solarized-dark.min.css' href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.6.0/styles/solarized-dark.min.css'
@ -21,7 +26,12 @@ class MyDocument extends Document {
<link rel='icon' type='image/png' sizes='32x32' href='/favicon-32x32.png' /> <link rel='icon' type='image/png' sizes='32x32' href='/favicon-32x32.png' />
<link rel='icon' type='image/png' sizes='96x96' href='/favicon-96x96.png' /> <link rel='icon' type='image/png' sizes='96x96' href='/favicon-96x96.png' />
<link rel='icon' type='image/png' sizes='16x16' href='/favicon-16x16.png' /> <link rel='icon' type='image/png' sizes='16x16' href='/favicon-16x16.png' />
<link rel='stylesheet' as='style' crossOrigin='anonymous' href='https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.6/dist/web/variable/pretendardvariable-dynamic-subset.css' /> <link
rel='stylesheet'
as='style'
crossOrigin='anonymous'
href='https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.6/dist/web/variable/pretendardvariable-dynamic-subset.css'
/>
{/* iOS */} {/* iOS */}
<link rel='apple-touch-icon' sizes='57x57' href='/static/apple-icon-57x57.png' /> <link rel='apple-touch-icon' sizes='57x57' href='/static/apple-icon-57x57.png' />
@ -35,43 +45,160 @@ class MyDocument extends Document {
<link rel='apple-touch-icon' sizes='180x180' href='/static/apple-icon-180x180.png' /> <link rel='apple-touch-icon' sizes='180x180' href='/static/apple-icon-180x180.png' />
<link rel='apple-touch-icon' sizes='256x256' href='/static/apple-icon-256x256.png' /> <link rel='apple-touch-icon' sizes='256x256' href='/static/apple-icon-256x256.png' />
<link rel='apple-touch-icon' sizes='512x512' href='/static/apple-icon-512x512.png' /> <link rel='apple-touch-icon' sizes='512x512' href='/static/apple-icon-512x512.png' />
<link rel='apple-touch-startup-image' href='/static/apple-splash-2048-2732.jpg' media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' /> <link
<link rel='apple-touch-startup-image' href='/static/apple-splash-2732-2048.jpg' media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' /> rel='apple-touch-startup-image'
<link rel='apple-touch-startup-image' href='/static/apple-splash-1668-2388.jpg' media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' /> href='/static/apple-splash-2048-2732.jpg'
<link rel='apple-touch-startup-image' href='/static/apple-splash-2388-1668.jpg' media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' /> media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
<link rel='apple-touch-startup-image' href='/static/apple-splash-1536-2048.jpg' media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' /> />
<link rel='apple-touch-startup-image' href='/static/apple-splash-2048-1536.jpg' media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' /> <link
<link rel='apple-touch-startup-image' href='/static/apple-splash-1668-2224.jpg' media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' /> rel='apple-touch-startup-image'
<link rel='apple-touch-startup-image' href='/static/apple-splash-2224-1668.jpg' media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' /> href='/static/apple-splash-2732-2048.jpg'
<link rel='apple-touch-startup-image' href='/static/apple-splash-1620-2160.jpg' media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' /> media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
<link rel='apple-touch-startup-image' href='/static/apple-splash-2160-1620.jpg' media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' /> />
<link rel='apple-touch-startup-image' href='/static/apple-splash-1284-2778.jpg' media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' /> <link
<link rel='apple-touch-startup-image' href='/static/apple-splash-2778-1284.jpg' media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' /> rel='apple-touch-startup-image'
<link rel='apple-touch-startup-image' href='/static/apple-splash-1170-2532.jpg' media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' /> href='/static/apple-splash-1668-2388.jpg'
<link rel='apple-touch-startup-image' href='/static/apple-splash-2532-1170.jpg' media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' /> media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
<link rel='apple-touch-startup-image' href='/static/apple-splash-1125-2436.jpg' media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' /> />
<link rel='apple-touch-startup-image' href='/static/apple-splash-2436-1125.jpg' media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' /> <link
<link rel='apple-touch-startup-image' href='/static/apple-splash-1242-2688.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' /> rel='apple-touch-startup-image'
<link rel='apple-touch-startup-image' href='/static/apple-splash-2688-1242.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' /> href='/static/apple-splash-2388-1668.jpg'
<link rel='apple-touch-startup-image' href='/static/apple-splash-828-1792.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' /> media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
<link rel='apple-touch-startup-image' href='/static/apple-splash-1792-828.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' /> />
<link rel='apple-touch-startup-image' href='/static/apple-splash-1242-2208.jpg' media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' /> <link
<link rel='apple-touch-startup-image' href='/static/apple-splash-2208-1242.jpg' media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' /> rel='apple-touch-startup-image'
<link rel='apple-touch-startup-image' href='/static/apple-splash-750-1334.jpg' media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' /> href='/static/apple-splash-1536-2048.jpg'
<link rel='apple-touch-startup-image' href='/static/apple-splash-1334-750.jpg' media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' /> media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
<link rel='apple-touch-startup-image' href='/static/apple-splash-640-1136.jpg' media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' /> />
<link rel='apple-touch-startup-image' href='/static/apple-splash-1136-640.jpg' media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' /> <link
rel='apple-touch-startup-image'
href='/static/apple-splash-2048-1536.jpg'
media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-1668-2224.jpg'
media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-2224-1668.jpg'
media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-1620-2160.jpg'
media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-2160-1620.jpg'
media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-1284-2778.jpg'
media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-2778-1284.jpg'
media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-1170-2532.jpg'
media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-2532-1170.jpg'
media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-1125-2436.jpg'
media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-2436-1125.jpg'
media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-1242-2688.jpg'
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-2688-1242.jpg'
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-828-1792.jpg'
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-1792-828.jpg'
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-1242-2208.jpg'
media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-2208-1242.jpg'
media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-750-1334.jpg'
media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-1334-750.jpg'
media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-640-1136.jpg'
media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
/>
<link
rel='apple-touch-startup-image'
href='/static/apple-splash-1136-640.jpg'
media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
/>
{/* Android */} {/* Android */}
<link rel='icon' type='image/png' sizes='192x192' href='/static/android-icon-192x192.png' /> <link
rel='icon'
type='image/png'
sizes='192x192'
href='/static/android-icon-192x192.png'
/>
{/* Others */} {/* Others */}
<link rel='shortcut icon' href='/favicon.ico' /> <link rel='shortcut icon' href='/favicon.ico' />
{/* SCRIPT */} {/* SCRIPT */}
<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 data-ad-client='ca-pub-4856582423981759' async src='//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'></script> <script
<script data-cfasync='false' async src='//www.googletagmanager.com/gtag/js?id=UA-165454387-1'></script> data-ad-client='ca-pub-4856582423981759'
async
src='//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'
></script>
<script
data-cfasync='false'
async
src='//www.googletagmanager.com/gtag/js?id=UA-165454387-1'
></script>
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: ` __html: `
@ -91,7 +218,7 @@ class MyDocument extends Document {
}} }}
/> />
</Head> </Head>
<body className='h-full text-black dark:text-gray-100 dark:bg-discord-dark bg-white overflow-x-hidden'> <body className='h-full overflow-x-hidden bg-white text-black dark:bg-discord-dark dark:text-gray-100'>
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>

View File

@ -8,24 +8,35 @@ const Container = dynamic(() => import('@components/Container'))
const MyError: NextPage = () => { const MyError: NextPage = () => {
return ( return (
<div <div className='flex h-screen select-none items-center px-20'>
className='flex items-center h-screen select-none px-20'
>
<Container> <Container>
<h2 className='text-4xl font-semibold'>{getRandom(ErrorMessage)}</h2> <h2 className='text-4xl font-semibold'>{getRandom(ErrorMessage)}</h2>
<p className='text-md mt-1'> . !</p> <p className='text-md mt-1'>
<a className='text-sm text-blue-500 hover:text-blue-400' href='https://status.koreanbots.dev' target='_blank' rel='noreferrer'> </a> . !
</p>
<a
className='text-sm text-blue-500 hover:text-blue-400'
href='https://status.koreanbots.dev'
target='_blank'
rel='noreferrer'
>
</a>
<div> <div>
<Link <Link
href='/discord' href='/discord'
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
className='text-lg hover:opacity-80 cursor-pointer'> className='cursor-pointer text-lg hover:opacity-80'
>
<i className='fab fa-discord' /> <i className='fab fa-discord' />
</Link> </Link>
<a href='https://twitter.com/koreanbots' target='_blank' rel='noreferrer' className='text-lg ml-2 hover:opacity-80 cursor-pointer'> <a
href='https://twitter.com/koreanbots'
target='_blank'
rel='noreferrer'
className='ml-2 cursor-pointer text-lg hover:opacity-80'
>
<i className='fab fa-twitter' /> <i className='fab fa-twitter' />
</a> </a>
</div> </div>

View File

@ -6,24 +6,33 @@ const Container = dynamic(() => import('@components/Container'))
const MyError: NextPage = () => { const MyError: NextPage = () => {
return ( return (
<div <div className='flex h-screen select-none items-center px-20'>
className='flex items-center h-screen select-none px-20'
>
<Container> <Container>
<h2 className='text-4xl font-semibold'> ...</h2> <h2 className='text-4xl font-semibold'> ...</h2>
<p className='text-md mt-1'> !</p> <p className='text-md mt-1'> !</p>
<a className='text-sm text-blue-500 hover:text-blue-400' href='https://status.koreanbots.dev' target='_blank' rel='noreferrer'> </a> <a
className='text-sm text-blue-500 hover:text-blue-400'
href='https://status.koreanbots.dev'
target='_blank'
rel='noreferrer'
>
</a>
<div> <div>
<Link <Link
href='/discord' href='/discord'
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
className='text-lg hover:opacity-80 cursor-pointer'> className='cursor-pointer text-lg hover:opacity-80'
>
<i className='fab fa-discord' /> <i className='fab fa-discord' />
</Link> </Link>
<a href='https://twitter.com/koreanbots' target='_blank' rel='noreferrer' className='text-lg ml-2 hover:opacity-80 cursor-pointer'> <a
href='https://twitter.com/koreanbots'
target='_blank'
rel='noreferrer'
className='ml-2 cursor-pointer text-lg hover:opacity-80'
>
<i className='fab fa-twitter' /> <i className='fab fa-twitter' />
</a> </a>
</div> </div>

View File

@ -10,41 +10,64 @@ import { ThemeColors } from '@utils/Constants'
const Container = dynamic(() => import('@components/Container')) const Container = dynamic(() => import('@components/Container'))
const About: NextPage = () => { const About: NextPage = () => {
return <div className='pb-10'> return (
<Docs title='소개' header={<h1 className='font-black text-4xl dark:text-koreanbots-blue'> .</h1>} subheader='한국 디스코드 리스트에서 자신에게 필요한 디스코드의 모든 것을 찾아보세요!'> <div className='pb-10'>
<Docs
title='소개'
header={
<h1 className='text-4xl font-black dark:text-koreanbots-blue'>
.
</h1>
}
subheader='한국 디스코드 리스트에서 자신에게 필요한 디스코드의 모든 것을 찾아보세요!'
>
<Container> <Container>
<div className='py-1'> <div className='py-1'>
<h1 className='font-bold text-5xl my-5'></h1> <h1 className='my-5 text-5xl font-bold'></h1>
<p className='text-lg'><span className='text-koreanbots-blue font-bold'> </span> , .</p> <p className='text-lg'>
<span className='font-bold text-koreanbots-blue'> </span>
,
.
</p>
<p className='text-lg'> !</p> <p className='text-lg'> !</p>
<Divider /> <Divider />
<h1 className='font-bold text-5xl my-5'></h1> <h1 className='my-5 text-5xl font-bold'></h1>
<div className='grid md:grid-cols-3 gap-12 px-4 pb-5'> <div className='grid gap-12 px-4 pb-5 md:grid-cols-3'>
<div className='mx-auto font-normal'> <div className='mx-auto font-normal'>
<h2 className='text-3xl mb-1 font-bold text-koreanbots-blue'> </h2> <h2 className='mb-1 text-3xl font-bold text-koreanbots-blue'> </h2>
<p className='text-base'> .</p> <p className='text-base'>
.
</p>
</div> </div>
<div className='mx-auto font-normal'> <div className='mx-auto font-normal'>
<h2 className='text-3xl mb-1 font-bold text-koreanbots-blue'> </h2> <h2 className='mb-1 text-3xl font-bold text-koreanbots-blue'> </h2>
<p className='text-base'> , .</p> <p className='text-base'>
, .
</p>
</div> </div>
<div className='mx-auto font-normal'> <div className='mx-auto font-normal'>
<h2 className='text-3xl mb-1 font-bold text-koreanbots-blue'>API </h2> <h2 className='mb-1 text-3xl font-bold text-koreanbots-blue'>API </h2>
<p className='text-base'>, , .<br /> API를 !</p> <p className='text-base'>
, , .
<br />
API를 !
</p>
</div> </div>
</div> </div>
<Divider /> <Divider />
<h1 className='font-bold text-5xl my-5'></h1> <h1 className='my-5 text-5xl font-bold'></h1>
<h2 className='font-semibold text-3xl mb-7'></h2> <h2 className='mb-7 text-3xl font-semibold'></h2>
<Segment> <Segment>
<h2 className='font-semibold text-xl py-10 text-center'> <h2 className='py-10 text-center text-xl font-semibold'>
<i className='fas fa-quote-left text-xs align-top' /> <i className='fas fa-quote-left align-top text-xs' />
. .
<i className='fas fa-quote-right text-xs align-bottom' /> <i className='fas fa-quote-right align-bottom text-xs' />
</h2> </h2>
</Segment> </Segment>
<Divider className='mt-7' /> <Divider className='mt-7' />
<h2 className='font-semibold text-3xl my-7'></h2> <h2 className='my-7 text-3xl font-semibold'></h2>
<Segment> <Segment>
<> <>
, , . , , .
@ -52,27 +75,36 @@ const About:NextPage = () => {
<div> <div>
<img src='/logo.png' alt='Logo' /> <img src='/logo.png' alt='Logo' />
<div className='text-right text-blue-400'> <div className='text-right text-blue-400'>
<a href='/logo.png' download='koreanbots.png'>.png</a> <a href='/logo.png' download='koreanbots.png'>
.png
</a>
</div> </div>
</div> </div>
</div> </div>
<h3 className='font-bold text-xl my-1'></h3> <h3 className='my-1 text-xl font-bold'></h3>
<p className='font-bold text-md my-1'>영문: Uni Sans Heavy | 한글: Gugi</p> <p className='text-md my-1 font-bold'>영문: Uni Sans Heavy | 한글: Gugi</p>
</> </>
</Segment> </Segment>
<Divider className='mt-7' /> <Divider className='mt-7' />
<h2 className='font-semibold text-3xl my-5'></h2> <h2 className='my-5 text-3xl font-semibold'></h2>
<div className='grid md:grid-cols-2 lg:grid-cols-4 gap-4'> <div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4'>
{ {ThemeColors.map((el) => (
ThemeColors.map(el => ( <ColorCard
<ColorCard key={el.color} header={el.name} first={el.rgb} second={el.hex} className={`bg-${el.color} ${el.color.includes('white') ? 'text-black' : 'text-white'}`} /> key={el.color}
)) header={el.name}
} first={el.rgb}
second={el.hex}
className={`bg-${el.color} ${
el.color.includes('white') ? 'text-black' : 'text-white'
}`}
/>
))}
</div> </div>
</div> </div>
</Container> </Container>
</Docs> </Docs>
</div> </div>
)
} }
export default About export default About

View File

@ -58,7 +58,7 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
- -
- ?`, - ?`,
_csrf: csrfToken, _csrf: csrfToken,
_captcha: 'captcha' _captcha: 'captcha',
} }
function toLogin() { function toLogin() {
@ -67,150 +67,325 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
} }
async function submitBot(value: AddBotSubmit, token: string) { async function submitBot(value: AddBotSubmit, token: string) {
const res = await Fetch<SubmittedBot>(`/bots/${value.id}`, { method: 'POST', body: JSON.stringify(cleanObject<AddBotSubmit>({ ...value, _captcha: token})) }) const res = await Fetch<SubmittedBot>(`/bots/${value.id}`, {
method: 'POST',
body: JSON.stringify(cleanObject<AddBotSubmit>({ ...value, _captcha: token })),
})
setData(res) setData(res)
} }
if(!logged) return <Login> if (!logged)
<NextSeo title='새로운 봇 추가하기' description='자신의 봇을 한국 디스코드 리스트에 등록하세요.' openGraph={{ return (
title:'새로운 봇 추가하기', description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.' <Login>
}} /> <NextSeo
title='새로운 봇 추가하기'
description='자신의 봇을 한국 디스코드 리스트에 등록하세요.'
openGraph={{
title: '새로운 봇 추가하기',
description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.',
}}
/>
</Login> </Login>
if(data?.data && data.code === 200) {
setTimeout(
() => redirectTo(router, `/pendingBots/${data.data.id}/${data.data.date}`),
1_000
) )
if (data?.data && data.code === 200) {
setTimeout(() => redirectTo(router, `/pendingBots/${data.data.id}/${data.data.date}`), 1_000)
} }
return ( return (
<Container paddingTop className='py-5'> <Container paddingTop className='py-5'>
<NextSeo title='새로운 봇 추가하기' description='자신의 봇을 한국 디스코드 리스트에 등록하세요.' openGraph={{ <NextSeo
title:'새로운 봇 추가하기', description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.' title='새로운 봇 추가하기'
}} /> description='자신의 봇을 한국 디스코드 리스트에 등록하세요.'
openGraph={{
title: '새로운 봇 추가하기',
description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.',
}}
/>
<h1 className='text-3xl font-bold'> </h1> <h1 className='text-3xl font-bold'> </h1>
<div className='mt-1 mb-5'> <div className='mb-5 mt-1'>
, <span className='font-semibold'>{user.tag === '0' ? `@${user.username}` : `${user.username}#${user.tag}`}</span>! <a role='button' tabIndex={0} onKeyDown={toLogin} onClick={toLogin} className='text-discord-blurple cursor-pointer outline-none'> ?</a> ,{' '}
<span className='font-semibold'>
{user.tag === '0' ? `@${user.username}` : `${user.username}#${user.tag}`}
</span>
!{' '}
<a
role='button'
tabIndex={0}
onKeyDown={toLogin}
onClick={toLogin}
className='cursor-pointer text-discord-blurple outline-none'
>
?
</a>
</div> </div>
{ {data ? (
data ? data.code == 200 && data.data ? <Message type='success'> data.code == 200 && data.data ? (
<Message type='success'>
<h2 className='text-lg font-extrabold'> !</h2> <h2 className='text-lg font-extrabold'> !</h2>
<p> ! .</p> <p> ! .</p>
</Message> : <Message type='error'> </Message>
) : (
<Message type='error'>
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2> <h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
<ul className='list-disc list-inside'> <ul className='list-inside list-disc'>
{data.errors?.map((el, n) => <li key={n}>{el}</li>)} {data.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul> </ul>
</Message>
</Message> : <></> )
} ) : (
<Formik initialValues={initialValues} <></>
)}
<Formik
initialValues={initialValues}
validationSchema={AddBotSubmitSchema} validationSchema={AddBotSubmitSchema}
onSubmit={() => setCaptcha(true)}> onSubmit={() => setCaptcha(true)}
>
{({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => ( {({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => (
<Form> <Form>
<div className='py-3'> <div className='py-3'>
<Message type='warning'> <Message type='warning'>
<h2 className='text-lg font-extrabold'> !</h2> <h2 className='text-lg font-extrabold'>
<ul className='list-disc list-inside'> !
<li><Link </h2>
<ul className='list-inside list-disc'>
<li>
<Link
href='/discord' href='/discord'
rel='noreferrer' rel='noreferrer'
target='_blank' target='_blank'
className='text-blue-500 hover:text-blue-600'> </Link> ?</li> className='text-blue-500 hover:text-blue-600'
<li> <Link >
</Link>
?
</li>
<li>
{' '}
<Link
href='/guidelines' href='/guidelines'
rel='noreferrer' rel='noreferrer'
target='_blank' target='_blank'
className='text-blue-500 hover:text-blue-600'></Link> ?</li> className='text-blue-500 hover:text-blue-600'
<li> ? , .</li> >
</Link>
?
</li>
<li>
? ,
.
</li>
<li>, API에 .</li> <li>, API에 .</li>
</ul> </ul>
</Message> </Message>
</div> </div>
<Label For='agree' error={errors.agree && touched.agree ? errors.agree : null} grid={false}> <Label
For='agree'
error={errors.agree && touched.agree ? errors.agree : null}
grid={false}
>
<div className='flex items-center'> <div className='flex items-center'>
<CheckBox name='agree' /> <CheckBox name='agree' />
<strong className='text-sm ml-2'> , .</strong> <strong className='ml-2 text-sm'>
,
.
</strong>
</div> </div>
</Label> </Label>
<Divider /> <Divider />
<Advertisement /> <Advertisement />
<Label For='id' label='봇 ID' labelDesc='봇의 클라이언트 ID를 의미합니다.' error={errors.id && touched.id ? errors.id : null} short required> <Label
For='id'
label='봇 ID'
labelDesc='봇의 클라이언트 ID를 의미합니다.'
error={errors.id && touched.id ? errors.id : null}
short
required
>
<Input name='id' placeholder='653534001742741552' /> <Input name='id' placeholder='653534001742741552' />
</Label> </Label>
<Label For='prefix' label='접두사' labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)' error={errors.prefix && touched.prefix ? errors.prefix : null} short required> <Label
For='prefix'
label='접두사'
labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)'
error={errors.prefix && touched.prefix ? errors.prefix : null}
short
required
>
<Input name='prefix' placeholder='!' /> <Input name='prefix' placeholder='!' />
</Label> </Label>
<Label For='library' label='라이브러리' labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.' short required error={errors.library && touched.library ? errors.library : null}> <Label
<Select options={library.map(el=> ({ label: el, value: el }))} handleChange={(value) => setFieldValue('library', value.value)} handleTouch={() => setFieldTouched('library', true)} /> For='library'
label='라이브러리'
labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.'
short
required
error={errors.library && touched.library ? errors.library : null}
>
<Select
options={library.map((el) => ({ label: el, value: el }))}
handleChange={(value) => setFieldValue('library', value.value)}
handleTouch={() => setFieldTouched('library', true)}
/>
</Label> </Label>
<Label For='category' label='카테고리' labelDesc='봇에 해당되는 카테고리를 선택해주세요' required error={errors.category && touched.category ? errors.category as string : null}> <Label
<Selects options={botCategories.map(el=> ({ label: el, value: el }))} handleChange={(value) => { For='category'
setFieldValue('category', value.map(v=> v.value)) label='카테고리'
}} handleTouch={() => setFieldTouched('category', true)} values={values.category as string[]} setValues={(value) => setFieldValue('category', value)} /> labelDesc='봇에 해당되는 카테고리를 선택해주세요'
<p className='text-gray-400 mt-1 text-sm'> 3 . . <strong> .</strong><br/> required
<a className='text-blue-500 hover:text-blue-400' href='https://contents.koreanbots.dev/categories'></a> !</p> error={errors.category && touched.category ? (errors.category as string) : null}
>
<Selects
options={botCategories.map((el) => ({ label: el, value: el }))}
handleChange={(value) => {
setFieldValue(
'category',
value.map((v) => v.value)
)
}}
handleTouch={() => setFieldTouched('category', true)}
values={values.category as string[]}
setValues={(value) => setFieldValue('category', value)}
/>
<p className='mt-1 text-sm text-gray-400'>
3 . .{' '}
<strong> .</strong>
<br />
<a
className='text-blue-500 hover:text-blue-400'
href='https://contents.koreanbots.dev/categories'
>
</a>
!
</p>
</Label> </Label>
<Divider /> <Divider />
<Label For='website' label='웹사이트' labelDesc='봇의 웹사이트를 작성해주세요.' error={errors.website && touched.website ? errors.website : null}> <Label
For='website'
label='웹사이트'
labelDesc='봇의 웹사이트를 작성해주세요.'
error={errors.website && touched.website ? errors.website : null}
>
<Input name='website' placeholder='https://koreanbots.dev' /> <Input name='website' placeholder='https://koreanbots.dev' />
</Label> </Label>
<Label For='git' label='Git URL' labelDesc='봇 소스코드의 Git 주소를 입력해주세요. (오픈소스인 경우)' error={errors.git && touched.git ? errors.git : null}> <Label
For='git'
label='Git URL'
labelDesc='봇 소스코드의 Git 주소를 입력해주세요. (오픈소스인 경우)'
error={errors.git && touched.git ? errors.git : null}
>
<Input name='git' placeholder='https://github.com/koreanbots/koreanbots' /> <Input name='git' placeholder='https://github.com/koreanbots/koreanbots' />
</Label> </Label>
<Label For='inviteLink' label='초대링크' labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.' error={errors.url && touched.url ? errors.url : null}> <Label
<Input name='url' placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0' /> For='inviteLink'
<span className='text-gray-400 mt-1 text-sm'> label='초대링크'
labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.'
error={errors.url && touched.url ? errors.url : null}
>
<Input
name='url'
placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0'
/>
<span className='mt-1 text-sm text-gray-400'>
<Link <Link
href='/calculator' href='/calculator'
rel='noreferrer' rel='noreferrer'
target='_blank' target='_blank'
className='text-blue-500 hover:text-blue-400'> className='text-blue-500 hover:text-blue-400'
>
</Link> ! </Link>
!
</span> </span>
</Label> </Label>
<Label For='discord' label='지원 디스코드 서버' labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)' error={errors.discord && touched.discord ? errors.discord : null} short> <Label
For='discord'
label='지원 디스코드 서버'
labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)'
error={errors.discord && touched.discord ? errors.discord : null}
short
>
<div className='flex items-center'> <div className='flex items-center'>
discord.gg/<Input name='discord' placeholder='JEh53MQ' /> discord.gg/
<Input name='discord' placeholder='JEh53MQ' />
</div> </div>
</Label> </Label>
<Divider /> <Divider />
<Label For='intro' label='봇 소개' labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required> <Label
For='intro'
label='봇 소개'
labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)'
error={errors.intro && touched.intro ? errors.intro : null}
required
>
<Input name='intro' placeholder='국내 봇을 한 곳에서.' /> <Input name='intro' placeholder='국내 봇을 한 곳에서.' />
</Label> </Label>
<Label For='intro' label='봇 설명' labelDesc={<> ! ( 1500)<br/> !</>} error={errors.desc && touched.desc ? errors.desc : null} required> <Label
<TextArea max={1500} name='desc' placeholder='봇에 대해 최대한 자세히 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.desc} setValue={(value) => setFieldValue('desc', value)} /> For='intro'
label='봇 설명'
labelDesc={
<>
! ( 1500)
<br />
!
</>
}
error={errors.desc && touched.desc ? errors.desc : null}
required
>
<TextArea
max={1500}
name='desc'
placeholder='봇에 대해 최대한 자세히 설명해주세요!'
theme={theme === 'dark' ? 'dark' : 'light'}
value={values.desc}
setValue={(value) => setFieldValue('desc', value)}
/>
</Label> </Label>
<Label For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다.'> <Label
For='preview'
label='설명 미리보기'
labelDesc='다음 결과는 실제와 다를 수 있습니다.'
>
<Segment> <Segment>
<Markdown text={values.desc} /> <Markdown text={values.desc} />
</Segment> </Segment>
</Label> </Label>
<Divider /> <Divider />
<p className='text-base mt-2 mb-5'> <p className='mb-5 mt-2 text-base'>
<span className='text-red-500 font-semibold'> *</span> = <span className='font-semibold text-red-500'> *</span> =
</p> </p>
{ {captcha ? (
captcha ? <Captcha ref={captchaRef} dark={theme === 'dark'} onVerify={(token) => { <Captcha
ref={captchaRef}
dark={theme === 'dark'}
onVerify={(token) => {
submitBot(values, token) submitBot(values, token)
window.scrollTo({ top: 0 }) window.scrollTo({ top: 0 })
setCaptcha(false) setCaptcha(false)
captchaRef?.current?.resetCaptcha() captchaRef?.current?.resetCaptcha()
}} /> : <> }}
{ />
touchedSumbit && !isValid && <div className='my-1 text-red-500 text-xs font-light'> . .</div> ) : (
} <>
<Button type='submit' onClick={() => { {touchedSumbit && !isValid && (
<div className='my-1 text-xs font-light text-red-500'>
. .
</div>
)}
<Button
type='submit'
onClick={() => {
setTouched(true) setTouched(true)
if (!isValid) window.scrollTo({ top: 0 }) if (!isValid) window.scrollTo({ top: 0 })
} }> }}
>
<> <>
<i className='far fa-paper-plane' /> <i className='far fa-paper-plane' />
</> </>
</Button> </Button>
</> </>
} )}
</Form> </Form>
)} )}
</Formik> </Formik>
@ -222,7 +397,13 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
export const getServerSideProps = async (ctx: NextPageContext) => { export const getServerSideProps = async (ctx: NextPageContext) => {
const parsed = parseCookie(ctx.req) const parsed = parseCookie(ctx.req)
const user = await get.Authorization(parsed?.token) const user = await get.Authorization(parsed?.token)
return { props: { logged: !!user, user: await get.user.load(user || ''), csrfToken: getToken(ctx.req, ctx.res) } } return {
props: {
logged: !!user,
user: await get.user.load(user || ''),
csrfToken: getToken(ctx.req, ctx.res),
},
}
} }
interface AddBotProps { interface AddBotProps {

View File

@ -30,7 +30,14 @@ const Container = dynamic(() => import('@components/Container'))
const Message = dynamic(() => import('@components/Message')) const Message = dynamic(() => import('@components/Message'))
const Captcha = dynamic(() => import('@components/Captcha')) const Captcha = dynamic(() => import('@components/Captcha'))
const AddServer:NextPage<AddServerProps> = ({ logged, user, csrfToken, server, serverData, theme }) => { const AddServer: NextPage<AddServerProps> = ({
logged,
user,
csrfToken,
server,
serverData,
theme,
}) => {
const [data, setData] = useState<ResponseProps<AddServerSubmit>>(null) const [data, setData] = useState<ResponseProps<AddServerSubmit>>(null)
const [captcha, setCaptcha] = useState(false) const [captcha, setCaptcha] = useState(false)
const [touchedSumbit, setTouched] = useState(false) const [touchedSumbit, setTouched] = useState(false)
@ -58,7 +65,7 @@ const AddServer:NextPage<AddServerProps> = ({ logged, user, csrfToken, server, s
- ?`, - ?`,
category: [], category: [],
_csrf: csrfToken, _csrf: csrfToken,
_captcha: 'captcha' _captcha: 'captcha',
} }
function toLogin() { function toLogin() {
@ -67,80 +74,146 @@ const AddServer:NextPage<AddServerProps> = ({ logged, user, csrfToken, server, s
} }
async function submitServer(id: string, value: AddServerSubmit, token: string) { async function submitServer(id: string, value: AddServerSubmit, token: string) {
const res = await Fetch<AddServerSubmit>(`/servers/${id}`, { method: 'POST', body: JSON.stringify(cleanObject<AddServerSubmit>({ ...value, _captcha: token })) }) const res = await Fetch<AddServerSubmit>(`/servers/${id}`, {
method: 'POST',
body: JSON.stringify(cleanObject<AddServerSubmit>({ ...value, _captcha: token })),
})
setData(res) setData(res)
} }
if(!logged) return <Login> if (!logged)
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{ return (
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.' <Login>
}} /> <NextSeo
title='새로운 서버 추가하기'
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
openGraph={{
title: '새로운 서버 추가하기',
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
}}
/>
</Login> </Login>
if(data?.data && data.code == 200) {
setTimeout(
() => redirectTo(router, `/servers/${router.query.id}`),
1_000
) )
if (data?.data && data.code == 200) {
setTimeout(() => redirectTo(router, `/servers/${router.query.id}`), 1_000)
} }
return ( return (
<Container paddingTop className='py-5'> <Container paddingTop className='py-5'>
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{ <NextSeo
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.' title='새로운 서버 추가하기'
}} /> description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
openGraph={{
title: '새로운 서버 추가하기',
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
}}
/>
<h1 className='text-3xl font-bold'> </h1> <h1 className='text-3xl font-bold'> </h1>
<div className='mt-1 mb-5'> <div className='mb-5 mt-1'>
, <span className='font-semibold'>{user.username}#{user.tag}</span>! <a role='button' tabIndex={0} onKeyDown={toLogin} onClick={toLogin} className='text-discord-blurple cursor-pointer outline-none'> ?</a> ,{' '}
<span className='font-semibold'>
{user.username}#{user.tag}
</span>
!{' '}
<a
role='button'
tabIndex={0}
onKeyDown={toLogin}
onClick={toLogin}
className='cursor-pointer text-discord-blurple outline-none'
>
?
</a>
</div> </div>
{ {data ? (
data ? data.code == 200 && data.data ? <Message type='success'> data.code == 200 && data.data ? (
<Message type='success'>
<h2 className='text-lg font-extrabold'> !</h2> <h2 className='text-lg font-extrabold'> !</h2>
<p> ! !</p> <p> ! !</p>
</Message> : <Message type='error'> </Message>
) : (
<Message type='error'>
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2> <h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
<ul className='list-disc list-inside'> <ul className='list-inside list-disc'>
{data.errors?.map((el, n) => <li key={n}>{el}</li>)} {data.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul> </ul>
</Message>
</Message> : <></> )
} ) : (
{ <></>
server ? <Message type='warning'> )}
{server ? (
<Message type='warning'>
<h2 className='text-lg font-extrabold'> .</h2> <h2 className='text-lg font-extrabold'> .</h2>
</Message> : </Message>
!serverData ? <Message type='info'> ) : !serverData ? (
<Message type='info'>
<h2 className='text-lg font-extrabold'> .</h2> <h2 className='text-lg font-extrabold'> .</h2>
<p> .</p> <p> .</p>
<p> 1 .</p> <p> 1 .</p>
</Message> </Message>
: serverData.admins.includes(user.id) || serverData.owner.includes(user.id) ? <Formik initialValues={initialValues} ) : serverData.admins.includes(user.id) || serverData.owner.includes(user.id) ? (
<Formik
initialValues={initialValues}
validationSchema={AddServerSubmitSchema} validationSchema={AddServerSubmitSchema}
onSubmit={() => setCaptcha(true)}> onSubmit={() => setCaptcha(true)}
>
{({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => ( {({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => (
<Form> <Form>
<div className='py-3'> <div className='py-3'>
<Message type='warning'> <Message type='warning'>
<h2 className='text-lg font-extrabold'> !</h2> <h2 className='text-lg font-extrabold'>
<ul className='list-disc list-inside'> !
<li><Link </h2>
<ul className='list-inside list-disc'>
<li>
<Link
href='/discord' href='/discord'
rel='noreferrer' rel='noreferrer'
target='_blank' target='_blank'
className='text-blue-500 hover:text-blue-600'> </Link> .</li> className='text-blue-500 hover:text-blue-600'
<li> <Link >
</Link>
.
</li>
<li>
{' '}
<Link
href='/guidelines' href='/guidelines'
rel='noreferrer' rel='noreferrer'
target='_blank' target='_blank'
className='text-blue-500 hover:text-blue-600'></Link> ?</li> className='text-blue-500 hover:text-blue-600'
<li> <strong></strong> .</li> >
<li> .</li>
<li>, API에 .</li> </Link>
?
</li>
<li>
<strong></strong>
.
</li>
<li>
.
</li>
<li>
,
API에 .
</li>
</ul> </ul>
</Message> </Message>
</div> </div>
<Label For='agree' error={errors.agree && touched.agree ? errors.agree : null} grid={false}> <Label
For='agree'
error={errors.agree && touched.agree ? errors.agree : null}
grid={false}
>
<div className='flex items-center'> <div className='flex items-center'>
<CheckBox name='agree' /> <CheckBox name='agree' />
<strong className='text-sm ml-2'> , .</strong> <strong className='ml-2 text-sm'>
,
.
</strong>
</div> </div>
</Label> </Label>
<Divider /> <Divider />
@ -151,58 +224,125 @@ const AddServer:NextPage<AddServerProps> = ({ logged, user, csrfToken, server, s
</p> </p>
</Label> </Label>
<Divider /> <Divider />
<Label For='category' label='카테고리' labelDesc='서버에 해당되는 카테고리를 선택해주세요' required error={errors.category && touched.category ? errors.category as string : null}> <Label
<Selects options={serverCategories.map(el=> ({ label: el, value: el }))} handleChange={(value) => { For='category'
setFieldValue('category', value.map(v=> v.value)) label='카테고리'
}} handleTouch={() => setFieldTouched('category', true)} values={values.category as string[]} setValues={(value) => setFieldValue('category', value)} /> labelDesc='서버에 해당되는 카테고리를 선택해주세요'
<span className='text-gray-400 mt-1 text-sm'> 3 . . <strong> .</strong></span> required
error={errors.category && touched.category ? (errors.category as string) : null}
>
<Selects
options={serverCategories.map((el) => ({ label: el, value: el }))}
handleChange={(value) => {
setFieldValue(
'category',
value.map((v) => v.value)
)
}}
handleTouch={() => setFieldTouched('category', true)}
values={values.category as string[]}
setValues={(value) => setFieldValue('category', value)}
/>
<span className='mt-1 text-sm text-gray-400'>
3 . .{' '}
<strong> .</strong>
</span>
</Label> </Label>
<Label For='invite' label='서버 초대코드' labelDesc='서버의 초대코드를 입력해주세요. (만료되지 않는 코드로 입력해주세요!)' error={errors.invite && touched.invite ? errors.invite : null} short required> <Label
For='invite'
label='서버 초대코드'
labelDesc='서버의 초대코드를 입력해주세요. (만료되지 않는 코드로 입력해주세요!)'
error={errors.invite && touched.invite ? errors.invite : null}
short
required
>
<div className='flex items-center'> <div className='flex items-center'>
discord.gg/<Input name='invite' placeholder='JEh53MQ' /> discord.gg/
<Input name='invite' placeholder='JEh53MQ' />
</div> </div>
</Label> </Label>
<Divider /> <Divider />
<Label For='intro' label='서버 소개' labelDesc='서버를 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required> <Label
For='intro'
label='서버 소개'
labelDesc='서버를 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)'
error={errors.intro && touched.intro ? errors.intro : null}
required
>
<Input name='intro' placeholder={getRandom(ServerIntroList)} /> <Input name='intro' placeholder={getRandom(ServerIntroList)} />
</Label> </Label>
<Label For='desc' label='서버 설명' labelDesc={<> ! ( 1500)<br/> !</>} error={errors.desc && touched.desc ? errors.desc : null} required> <Label
<TextArea max={1500} name='desc' placeholder='서버에 대해 최대한 자세히 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.desc} setValue={(value) => setFieldValue('desc', value)} /> For='desc'
label='서버 설명'
labelDesc={
<>
! ( 1500)
<br />
!
</>
}
error={errors.desc && touched.desc ? errors.desc : null}
required
>
<TextArea
max={1500}
name='desc'
placeholder='서버에 대해 최대한 자세히 설명해주세요!'
theme={theme === 'dark' ? 'dark' : 'light'}
value={values.desc}
setValue={(value) => setFieldValue('desc', value)}
/>
</Label> </Label>
<Label For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다.'> <Label
For='preview'
label='설명 미리보기'
labelDesc='다음 결과는 실제와 다를 수 있습니다.'
>
<Segment> <Segment>
<Markdown text={values.desc} /> <Markdown text={values.desc} />
</Segment> </Segment>
</Label> </Label>
<Divider /> <Divider />
<p className='text-base mt-2 mb-5'> <p className='mb-5 mt-2 text-base'>
<span className='text-red-500 font-semibold'> *</span> = <span className='font-semibold text-red-500'> *</span> =
</p> </p>
{ {captcha ? (
captcha ? <Captcha ref={captchaRef} dark={theme === 'dark'} onVerify={(token) => { <Captcha
ref={captchaRef}
dark={theme === 'dark'}
onVerify={(token) => {
submitServer(router.query.id as string, values, token) submitServer(router.query.id as string, values, token)
window.scrollTo({ top: 0 }) window.scrollTo({ top: 0 })
setCaptcha(false) setCaptcha(false)
captchaRef?.current?.resetCaptcha() captchaRef?.current?.resetCaptcha()
}} /> : <> }}
{ />
touchedSumbit && !isValid && <div className='my-1 text-red-500 text-xs font-light'> . .</div> ) : (
} <>
<Button type='submit' onClick={() => { {touchedSumbit && !isValid && (
<div className='my-1 text-xs font-light text-red-500'>
. .
</div>
)}
<Button
type='submit'
onClick={() => {
setTouched(true) setTouched(true)
if (!isValid) window.scrollTo({ top: 0 }) if (!isValid) window.scrollTo({ top: 0 })
} }> }}
>
<> <>
<i className='far fa-paper-plane' /> <i className='far fa-paper-plane' />
</> </>
</Button> </Button>
</> </>
} )}
</Form> </Form>
)} )}
</Formik> </Formik>
: <Forbidden /> ) : (
} <Forbidden />
)}
</Container> </Container>
) )
} }
@ -212,12 +352,16 @@ export const getServerSideProps = async (ctx: NextPageContext) => {
const user = await get.Authorization(parsed?.token) const user = await get.Authorization(parsed?.token)
const server = (await get.server.load(ctx.query.id as string)) || null const server = (await get.server.load(ctx.query.id as string)) || null
const serverData = (await get.serverData(ctx.query.id as string)) || null const serverData = (await get.serverData(ctx.query.id as string)) || null
return { props: { return {
logged: !!user, user: await get.user.load(user || ''), props: {
logged: !!user,
user: await get.user.load(user || ''),
csrfToken: getToken(ctx.req, ctx.res), csrfToken: getToken(ctx.req, ctx.res),
server, server,
serverData: (+new Date() - +new Date(serverData?.updatedAt)) < 2 * 60 * 1000 ? serverData : null serverData:
} } +new Date() - +new Date(serverData?.updatedAt) < 2 * 60 * 1000 ? serverData : null,
},
}
} }
interface AddServerProps { interface AddServerProps {

View File

@ -13,39 +13,70 @@ const Login = dynamic(() => import('@components/Login'))
const Container = dynamic(() => import('@components/Container')) const Container = dynamic(() => import('@components/Container'))
const AddBot: NextPage<AddBotProps> = ({ logged, guilds }) => { const AddBot: NextPage<AddBotProps> = ({ logged, guilds }) => {
if(!logged) return <Login> if (!logged)
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{ return (
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.' <Login>
}} /> <NextSeo
title='새로운 서버 추가하기'
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
openGraph={{
title: '새로운 서버 추가하기',
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
}}
/>
</Login> </Login>
return <Container paddingTop className='py-5'> )
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{ return (
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.' <Container paddingTop className='py-5'>
}} /> <NextSeo
title='새로운 서버 추가하기'
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
openGraph={{
title: '새로운 서버 추가하기',
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
}}
/>
<h1 className='text-3xl font-bold'> </h1> <h1 className='text-3xl font-bold'> </h1>
<p className='text-gray-400'> .</p> <p className='text-gray-400'> .</p>
<p className='text-gray-400 pb-5'> . , 1 .</p> <p className='pb-5 text-gray-400'>
. , 1 .
</p>
<Advertisement /> <Advertisement />
<ResponsiveGrid> <ResponsiveGrid>
{ {guilds
guilds.sort((a ,b) => (+!!b.data || 0) - (+!!a.data || 0)).map(g => ( .sort((a, b) => (+!!b.data || 0) - (+!!a.data || 0))
.map((g) => (
<ServerCard type={g.exists ? 'manage' : 'add'} server={g} key={g.id} /> <ServerCard type={g.exists ? 'manage' : 'add'} server={g} key={g.id} />
)) ))}
}
</ResponsiveGrid> </ResponsiveGrid>
<Advertisement /> <Advertisement />
</Container> </Container>
)
} }
export const getServerSideProps = async (ctx: NextPageContext) => { export const getServerSideProps = async (ctx: NextPageContext) => {
const parsed = parseCookie(ctx.req) const parsed = parseCookie(ctx.req)
const user = await get.Authorization(parsed?.token) const user = await get.Authorization(parsed?.token)
const guilds = (await get.userGuilds.load(user || ''))?.filter(g=> (g.permissions & 8) || g.owner).map(async g => { const guilds = (await get.userGuilds.load(user || ''))
const server = (await get.server.load(g.id)) ?.filter((g) => g.permissions & 8 || g.owner)
.map(async (g) => {
const server = await get.server.load(g.id)
const data = await get.serverData(g.id) const data = await get.serverData(g.id)
return { ...g, ...(server || {}), ...((+new Date() - +new Date(data?.updatedAt)) < 2 * 60 * 1000 ? { data } : {}), members: data?.memberCount || null, exists: !!server } return {
...g,
...(server || {}),
...(+new Date() - +new Date(data?.updatedAt) < 2 * 60 * 1000 ? { data } : {}),
members: data?.memberCount || null,
exists: !!server,
}
}) })
return { props: { logged: !!user || !!guilds, user: await get.user.load(user || ''), guilds: guilds ? (await Promise.all(guilds)).filter(g => !g?.exists) : null } } return {
props: {
logged: !!user || !!guilds,
user: await get.user.load(user || ''),
guilds: guilds ? (await Promise.all(guilds)).filter((g) => !g?.exists) : null,
},
}
} }
interface AddBotProps { interface AddBotProps {
@ -53,7 +84,7 @@ interface AddBotProps {
user: User user: User
csrfToken: string csrfToken: string
theme: Theme theme: Theme
guilds: (RawGuild & { data: ServerData, exists?: boolean })[] guilds: (RawGuild & { data: ServerData; exists?: boolean })[]
} }
export default AddBot export default AddBot

View File

@ -13,8 +13,8 @@ import RequestHandler from '@utils/RequestHandler'
const Callback = RequestHandler().get(async (req: ApiRequest, res) => { const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
const validate = await OauthCallbackSchema.validate(req.query) const validate = await OauthCallbackSchema.validate(req.query)
.then(r => r) .then((r) => r)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
@ -35,7 +35,7 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
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, {
@ -43,7 +43,7 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
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({ const userToken = await update.assignToken({
id: user.id, id: user.id,
@ -53,11 +53,13 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
email: user.email, email: user.email,
username: user.username, username: user.username,
discriminator: user.discriminator, discriminator: user.discriminator,
verified: user.verified verified: user.verified,
}) })
if(userToken === 1) return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/unverified') if (userToken === 1)
else if(userToken === 2) return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/blocked') return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/unverified')
else if (userToken === 2)
return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/blocked')
const info = verify(userToken) const info = verify(userToken)
res.setHeader( res.setHeader(
'set-cookie', 'set-cookie',

View File

@ -10,8 +10,8 @@ import RequestHandler from '@utils/RequestHandler'
const Callback = RequestHandler().get(async (req: ApiRequest, res) => { const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
const validate = await OauthCallbackSchema.validate(req.query) const validate = await OauthCallbackSchema.validate(req.query)
.then(r => r) .then((r) => r)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
@ -20,21 +20,29 @@ const Callback = RequestHandler().get(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 token: GithubTokenInfo = await fetch(SpecialEndPoints.Github.Token(process.env.GITHUB_CLIENT_ID, process.env.GITHUB_CLIENT_SECRET,req.query.code), { const token: GithubTokenInfo = await fetch(
SpecialEndPoints.Github.Token(
process.env.GITHUB_CLIENT_ID,
process.env.GITHUB_CLIENT_SECRET,
req.query.code
),
{
method: 'POST', method: 'POST',
headers: { headers: {
Accept: 'application/json' Accept: 'application/json',
}, },
}).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 github: { login: string } = await fetch(SpecialEndPoints.Github.Me, { const github: { login: string } = await fetch(SpecialEndPoints.Github.Me, {
headers: { headers: {
Authorization: `token ${token.access_token}` Authorization: `token ${token.access_token}`,
} },
}).then(r => r.json()) }).then((r) => r.json())
const result = await update.Github(user, github.login) const result = await update.Github(user, github.login)
if(result === 0) return ResponseWrapper(res, { code: 400, message: '이미 등록되어있는 깃허브 계정입니다.' }) if (result === 0)
return ResponseWrapper(res, { code: 400, message: '이미 등록되어있는 깃허브 계정입니다.' })
get.user.clear(user) get.user.clear(user)
res.redirect(301, '/panel') res.redirect(301, '/panel')
}) })

View File

@ -5,11 +5,9 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import { get, update } from '@utils/Query' import { get, update } from '@utils/Query'
import { checkToken } from '@utils/Csrf' import { checkToken } from '@utils/Csrf'
const Github = RequestHandler().get(async (_req: NextApiRequest, res: NextApiResponse) => { const Github = RequestHandler()
res.redirect( .get(async (_req: NextApiRequest, res: NextApiResponse) => {
301, res.redirect(301, generateOauthURL('github', process.env.GITHUB_CLIENT_ID))
generateOauthURL('github', process.env.GITHUB_CLIENT_ID)
)
}) })
.delete(async (req: DeleteApiRequest, res) => { .delete(async (req: DeleteApiRequest, res) => {
const user = await get.Authorization(req.cookies.token) const user = await get.Authorization(req.cookies.token)

View File

@ -11,7 +11,9 @@ const rateLimiter = rateLimit({
windowMs: 60 * 1000, windowMs: 60 * 1000,
max: 150, max: 150,
handler: async (_req, res) => { handler: async (_req, res) => {
const img = await get.images.user.load(DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' })) const img = await get.images.user.load(
DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' })
)
res.setHeader('Content-Type', 'image/png') res.setHeader('Content-Type', 'image/png')
res.setHeader('Cache-Control', 'no-cache') res.setHeader('Cache-Control', 'no-cache')
res.send(img) res.send(img)
@ -20,7 +22,7 @@ const rateLimiter = rateLimit({
skip: (_req, res) => { skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global') res.removeHeader('X-RateLimit-Global')
return false return false
} },
}) })
const Avatar = RequestHandler() const Avatar = RequestHandler()
@ -31,7 +33,9 @@ const Avatar = RequestHandler()
const splitted = param.split('.') const splitted = param.split('.')
let ext = splitted[1] let ext = splitted[1]
const id = splitted[0] const id = splitted[0]
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false }).then(el=> el).catch(e=> { const validated = await ImageOptionsSchema.validate({ id, ext, size }, { 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
}) })
@ -39,21 +43,31 @@ const Avatar = RequestHandler()
const user = await get.discord.user.load(id) const user = await get.discord.user.load(id)
let img: Buffer let img: Buffer
if(!user?.avatar) img = await get.images.user.load(DiscordEnpoints.CDN.default(user?.discriminator ? Number(user.discriminator) % 5 : Math.floor(Math.random() * 6), { format: 'png', size: validated.size })) if (!user?.avatar)
else img = await get.images.user.load(DiscordEnpoints.CDN.user(id, user.avatar, { format: validated.ext === 'gif' && !user.avatar.startsWith('a_') ? 'png' : validated.ext })) img = await get.images.user.load(
DiscordEnpoints.CDN.default(
user?.discriminator ? Number(user.discriminator) % 5 : Math.floor(Math.random() * 6),
{ format: 'png', size: validated.size }
)
)
else
img = await get.images.user.load(
DiscordEnpoints.CDN.user(id, user.avatar, {
format: validated.ext === 'gif' && !user.avatar.startsWith('a_') ? 'png' : validated.ext,
})
)
if (!img) { if (!img) {
img = await get.images.user.load(DiscordEnpoints.CDN.default(user.discriminator, { format: 'png', size: validated.size })) img = await get.images.user.load(
DiscordEnpoints.CDN.default(user.discriminator, { format: 'png', size: validated.size })
)
ext = 'png' ext = 'png'
} }
res.setHeader('Content-Type', `image/${ext}`) res.setHeader('Content-Type', `image/${ext}`)
res.setHeader('Cache-Control', 'public, max-age=86400') res.setHeader('Cache-Control', 'public, max-age=86400')
res.send(img) res.send(img)
}) })
interface ApiRequest extends NextApiRequest { interface ApiRequest extends NextApiRequest {
query: { query: {
id: string id: string

View File

@ -11,7 +11,9 @@ const rateLimiter = rateLimit({
windowMs: 60 * 1000, windowMs: 60 * 1000,
max: 150, max: 150,
handler: async (_req, res) => { handler: async (_req, res) => {
const img = await get.images.server.load(DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' })) const img = await get.images.server.load(
DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' })
)
res.setHeader('Content-Type', 'image/png') res.setHeader('Content-Type', 'image/png')
res.setHeader('Cache-Control', 'no-cache') res.setHeader('Cache-Control', 'no-cache')
res.send(img) res.send(img)
@ -20,7 +22,7 @@ const rateLimiter = rateLimit({
skip: (_req, res) => { skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global') res.removeHeader('X-RateLimit-Global')
return false return false
} },
}) })
const Icon = RequestHandler() const Icon = RequestHandler()
@ -31,7 +33,9 @@ const Icon = RequestHandler()
const splitted = param.split('.') const splitted = param.split('.')
let ext = splitted[1] let ext = splitted[1]
const id = splitted[0] const id = splitted[0]
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false }).then(el=> el).catch(e=> { const validated = await ImageOptionsSchema.validate({ id, ext, size }, { 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
}) })
@ -40,20 +44,24 @@ const Icon = RequestHandler()
const guild = await get.server.load(id) const guild = await get.server.load(id)
let img: Buffer let img: Buffer
if (!guild?.icon) img = await get.images.server.load(DiscordEnpoints.CDN.default(+id % 4)) if (!guild?.icon) img = await get.images.server.load(DiscordEnpoints.CDN.default(+id % 4))
else img = await get.images.server.load(DiscordEnpoints.CDN.guild(id, guild.icon, { format: validated.ext === 'gif' && !guild.icon.startsWith('a_') ? 'png' : validated.ext })) else
img = await get.images.server.load(
DiscordEnpoints.CDN.guild(id, guild.icon, {
format: validated.ext === 'gif' && !guild.icon.startsWith('a_') ? 'png' : validated.ext,
})
)
if (!img) { if (!img) {
img = await get.images.server.load(DiscordEnpoints.CDN.default(+id % 4, { format: 'png', size: validated.size })) img = await get.images.server.load(
DiscordEnpoints.CDN.default(+id % 4, { format: 'png', size: validated.size })
)
ext = 'png' ext = 'png'
} }
res.setHeader('Content-Type', `image/${ext}`) res.setHeader('Content-Type', `image/${ext}`)
res.setHeader('Cache-Control', 'public, max-age=86400') res.setHeader('Cache-Control', 'public, max-age=86400')
res.send(img) res.send(img)
}) })
interface ApiRequest extends NextApiRequest { interface ApiRequest extends NextApiRequest {
query: { query: {
id: string id: string

View File

@ -20,7 +20,7 @@ const limiter = rateLimit({
res.removeHeader('X-RateLimit-Global') res.removeHeader('X-RateLimit-Global')
if (!req.headers.authorization) return true if (!req.headers.authorization) return true
else return false else return false
} },
}) })
const BotStats = RequestHandler() const BotStats = RequestHandler()
@ -28,28 +28,50 @@ const BotStats = RequestHandler()
.post(async (req: PostApiRequest, res) => { .post(async (req: PostApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.token) const bot = await get.BotAuthorization(req.headers.token)
if (!bot) return ResponseWrapper(res, { code: 401, version: 1 }) if (!bot) return ResponseWrapper(res, { code: 401, version: 1 })
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, { abortEarly: false }) const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, {
.then(el => el) abortEarly: false,
.catch(e => { })
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
if (!validated) return if (!validated) return
const botInfo = await get.bot.load(bot) const botInfo = await get.bot.load(bot)
if(!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.', version: 1 }) if (!botInfo)
return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.', version: 1 })
if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403, version: 1 }) if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403, version: 1 })
const d = await update.updateServer(botInfo.id, validated.servers, undefined) const d = await update.updateServer(botInfo.id, validated.servers, undefined)
if(d===1 || d===2) return ResponseWrapper(res, { code: 403, message: `서버 수를 ${[null, '1만', '100만'][d]} 이상으로 설정하실 수 없습니다. 문의해주세요.`, version: 1 }) if (d === 1 || d === 2)
return ResponseWrapper(res, {
code: 403,
message: `서버 수를 ${
[null, '1만', '100만'][d]
} . .`,
version: 1,
})
get.bot.clear(bot) get.bot.clear(bot)
await webhookClients.internal.statsLog.send({ await webhookClients.internal.statsLog.send({
content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(`${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${validated.servers} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(validated.servers - botInfo.servers)})`, 'diff')}`, content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(
embeds: [new EmbedBuilder().setDescription(`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(botInfo.id)}`)] `${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${
validated.servers
} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(
validated.servers - botInfo.servers
)})`,
'diff'
)}`,
embeds: [
new EmbedBuilder().setDescription(
`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(
botInfo.id
)}`
),
],
}) })
return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.', version: 1 }) return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.', version: 1 })
}) })
interface PostApiRequest extends NextApiRequest { interface PostApiRequest extends NextApiRequest {
headers: { headers: {
token: string token: string

View File

@ -5,11 +5,14 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import Yup from '@utils/Yup' import Yup from '@utils/Yup'
import { VOTE_COOLDOWN } from '@utils/Constants' import { VOTE_COOLDOWN } from '@utils/Constants'
const BotVoted = RequestHandler() const BotVoted = RequestHandler().get(async (req: ApiRequest, res) => {
.get(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.token) const bot = await get.BotAuthorization(req.headers.token)
if (!bot) return ResponseWrapper(res, { code: 401, version: 1 }) if (!bot) return ResponseWrapper(res, { code: 401, version: 1 })
const userID = await Yup.string().required().validate(req.query.id).then(el => el).catch(() => null) const userID = await Yup.string()
.required()
.validate(req.query.id)
.then((el) => el)
.catch(() => null)
if (!userID) return ResponseWrapper(res, { code: 400, version: 1 }) if (!userID) return ResponseWrapper(res, { code: 400, version: 1 })
const result = await get.botVote(userID, bot) const result = await get.botVote(userID, bot)
return res.json({ code: 200, voted: +new Date() < result + VOTE_COOLDOWN }) return res.json({ code: 200, voted: +new Date() < result + VOTE_COOLDOWN })

View File

@ -17,8 +17,8 @@ const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
const csrfValidated = checkToken(req, res, req.body._csrf) const csrfValidated = checkToken(req, res, req.body._csrf)
if (!csrfValidated) return if (!csrfValidated) return
const validated = await DeveloperBotSchema.validate(req.body, { abortEarly: false }) const validated = await DeveloperBotSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
@ -26,11 +26,18 @@ const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
if (!validated) return if (!validated) return
const bot = await get.bot.load(req.query.id) const bot = await get.bot.load(req.query.id)
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' }) if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 }) if (!(bot.owners as User[]).find((el) => el.id === user))
return ResponseWrapper(res, { code: 403 })
if (validated.webhookURL) { if (validated.webhookURL) {
const key = await verifyWebhook(validated.webhookURL) const key = await verifyWebhook(validated.webhookURL)
if (key === false) { if (key === false) {
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] }) return ResponseWrapper(res, {
code: 400,
message: '웹후크 주소를 검증할 수 없습니다.',
errors: [
'웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.',
],
})
} }
const client = webhookClients.bot.get(req.query.id) const client = webhookClients.bot.get(req.query.id)
if (client && validated.webhookURL !== client.url) { if (client && validated.webhookURL !== client.url) {

View File

@ -14,8 +14,8 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
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 ResetTokenSchema.validate(req.body, { abortEarly: false }) const validated = await ResetTokenSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
@ -23,7 +23,8 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
if (!validated) return if (!validated) return
const bot = await get.bot.load(req.query.id) const bot = await get.bot.load(req.query.id)
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' }) if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 }) if (!(bot.owners as User[]).find((el) => el.id === user))
return ResponseWrapper(res, { code: 403 })
const d = await update.resetBotToken(req.query.id, validated.token) const d = await update.resetBotToken(req.query.id, validated.token)
if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' }) if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
return ResponseWrapper(res, { code: 200, data: { token: d } }) return ResponseWrapper(res, { code: 200, data: { token: d } })

View File

@ -17,8 +17,8 @@ const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) =
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 DeveloperServerSchema.validate(req.body, { abortEarly: false }) const validated = await DeveloperServerSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
@ -30,7 +30,13 @@ const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) =
if (validated.webhookURL) { if (validated.webhookURL) {
const key = await verifyWebhook(validated.webhookURL) const key = await verifyWebhook(validated.webhookURL)
if (key === false) { if (key === false) {
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] }) return ResponseWrapper(res, {
code: 400,
message: '웹후크 주소를 검증할 수 없습니다.',
errors: [
'웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.',
],
})
} }
const client = webhookClients.server.get(req.query.id) const client = webhookClients.server.get(req.query.id)
if (client && validated.webhookURL !== client.url) { if (client && validated.webhookURL !== client.url) {

View File

@ -12,8 +12,8 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
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 ResetTokenSchema.validate(req.body, { abortEarly: false }) const validated = await ResetTokenSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
@ -21,8 +21,14 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
if (!validated) return if (!validated) return
const server = await get.server.load(req.query.id) const server = await get.server.load(req.query.id)
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' }) if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
if(server.state === 'unreachable') return ResponseWrapper(res, { code: 400, message: '서버 정보를 불러올 수 없습니다.', errors: ['서버에서 봇이 추방되었거나, 봇이 오프라인이여서 서버 정보를 갱신할 수 없습니다.'] }) if (server.state === 'unreachable')
if (!(await get.serverOwners(server.id)).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 }) return ResponseWrapper(res, {
code: 400,
message: '서버 정보를 불러올 수 없습니다.',
errors: ['서버에서 봇이 추방되었거나, 봇이 오프라인이여서 서버 정보를 갱신할 수 없습니다.'],
})
if (!(await get.serverOwners(server.id)).find((el) => el.id === user))
return ResponseWrapper(res, { code: 403 })
const d = await update.resetServerToken(req.query.id, validated.token) const d = await update.resetServerToken(req.query.id, validated.token)
if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' }) if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
return ResponseWrapper(res, { code: 200, data: { token: d } }) return ResponseWrapper(res, { code: 200, data: { token: d } })

View File

@ -6,10 +6,23 @@ import tracer from 'dd-trace'
import { CaptchaVerify, get, put, remove, update } from '@utils/Query' import { CaptchaVerify, get, put, remove, update } from '@utils/Query'
import ResponseWrapper from '@utils/ResponseWrapper' import ResponseWrapper from '@utils/ResponseWrapper'
import { checkToken } from '@utils/Csrf' import { checkToken } from '@utils/Csrf'
import { AddBotSubmit, AddBotSubmitSchema, CsrfCaptcha, ManageBot, ManageBotSchema } from '@utils/Yup' import {
AddBotSubmit,
AddBotSubmitSchema,
CsrfCaptcha,
ManageBot,
ManageBotSchema,
} from '@utils/Yup'
import RequestHandler from '@utils/RequestHandler' import RequestHandler from '@utils/RequestHandler'
import { User } from '@types' import { User } from '@types'
import { checkUserFlag, diff, inspect, makeDiscordCodeblock, objectDiff, serialize } from '@utils/Tools' import {
checkUserFlag,
diff,
inspect,
makeDiscordCodeblock,
objectDiff,
serialize,
} from '@utils/Tools'
import { discordLog, getMainGuild, webhookClients } from '@utils/DiscordBot' import { discordLog, getMainGuild, webhookClients } from '@utils/DiscordBot'
import { KoreanbotsEndPoints } from '@utils/Constants' import { KoreanbotsEndPoints } from '@utils/Constants'
@ -21,7 +34,7 @@ const patchLimiter = rateLimit({
skip: (_req, res) => { skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global') res.removeHeader('X-RateLimit-Global')
return false return false
} },
}) })
const Bots = RequestHandler() const Bots = RequestHandler()
.get(async (req: GetApiRequest, res) => { .get(async (req: GetApiRequest, res) => {
@ -38,8 +51,8 @@ const Bots = RequestHandler()
if (!csrfValidated) return if (!csrfValidated) return
const validated = await AddBotSubmitSchema.validate(req.body, { abortEarly: false }) const validated = await AddBotSubmitSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
@ -82,30 +95,53 @@ const Bots = RequestHandler()
return ResponseWrapper(res, { return ResponseWrapper(res, {
code: 403, code: 403,
message: '더 이상 해당 봇에 대한 심사 요청을 하실 수 없습니다.', message: '더 이상 해당 봇에 대한 심사 요청을 하실 수 없습니다.',
errors: ['해당 봇은 심사에서 3회 이상 거부되었습니다. 더 이상의 심사를 요청하실 수 없습니다.', '이의 제기를 원하시는 경우 디스코드 서버를 통해 문의해주세요.'], errors: [
'해당 봇은 심사에서 3회 이상 거부되었습니다. 더 이상의 심사를 요청하실 수 없습니다.',
'이의 제기를 원하시는 경우 디스코드 서버를 통해 문의해주세요.',
],
}) })
get.botSubmits.clear(user) get.botSubmits.clear(user)
await discordLog('BOT/SUBMIT', user, new EmbedBuilder().setDescription(`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(result.id, result.date)})`), { await discordLog(
'BOT/SUBMIT',
user,
new EmbedBuilder().setDescription(
`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(
result.id,
result.date
)})`
),
{
content: inspect(serialize(result)), content: inspect(serialize(result)),
format: 'js' format: 'js',
}) }
)
const userinfo = await get.user.load(user) const userinfo = await get.user.load(user)
await webhookClients.internal.reviewLog.send({ await webhookClients.internal.reviewLog.send({
embeds: [ embeds: [
new EmbedBuilder() new EmbedBuilder()
.setAuthor({ .setAuthor({
name: userinfo.tag === '0' ? `${userinfo.globalName} (@${userinfo.username})` : `${userinfo.username}#${userinfo.tag}`, name:
iconURL: KoreanbotsEndPoints.URL.root + KoreanbotsEndPoints.CDN.avatar(userinfo.id, { format: 'png', size: 256 }), userinfo.tag === '0'
url: KoreanbotsEndPoints.URL.user(userinfo.id) ? `${userinfo.globalName} (@${userinfo.username})`
: `${userinfo.username}#${userinfo.tag}`,
iconURL:
KoreanbotsEndPoints.URL.root +
KoreanbotsEndPoints.CDN.avatar(userinfo.id, { format: 'png', size: 256 }),
url: KoreanbotsEndPoints.URL.user(userinfo.id),
}) })
.setTitle('대기 중') .setTitle('대기 중')
.setColor(Colors.Grey) .setColor(Colors.Grey)
.setDescription(`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(result.id, result.date)})`) .setDescription(
.setTimestamp() `[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(
] result.id,
result.date
)})`
)
.setTimestamp(),
],
}) })
tracer.trace('botSubmits.submitted', span => { tracer.trace('botSubmits.submitted', (span) => {
span.setTag('id', result.id) span.setTag('id', result.id)
span.setTag('date', result.date) span.setTag('date', result.date)
span.setTag('user', userinfo.id) span.setTag('user', userinfo.id)
@ -119,37 +155,64 @@ const Bots = RequestHandler()
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' }) if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
if ((bot.owners as User[])[0].id !== user) return ResponseWrapper(res, { code: 403 }) if ((bot.owners as User[])[0].id !== user) return ResponseWrapper(res, { code: 403 })
const userInfo = await get.user.load(user) const userInfo = await get.user.load(user)
if(['reported', 'blocked', 'archived'].includes(bot.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 봇은 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] }) if (
['reported', 'blocked', 'archived'].includes(bot.state) &&
!checkUserFlag(userInfo?.flags, 'staff')
)
return ResponseWrapper(res, {
code: 403,
message: '해당 봇은 수정할 수 없습니다.',
errors: ['오류라고 생각되면 문의해주세요.'],
})
const csrfValidated = checkToken(req, res, req.body._csrf) const csrfValidated = checkToken(req, res, req.body._csrf)
if (!csrfValidated) return if (!csrfValidated) return
const captcha = await CaptchaVerify(req.body._captcha) const captcha = await CaptchaVerify(req.body._captcha)
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' }) if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
if(req.body.name !== bot.name) return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' }) if (req.body.name !== bot.name)
return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' })
await remove.bot(bot.id) await remove.bot(bot.id)
await getMainGuild().members.cache.get(bot.id)?.kick('봇 삭제됨.') await getMainGuild().members.cache.get(bot.id)?.kick('봇 삭제됨.')
get.user.clear(user) get.user.clear(user)
await discordLog('BOT/DELETE', user, (new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`)), await discordLog(
'BOT/DELETE',
user,
new EmbedBuilder().setDescription(
`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`
),
{ {
content: inspect(bot), content: inspect(bot),
format: 'js' format: 'js',
} }
) )
return ResponseWrapper(res, { code: 200, message: '성공적으로 삭제했습니다.' }) return ResponseWrapper(res, { code: 200, message: '성공적으로 삭제했습니다.' })
}) })
.patch(patchLimiter).patch(async (req: PatchApiRequest, res) => { .patch(patchLimiter)
.patch(async (req: PatchApiRequest, res) => {
const bot = await get.bot.load(req.query.id) const bot = await get.bot.load(req.query.id)
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' }) if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
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 userInfo = await get.user.load(user) const userInfo = await get.user.load(user)
if(['reported', 'blocked', 'archived'].includes(bot.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 봇은 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] }) if (
if(!(bot.owners as User[]).find(el => el.id === user) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403 }) ['reported', 'blocked', 'archived'].includes(bot.state) &&
!checkUserFlag(userInfo?.flags, 'staff')
)
return ResponseWrapper(res, {
code: 403,
message: '해당 봇은 수정할 수 없습니다.',
errors: ['오류라고 생각되면 문의해주세요.'],
})
if (
!(bot.owners as User[]).find((el) => el.id === user) &&
!checkUserFlag(userInfo?.flags, 'staff')
)
return ResponseWrapper(res, { code: 403 })
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 ManageBotSchema.validate(req.body, { abortEarly: false }) const validated = await ManageBotSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
@ -160,24 +223,43 @@ const Bots = RequestHandler()
if (result === 0) return ResponseWrapper(res, { code: 400 }) if (result === 0) return ResponseWrapper(res, { code: 400 })
else { else {
get.bot.clear(req.query.id) get.bot.clear(req.query.id)
const embed = new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`) const embed = new EmbedBuilder().setDescription(
const diffData = objectDiff( `${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`
{ prefix: bot.prefix, library: bot.lib, web: bot.web, git: bot.git, url: bot.url, discord: bot.discord, intro: bot.intro, category: JSON.stringify(bot.category) },
{ prefix: validated.prefix, library: validated.library, web: validated.website, git: validated.git, url: validated.url, discord: validated.discord, intro: validated.intro, category: JSON.stringify(validated.category) }
) )
diffData.forEach(d => { const diffData = objectDiff(
embed.addFields({name: d[0], value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff')
})
})
await discordLog('BOT/EDIT', user, embed,
{ {
content: `--- 설명\n${diff(bot.desc, validated.desc, true)}`, prefix: bot.prefix,
format: 'diff' library: bot.lib,
web: bot.web,
git: bot.git,
url: bot.url,
discord: bot.discord,
intro: bot.intro,
category: JSON.stringify(bot.category),
},
{
prefix: validated.prefix,
library: validated.library,
web: validated.website,
git: validated.git,
url: validated.url,
discord: validated.discord,
intro: validated.intro,
category: JSON.stringify(validated.category),
} }
) )
diffData.forEach((d) => {
embed.addFields({
name: d[0],
value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff'),
})
})
await discordLog('BOT/EDIT', user, embed, {
content: `--- 설명\n${diff(bot.desc, validated.desc, true)}`,
format: 'diff',
})
return ResponseWrapper(res, { code: 200 }) return ResponseWrapper(res, { code: 200 })
} }
}) })
interface GetApiRequest extends NextApiRequest { interface GetApiRequest extends NextApiRequest {
@ -195,7 +277,7 @@ interface PatchApiRequest extends GetApiRequest {
} }
interface DeleteApiRequest extends GetApiRequest { interface DeleteApiRequest extends GetApiRequest {
body: CsrfCaptcha & { name: string } | null body: (CsrfCaptcha & { name: string }) | null
} }
export default Bots export default Bots

View File

@ -11,18 +11,26 @@ import { discordLog } from '@utils/DiscordBot'
import { EmbedBuilder } from 'discord.js' import { EmbedBuilder } from 'discord.js'
import { KoreanbotsEndPoints } from '@utils/Constants' import { KoreanbotsEndPoints } from '@utils/Constants'
const BotOwners = RequestHandler() const BotOwners = RequestHandler().patch(async (req: PostApiRequest, res) => {
.patch(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token) const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 }) if (!user) return ResponseWrapper(res, { code: 401 })
const userinfo = await get.user.load(user) const userinfo = await get.user.load(user)
const bot = await get.bot.load(req.query.id) const bot = await get.bot.load(req.query.id)
if (!bot) return ResponseWrapper(res, { code: 404 }) if (!bot) return ResponseWrapper(res, { code: 404 })
if((bot.owners as User[])[0].id !== user && !checkUserFlag(userinfo.flags, 'staff')) return ResponseWrapper(res, { code: 403 }) if ((bot.owners as User[])[0].id !== user && !checkUserFlag(userinfo.flags, 'staff'))
if(['reported', 'blocked', 'archived'].includes(bot.state) && !checkUserFlag(userinfo.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 봇은 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] }) return ResponseWrapper(res, { code: 403 })
if (
['reported', 'blocked', 'archived'].includes(bot.state) &&
!checkUserFlag(userinfo.flags, 'staff')
)
return ResponseWrapper(res, {
code: 403,
message: '해당 봇은 수정할 수 없습니다.',
errors: ['오류라고 생각되면 문의해주세요.'],
})
const validated = await EditBotOwnerSchema.validate(req.body, { abortEarly: false }) const validated = await EditBotOwnerSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
@ -31,19 +39,40 @@ const BotOwners = RequestHandler()
if (!csrfValidated) return if (!csrfValidated) return
const captcha = await CaptchaVerify(validated._captcha) const captcha = await CaptchaVerify(validated._captcha)
if (!captcha) return if (!captcha) return
const userFetched: User[] = await Promise.all(validated.owners.map((u: string) => get.user.load(u))) const userFetched: User[] = await Promise.all(
if(userFetched.indexOf(null) !== -1) return ResponseWrapper(res, { code: 400, message: '올바르지 않은 유저 ID를 포함하고 있습니다.' }) validated.owners.map((u: string) => get.user.load(u))
if(userFetched.length > 1 && userFetched[0].id !== (bot.owners as User[])[0].id) return ResponseWrapper(res, { code: 400, errors: ['소유자를 이전할 때는 다른 관리자를 포함할 수 없습니다.'] }) )
if (userFetched.indexOf(null) !== -1)
return ResponseWrapper(res, {
code: 400,
message: '올바르지 않은 유저 ID를 포함하고 있습니다.',
})
if (userFetched.length > 1 && userFetched[0].id !== (bot.owners as User[])[0].id)
return ResponseWrapper(res, {
code: 400,
errors: ['소유자를 이전할 때는 다른 관리자를 포함할 수 없습니다.'],
})
await update.botOwners(bot.id, validated.owners) await update.botOwners(bot.id, validated.owners)
get.user.clear(user) get.user.clear(user)
await discordLog('BOT/OWNERS', userinfo.id, (new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`)), null, makeDiscordCodeblock(diff(JSON.stringify(bot.owners.map(el => el.id)), JSON.stringify(validated.owners)), 'diff')) await discordLog(
'BOT/OWNERS',
userinfo.id,
new EmbedBuilder().setDescription(
`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`
),
null,
makeDiscordCodeblock(
diff(JSON.stringify(bot.owners.map((el) => el.id)), JSON.stringify(validated.owners)),
'diff'
)
)
return ResponseWrapper(res, { code: 200 }) return ResponseWrapper(res, { code: 200 })
}) })
interface PostApiRequest extends NextApiRequest { interface PostApiRequest extends NextApiRequest {
query: { query: {
id: string id: string
}, }
body: EditBotOwner body: EditBotOwner
} }

View File

@ -18,10 +18,11 @@ const limiter = rateLimit({
skip: (_req, res) => { skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global') res.removeHeader('X-RateLimit-Global')
return false return false
} },
}) })
const BotReport = RequestHandler().post(limiter) const BotReport = RequestHandler()
.post(limiter)
.post(async (req: PostApiRequest, res) => { .post(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token) const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 }) if (!user) return ResponseWrapper(res, { code: 401 })
@ -31,18 +32,21 @@ const BotReport = RequestHandler().post(limiter)
if (!csrfValidated) return if (!csrfValidated) return
if (!req.body) return ResponseWrapper(res, { code: 400 }) if (!req.body) return ResponseWrapper(res, { code: 400 })
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false }) const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .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
await webhookClients.internal.reportChannel.send({ threadName: `봇-${bot.id}`, content: `Reported by <@${user}> (${user})\nReported **${bot.name}** <@${bot.id}> (${bot.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``, allowedMentions: { parse: ['users'] }}) await webhookClients.internal.reportChannel.send({
threadName: `봇-${bot.id}`,
content: `Reported by <@${user}> (${user})\nReported **${bot.name}** <@${bot.id}> (${bot.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``,
allowedMentions: { parse: ['users'] },
})
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' }) return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
}) })
interface PostApiRequest extends NextApiRequest { interface PostApiRequest extends NextApiRequest {
body: Report | null body: Report | null
query: { query: {

View File

@ -23,7 +23,7 @@ const limiter = rateLimit({
res.removeHeader('X-RateLimit-Global') res.removeHeader('X-RateLimit-Global')
if (!req.headers.authorization) return true if (!req.headers.authorization) return true
else return false else return false
} },
}) })
const patchLimiter = rateLimit({ const patchLimiter = rateLimit({
@ -36,17 +36,20 @@ const patchLimiter = rateLimit({
skip: (_req, res) => { skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global') res.removeHeader('X-RateLimit-Global')
return false return false
} },
}) })
const BotStats = RequestHandler().post(limiter) const BotStats = RequestHandler()
.post(limiter)
.post(async (req: PostApiRequest, res) => { .post(async (req: PostApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization) const bot = await get.BotAuthorization(req.headers.authorization)
if (!bot) return ResponseWrapper(res, { code: 401 }) if (!bot) return ResponseWrapper(res, { code: 401 })
if (!req.body) return ResponseWrapper(res, { code: 400 }) if (!req.body) return ResponseWrapper(res, { code: 400 })
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, { abortEarly: false }) const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, {
.then(el => el) abortEarly: false,
.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
}) })
@ -56,7 +59,13 @@ const BotStats = RequestHandler().post(limiter)
if (!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' }) if (!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403 }) if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403 })
const d = await update.updateServer(botInfo.id, validated.servers, validated.shards) const d = await update.updateServer(botInfo.id, validated.servers, validated.shards)
if(d===1 || d===2) return ResponseWrapper(res, { code: 403, message: `서버 수를 ${[null, '1만', '100만'][d]} 이상으로 설정하실 수 없습니다. 문의해주세요.` }) if (d === 1 || d === 2)
return ResponseWrapper(res, {
code: 403,
message: `서버 수를 ${
[null, '1만', '100만'][d]
} . .`,
})
get.bot.clear(req.query.id) get.bot.clear(req.query.id)
if (validated.servers && botInfo.servers !== validated.servers) { if (validated.servers && botInfo.servers !== validated.servers) {
sendWebhook(botInfo, { sendWebhook(botInfo, {
@ -67,27 +76,44 @@ const BotStats = RequestHandler().post(limiter)
before: botInfo.servers, before: botInfo.servers,
after: validated.servers, after: validated.servers,
}, },
timestamp: Date.now() timestamp: Date.now(),
}) })
} }
await webhookClients.internal.statsLog.send({ await webhookClients.internal.statsLog.send({
content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(`${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${validated.servers} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(validated.servers - botInfo.servers)})`, 'diff')}`, content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(
embeds: [new EmbedBuilder().setDescription(`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(botInfo.id)}))`)] `${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${
validated.servers
} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(
validated.servers - botInfo.servers
)})`,
'diff'
)}`,
embeds: [
new EmbedBuilder().setDescription(
`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(
botInfo.id
)}))`
),
],
}) })
return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.' }) return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.' })
}) })
.patch(patchLimiter).patch(async (req: ApiRequest, res) => { .patch(patchLimiter)
.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 userinfo = await get.user.load(user) const userinfo = await get.user.load(user)
const bot = await get.bot.load(req.query.id) const bot = await get.bot.load(req.query.id)
if (!bot) return ResponseWrapper(res, { code: 404 }) if (!bot) return ResponseWrapper(res, { code: 404 })
if(!(bot.owners as User[]).find(el => el.id === user) && !checkUserFlag(userinfo.flags, 'staff')) return ResponseWrapper(res, { code: 403 }) if (
!(bot.owners as User[]).find((el) => el.id === user) &&
!checkUserFlag(userinfo.flags, 'staff')
)
return ResponseWrapper(res, { code: 403 })
get.bot.clear(req.query.id) get.bot.clear(req.query.id)
return ResponseWrapper(res, { code: 200 }) return ResponseWrapper(res, { code: 200 })
}) })
interface ApiRequest extends NextApiRequest { interface ApiRequest extends NextApiRequest {
query: { query: {
id: string id: string

View File

@ -14,13 +14,21 @@ const BotVote = RequestHandler()
const bot = await get.BotAuthorization(req.headers.authorization) const bot = await get.BotAuthorization(req.headers.authorization)
if (!bot) return ResponseWrapper(res, { code: 401 }) if (!bot) return ResponseWrapper(res, { code: 401 })
if (req.query.id !== bot) return ResponseWrapper(res, { code: 403 }) if (req.query.id !== bot) return ResponseWrapper(res, { code: 403 })
const userID = await Yup.string().required().label('userID').validate(req.query.userID).then(el => el).catch(e => { const userID = await Yup.string()
.required()
.label('userID')
.validate(req.query.userID)
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
if (!userID) return ResponseWrapper(res, { code: 400 }) if (!userID) return ResponseWrapper(res, { code: 400 })
const result = await get.botVote(userID, bot) const result = await get.botVote(userID, bot)
return ResponseWrapper(res, { code: 200, data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result } }) return ResponseWrapper(res, {
code: 200,
data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result },
})
}) })
.post(async (req: PostApiRequest, res) => { .post(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token) const user = await get.Authorization(req.cookies.token)
@ -42,13 +50,12 @@ const BotVote = RequestHandler()
type: WebhookType.HeartChange, type: WebhookType.HeartChange,
before: bot.votes, before: bot.votes,
after: bot.votes + 1, after: bot.votes + 1,
userId: user userId: user,
}, },
timestamp: Date.now() timestamp: Date.now(),
}) })
return ResponseWrapper(res, { code: 200 }) return ResponseWrapper(res, { code: 200 })
} } else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
}) })
interface ApiRequest extends NextApiRequest { interface ApiRequest extends NextApiRequest {

View File

@ -6,9 +6,15 @@ import { Bot, List } from '@types'
import Yup from '@utils/Yup' import Yup from '@utils/Yup'
const VotesList = RequestHandler().get(async (req, res) => { const VotesList = RequestHandler().get(async (req, res) => {
const page = await Yup.number().positive().integer().notRequired().default(1).label('페이지').validate(req.query.page) const page = await Yup.number()
.then(el => el) .positive()
.catch(e => { .integer()
.notRequired()
.default(1)
.label('페이지')
.validate(req.query.page)
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
}) })
if (!page) return if (!page) return

View File

@ -8,21 +8,32 @@ import { get, update } from '@utils/Query'
import { DiscordBot, webhookClients } from '@utils/DiscordBot' import { DiscordBot, webhookClients } from '@utils/DiscordBot'
import { KoreanbotsEndPoints } from '@utils/Constants' import { KoreanbotsEndPoints } from '@utils/Constants'
const ApproveBotSubmit = RequestHandler() const ApproveBotSubmit = RequestHandler().post(async (req: ApiRequest, res) => {
.post(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization) const bot = await get.BotAuthorization(req.headers.authorization)
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 }) if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
const submit = await get.botSubmit.load(JSON.stringify({ id: req.query.id, date: req.query.date })) const submit = await get.botSubmit.load(
JSON.stringify({ id: req.query.id, date: req.query.date })
)
if (!submit) return ResponseWrapper(res, { code: 404 }) if (!submit) return ResponseWrapper(res, { code: 404 })
if(submit.state !== 0) return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' }) if (submit.state !== 0)
return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
const result = await update.approveBotSubmission(submit.id, submit.date) const result = await update.approveBotSubmission(submit.id, submit.date)
if (!result) return ResponseWrapper(res, { code: 400 }) if (!result) return ResponseWrapper(res, { code: 400 })
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date })) get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
get.bot.clear(req.query.id) get.bot.clear(req.query.id)
const embed = new EmbedBuilder().setTitle('승인').setColor(Colors.Green).setDescription(`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(submit.id, submit.date)})`).setTimestamp() const embed = new EmbedBuilder()
.setTitle('승인')
.setColor(Colors.Green)
.setDescription(
`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(
submit.id,
submit.date
)})`
)
.setTimestamp()
if (req.body.reviewer) embed.addFields({ name: '📃 정보', value: `심사자: ${req.body.reviewer}` }) if (req.body.reviewer) embed.addFields({ name: '📃 정보', value: `심사자: ${req.body.reviewer}` })
await webhookClients.internal.reviewLog.send({ embeds: [embed] }) await webhookClients.internal.reviewLog.send({ embeds: [embed] })
tracer.trace('botSubmits.approve', span => { tracer.trace('botSubmits.approve', (span) => {
span.setTag('id', submit.id) span.setTag('id', submit.id)
span.setTag('date', submit.date) span.setTag('date', submit.date)
span.setTag('reviewer', req.body.reviewer) span.setTag('reviewer', req.body.reviewer)

View File

@ -8,22 +8,53 @@ import { get, update } from '@utils/Query'
import { DiscordBot, webhookClients } from '@utils/DiscordBot' import { DiscordBot, webhookClients } from '@utils/DiscordBot'
import { BotSubmissionDenyReasonPresetsName, KoreanbotsEndPoints } from '@utils/Constants' import { BotSubmissionDenyReasonPresetsName, KoreanbotsEndPoints } from '@utils/Constants'
const DenyBotSubmit = RequestHandler() const DenyBotSubmit = RequestHandler().post(async (req: ApiRequest, res) => {
.post(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization) const bot = await get.BotAuthorization(req.headers.authorization)
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 }) if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
const submit = await get.botSubmit.load(JSON.stringify({ id: req.query.id, date: req.query.date })) const submit = await get.botSubmit.load(
JSON.stringify({ id: req.query.id, date: req.query.date })
)
if (!submit) return ResponseWrapper(res, { code: 404 }) if (!submit) return ResponseWrapper(res, { code: 404 })
if(submit.state !== 0) return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' }) if (submit.state !== 0)
return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
await update.denyBotSubmission(submit.id, submit.date, req.body.reason) await update.denyBotSubmission(submit.id, submit.date, req.body.reason)
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date })) get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
const embed = new EmbedBuilder().setTitle('거부').setColor(Colors.Red).setDescription(`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(submit.id, submit.date)})`).setTimestamp() const embed = new EmbedBuilder()
if(req.body.reviewer || req.body.reason) embed.addFields({name: '📃 정보', value: `${req.body.reason ? `사유: ${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`: ''}${req.body.reviewer ? `심사자: ${req.body.reviewer}` : ''}`}) .setTitle('거부')
.setColor(Colors.Red)
.setDescription(
`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(
submit.id,
submit.date
)})`
)
.setTimestamp()
if (req.body.reviewer || req.body.reason)
embed.addFields({
name: '📃 정보',
value: `${
req.body.reason
? `사유: ${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`
: ''
}${req.body.reviewer ? `심사자: ${req.body.reviewer}` : ''}`,
})
await webhookClients.internal.reviewLog.send({ embeds: [embed] }) await webhookClients.internal.reviewLog.send({ embeds: [embed] })
const openEmbed = new EmbedBuilder().setTitle('거부').setColor(Colors.Red).setDescription(`<@${submit.id}> (${submit.id})`).setTimestamp() const openEmbed = new EmbedBuilder()
if(req.body.reason) openEmbed.addFields({name: '📃 사유', value: `${req.body.reason ? `${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`: '없음'}`}) .setTitle('거부')
.setColor(Colors.Red)
.setDescription(`<@${submit.id}> (${submit.id})`)
.setTimestamp()
if (req.body.reason)
openEmbed.addFields({
name: '📃 사유',
value: `${
req.body.reason
? `${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`
: '없음'
}`,
})
await webhookClients.internal.openReviewLog.send({ embeds: [openEmbed] }) await webhookClients.internal.openReviewLog.send({ embeds: [openEmbed] })
tracer.trace('botSubmits.deny', span => { tracer.trace('botSubmits.deny', (span) => {
span.setTag('id', submit.id) span.setTag('id', submit.id)
span.setTag('date', submit.date) span.setTag('date', submit.date)
span.setTag('reviewer', req.body.reviewer) span.setTag('reviewer', req.body.reviewer)

View File

@ -5,11 +5,12 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import { get } from '@utils/Query' import { get } from '@utils/Query'
import { DiscordBot } from '@utils/DiscordBot' import { DiscordBot } from '@utils/DiscordBot'
const BotSubmit = RequestHandler() const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
.get(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization) const bot = await get.BotAuthorization(req.headers.authorization)
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 }) if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
const submit = await get.botSubmit.load(JSON.stringify({ id: req.query.id, date: req.query.date })) const submit = await get.botSubmit.load(
JSON.stringify({ id: req.query.id, date: req.query.date })
)
if (!submit) return ResponseWrapper(res, { code: 404 }) if (!submit) return ResponseWrapper(res, { code: 404 })
return ResponseWrapper(res, { code: 200, data: submit }) return ResponseWrapper(res, { code: 200, data: submit })
}) })

View File

@ -5,8 +5,7 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import { get } from '@utils/Query' import { get } from '@utils/Query'
import { DiscordBot } from '@utils/DiscordBot' import { DiscordBot } from '@utils/DiscordBot'
const BotSubmit = RequestHandler() const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
.get(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization) const bot = await get.BotAuthorization(req.headers.authorization)
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 }) if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
return ResponseWrapper(res, { code: 200, data: await get.botSubmitHistory(req.query.id) }) return ResponseWrapper(res, { code: 200, data: await get.botSubmitHistory(req.query.id) })

View File

@ -5,8 +5,7 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import { get } from '@utils/Query' import { get } from '@utils/Query'
import { DiscordBot } from '@utils/DiscordBot' import { DiscordBot } from '@utils/DiscordBot'
const BotSubmit = RequestHandler() const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
.get(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization) const bot = await get.BotAuthorization(req.headers.authorization)
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 }) if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
const submits = await get.botSubmitList() const submits = await get.botSubmitList()

View File

@ -3,8 +3,7 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import { get } from '@utils/Query' import { get } from '@utils/Query'
import { DiscordBot } from '@utils/DiscordBot' import { DiscordBot } from '@utils/DiscordBot'
const BotSubmits = RequestHandler() const BotSubmits = RequestHandler().get(async (req, res) => {
.get(async (req, res) => {
const bot = await get.BotAuthorization(req.headers.authorization) const bot = await get.BotAuthorization(req.headers.authorization)
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 }) if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
const submits = await get.botSubmitList() const submits = await get.botSubmitList()

View File

@ -1,8 +1,7 @@
import RequestHandler from '@utils/RequestHandler' import RequestHandler from '@utils/RequestHandler'
import ResponseWrapper from '@utils/ResponseWrapper' import ResponseWrapper from '@utils/ResponseWrapper'
const BotSubmits = RequestHandler() const BotSubmits = RequestHandler().get(async (_req, res) => {
.get(async (_req, res) => {
return ResponseWrapper(res, { code: 403, message: 'Private API' }) return ResponseWrapper(res, { code: 403, message: 'Private API' })
}) })

View File

@ -1,4 +1,3 @@
import { NextApiHandler } from 'next' import { NextApiHandler } from 'next'
import { getMainGuild } from '@utils/DiscordBot' import { getMainGuild } from '@utils/DiscordBot'
import RequestHandler from '@utils/RequestHandler' import RequestHandler from '@utils/RequestHandler'

View File

@ -9,8 +9,8 @@ import { Bot, Server, List } from '@types'
const Search = RequestHandler().get(async (req: ApiRequest, res) => { const Search = RequestHandler().get(async (req: ApiRequest, res) => {
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: 1 }) const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: 1 })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
}) })
if (!validated) return if (!validated) return
@ -27,7 +27,10 @@ const Search = RequestHandler().get(async (req: ApiRequest, res) => {
} catch { } catch {
return ResponseWrapper(res, { code: 400, message: '검색 문법이 잘못되었습니다.' }) return ResponseWrapper(res, { code: 400, message: '검색 문법이 잘못되었습니다.' })
} }
return ResponseWrapper<{ bots: Bot[], servers: Server[] }>(res, { code: 200, data: { bots: botResult?.data || [], servers: serverResult?.data || [] } }) return ResponseWrapper<{ bots: Bot[]; servers: Server[] }>(res, {
code: 200,
data: { bots: botResult?.data || [], servers: serverResult?.data || [] },
})
}) })
interface ApiRequest extends NextApiRequest { interface ApiRequest extends NextApiRequest {

View File

@ -8,9 +8,12 @@ import { SearchQuerySchema } from '@utils/Yup'
import { Bot, List } from '@types' import { Bot, List } from '@types'
const SearchBots = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => { const SearchBots = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: req.query.page }) const validated = await SearchQuerySchema.validate({
.then(el => el) q: req.query.q || req.query.query,
.catch(e => { page: req.query.page,
})
.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

View File

@ -8,9 +8,12 @@ import { SearchQuerySchema } from '@utils/Yup'
import { Server, List } from '@types' import { Server, List } from '@types'
const SearchServers = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => { const SearchServers = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: req.query.page }) const validated = await SearchQuerySchema.validate({
.then(el => el) q: req.query.q || req.query.query,
.catch(e => { page: req.query.page,
})
.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

View File

@ -5,9 +5,22 @@ import { EmbedBuilder, RESTJSONErrorCodes } from 'discord.js'
import { CaptchaVerify, get, put, remove, update } from '@utils/Query' import { CaptchaVerify, get, put, remove, update } from '@utils/Query'
import ResponseWrapper from '@utils/ResponseWrapper' import ResponseWrapper from '@utils/ResponseWrapper'
import { checkToken } from '@utils/Csrf' import { checkToken } from '@utils/Csrf'
import { AddServerSubmitSchema, AddServerSubmit, CsrfCaptcha, ManageServerSchema, ManageServer } from '@utils/Yup' import {
AddServerSubmitSchema,
AddServerSubmit,
CsrfCaptcha,
ManageServerSchema,
ManageServer,
} from '@utils/Yup'
import RequestHandler from '@utils/RequestHandler' import RequestHandler from '@utils/RequestHandler'
import { checkUserFlag, diff, inspect, makeDiscordCodeblock, objectDiff, serialize } from '@utils/Tools' import {
checkUserFlag,
diff,
inspect,
makeDiscordCodeblock,
objectDiff,
serialize,
} from '@utils/Tools'
import { DiscordBot, discordLog } from '@utils/DiscordBot' import { DiscordBot, discordLog } from '@utils/DiscordBot'
import { KoreanbotsEndPoints } from '@utils/Constants' import { KoreanbotsEndPoints } from '@utils/Constants'
@ -19,7 +32,7 @@ const patchLimiter = rateLimit({
skip: (_req, res) => { skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global') res.removeHeader('X-RateLimit-Global')
return false return false
} },
}) })
const Servers = RequestHandler() const Servers = RequestHandler()
.get(async (req: GetApiRequest, res) => { .get(async (req: GetApiRequest, res) => {
@ -36,8 +49,8 @@ const Servers = RequestHandler()
if (!csrfValidated) return if (!csrfValidated) return
const validated = await AddServerSubmitSchema.validate(req.body, { abortEarly: false }) const validated = await AddServerSubmitSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
@ -49,7 +62,7 @@ const Servers = RequestHandler()
if (result === 1) if (result === 1)
return ResponseWrapper(res, { return ResponseWrapper(res, {
code: 400, code: 400,
message: '이미 등록된 서버 입니다.' message: '이미 등록된 서버 입니다.',
}) })
else if (result === 2) else if (result === 2)
return ResponseWrapper(res, { return ResponseWrapper(res, {
@ -57,7 +70,7 @@ const Servers = RequestHandler()
message: '봇이 초대되지 않았습니다.', message: '봇이 초대되지 않았습니다.',
errors: [ errors: [
'서버에 봇이 초대되지 않았습니다.', '서버에 봇이 초대되지 않았습니다.',
'이미 봇을 초대하셨다면, 잠시 후 다시 시도해주세요.' '이미 봇을 초대하셨다면, 잠시 후 다시 시도해주세요.',
], ],
}) })
else if (result === 3) else if (result === 3)
@ -66,7 +79,7 @@ const Servers = RequestHandler()
message: '서버의 관리자가 아닙니다.', message: '서버의 관리자가 아닙니다.',
errors: [ errors: [
'해당 서버를 등록할 권한이 없습니다.', '해당 서버를 등록할 권한이 없습니다.',
'서버에서 관리자 권한이 있으신지 확인해주세요.' '서버에서 관리자 권한이 있으신지 확인해주세요.',
], ],
}) })
else if (result === 4) else if (result === 4)
@ -75,14 +88,21 @@ const Servers = RequestHandler()
message: '올바르지 않은 초대 코드 입니다.', message: '올바르지 않은 초대 코드 입니다.',
errors: [ errors: [
'올바른 초대코드를 입력하셨는지 확인해주세요.', '올바른 초대코드를 입력하셨는지 확인해주세요.',
'만료되지 않는 초대코드인지 확인해주세요.' '만료되지 않는 초대코드인지 확인해주세요.',
], ],
}) })
get.user.clear(user) get.user.clear(user)
await discordLog('SERVER/SUBMIT', user, new EmbedBuilder().setDescription(`[${req.query.id}](${KoreanbotsEndPoints.URL.server(req.query.id)})`), { await discordLog(
'SERVER/SUBMIT',
user,
new EmbedBuilder().setDescription(
`[${req.query.id}](${KoreanbotsEndPoints.URL.server(req.query.id)})`
),
{
content: inspect(serialize(validated)), content: inspect(serialize(validated)),
format: 'js' format: 'js',
}) }
)
return ResponseWrapper(res, { code: 200, data: result }) return ResponseWrapper(res, { code: 200, data: result })
}) })
@ -92,72 +112,118 @@ const Servers = RequestHandler()
const server = await get.server.load(req.query.id) const server = await get.server.load(req.query.id)
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버 입니다.' }) if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버 입니다.' })
const data = await get.serverData(req.query.id) const data = await get.serverData(req.query.id)
if((!data || server.state === 'unreachable') && (await DiscordBot.fetchInvite(server.invite).catch((e) => e.code !== RESTJSONErrorCodes.UnknownInvite))) return ResponseWrapper(res, { code: 400, message: '해당 서버의 정보를 불러올 수 없습니다.', errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'] }) if (
(!data || server.state === 'unreachable') &&
(await DiscordBot.fetchInvite(server.invite).catch(
(e) => e.code !== RESTJSONErrorCodes.UnknownInvite
))
)
return ResponseWrapper(res, {
code: 400,
message: '해당 서버의 정보를 불러올 수 없습니다.',
errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'],
})
if (![data.owner, ...data.admins].includes(user)) return ResponseWrapper(res, { code: 403 }) if (![data.owner, ...data.admins].includes(user)) return ResponseWrapper(res, { code: 403 })
const userInfo = await get.user.load(user) const userInfo = await get.user.load(user)
if(['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 서버는 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] }) if (['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff'))
return ResponseWrapper(res, {
code: 403,
message: '해당 서버는 수정할 수 없습니다.',
errors: ['오류라고 생각되면 문의해주세요.'],
})
const csrfValidated = checkToken(req, res, req.body._csrf) const csrfValidated = checkToken(req, res, req.body._csrf)
if (!csrfValidated) return if (!csrfValidated) return
const captcha = await CaptchaVerify(req.body._captcha) const captcha = await CaptchaVerify(req.body._captcha)
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' }) if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
if(req.body.name !== server.name) return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' }) if (req.body.name !== server.name)
return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' })
await remove.server(server.id) await remove.server(server.id)
get.user.clear(user) get.user.clear(user)
await discordLog('SERVER/DELETE', user, (new EmbedBuilder().setDescription(`${server.name} - [${server.id}](${KoreanbotsEndPoints.URL.bot(server.id)}))`)), await discordLog(
'SERVER/DELETE',
user,
new EmbedBuilder().setDescription(
`${server.name} - [${server.id}](${KoreanbotsEndPoints.URL.bot(server.id)}))`
),
{ {
content: inspect(server), content: inspect(server),
format: 'js' format: 'js',
} }
) )
return ResponseWrapper(res, { code: 200, message: '성공적으로 삭제했습니다.' }) return ResponseWrapper(res, { code: 200, message: '성공적으로 삭제했습니다.' })
}) })
.patch(patchLimiter).patch(async (req: PatchApiRequest, res) => { .patch(patchLimiter)
.patch(async (req: PatchApiRequest, res) => {
const server = await get.server.load(req.query.id) const server = await get.server.load(req.query.id)
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' }) if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
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 userInfo = await get.user.load(user) const userInfo = await get.user.load(user)
const data = await get.serverData(req.query.id) const data = await get.serverData(req.query.id)
if(!data || server.state === 'unreachable') return ResponseWrapper(res, { code: 400, message: '해당 서버의 정보를 불러올 수 없습니다.', errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'] }) if (!data || server.state === 'unreachable')
if(![data.owner, ...data.admins].includes(user) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403 }) return ResponseWrapper(res, {
if(['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 서버는 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] }) code: 400,
message: '해당 서버의 정보를 불러올 수 없습니다.',
errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'],
})
if (![data.owner, ...data.admins].includes(user) && !checkUserFlag(userInfo?.flags, 'staff'))
return ResponseWrapper(res, { code: 403 })
if (['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff'))
return ResponseWrapper(res, {
code: 403,
message: '해당 서버는 수정할 수 없습니다.',
errors: ['오류라고 생각되면 문의해주세요.'],
})
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 ManageServerSchema.validate(req.body, { abortEarly: false }) const validated = await ManageServerSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
if (!validated) return if (!validated) return
const invite = await DiscordBot.fetchInvite(validated.invite).catch(() => null) const invite = await DiscordBot.fetchInvite(validated.invite).catch(() => null)
if(invite?.guild.id !== server.id || invite.expiresAt) return ResponseWrapper(res, { code: 400, message: '올바르지 않은 초대코드입니다.', errors: ['입력하신 초대코드가 올바르지 않습니다. 올바른 초대코드를 입력했는지 다시 한 번 확인해주세요.', '만료되지 않는 초대코드인지 확인해주세요.'] }) if (invite?.guild.id !== server.id || invite.expiresAt)
return ResponseWrapper(res, {
code: 400,
message: '올바르지 않은 초대코드입니다.',
errors: [
'입력하신 초대코드가 올바르지 않습니다. 올바른 초대코드를 입력했는지 다시 한 번 확인해주세요.',
'만료되지 않는 초대코드인지 확인해주세요.',
],
})
const result = await update.server(req.query.id, validated) const result = await update.server(req.query.id, validated)
if (result === 0) return ResponseWrapper(res, { code: 400 }) if (result === 0) return ResponseWrapper(res, { code: 400 })
else { else {
get.server.clear(req.query.id) get.server.clear(req.query.id)
const embed = new EmbedBuilder().setDescription(`${server.name} - ([${server.id}](${KoreanbotsEndPoints.URL.server(server.id)}))`) const embed = new EmbedBuilder().setDescription(
`${server.name} - ([${server.id}](${KoreanbotsEndPoints.URL.server(server.id)}))`
)
const diffData = objectDiff( const diffData = objectDiff(
{ intro: server.intro, invite: server.invite, category: JSON.stringify(server.category) }, { intro: server.intro, invite: server.invite, category: JSON.stringify(server.category) },
{ intro: validated.intro, invite: validated.invite, category: JSON.stringify(validated.category) },
)
diffData.forEach(d => {
embed.addFields({name: d[0], value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff')
})
})
await discordLog('SERVER/EDIT', user, embed,
{ {
content: `--- 설명\n${diff(server.desc, validated.desc, true)}`, intro: validated.intro,
format: 'diff' invite: validated.invite,
category: JSON.stringify(validated.category),
} }
) )
diffData.forEach((d) => {
embed.addFields({
name: d[0],
value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff'),
})
})
await discordLog('SERVER/EDIT', user, embed, {
content: `--- 설명\n${diff(server.desc, validated.desc, true)}`,
format: 'diff',
})
return ResponseWrapper(res, { code: 200 }) return ResponseWrapper(res, { code: 200 })
} }
}) })
interface GetApiRequest extends NextApiRequest { interface GetApiRequest extends NextApiRequest {
@ -175,7 +241,7 @@ interface PatchApiRequest extends GetApiRequest {
} }
interface DeleteApiRequest extends GetApiRequest { interface DeleteApiRequest extends GetApiRequest {
body: CsrfCaptcha & { name: string } | null body: (CsrfCaptcha & { name: string }) | null
} }
export default Servers export default Servers

View File

@ -4,8 +4,7 @@ import RequestHandler from '@utils/RequestHandler'
import ResponseWrapper from '@utils/ResponseWrapper' import ResponseWrapper from '@utils/ResponseWrapper'
import { get } from '@utils/Query' import { get } from '@utils/Query'
const ServerOwners = RequestHandler() const ServerOwners = RequestHandler().get(async (req: GetApiRequest, res) => {
.get(async (req: GetApiRequest, res) => {
const owners = await get.serverOwners(req.query.id) const owners = await get.serverOwners(req.query.id)
if (!owners) return ResponseWrapper(res, { code: 404 }) if (!owners) return ResponseWrapper(res, { code: 404 })
return ResponseWrapper(res, { code: 200, data: owners }) return ResponseWrapper(res, { code: 200, data: owners })

View File

@ -18,10 +18,11 @@ const limiter = rateLimit({
skip: (_req, res) => { skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global') res.removeHeader('X-RateLimit-Global')
return false return false
} },
}) })
const ServerReport = RequestHandler().post(limiter) const ServerReport = RequestHandler()
.post(limiter)
.post(async (req: PostApiRequest, res) => { .post(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token) const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 }) if (!user) return ResponseWrapper(res, { code: 401 })
@ -31,18 +32,21 @@ const ServerReport = RequestHandler().post(limiter)
if (!csrfValidated) return if (!csrfValidated) return
if (!req.body) return ResponseWrapper(res, { code: 400 }) if (!req.body) return ResponseWrapper(res, { code: 400 })
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false }) const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .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
await webhookClients.internal.reportChannel.send({ threadName: `서버-${server.id}`, content: `Reported by <@${user}> (${user})\nReported **${server.name}** (${server.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``, allowedMentions: { parse: ['users'] }}) await webhookClients.internal.reportChannel.send({
threadName: `서버-${server.id}`,
content: `Reported by <@${user}> (${user})\nReported **${server.name}** (${server.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``,
allowedMentions: { parse: ['users'] },
})
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' }) return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
}) })
interface PostApiRequest extends NextApiRequest { interface PostApiRequest extends NextApiRequest {
body: Report | null body: Report | null
query: { query: {

View File

@ -14,13 +14,21 @@ const ServerVote = RequestHandler()
const server = await get.ServerAuthorization(req.headers.authorization) const server = await get.ServerAuthorization(req.headers.authorization)
if (!server) return ResponseWrapper(res, { code: 401 }) if (!server) return ResponseWrapper(res, { code: 401 })
if (req.query.id !== server) return ResponseWrapper(res, { code: 403 }) if (req.query.id !== server) return ResponseWrapper(res, { code: 403 })
const userID = await Yup.string().required().label('userID').validate(req.query.userID).then(el => el).catch(e => { const userID = await Yup.string()
.required()
.label('userID')
.validate(req.query.userID)
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })
if (!userID) return ResponseWrapper(res, { code: 400 }) if (!userID) return ResponseWrapper(res, { code: 400 })
const result = await get.vote(userID, server, 'server') const result = await get.vote(userID, server, 'server')
return ResponseWrapper(res, { code: 200, data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result } }) return ResponseWrapper(res, {
code: 200,
data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result },
})
}) })
.post(async (req: PostApiRequest, res) => { .post(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token) const user = await get.Authorization(req.cookies.token)
@ -42,13 +50,12 @@ const ServerVote = RequestHandler()
type: WebhookType.HeartChange, type: WebhookType.HeartChange,
before: server.votes, before: server.votes,
after: server.votes + 1, after: server.votes + 1,
userId: user userId: user,
}, },
timestamp: Date.now() timestamp: Date.now(),
}) })
return ResponseWrapper(res, { code: 200 }) return ResponseWrapper(res, { code: 200 })
} } else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
}) })
interface ApiRequest extends NextApiRequest { interface ApiRequest extends NextApiRequest {

View File

@ -17,10 +17,11 @@ const limiter = rateLimit({
skip: (_req, res) => { skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global') res.removeHeader('X-RateLimit-Global')
return false return false
} },
}) })
const UserReport = RequestHandler().post(limiter) const UserReport = RequestHandler()
.post(limiter)
.post(async (req: PostApiRequest, res) => { .post(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token) const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 }) if (!user) return ResponseWrapper(res, { code: 401 })
@ -30,18 +31,27 @@ const UserReport = RequestHandler().post(limiter)
if (!csrfValidated) return if (!csrfValidated) return
if (!req.body) return ResponseWrapper(res, { code: 400 }) if (!req.body) return ResponseWrapper(res, { code: 400 })
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false }) const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
.then(el => el) .then((el) => el)
.catch(e => { .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
await webhookClients.internal.reportChannel.send({ threadName: `유저-${userInfo.id}`, content: `Reported by <@${user}> (${user})\nReported **${userInfo.tag === '0' ? userInfo.globalName + ' @' + userInfo.username : userInfo.username + '#' + userInfo.tag}** <@${userInfo.id}> (${userInfo.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``, allowedMentions: { parse: ['users'] }}) await webhookClients.internal.reportChannel.send({
threadName: `유저-${userInfo.id}`,
content: `Reported by <@${user}> (${user})\nReported **${
userInfo.tag === '0'
? userInfo.globalName + ' @' + userInfo.username
: userInfo.username + '#' + userInfo.tag
}** <@${userInfo.id}> (${userInfo.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${
req.body.description
}\`\`\``,
allowedMentions: { parse: ['users'] },
})
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' }) return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
}) })
interface PostApiRequest extends NextApiRequest { interface PostApiRequest extends NextApiRequest {
body: Report | null body: Report | null
query: { query: {

View File

@ -20,8 +20,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
scale, scale,
icon, icon,
}) })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })

View File

@ -20,8 +20,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
scale, scale,
icon, icon,
}) })
.then(el => el) .then((el) => el)
.catch(e => { .catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors }) ResponseWrapper(res, { code: 400, errors: e.errors })
return null return null
}) })

View File

@ -45,7 +45,10 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
const router = useRouter() const router = useRouter()
async function submitBot(value: ManageBot) { async function submitBot(value: ManageBot) {
const res = await Fetch(`/bots/${bot.id}`, { method: 'PATCH', body: JSON.stringify(cleanObject<ManageBot>(value)) }) const res = await Fetch(`/bots/${bot.id}`, {
method: 'PATCH',
body: JSON.stringify(cleanObject<ManageBot>(value)),
})
setData(res) setData(res)
} }
@ -56,15 +59,23 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
} }
if (!bot) return <NotFound /> if (!bot) return <NotFound />
if(!user) return <Login> if (!user)
return (
<Login>
<NextSeo title='봇 정보 수정하기' description='봇의 정보를 수정합니다.' /> <NextSeo title='봇 정보 수정하기' description='봇의 정보를 수정합니다.' />
</Login> </Login>
if(!(bot.owners as User[]).find(el => el.id === user.id) && !checkUserFlag(user.flags, 'staff')) return <Forbidden /> )
if (
!(bot.owners as User[]).find((el) => el.id === user.id) &&
!checkUserFlag(user.flags, 'staff')
)
return <Forbidden />
return ( return (
<Container paddingTop className='pt-5 pb-10'> <Container paddingTop className='pb-10 pt-5'>
<NextSeo title={`${bot.name} 수정하기`} description='봇의 정보를 수정합니다.' /> <NextSeo title={`${bot.name} 수정하기`} description='봇의 정보를 수정합니다.' />
<h1 className='text-3xl font-bold mb-8'> </h1> <h1 className='mb-8 text-3xl font-bold'> </h1>
<Formik initialValues={cleanObject({ <Formik
initialValues={cleanObject({
agree: false, agree: false,
id: bot.id, id: bot.id,
prefix: bot.prefix, prefix: bot.prefix,
@ -76,88 +87,191 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
url: bot.url, url: bot.url,
git: bot.git, git: bot.git,
discord: bot.discord, discord: bot.discord,
_csrf: csrfToken _csrf: csrfToken,
})} })}
validationSchema={ManageBotSchema} validationSchema={ManageBotSchema}
onSubmit={submitBot}> onSubmit={submitBot}
>
{({ errors, touched, values, setFieldTouched, setFieldValue }) => ( {({ errors, touched, values, setFieldTouched, setFieldValue }) => (
<Form> <Form>
<div className='md:flex text-center md:text-left'> <div className='text-center md:flex md:text-left'>
<DiscordAvatar userID={bot.id} className='md:mx-1 mx-auto rounded-full'/> <DiscordAvatar userID={bot.id} className='mx-auto rounded-full md:mx-1' />
<div className='md:w-2/3 px-8 py-6'> <div className='px-8 py-6 md:w-2/3'>
<h1 className='text-3xl font-bold'>{bot.name}#{bot.tag}</h1> <h1 className='text-3xl font-bold'>
{bot.name}#{bot.tag}
</h1>
<h2>ID: {bot.id}</h2> <h2>ID: {bot.id}</h2>
</div> </div>
</div> </div>
{ {data ? (
data ? data.code === 200 ? <div className='mt-4'> data.code === 200 ? (
<div className='mt-4'>
<Redirect to={makeBotURL(bot)}> <Redirect to={makeBotURL(bot)}>
<Message type='success'> <Message type='success'>
<h2 className='text-lg font-extrabold'> .</h2> <h2 className='text-lg font-extrabold'> .</h2>
<p> !</p> <p> !</p>
</Message> </Message>
</Redirect> </Redirect>
</div>
</div> : <div className='mt-4'> ) : (
<div className='mt-4'>
<Message type='error'> <Message type='error'>
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2> <h2 className='text-lg font-extrabold'>
<ul className='list-disc list-inside'> {data.message || '오류가 발생했습니다.'}
</h2>
<ul className='list-inside list-disc'>
{data.errors?.map((el, n) => <li key={n}>{el}</li>)} {data.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul> </ul>
</Message> </Message>
</div> : '' </div>
} )
<Label For='prefix' label='접두사' labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)' error={errors.prefix && touched.prefix ? errors.prefix : null} short required> ) : (
''
)}
<Label
For='prefix'
label='접두사'
labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)'
error={errors.prefix && touched.prefix ? errors.prefix : null}
short
required
>
<Input name='prefix' placeholder='!' /> <Input name='prefix' placeholder='!' />
</Label> </Label>
<Label For='library' label='라이브러리' labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.' short required error={errors.library && touched.library ? errors.library : null}> <Label
<Select value={{ label: bot.lib, value: bot.lib }} options={library.map(el=> ({ label: el, value: el }))} handleChange={(value) => setFieldValue('library', value.value)} handleTouch={() => setFieldTouched('library', true)} /> For='library'
label='라이브러리'
labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.'
short
required
error={errors.library && touched.library ? errors.library : null}
>
<Select
value={{ label: bot.lib, value: bot.lib }}
options={library.map((el) => ({ label: el, value: el }))}
handleChange={(value) => setFieldValue('library', value.value)}
handleTouch={() => setFieldTouched('library', true)}
/>
</Label> </Label>
<Label For='category' label='카테고리' labelDesc='봇에 해당되는 카테고리를 선택해주세요' required error={errors.category && touched.category ? errors.category as string : null}> <Label
<Selects options={botCategories.map(el=> ({ label: el, value: el }))} handleChange={(value) => { For='category'
setFieldValue('category', value.map(v=> v.value)) label='카테고리'
}} handleTouch={() => setFieldTouched('category', true)} values={values.category as string[]} setValues={(value) => setFieldValue('category', value)} /> labelDesc='봇에 해당되는 카테고리를 선택해주세요'
<span className='text-gray-400 mt-1 text-sm'> 3 . . <strong> .</strong></span> required
error={errors.category && touched.category ? (errors.category as string) : null}
>
<Selects
options={botCategories.map((el) => ({ label: el, value: el }))}
handleChange={(value) => {
setFieldValue(
'category',
value.map((v) => v.value)
)
}}
handleTouch={() => setFieldTouched('category', true)}
values={values.category as string[]}
setValues={(value) => setFieldValue('category', value)}
/>
<span className='mt-1 text-sm text-gray-400'>
3 . .{' '}
<strong> .</strong>
</span>
</Label> </Label>
<Divider /> <Divider />
<Label For='website' label='웹사이트' labelDesc='봇의 웹사이트를 작성해주세요.' error={errors.website && touched.website ? errors.website : null}> <Label
For='website'
label='웹사이트'
labelDesc='봇의 웹사이트를 작성해주세요.'
error={errors.website && touched.website ? errors.website : null}
>
<Input name='website' placeholder='https://koreanbots.dev' /> <Input name='website' placeholder='https://koreanbots.dev' />
</Label> </Label>
<Label For='git' label='Git URL' labelDesc='봇 소스코드의 Git 주소를 입력해주세요 (오픈소스인 경우)' error={errors.git && touched.git ? errors.git : null}> <Label
For='git'
label='Git URL'
labelDesc='봇 소스코드의 Git 주소를 입력해주세요 (오픈소스인 경우)'
error={errors.git && touched.git ? errors.git : null}
>
<Input name='git' placeholder='https://github.com/koreanbots/koreanbots' /> <Input name='git' placeholder='https://github.com/koreanbots/koreanbots' />
</Label> </Label>
<Label For='inviteLink' label='초대링크' labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.' error={errors.url && touched.url ? errors.url : null}> <Label
<Input name='url' placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0' /> For='inviteLink'
<span className='text-gray-400 mt-1 text-sm'> label='초대링크'
labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.'
error={errors.url && touched.url ? errors.url : null}
>
<Input
name='url'
placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0'
/>
<span className='mt-1 text-sm text-gray-400'>
<Link <Link
href='/calculator' href='/calculator'
rel='noreferrer' rel='noreferrer'
target='_blank' target='_blank'
className='text-blue-500 hover:text-blue-400'> className='text-blue-500 hover:text-blue-400'
>
</Link> ! </Link>
!
</span> </span>
</Label> </Label>
<Label For='discord' label='지원 디스코드 서버' labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)' error={errors.discord && touched.discord ? errors.discord : null} short> <Label
For='discord'
label='지원 디스코드 서버'
labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)'
error={errors.discord && touched.discord ? errors.discord : null}
short
>
<div className='flex items-center'> <div className='flex items-center'>
discord.gg/<Input name='discord' placeholder='JEh53MQ' /> discord.gg/
<Input name='discord' placeholder='JEh53MQ' />
</div> </div>
</Label> </Label>
<Divider /> <Divider />
<Label For='intro' label='봇 소개' labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required> <Label
For='intro'
label='봇 소개'
labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)'
error={errors.intro && touched.intro ? errors.intro : null}
required
>
<Input name='intro' placeholder='국내 봇을 한 곳에서.' /> <Input name='intro' placeholder='국내 봇을 한 곳에서.' />
</Label> </Label>
<Label For='intro' label='봇 설명' labelDesc={<> ! ( 1500)<br/> !</>} error={errors.desc && touched.desc ? errors.desc : null} required> <Label
<TextArea name='desc' placeholder='봇에 대해 최대한 자세히 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.desc} setValue={(value) => setFieldValue('desc', value)} max={1500} /> For='intro'
label='봇 설명'
labelDesc={
<>
! ( 1500)
<br />
!
</>
}
error={errors.desc && touched.desc ? errors.desc : null}
required
>
<TextArea
name='desc'
placeholder='봇에 대해 최대한 자세히 설명해주세요!'
theme={theme === 'dark' ? 'dark' : 'light'}
value={values.desc}
setValue={(value) => setFieldValue('desc', value)}
max={1500}
/>
</Label> </Label>
<Label For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다'> <Label
For='preview'
label='설명 미리보기'
labelDesc='다음 결과는 실제와 다를 수 있습니다'
>
<Segment> <Segment>
<Markdown text={values.desc} /> <Markdown text={values.desc} />
</Segment> </Segment>
</Label> </Label>
<Divider /> <Divider />
<p className='text-base mt-2 mb-5'> <p className='mb-5 mt-2 text-base'>
<span className='text-red-500 font-semibold'> *</span> = <span className='font-semibold text-red-500'> *</span> =
</p> </p>
<Button type='submit' onClick={() => window.scrollTo({ top: 0 })}> <Button type='submit' onClick={() => window.scrollTo({ top: 0 })}>
<> <>
@ -167,24 +281,41 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
</Form> </Form>
)} )}
</Formik> </Formik>
{ {(checkUserFlag(user.flags, 'staff') || (bot.owners as User[])[0].id === user.id) && (
(checkUserFlag(user.flags, 'staff') || (bot.owners as User[])[0].id === user.id) && <div className='py-4'> <div className='py-4'>
<Divider /> <Divider />
<h2 className='text-2xl font-semibold pb-2'></h2> <h2 className='pb-2 text-2xl font-semibold'></h2>
<Segment> <Segment>
<div className='lg:flex items-center'> <div className='items-center lg:flex'>
<div className='grow py-1'> <div className='grow py-1'>
<h3 className='text-lg font-semibold'> </h3> <h3 className='text-lg font-semibold'> </h3>
<p className='text-gray-400'> .</p> <p className='text-gray-400'> .</p>
</div> </div>
<Button onClick={() => setAdminModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-user-cog' /> </Button> <Button
<Modal full header='관리자 수정' isOpen={adminModal} dark={theme === 'dark'} onClose={() => setAdminModal(false)} closeIcon> onClick={() => setAdminModal(true)}
<Formik initialValues={{ owners: (bot.owners as User[]), id: '', _captcha: '' }} onSubmit={async (v) => { className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
const res = await Fetch(`/bots/${bot.id}/owners`, { method: 'PATCH', body: JSON.stringify({ >
<i className='fas fa-user-cog' />
</Button>
<Modal
full
header='관리자 수정'
isOpen={adminModal}
dark={theme === 'dark'}
onClose={() => setAdminModal(false)}
closeIcon
>
<Formik
initialValues={{ owners: bot.owners as User[], id: '', _captcha: '' }}
onSubmit={async (v) => {
const res = await Fetch(`/bots/${bot.id}/owners`, {
method: 'PATCH',
body: JSON.stringify({
_captcha: v._captcha, _captcha: v._captcha,
_csrf: csrfToken, _csrf: csrfToken,
owners: v.owners.map(el => el.id) owners: v.owners.map((el) => el.id),
}) }) }),
})
if (res.code === 200) { if (res.code === 200) {
alert('성공적으로 수정했습니다.') alert('성공적으로 수정했습니다.')
router.push(makeBotURL(bot)) router.push(makeBotURL(bot))
@ -192,38 +323,64 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
alert(res.message) alert(res.message)
setAdminModal(false) setAdminModal(false)
} }
}}> }}
{ >
({ values, setFieldValue }) => <Form> {({ values, setFieldValue }) => (
<Form>
<Message type='warning'> <Message type='warning'>
<p> . .</p> <p>
.
.
</p>
</Message> </Message>
<div className='py-4'> <div className='py-4'>
<h2 className='text-md my-1'> ID를 .</h2> <h2 className='text-md my-1'> ID를 .</h2>
<div className='flex flex-wrap'> <div className='flex flex-wrap'>
{ {(values.owners as User[]).map((el, n) => (
(values.owners as User[]).map((el, n) => <Tag className='flex items-center' text={<> <Tag
<DiscordAvatar userID={el.id} size={128} className='w-6 h-6 mr-1 rounded-full' /> {el.tag === '0' ? `${el.globalName} (@${el.username})` : `${el.username}#${el.tag}`} className='flex items-center'
{ text={
n !== 0 && <button className='ml-0.5 hover:text-red-500' onClick={() => { <>
setFieldValue('owners', (() => { <DiscordAvatar
userID={el.id}
size={128}
className='mr-1 h-6 w-6 rounded-full'
/>{' '}
{el.tag === '0'
? `${el.globalName} (@${el.username})`
: `${el.username}#${el.tag}`}
{n !== 0 && (
<button
className='ml-0.5 hover:text-red-500'
onClick={() => {
setFieldValue(
'owners',
(() => {
const arr = [...values.owners] const arr = [...values.owners]
arr.splice(n, 1) arr.splice(n, 1)
return arr return arr
})()) })()
}}> )
}}
>
<i className='fas fa-times' /> <i className='fas fa-times' />
</button> </button>
)}
</>
} }
</>} key={el.id} />) key={el.id}
} />
))}
</div> </div>
<div className='flex'> <div className='flex'>
<div className='grow pr-2'> <div className='grow pr-2'>
<Input name='id' placeholder='추가할 유저 ID' /> <Input name='id' placeholder='추가할 유저 ID' />
</div> </div>
<Button className='w-16 bg-discord-blurple' onClick={async () => { <Button
if(values.owners.find(el => el.id === values.id)) return alert('이미 존재하는 유저입니다.') className='w-16 bg-discord-blurple'
onClick={async () => {
if (values.owners.find((el) => el.id === values.id))
return alert('이미 존재하는 유저입니다.')
const user = await getUser(values.id) const user = await getUser(values.id)
const arr = [...values.owners] const arr = [...values.owners]
if (!user) return alert('올바르지 않은 유저입니다.') if (!user) return alert('올바르지 않은 유저입니다.')
@ -232,32 +389,63 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
setFieldValue('owners', arr) setFieldValue('owners', arr)
setFieldValue('id', '') setFieldValue('id', '')
} }
}}> }}
>
<i className='fas fa-user-plus text-white' /> <i className='fas fa-user-plus text-white' />
</Button> </Button>
</div> </div>
</div> </div>
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} /> <Captcha
<Button disabled={!values._captcha} className={`mt-2 bg-red-500 text-white ${!values._captcha ? 'opacity-80' : 'hover:opacity-80'}`} type='submit'><i className='fas fa-save text-sm' /> </Button> dark={theme === 'dark'}
onVerify={(k) => setFieldValue('_captcha', k)}
/>
<Button
disabled={!values._captcha}
className={`mt-2 bg-red-500 text-white ${
!values._captcha ? 'opacity-80' : 'hover:opacity-80'
}`}
type='submit'
>
<i className='fas fa-save text-sm' />
</Button>
</Form> </Form>
} )}
</Formik> </Formik>
</Modal> </Modal>
</div> </div>
<Divider /> <Divider />
<div className='lg:flex items-center'> <div className='items-center lg:flex'>
<div className='grow py-1'> <div className='grow py-1'>
<h3 className='text-lg font-semibold'> </h3> <h3 className='text-lg font-semibold'> </h3>
<p className='text-gray-400'> . .</p> <p className='text-gray-400'>
. .
</p>
</div> </div>
<Button onClick={() => setTransferModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-exchange-alt' /> </Button> <Button
<Modal full header={`${bot.name} 소유권 이전하기`} isOpen={transferModal} dark={theme === 'dark'} onClose={() => setTransferModal(false)} closeIcon> onClick={() => setTransferModal(true)}
<Formik initialValues={{ ownerID: '', name: '', _captcha: '' }} onSubmit={async (v) => { className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
const res = await Fetch(`/bots/${bot.id}/owners`, { method: 'PATCH', body: JSON.stringify({ >
<i className='fas fa-exchange-alt' />
</Button>
<Modal
full
header={`${bot.name} 소유권 이전하기`}
isOpen={transferModal}
dark={theme === 'dark'}
onClose={() => setTransferModal(false)}
closeIcon
>
<Formik
initialValues={{ ownerID: '', name: '', _captcha: '' }}
onSubmit={async (v) => {
const res = await Fetch(`/bots/${bot.id}/owners`, {
method: 'PATCH',
body: JSON.stringify({
_captcha: v._captcha, _captcha: v._captcha,
_csrf: csrfToken, _csrf: csrfToken,
owners: [ v.ownerID ] owners: [v.ownerID],
}) }) }),
})
if (res.code === 200) { if (res.code === 200) {
alert('성공적으로 소유권을 이전했습니다.') alert('성공적으로 소유권을 이전했습니다.')
router.push('/') router.push('/')
@ -265,62 +453,119 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
alert(res.message) alert(res.message)
setTransferModal(false) setTransferModal(false)
} }
}}> }}
{ >
({ values, setFieldValue }) => <Form> {({ values, setFieldValue }) => (
<Form>
<Message type='warning'> <Message type='warning'>
<h2 className='text-2xl font-bold'>!</h2> <h2 className='text-2xl font-bold'>!</h2>
<p> , .</p> <p>
,
.
</p>
</Message> </Message>
<div className='py-4'> <div className='py-4'>
<h2 className='text-md my-1'> ID를 .</h2> <h2 className='text-md my-1'> ID를 .</h2>
<Input name='ownerID' placeholder='이전할 유저 ID' /> <Input name='ownerID' placeholder='이전할 유저 ID' />
<Divider /> <Divider />
<h2 className='text-md my-1'> <strong>{bot.name}</strong>{getJosaPicker('을')(bot.name)} .</h2> <h2 className='text-md my-1'>
<strong>{bot.name}</strong>
{getJosaPicker('을')(bot.name)} .
</h2>
<Input name='name' placeholder={bot.name} /> <Input name='name' placeholder={bot.name} />
</div> </div>
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} /> <Captcha
<Button disabled={!values.ownerID || values.name !== bot.name || !values._captcha} className={`mt-4 bg-red-500 text-white ${!values.ownerID ||values.name !== bot.name || !values._captcha ? 'opacity-80' : 'hover:opacity-80'}`} type='submit'><i className='fas fa-exchange-alt' /> </Button> dark={theme === 'dark'}
onVerify={(k) => setFieldValue('_captcha', k)}
/>
<Button
disabled={!values.ownerID || values.name !== bot.name || !values._captcha}
className={`mt-4 bg-red-500 text-white ${
!values.ownerID || values.name !== bot.name || !values._captcha
? 'opacity-80'
: 'hover:opacity-80'
}`}
type='submit'
>
<i className='fas fa-exchange-alt' />
</Button>
</Form> </Form>
} )}
</Formik> </Formik>
</Modal> </Modal>
</div> </div>
<Divider /> <Divider />
<div className='lg:flex items-center'> <div className='items-center lg:flex'>
<div className='grow py-1'> <div className='grow py-1'>
<h3 className='text-lg font-semibold'> </h3> <h3 className='text-lg font-semibold'> </h3>
<p className='text-gray-400'> .</p> <p className='text-gray-400'> .</p>
</div> </div>
<Button onClick={() => setDeleteModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-trash' /> </Button> <Button
<Modal full header={`${bot.name} 삭제하기`} isOpen={deleteModal} dark={theme === 'dark'} onClose={() => setDeleteModal(false)} closeIcon> onClick={() => setDeleteModal(true)}
<Formik initialValues={{ name: '', _captcha: '', _csrf: csrfToken }} onSubmit={async (v) => { className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
const res = await Fetch(`/bots/${bot.id}`, { method: 'DELETE', body: JSON.stringify(v) }) >
<i className='fas fa-trash' />
</Button>
<Modal
full
header={`${bot.name} 삭제하기`}
isOpen={deleteModal}
dark={theme === 'dark'}
onClose={() => setDeleteModal(false)}
closeIcon
>
<Formik
initialValues={{ name: '', _captcha: '', _csrf: csrfToken }}
onSubmit={async (v) => {
const res = await Fetch(`/bots/${bot.id}`, {
method: 'DELETE',
body: JSON.stringify(v),
})
if (res.code === 200) { if (res.code === 200) {
alert('성공적으로 삭제하였습니다.') alert('성공적으로 삭제하였습니다.')
redirectTo(router, '/') redirectTo(router, '/')
} } else alert(res.message)
else alert(res.message) }}
}}> >
{ {({ values, setFieldValue }) => (
({ values, setFieldValue }) => <Form> <Form>
<Message type='warning'> <Message type='warning'>
<p> .<br/> .</p> <p>
<p> <strong>{bot.name}</strong>{getJosaPicker('을')(bot.name)} .</p> .
<br />
.
</p>
<p>
<strong>{bot.name}</strong>
{getJosaPicker('을')(bot.name)} .
</p>
</Message> </Message>
<div className='py-4'> <div className='py-4'>
<Input name='name' placeholder={bot.name} /> <Input name='name' placeholder={bot.name} />
</div> </div>
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} /> <Captcha
<Button disabled={values.name !== bot.name || !values._captcha} className={`mt-4 bg-red-500 text-white ${values.name !== bot.name || !values._captcha ? 'opacity-80' : 'hover:opacity-80'}`} type='submit'><i className='fas fa-trash' /> </Button> dark={theme === 'dark'}
onVerify={(k) => setFieldValue('_captcha', k)}
/>
<Button
disabled={values.name !== bot.name || !values._captcha}
className={`mt-4 bg-red-500 text-white ${
values.name !== bot.name || !values._captcha
? 'opacity-80'
: 'hover:opacity-80'
}`}
type='submit'
>
<i className='fas fa-trash' />
</Button>
</Form> </Form>
} )}
</Formik> </Formik>
</Modal> </Modal>
</div> </div>
</Segment> </Segment>
</div> </div>
} )}
</Container> </Container>
) )
} }
@ -328,7 +573,13 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
export const getServerSideProps = async (ctx: Context) => { export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req) const parsed = parseCookie(ctx.req)
const user = await get.Authorization(parsed?.token) const user = await get.Authorization(parsed?.token)
return { props: { bot: await get.bot.load(ctx.query.id), user: await get.user.load(user || ''), csrfToken: getToken(ctx.req, ctx.res) } } return {
props: {
bot: await get.bot.load(ctx.query.id),
user: await get.user.load(user || ''),
csrfToken: getToken(ctx.req, ctx.res),
},
}
} }
interface ManageBotProps { interface ManageBotProps {

View File

@ -47,56 +47,84 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
}, []) }, [])
if (!data?.id) return <NotFound /> if (!data?.id) return <NotFound />
return ( return (
<div style={bg ? { background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${data.bg}") center top / cover no-repeat fixed` } : {}}> <div
style={
bg
? {
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${data.bg}") center top / cover no-repeat fixed`,
}
: {}
}
>
<Container paddingTop className='py-10'> <Container paddingTop className='py-10'>
<NextSeo <NextSeo
title={data.name} title={data.name}
description={data.intro} description={data.intro}
twitter={{ twitter={{
cardType: 'summary_large_image' cardType: 'summary_large_image',
}} }}
openGraph={{ openGraph={{
images: [ images: [
{ {
url: KoreanbotsEndPoints.OG.bot(data.id, data.name, data.intro, data.category, [formatNumber(data.votes), formatNumber(data.servers)]), url: KoreanbotsEndPoints.OG.bot(data.id, data.name, data.intro, data.category, [
formatNumber(data.votes),
formatNumber(data.servers),
]),
width: 2048, width: 2048,
height: 1170, height: 1170,
alt: 'Bot Preview Image' alt: 'Bot Preview Image',
} },
] ],
}} }}
/> />
{ {data.state === 'blocked' ? (
data.state === 'blocked' ? <div className='pb-40'> <div className='pb-40'>
<Message type='error'> <Message type='error'>
<h2 className='text-lg font-extrabold'> .</h2> <h2 className='text-lg font-extrabold'> .</h2>
</Message> </Message>
</div> </div>
: data.category.includes('NSFW') && !nsfw ? <NSFW onClick={() => setNSFW(true)} onDisableClick={() => localStorage.nsfw = true} /> ) : data.category.includes('NSFW') && !nsfw ? (
: <> <NSFW onClick={() => setNSFW(true)} onDisableClick={() => (localStorage.nsfw = true)} />
) : (
<>
<div className='w-full pb-2'> <div className='w-full pb-2'>
{ {data.state === 'private' ? (
data.state === 'private' ? <Message type='info'> <Message type='info'>
<h2 className='text-lg font-extrabold'> .</h2> <h2 className='text-lg font-extrabold'>
<p> . .</p> .
</Message> : </h2>
data.state === 'reported' ? <p>
.
.
</p>
</Message>
) : data.state === 'reported' ? (
<Message type='error'> <Message type='error'>
<h2 className='text-lg font-extrabold'> , .</h2> <h2 className='text-lg font-extrabold'>
, .
</h2>
<p> .</p> <p> .</p>
<p> <Link href='/guidelines' className='text-blue-500 hover:text-blue-400'></Link> <Link href='/discord' className='text-blue-500 hover:text-blue-400'> </Link> .</p> <p>
</Message> : '' {' '}
} <Link href='/guidelines' className='text-blue-500 hover:text-blue-400'>
</Link>
{' '}
<Link href='/discord' className='text-blue-500 hover:text-blue-400'>
</Link>
.
</p>
</Message>
) : (
''
)}
</div> </div>
<div className='lg:flex w-full'> <div className='w-full lg:flex'>
<div className='w-full text-center lg:w-2/12'> <div className='w-full text-center lg:w-2/12'>
<DiscordAvatar <DiscordAvatar userID={data.id} size={256} className='w-full rounded-full' />
userID={data.id}
size={256}
className='w-full rounded-full'
/>
</div> </div>
<div className='grow px-5 py-12 w-full text-center lg:w-5/12 lg:text-left'> <div className='w-full grow px-5 py-12 text-center lg:w-5/12 lg:text-left'>
<Tag <Tag
circular circular
text={ text={
@ -109,54 +137,66 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
<h1 className='mb-2 mt-3 text-4xl font-bold' style={bg ? { color: 'white' } : {}}> <h1 className='mb-2 mt-3 text-4xl font-bold' style={bg ? { color: 'white' } : {}}>
{data.name}{' '} {data.name}{' '}
{checkBotFlag(data.flags, 'trusted') ? ( {checkBotFlag(data.flags, 'trusted') ? (
<Tooltip placement='bottom' overlay='해당 봇은 한국 디스코드 리스트에서 엄격한 기준을 통과한 봇입니다!'> <Tooltip
<span className='text-koreanbots-blue text-3xl'> placement='bottom'
overlay='해당 봇은 한국 디스코드 리스트에서 엄격한 기준을 통과한 봇입니다!'
>
<span className='text-3xl text-koreanbots-blue'>
<i className='fas fa-award' /> <i className='fas fa-award' />
</span> </span>
</Tooltip> </Tooltip>
) : ''} ) : (
''
)}
</h1> </h1>
<p className={`${bg ? 'text-gray-300' : 'dark:text-gray-300 text-gray-800'} text-base`}>{data.intro}</p> <p
className={`${
bg ? 'text-gray-300' : 'text-gray-800 dark:text-gray-300'
} text-base`}
>
{data.intro}
</p>
</div> </div>
<div className='w-full lg:w-1/4'> <div className='w-full lg:w-1/4'>
{ {data.state === 'ok' && (
data.state === 'ok' && <LongButton <LongButton newTab href={`/bots/${router.query.id}/invite`}>
newTab
href={`/bots/${router.query.id}/invite`}
>
<h4 className='whitespace-nowrap'> <h4 className='whitespace-nowrap'>
<i className='fas fa-user-plus text-discord-blurple' /> <i className='fas fa-user-plus text-discord-blurple' />
</h4> </h4>
</LongButton> </LongButton>
} )}
<Link href={`/bots/${router.query.id}/vote`} legacyBehavior> <Link href={`/bots/${router.query.id}/vote`} legacyBehavior>
<LongButton> <LongButton>
<h4> <h4>
<i className='fas fa-heart text-red-600' /> <i className='fas fa-heart text-red-600' />
</h4> </h4>
<span className='ml-1 px-2 text-center text-black dark:text-gray-400 text-sm bg-little-white-hover dark:bg-very-black rounded-lg'> <span className='ml-1 rounded-lg bg-little-white-hover px-2 text-center text-sm text-black dark:bg-very-black dark:text-gray-400'>
{formatNumber(data.votes)} {formatNumber(data.votes)}
</span> </span>
</LongButton> </LongButton>
</Link> </Link>
{ {((data.owners as User[]).find((el) => el.id === user?.id) ||
((data.owners as User[]).find(el => el.id === user?.id) || checkUserFlag(user?.flags, 'staff')) && <LongButton href={`/bots/${data.id}/edit`}> checkUserFlag(user?.flags, 'staff')) && (
<LongButton href={`/bots/${data.id}/edit`}>
<h4> <h4>
<i className='fas fa-cogs' /> <i className='fas fa-cogs' />
</h4> </h4>
</LongButton> </LongButton>
} )}
{ {((data.owners as User[]).find((el) => el.id === user?.id) ||
((data.owners as User[]).find(el => el.id === user?.id) || checkUserFlag(user?.flags, 'staff')) && <LongButton onClick={async() => { checkUserFlag(user?.flags, 'staff')) && (
<LongButton
onClick={async () => {
const res = await Fetch(`/bots/${data.id}/stats`, { method: 'PATCH' }) const res = await Fetch(`/bots/${data.id}/stats`, { method: 'PATCH' })
if (res.code !== 200) return alert(res.message) if (res.code !== 200) return alert(res.message)
else window.location.reload() else window.location.reload()
}}> }}
>
<h4> <h4>
<i className='fas fa-sync' /> <i className='fas fa-sync' />
</h4> </h4>
</LongButton> </LongButton>
} )}
</div> </div>
</div> </div>
<Divider className='px-5' /> <Divider className='px-5' />
@ -166,7 +206,7 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
<div className='lg:flex lg:flex-row-reverse' style={bg ? { color: 'white' } : {}}> <div className='lg:flex lg:flex-row-reverse' style={bg ? { color: 'white' } : {}}>
<div className='mb-1 w-full lg:w-1/4'> <div className='mb-1 w-full lg:w-1/4'>
<h2 className='3xl mb-2 font-bold'></h2> <h2 className='3xl mb-2 font-bold'></h2>
<div className='grid gap-4 grid-cols-2 px-4 py-4 text-black dark:text-gray-400 dark:bg-discord-black bg-little-white rounded-sm'> <div className='grid grid-cols-2 gap-4 rounded-sm bg-little-white px-4 py-4 text-black dark:bg-discord-black dark:text-gray-400'>
<div> <div>
<i className='far fa-flag' /> <i className='far fa-flag' />
</div> </div>
@ -177,36 +217,36 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
<i className='fas fa-users' /> <i className='fas fa-users' />
</div> </div>
<div>{data.servers || 'N/A'}</div> <div>{data.servers || 'N/A'}</div>
{ {data.shards && data.servers > 1500 && (
data.shards && data.servers > 1500 && <> <>
<div> <div>
<i className='fas fa-sitemap' /> <i className='fas fa-sitemap' />
</div> </div>
<div>{data.shards}</div> <div>{data.shards}</div>
</> </>
} )}
<div> <div>
<i className='fas fa-calendar-day' /> <i className='fas fa-calendar-day' />
</div> </div>
<div>{Day(date).fromNow(false)}</div> <div>{Day(date).fromNow(false)}</div>
{ {checkBotFlag(data.flags, 'verified') ? (
checkBotFlag(data.flags, 'verified') ?
<Tooltip overlay='해당 봇은 디스코드측에서 인증된 봇입니다.'> <Tooltip overlay='해당 봇은 디스코드측에서 인증된 봇입니다.'>
<div className='col-span-2'> <div className='col-span-2'>
<i className='fas fa-check text-discord-blurple' /> <i className='fas fa-check text-discord-blurple' />
</div> </div>
</Tooltip> </Tooltip>
: '' ) : (
} ''
)}
</div> </div>
<h2 className='3xl mb-2 mt-2 font-bold'></h2> <h2 className='3xl mb-2 mt-2 font-bold'></h2>
<div className='flex flex-wrap'> <div className='flex flex-wrap'>
{data.category.map(el => ( {data.category.map((el) => (
<Tag key={el} text={el} href={`/bots/categories/${el}`} /> <Tag key={el} text={el} href={`/bots/categories/${el}`} />
))} ))}
</div> </div>
<h2 className='3xl mb-2 mt-2 font-bold'></h2> <h2 className='3xl mb-2 mt-2 font-bold'></h2>
{(data.owners as User[]).map(el => ( {(data.owners as User[]).map((el) => (
<Owner <Owner
key={el.id} key={el.id}
id={el.id} id={el.id}
@ -218,33 +258,55 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
<div className='list grid'> <div className='list grid'>
<Link <Link
href={`/bots/${router.query.id}/report`} href={`/bots/${router.query.id}/report`}
className='text-red-600 hover:underline cursor-pointer' className='cursor-pointer text-red-600 hover:underline'
aria-hidden='true'> aria-hidden='true'
>
<i className='far fa-flag' /> <i className='far fa-flag' />
</Link> </Link>
<Modal header={`${data.name}#${data.tag} 신고하기`} closeIcon isOpen={reportModal} onClose={() => { <Modal
header={`${data.name}#${data.tag} 신고하기`}
closeIcon
isOpen={reportModal}
onClose={() => {
setReportModal(false) setReportModal(false)
setReportRes(null) setReportRes(null)
}} full dark={theme === 'dark'}> }}
{ full
reportRes?.code === 200 ? <Message type='success'> dark={theme === 'dark'}
>
{reportRes?.code === 200 ? (
<Message type='success'>
<h2 className='text-lg font-semibold'> !</h2> <h2 className='text-lg font-semibold'> !</h2>
<p> ! <a className='text-blue-600 hover:text-blue-500' href='/discord'> </a> </p> <p>
</Message> : <Formik onSubmit={async (body) => { !{' '}
const res = await Fetch(`/bots/${data.id}/report`, { method: 'POST', body: JSON.stringify(body) }) <a className='text-blue-600 hover:text-blue-500' href='/discord'>
</a>
</p>
</Message>
) : (
<Formik
onSubmit={async (body) => {
const res = await Fetch(`/bots/${data.id}/report`, {
method: 'POST',
body: JSON.stringify(body),
})
setReportRes(res) setReportRes(res)
}} validationSchema={ReportSchema} initialValues={{ }}
validationSchema={ReportSchema}
initialValues={{
category: null, category: null,
description: '', description: '',
_csrf: csrfToken _csrf: csrfToken,
}}> }}
{ >
({ errors, touched, values, setFieldValue }) => ( {({ errors, touched, values, setFieldValue }) => (
<Form> <Form>
<div className='mb-5'> <div className='mb-5'>
{ {reportRes && (
reportRes && <div className='my-5'> <div className='my-5'>
<Message type='error'> <Message type='error'>
<h2 className='text-lg font-semibold'>{reportRes.message}</h2> <h2 className='text-lg font-semibold'>{reportRes.message}</h2>
<ul className='list-disc'> <ul className='list-disc'>
@ -252,34 +314,64 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
</ul> </ul>
</Message> </Message>
</div> </div>
} )}
<h3 className='font-bold'> </h3> <h3 className='font-bold'> </h3>
<p className='text-gray-400 text-sm mb-1'> .</p> <p className='mb-1 text-sm text-gray-400'>
{ .
reportCats.map(el => </p>
{reportCats.map((el) => (
<div key={el}> <div key={el}>
<label> <label>
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' /> <Field
type='radio'
name='category'
value={el}
className='mr-1.5 py-2'
/>
{el} {el}
</label> </label>
</div> </div>
) ))}
} <div className='mt-1 text-xs font-light text-red-500'>
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category as string: null}</div> {errors.category && touched.category
<h3 className='font-bold mt-2'></h3> ? (errors.category as string)
<p className='text-gray-400 text-sm mb-1'> .</p> : null}
<TextArea name='description' placeholder='최대한 자세하게 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.description} setValue={(value) => setFieldValue('description', value)} /> </div>
<div className='mt-1 text-red-500 text-xs font-light'>{errors.description && touched.description ? errors.description : null}</div> <h3 className='mt-2 font-bold'></h3>
<p className='mb-1 text-sm text-gray-400'>
.
</p>
<TextArea
name='description'
placeholder='최대한 자세하게 설명해주세요!'
theme={theme === 'dark' ? 'dark' : 'light'}
value={values.description}
setValue={(value) => setFieldValue('description', value)}
/>
<div className='mt-1 text-xs font-light text-red-500'>
{errors.description && touched.description
? errors.description
: null}
</div>
</div> </div>
<div className='text-right'> <div className='text-right'>
<Button className='bg-gray-500 hover:opacity-90 text-white' onClick={()=> setReportModal(false)}></Button> <Button
<Button type='submit' className='bg-red-500 hover:opacity-90 text-white'></Button> className='bg-gray-500 text-white hover:opacity-90'
onClick={() => setReportModal(false)}
>
</Button>
<Button
type='submit'
className='bg-red-500 text-white hover:opacity-90'
>
</Button>
</div> </div>
</Form> </Form>
) )}
}
</Formik> </Formik>
} )}
</Modal> </Modal>
{data.discord && ( {data.discord && (
<a <a
@ -310,23 +402,40 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
className='hover:underline' className='hover:underline'
href={data.git} href={data.git}
> >
<i className={`fab fa-${git[new URL(data.git).hostname]?.icon ?? 'git-alt'}`} /> <i
className={`fab fa-${git[new URL(data.git).hostname]?.icon ?? 'git-alt'}`}
/>
{git[new URL(data.git).hostname]?.text ?? 'Git'} {git[new URL(data.git).hostname]?.text ?? 'Git'}
</a> </a>
)} )}
</div> </div>
<Advertisement size='tall' /> <Advertisement size='tall' />
</div> </div>
<div className='w-full lg:pr-5 lg:w-3/4'> <div className='w-full lg:w-3/4 lg:pr-5'>
{ {checkBotFlag(data.flags, 'hackerthon') ? (
checkBotFlag(data.flags, 'hackerthon') ? <Segment className='mt-10'> <Segment className='mt-10'>
<h1 className='text-3xl font-semibold'> <h1 className='text-3xl font-semibold'>
<i className='fas fa-trophy mr-4 my-2 text-amber-300' /> ! <i className='fas fa-trophy my-2 mr-4 text-amber-300' />
!
</h1> </h1>
<p> "한국 디스코드 리스트 제1회 해커톤" .</p> <p>
<p> <a className='text-blue-500 hover:text-blue-400' href='https://blog.koreanbots.dev/first-hackathon-results/'> </a> .</p> " 1
</Segment> : '' " .
} </p>
<p>
{' '}
<a
className='text-blue-500 hover:text-blue-400'
href='https://blog.koreanbots.dev/first-hackathon-results/'
>
</a>
.
</p>
</Segment>
) : (
''
)}
<Segment className='my-4'> <Segment className='my-4'>
<Markdown text={desc} /> <Markdown text={desc} />
</Segment> </Segment>
@ -334,7 +443,7 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
</div> </div>
</div> </div>
</> </>
} )}
</Container> </Container>
</div> </div>
) )
@ -343,20 +452,25 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
export const getServerSideProps = async (ctx: Context) => { export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req) const parsed = parseCookie(ctx.req)
const data = await get.bot.load(ctx.query.id) const data = await get.bot.load(ctx.query.id)
if(!data) return { if (!data)
return {
props: { props: {
data data,
} },
} }
const desc = await get.botDescSafe(data.id) const desc = await get.botDescSafe(data.id)
const user = await get.Authorization(parsed?.token) const user = await get.Authorization(parsed?.token)
if((checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) && data.vanity && data.vanity !== ctx.query.id) { if (
(checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) &&
data.vanity &&
data.vanity !== ctx.query.id
) {
return { return {
redirect: { redirect: {
destination: `/bots/${data.vanity}`, destination: `/bots/${data.vanity}`,
permanent: true permanent: true,
}, },
props: {} props: {},
} }
} }
return { return {
@ -365,7 +479,7 @@ export const getServerSideProps = async (ctx: Context) => {
desc, desc,
date: Number(SnowflakeUtil.deconstruct(data.id ?? '0').timestamp), date: Number(SnowflakeUtil.deconstruct(data.id ?? '0').timestamp),
user: await get.user.load(user || ''), user: await get.user.load(user || ''),
csrfToken: getToken(ctx.req, ctx.res) csrfToken: getToken(ctx.req, ctx.res),
}, },
} }
} }

View File

@ -8,12 +8,24 @@ const Invite: NextPage = () => <NotFound />
export const getServerSideProps: GetServerSideProps = async (ctx) => { export const getServerSideProps: GetServerSideProps = async (ctx) => {
const data = await get.bot.load(ctx.query.id as string) const data = await get.bot.load(ctx.query.id as string)
if (!data) return { props: {} } if (!data) return { props: {} }
const record = await Bots.updateOne({ _id: data.id, 'inviteMetrix.day': getYYMMDD() }, { $inc: { 'inviteMetrix.$.count': 1 } }) const record = await Bots.updateOne(
if(record.matchedCount === 0) await Bots.findByIdAndUpdate(data.id, { $push: { inviteMetrix: { count: 1 } } }, { upsert: true }) { _id: data.id, 'inviteMetrix.day': getYYMMDD() },
{ $inc: { 'inviteMetrix.$.count': 1 } }
)
if (record.matchedCount === 0)
await Bots.findByIdAndUpdate(
data.id,
{ $push: { inviteMetrix: { count: 1 } } },
{ upsert: true }
)
ctx.res.statusCode = 307 ctx.res.statusCode = 307
ctx.res.setHeader('Location', data.url || `https://discordapp.com/oauth2/authorize?client_id=${data.id}&scope=bot&permissions=0`) ctx.res.setHeader(
'Location',
data.url ||
`https://discordapp.com/oauth2/authorize?client_id=${data.id}&scope=bot&permissions=0`
)
return { return {
props: {} props: {},
} }
} }

View File

@ -19,7 +19,6 @@ import { getJosaPicker } from 'josa'
import { reportCats } from '@utils/Constants' import { reportCats } from '@utils/Constants'
import { NextSeo } from 'next-seo' import { NextSeo } from 'next-seo'
const Container = dynamic(() => import('@components/Container')) const Container = dynamic(() => import('@components/Container'))
const Message = dynamic(() => import('@components/Message')) const Message = dynamic(() => import('@components/Message'))
const Login = dynamic(() => import('@components/Login')) const Login = dynamic(() => import('@components/Login'))
@ -27,33 +26,54 @@ const Login = dynamic(() => import('@components/Login'))
const ReportBot: NextPage<ReportBotProps> = ({ data, user, csrfToken }) => { const ReportBot: NextPage<ReportBotProps> = ({ data, user, csrfToken }) => {
const [reportRes, setReportRes] = useState<ResponseProps<unknown>>(null) const [reportRes, setReportRes] = useState<ResponseProps<unknown>>(null)
if (!data?.id) return <NotFound /> if (!data?.id) return <NotFound />
if(!user) return <Login> if (!user)
return (
<Login>
<NextSeo title='신고하기' /> <NextSeo title='신고하기' />
</Login> </Login>
)
return ( return (
<Container paddingTop className='py-10'> <Container paddingTop className='py-10'>
<NextSeo title={`${data.name} 신고하기`} /> <NextSeo title={`${data.name} 신고하기`} />
<Link href={makeBotURL(data)} className='text-blue-500 hover:opacity-80'> <Link href={makeBotURL(data)} className='text-blue-500 hover:opacity-80'>
<i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.name}</strong>{getJosaPicker('로')(data.name)} <i className='fas fa-arrow-left mb-3 mt-3' /> <strong>{data.name}</strong>
{getJosaPicker('로')(data.name)}
</Link> </Link>
{ {reportRes?.code === 200 ? (
reportRes?.code === 200 ? <Message type='success'> <Message type='success'>
<h2 className='text-lg font-semibold'> !</h2> <h2 className='text-lg font-semibold'> !</h2>
<p> . <strong> <a className='text-blue-600 hover:text-blue-500' href='/discord'> </a> !!</strong></p> <p>
</Message> : <Formik onSubmit={async (body) => { .{' '}
const res = await Fetch(`/bots/${data.id}/report`, { method: 'POST', body: JSON.stringify(body) }) <strong>
{' '}
<a className='text-blue-600 hover:text-blue-500' href='/discord'>
</a>
!!
</strong>
</p>
</Message>
) : (
<Formik
onSubmit={async (body) => {
const res = await Fetch(`/bots/${data.id}/report`, {
method: 'POST',
body: JSON.stringify(body),
})
setReportRes(res) setReportRes(res)
}} validationSchema={ReportSchema} initialValues={{ }}
validationSchema={ReportSchema}
initialValues={{
category: null, category: null,
description: '', description: '',
_csrf: csrfToken _csrf: csrfToken,
}}> }}
{ >
({ errors, touched, values, setFieldValue }) => ( {({ errors, touched, values, setFieldValue }) => (
<Form> <Form>
<div className='mb-5'> <div className='mb-5'>
{ {reportRes && (
reportRes && <div className='my-5'> <div className='my-5'>
<Message type='error'> <Message type='error'>
<h2 className='text-lg font-semibold'>{reportRes.message}</h2> <h2 className='text-lg font-semibold'>{reportRes.message}</h2>
<ul className='list-disc'> <ul className='list-disc'>
@ -61,52 +81,82 @@ const ReportBot: NextPage<ReportBotProps> = ({ data, user, csrfToken }) => {
</ul> </ul>
</Message> </Message>
</div> </div>
} )}
<h3 className='font-bold'> </h3> <h3 className='font-bold'> </h3>
<p className='text-gray-400 text-sm mb-1'> .</p> <p className='mb-1 text-sm text-gray-400'> .</p>
{ {reportCats.map((el) => (
reportCats.map(el =>
<div key={el}> <div key={el}>
<label> <label>
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' /> <Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
{el} {el}
</label> </label>
</div> </div>
) ))}
} <div className='mt-1 text-xs font-light text-red-500'>
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category as string: null}</div> {errors.category && touched.category ? (errors.category as string) : null}
{ </div>
values.category && <> {values.category && (
<>
{ {
{ {
[reportCats[2]]: <Message type='info'> [reportCats[2]]: (
<h3 className='font-bold text-xl'> ?</h3> <Message type='info'>
<h3 className='text-xl font-bold'>
?
</h3>
<p> .</p> <p> .</p>
<p className='list-disc list-item list-inside'> 1393 | 1388</p> <p className='list-item list-inside list-disc'>
</Message>, 1393 | 1388
[reportCats[5]]: <DMCA values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />, </p>
[reportCats[6]]: <Message type='warning'>
<h3 className='font-bold text-xl'> ?</h3>
<p><a className='text-blue-400' target='_blank' rel='noreferrer' href='http://dis.gd/report'> </a> .</p>
</Message> </Message>
),
[reportCats[5]]: (
<DMCA
values={values}
errors={errors}
touched={touched}
setFieldValue={setFieldValue}
/>
),
[reportCats[6]]: (
<Message type='warning'>
<h3 className='text-xl font-bold'>
?
</h3>
<p>
<a
className='text-blue-400'
target='_blank'
rel='noreferrer'
href='http://dis.gd/report'
>
</a>
.
</p>
</Message>
),
}[values.category] }[values.category]
} }
{ {!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].includes(values.category) && (
!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].includes(values.category) && <> <>
<h3 className='font-bold mt-2'></h3> <h3 className='mt-2 font-bold'></h3>
<p className='text-gray-400 text-sm mb-1'> .</p> <p className='mb-1 text-sm text-gray-400'> .</p>
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} /> <TextField
values={values}
errors={errors}
touched={touched}
setFieldValue={setFieldValue}
/>
</> </>
} )}
</> </>
} )}
</div> </div>
</Form> </Form>
) )}
}
</Formik> </Formik>
} )}
</Container> </Container>
) )
} }
@ -120,7 +170,7 @@ export const getServerSideProps = async (ctx: Context) => {
props: { props: {
csrfToken: getToken(ctx.req, ctx.res), csrfToken: getToken(ctx.req, ctx.res),
data, data,
user: await get.user.load(user || '') user: await get.user.load(user || ''),
}, },
} }
} }

View File

@ -19,7 +19,6 @@ import { getJosaPicker } from 'josa'
import { KoreanbotsEndPoints } from '@utils/Constants' import { KoreanbotsEndPoints } from '@utils/Constants'
import { NextSeo } from 'next-seo' import { NextSeo } from 'next-seo'
const Container = dynamic(() => import('@components/Container')) const Container = dynamic(() => import('@components/Container'))
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar')) const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
const Button = dynamic(() => import('@components/Button')) const Button = dynamic(() => import('@components/Button'))
@ -34,72 +33,118 @@ const VoteBot: NextPage<VoteBotProps> = ({ data, user, theme, csrfToken }) => {
const [result, setResult] = useState<ResponseProps<{ retryAfter?: number }>>(null) const [result, setResult] = useState<ResponseProps<{ retryAfter?: number }>>(null)
const router = useRouter() const router = useRouter()
if (!data?.id) return <NotFound /> if (!data?.id) return <NotFound />
if(!user) return <Login> if (!user)
<NextSeo title={data.name} description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`} openGraph={{ return (
<Login>
<NextSeo
title={data.name}
description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`}
openGraph={{
images: [ images: [
{ {
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }), url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
width: 256, width: 256,
height: 256, height: 256,
alt: 'Bot Avatar' alt: 'Bot Avatar',
} },
] ],
}} /> }}
/>
</Login> </Login>
)
if((checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) && data.vanity && data.vanity !== router.query.id) router.push(`/bots/${data.vanity}/vote?csrfToken=${csrfToken}`) if (
(checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) &&
data.vanity &&
data.vanity !== router.query.id
)
router.push(`/bots/${data.vanity}/vote?csrfToken=${csrfToken}`)
return ( return (
<Container paddingTop className='py-10'> <Container paddingTop className='py-10'>
<NextSeo title={data.name} description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`} openGraph={{ <NextSeo
title={data.name}
description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`}
openGraph={{
images: [ images: [
{ {
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }), url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
width: 256, width: 256,
height: 256, height: 256,
alt: 'Bot Avatar' alt: 'Bot Avatar',
} },
] ],
}} /> }}
{ />
data.state === 'blocked' ? <div className='pb-40'> {data.state === 'blocked' ? (
<div className='pb-40'>
<Message type='error'> <Message type='error'>
<h2 className='text-lg font-extrabold'> .</h2> <h2 className='text-lg font-extrabold'> .</h2>
</Message> </Message>
</div> : <> </div>
) : (
<>
<Advertisement /> <Advertisement />
<Link href={makeBotURL(data)} className='text-blue-500 hover:opacity-80'> <Link href={makeBotURL(data)} className='text-blue-500 hover:opacity-80'>
<i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.name}</strong>{getJosaPicker('로')(data.name)} <i className='fas fa-arrow-left mb-3 mt-3' /> <strong>{data.name}</strong>
{getJosaPicker('로')(data.name)}
</Link> </Link>
<Segment className='mb-16 py-8'> <Segment className='mb-16 py-8'>
<div className='text-center'> <div className='text-center'>
<DiscordAvatar userID={data.id} className='mx-auto w-52 h-52 bg-white mb-4 rounded-full' /> <DiscordAvatar
<Tag text={<span><i className='fas fa-heart text-red-600' /> {data.votes}</span>} dark /> userID={data.id}
<h1 className='text-3xl font-bold mt-3'>{data.name}</h1> className='mx-auto mb-4 h-52 w-52 rounded-full bg-white'
/>
<Tag
text={
<span>
<i className='fas fa-heart text-red-600' /> {data.votes}
</span>
}
dark
/>
<h1 className='mt-3 text-3xl font-bold'>{data.name}</h1>
<h4 className='text-md mt-1'>12 .</h4> <h4 className='text-md mt-1'>12 .</h4>
<div className='inline-block mt-2'> <div className='mt-2 inline-block'>
{ {votingStatus === 0 ? (
votingStatus === 0 ? <Button onClick={()=> setVotingStatus(1)}> <Button onClick={() => setVotingStatus(1)}>
<><i className='far fa-heart text-red-600'/> </> <>
<i className='far fa-heart text-red-600' />
</>
</Button> </Button>
: votingStatus === 1 ? <Captcha dark={theme === 'dark'} onVerify={async (key) => { ) : votingStatus === 1 ? (
const res = await Fetch<{ retryAfter: number }|unknown>(`/bots/${data.id}/vote`, { method: 'POST', body: JSON.stringify({ _csrf: csrfToken, _captcha: key }) }) <Captcha
dark={theme === 'dark'}
onVerify={async (key) => {
const res = await Fetch<{ retryAfter: number } | unknown>(
`/bots/${data.id}/vote`,
{
method: 'POST',
body: JSON.stringify({ _csrf: csrfToken, _captcha: key }),
}
)
setResult(res) setResult(res)
setVotingStatus(2) setVotingStatus(2)
}} }}
/> />
: result.code === 200 ? <h2 className='text-2xl font-bold'> !</h2> ) : result.code === 200 ? (
: result.code === 429 ? <> <h2 className='text-2xl font-bold'> !</h2>
) : result.code === 429 ? (
<>
<h2 className='text-2xl font-bold'> .</h2> <h2 className='text-2xl font-bold'> .</h2>
<h4 className='text-md mt-1'>{Day(+new Date() + result.data?.retryAfter).fromNow()} .</h4> <h4 className='text-md mt-1'>
{Day(+new Date() + result.data?.retryAfter).fromNow()}
.
</h4>
</> </>
: <p>{result.message}</p> ) : (
} <p>{result.message}</p>
)}
</div> </div>
</div> </div>
</Segment> </Segment>
<Advertisement /></> <Advertisement />
} </>
)}
</Container> </Container>
) )
} }
@ -113,7 +158,7 @@ export const getServerSideProps = async (ctx: Context) => {
props: { props: {
csrfToken: getToken(ctx.req, ctx.res), csrfToken: getToken(ctx.req, ctx.res),
data, data,
user: await get.user.load(user || '') user: await get.user.load(user || ''),
}, },
} }
} }

View File

@ -25,42 +25,68 @@ const Category: NextPage<CategoryProps> = ({ data, query }) => {
useEffect(() => { useEffect(() => {
setNSFW(localStorage.nsfw) setNSFW(localStorage.nsfw)
}, []) }, [])
if(!data || data.data.length === 0 || data.totalPage < Number(query.page)) return <NotFound message={data?.data.length === 0 ? '해당 카테고리에 해당되는 봇이 존재하지 않습니다.' : null} /> if (!data || data.data.length === 0 || data.totalPage < Number(query.page))
return <> return (
<Hero type='bots' header={`${query.category} 카테고리 봇들`} description={`다양한 "${query.category}" 카테고리의 봇들을 만나보세요.`} /> <NotFound
{ message={
query.category === 'NSFW' && !nsfw ? <NSFW onClick={() => setNSFW(true)} onDisableClick={() => localStorage.nsfw = true} /> data?.data.length === 0 ? '해당 카테고리에 해당되는 봇이 존재하지 않습니다.' : null
: <Container>
{
router.query.category === '빗금 명령어' && <Segment className='mb-4'>
<h1 className='text-2xl font-bold pt-3.5 pb-1'> </h1>
<Markdown text={'빗금 명렁어는 디스코드 채팅창에 `/` 를 입력하여 사용할 수 있습니다.'} />
</Segment>
} }
/>
)
return (
<>
<Hero
type='bots'
header={`${query.category} 카테고리 봇들`}
description={`다양한 "${query.category}" 카테고리의 봇들을 만나보세요.`}
/>
{query.category === 'NSFW' && !nsfw ? (
<NSFW onClick={() => setNSFW(true)} onDisableClick={() => (localStorage.nsfw = true)} />
) : (
<Container>
{router.query.category === '빗금 명령어' && (
<Segment className='mb-4'>
<h1 className='pb-1 pt-3.5 text-2xl font-bold'> </h1>
<Markdown
text={'빗금 명렁어는 디스코드 채팅창에 `/` 를 입력하여 사용할 수 있습니다.'}
/>
</Segment>
)}
<Advertisement /> <Advertisement />
<ResponsiveGrid> <ResponsiveGrid>
{ {data.data.map((bot) => (
data.data.map(bot => <BotCard key={bot.id} bot={bot} /> ) <BotCard key={bot.id} bot={bot} />
} ))}
</ResponsiveGrid> </ResponsiveGrid>
<Paginator totalPage={data.totalPage} currentPage={data.currentPage} pathname={`/bots/categories/${query.category}`} /> <Paginator
totalPage={data.totalPage}
currentPage={data.currentPage}
pathname={`/bots/categories/${query.category}`}
/>
<Advertisement /> <Advertisement />
</Container> </Container>
} )}
</> </>
)
} }
export const getServerSideProps = async (ctx: Context) => { export const getServerSideProps = async (ctx: Context) => {
let data: List<Bot> let data: List<Bot>
if (!ctx.query.page) ctx.query.page = '1' if (!ctx.query.page) ctx.query.page = '1'
const validate = await botCategoryListArgumentSchema.validate(ctx.query).then(el => el).catch(() => null) const validate = await botCategoryListArgumentSchema
.validate(ctx.query)
.then((el) => el)
.catch(() => null)
if (!validate || isNaN(Number(ctx.query.page))) data = null if (!validate || isNaN(Number(ctx.query.page))) data = null
else data = await get.list.category.load(JSON.stringify({ page: Number(ctx.query.page), category: ctx.query.category })) else
data = await get.list.category.load(
JSON.stringify({ page: Number(ctx.query.page), category: ctx.query.category })
)
return { return {
props: { props: {
data, data,
query: ctx.query query: ctx.query,
} },
} }
} }

View File

@ -4,32 +4,56 @@ import { NextSeo } from 'next-seo'
import { botCategories, botCategoryIcon } from '@utils/Constants' import { botCategories, botCategoryIcon } from '@utils/Constants'
const Container = dynamic(() => import('@components/Container')) const Container = dynamic(() => import('@components/Container'))
const Advertisement = dynamic(() => import('@components/Advertisement')) const Advertisement = dynamic(() => import('@components/Advertisement'))
const Tag = dynamic(() => import('@components/Tag')) const Tag = dynamic(() => import('@components/Tag'))
const Segment = dynamic(() => import('@components/Segment')) const Segment = dynamic(() => import('@components/Segment'))
const Categories: NextPage = () => { const Categories: NextPage = () => {
return <Container paddingTop> return (
<Container paddingTop>
<NextSeo title='전체 카테고리' description='한국 디스코드 리스트의 전체 카테고리입니다.' /> <NextSeo title='전체 카테고리' description='한국 디스코드 리스트의 전체 카테고리입니다.' />
<h1 className='text-2xl font-bold mt-2 mb-5'> </h1> <h1 className='mb-5 mt-2 text-2xl font-bold'> </h1>
<Segment className='mb-10'> <Segment className='mb-10'>
<div className='text-center flex flex-wrap mt-1.5'> <div className='mt-1.5 flex flex-wrap text-center'>
{ {botCategories.map((t) => (
botCategories.map(t => <Tag key={t} text={<> <Tag
{ key={t}
{ '빗금 명령어': <span className='fa-stack' style={{ fontSize: '1em', height: '1.2em', lineHeight: '1em', width: '20px', verticalAlign: 'middle' }}> text={
<>
{{
'빗금 명령어': (
<span
className='fa-stack'
style={{
fontSize: '1em',
height: '1.2em',
lineHeight: '1em',
width: '20px',
verticalAlign: 'middle',
}}
>
<i className='fas fa-square fa-stack-1x fa-md' /> <i className='fas fa-square fa-stack-1x fa-md' />
<i className='fas fa-slash fa-rotate-90 fa-xs fa-stack-1x fa-inverse' style={{ fontSize: '0.3rem' }} /> <i
</span> }[t] ?? <i className={botCategoryIcon[t]} /> className='fas fa-slash fa-rotate-90 fa-xs fa-stack-1x fa-inverse'
} {t} style={{ fontSize: '0.3rem' }}
</>} href={`/bots/categories/${t}`} dark bigger /> ) />
</span>
),
}[t] ?? <i className={botCategoryIcon[t]} />}{' '}
{t}
</>
} }
href={`/bots/categories/${t}`}
dark
bigger
/>
))}
</div> </div>
</Segment> </Segment>
<Advertisement /> <Advertisement />
</Container> </Container>
)
} }
export default Categories export default Categories

View File

@ -18,36 +18,42 @@ const Index: NextPage<IndexProps> = ({ votes, newBots, trusted }) => {
<Hero type='bots' /> <Hero type='bots' />
<Container className='pb-10'> <Container className='pb-10'>
<Advertisement /> <Advertisement />
<h1 className='text-3xl font-bold mt-10 mb-2'> <h1 className='mb-2 mt-10 text-3xl font-bold'>
<i className='far fa-heart mr-3 text-pink-600' /> <i className='far fa-heart mr-3 text-pink-600' />
</h1> </h1>
<p className='text-base'> !</p> <p className='text-base'> !</p>
<ResponsiveGrid> <ResponsiveGrid>
{ {votes.data.map((bot) => (
votes.data.map(bot=> <BotCard key={bot.id} bot={bot} />) <BotCard key={bot.id} bot={bot} />
} ))}
</ResponsiveGrid> </ResponsiveGrid>
<Paginator totalPage={votes.totalPage} currentPage={votes.currentPage} pathname='/bots/list/votes' /> <Paginator
totalPage={votes.totalPage}
currentPage={votes.currentPage}
pathname='/bots/list/votes'
/>
<Advertisement /> <Advertisement />
<h1 className='text-3xl font-bold mb-2'> <h1 className='mb-2 text-3xl font-bold'>
<i className='fa fa-check mr-3 mt-10 text-emerald-500' /> <i className='fa fa-check mr-3 mt-10 text-emerald-500' />
</h1> </h1>
<p className='text-base'> !!</p> <p className='text-base'> !!</p>
<ResponsiveGrid> <ResponsiveGrid>
{ {trusted.data.slice(0, 4).map((bot) => (
trusted.data.slice(0, 4).map(bot=> <BotCard key={bot.id} bot={bot} />) <BotCard key={bot.id} bot={bot} />
} ))}
</ResponsiveGrid> </ResponsiveGrid>
<h1 className='text-3xl font-bold mt-20 mb-2'> <h1 className='mb-2 mt-20 text-3xl font-bold'>
<i className='far fa-star mr-3 text-amber-500' /> <i className='far fa-star mr-3 text-amber-500' />
</h1> </h1>
<p className='text-base'> .</p> <p className='text-base'> .</p>
<ResponsiveGrid> <ResponsiveGrid>
{ {newBots.data.slice(0, 4).map((bot) => (
newBots.data.slice(0, 4).map(bot=> <BotCard key={bot.id} bot={bot} />) <BotCard key={bot.id} bot={bot} />
} ))}
</ResponsiveGrid> </ResponsiveGrid>
<LongButton href='/bots/list/new' center></LongButton> <LongButton href='/bots/list/new' center>
</LongButton>
<Advertisement /> <Advertisement />
</Container> </Container>
</> </>
@ -60,7 +66,6 @@ export const getServerSideProps = async() => {
const trusted = await Query.get.list.trusted.load(1) const trusted = await Query.get.list.trusted.load(1)
return { props: { votes, newBots, trusted } } return { props: { votes, newBots, trusted } }
} }
interface IndexProps { interface IndexProps {

View File

@ -11,26 +11,32 @@ const Container = dynamic(() => import('@components/Container'))
const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid')) const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid'))
const New: NextPage<NewProps> = ({ data }) => { const New: NextPage<NewProps> = ({ data }) => {
return <> return (
<Hero type='bots' header='새로운 봇' description='최근에 한국 디스코드 리스트에 추가된 봇들입니다!' /> <>
<Hero
type='bots'
header='새로운 봇'
description='최근에 한국 디스코드 리스트에 추가된 봇들입니다!'
/>
<Container className='pb-10'> <Container className='pb-10'>
<Advertisement /> <Advertisement />
<ResponsiveGrid> <ResponsiveGrid>
{ {data.data.map((bot) => (
data.data.map(bot => <BotCard key={bot.id} bot={bot} /> ) <BotCard key={bot.id} bot={bot} />
} ))}
</ResponsiveGrid> </ResponsiveGrid>
<Advertisement /> <Advertisement />
</Container> </Container>
</> </>
)
} }
export const getServerSideProps = async () => { export const getServerSideProps = async () => {
const data = await get.list.new.load(1) const data = await get.list.new.load(1)
return { return {
props: { props: {
data data,
} },
} }
} }

View File

@ -18,33 +18,42 @@ const Paginator = dynamic(() => import('@components/Paginator'))
const Votes: NextPage<VotesProps> = ({ data }) => { const Votes: NextPage<VotesProps> = ({ data }) => {
const router = useRouter() const router = useRouter()
if(!data || data.data.length === 0 || data.totalPage < Number(router.query.page)) return <NotFound /> if (!data || data.data.length === 0 || data.totalPage < Number(router.query.page))
return <> return <NotFound />
return (
<>
<Hero type='bots' header='하트 랭킹' description='하트를 많이 받은 봇들의 순위입니다!' /> <Hero type='bots' header='하트 랭킹' description='하트를 많이 받은 봇들의 순위입니다!' />
<section id='list'> <section id='list'>
<Container className='pb-10'> <Container className='pb-10'>
<Advertisement /> <Advertisement />
<ResponsiveGrid> <ResponsiveGrid>
{ {data.data.map((bot) => (
data.data.map(bot => <BotCard key={bot.id} bot={bot} /> ) <BotCard key={bot.id} bot={bot} />
} ))}
</ResponsiveGrid> </ResponsiveGrid>
<Paginator totalPage={data.totalPage} currentPage={data.currentPage} pathname='/bots/list/votes' /> <Paginator
totalPage={data.totalPage}
currentPage={data.currentPage}
pathname='/bots/list/votes'
/>
<Advertisement /> <Advertisement />
</Container> </Container>
</section> </section>
</> </>
)
} }
export const getServerSideProps = async (ctx: Context) => { export const getServerSideProps = async (ctx: Context) => {
let data: List<Bot> let data: List<Bot>
if (!ctx.query.page) ctx.query.page = '1' if (!ctx.query.page) ctx.query.page = '1'
const validate = await PageCount.validate(ctx.query.page).then(el => el).catch(() => null) const validate = await PageCount.validate(ctx.query.page)
.then((el) => el)
.catch(() => null)
if (!validate || isNaN(Number(ctx.query.page))) data = null if (!validate || isNaN(Number(ctx.query.page))) data = null
else data = await get.list.votes.load(Number(ctx.query.page)) else data = await get.list.votes.load(Number(ctx.query.page))
return { return {
props: { props: {
data data,
} },
} }
} }

View File

@ -8,7 +8,6 @@ import { get } from '@utils/Query'
import { SearchQuerySchema } from '@utils/Yup' import { SearchQuerySchema } from '@utils/Yup'
import { KoreanbotsEndPoints } from '@utils/Constants' import { KoreanbotsEndPoints } from '@utils/Constants'
const Hero = dynamic(() => import('@components/Hero')) const Hero = dynamic(() => import('@components/Hero'))
const Advertisement = dynamic(() => import('@components/Advertisement')) const Advertisement = dynamic(() => import('@components/Advertisement'))
const BotCard = dynamic(() => import('@components/BotCard')) const BotCard = dynamic(() => import('@components/BotCard'))
@ -18,35 +17,52 @@ const Paginator = dynamic(() => import('@components/Paginator'))
const LongButton = dynamic(() => import('@components/LongButton')) const LongButton = dynamic(() => import('@components/LongButton'))
const Redirect = dynamic(() => import('@components/Redirect')) const Redirect = dynamic(() => import('@components/Redirect'))
const SearchComponent: FC<{data: List<Bot>, query: URLQuery }> = ({ data, query }) => { const SearchComponent: FC<{ data: List<Bot>; query: URLQuery }> = ({ data, query }) => {
return <div className='py-10'> return (
{ !data || data.data.length === 0 ? <h1 className='text-3xl font-bold text-center py-20'> .</h1> : <div className='py-10'>
{!data || data.data.length === 0 ? (
<h1 className='py-20 text-center text-3xl font-bold'> .</h1>
) : (
<> <>
<ResponsiveGrid> <ResponsiveGrid>
{ {data.data.map((el) => (
data.data.map(el => <BotCard key={el.id} bot={el as Bot} /> ) <BotCard key={el.id} bot={el as Bot} />
} ))}
</ResponsiveGrid> </ResponsiveGrid>
<Paginator totalPage={data.totalPage} currentPage={data.currentPage} pathname='/search' searchParams={query} /> <Paginator
totalPage={data.totalPage}
currentPage={data.currentPage}
pathname='/search'
searchParams={query}
/>
</> </>
} )}
</div> </div>
)
} }
const Search: NextPage<SearchProps> = ({ botData, query }) => { const Search: NextPage<SearchProps> = ({ botData, query }) => {
if (!query?.q) return <Redirect text={false} to='/' /> if (!query?.q) return <Redirect text={false} to='/' />
return <> return (
<Hero type='bots' header={`"${query.q}" 검색 결과`} description={`'${query.q}' 에 대한 검색 결과입니다.`} /> <>
<Hero
type='bots'
header={`"${query.q}" 검색 결과`}
description={`'${query.q}' 에 대한 검색 결과입니다.`}
/>
<Container> <Container>
<section id='list'> <section id='list'>
<Advertisement /> <Advertisement />
<h1 className='text-4xl font-bold'></h1> <h1 className='text-4xl font-bold'></h1>
<SearchComponent data={botData} query={query} /> <SearchComponent data={botData} query={query} />
<h1 className='text-2xl font-bold py-10'> ?</h1> <h1 className='py-10 text-2xl font-bold'> ?</h1>
<LongButton center href={KoreanbotsEndPoints.URL.searchServer(query.q)}> </LongButton> <LongButton center href={KoreanbotsEndPoints.URL.searchServer(query.q)}>
</LongButton>
<Advertisement /> <Advertisement />
</section> </section>
</Container> </Container>
</> </>
)
} }
export const getServerSideProps = async (ctx: Context) => { export const getServerSideProps = async (ctx: Context) => {
@ -55,24 +71,28 @@ export const getServerSideProps = async(ctx: Context) => {
return { return {
redirect: { redirect: {
destination: '/', destination: '/',
permanent: true permanent: true,
}, },
props: {} props: {},
} }
} }
if (!ctx.query.page) ctx.query.page = '1' if (!ctx.query.page) ctx.query.page = '1'
const validate = await SearchQuerySchema.validate(ctx.query).then(el => el).catch(() => null) const validate = await SearchQuerySchema.validate(ctx.query)
.then((el) => el)
.catch(() => null)
if (!validate || isNaN(Number(ctx.query.page))) return { props: { query: ctx.query } } if (!validate || isNaN(Number(ctx.query.page))) return { props: { query: ctx.query } }
else { else {
return { return {
props: { props: {
botData: await get.list.search.load(JSON.stringify({ query: ctx.query.q || '', page: ctx.query.page })).then(el => el).catch(() => null), botData: await get.list.search
query: ctx.query .load(JSON.stringify({ query: ctx.query.q || '', page: ctx.query.page }))
.then((el) => el)
.catch(() => null),
query: ctx.query,
},
} }
} }
} }
}
interface SearchProps { interface SearchProps {
botData?: List<Bot> botData?: List<Bot>

View File

@ -12,79 +12,110 @@ const Input = dynamic(() => import('@components/Form/Input'))
const Calculator: NextPage<CalculatorProps> = ({ query }) => { const Calculator: NextPage<CalculatorProps> = ({ query }) => {
const [value, setValue] = useState<{ [perm: string]: boolean }>({}) const [value, setValue] = useState<{ [perm: string]: boolean }>({})
const Perm = ({ name, perm, yellow }:{ name: string, perm: number, yellow?: boolean }) => { const Perm = ({ name, perm, yellow }: { name: string; perm: number; yellow?: boolean }) => {
return <li> return (
<li>
<label className='inline-flex items-center py-1'> <label className='inline-flex items-center py-1'>
<input className='form-checkbox text-discord-blurple bg-gray-300 h-5 w-5 rounded' type='checkbox' checked={value[perm]} onChange={() => { <input
className='form-checkbox h-5 w-5 rounded bg-gray-300 text-discord-blurple'
type='checkbox'
checked={value[perm]}
onChange={() => {
setValue({ ...value, [perm]: !value[perm] }) setValue({ ...value, [perm]: !value[perm] })
}} /> }}
/>
<span className={`ml-2.5 text-lg ${yellow ? 'text-amber-500' : ''}`}>{name}</span> <span className={`ml-2.5 text-lg ${yellow ? 'text-amber-500' : ''}`}>{name}</span>
</label> </label>
</li> </li>
)
} }
return <Container paddingTop className='pb-10'> return (
<NextSeo title='봇 초대링크 생성기' description='디스코드 봇 초대링크를 간편하게 생성하세요' openGraph={{ <Container paddingTop className='pb-10'>
<NextSeo
title='봇 초대링크 생성기'
description='디스코드 봇 초대링크를 간편하게 생성하세요'
openGraph={{
title: '봇 초대링크 생성기', title: '봇 초대링크 생성기',
description: '디스코드 봇 초대링크를 간편하게 생성하세요' description: '디스코드 봇 초대링크를 간편하게 생성하세요',
}} /> }}
<h1 className='text-4xl font-bold mt-2 mb-4'> </h1> />
<div className='text-2xl font-bold inline-flex items-center'>: {String(Object.keys(value).filter(el => value[el]).map(el => Number(el)).reduce((prev, curr) => BigInt(prev) | BigInt(curr), BigInt(0)))} <h1 className='mb-4 mt-2 text-4xl font-bold'> </h1>
<span className='ml-2 text-lg font-semibold'>= { Object.keys(value).filter(el => value[el]).map(el => `0x${Number(el).toString(16)}`).join(' | ') }</span> <div className='inline-flex items-center text-2xl font-bold'>
:{' '}
{String(
Object.keys(value)
.filter((el) => value[el])
.map((el) => Number(el))
.reduce((prev, curr) => BigInt(prev) | BigInt(curr), BigInt(0))
)}
<span className='ml-2 text-lg font-semibold'>
={' '}
{Object.keys(value)
.filter((el) => value[el])
.map((el) => `0x${Number(el).toString(16)}`)
.join(' | ')}
</span>
</div> </div>
<div className='grid gap-2 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 mt-2'> <div className='mt-2 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3'>
<div> <div>
<h2 className='text-2xl font-bold'> </h2> <h2 className='text-2xl font-bold'> </h2>
<ul> <ul>
{ {GuildPermissions.general.map((el) => (
GuildPermissions.general.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />) <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
} ))}
</ul> </ul>
</div> </div>
<div> <div>
<h2 className='text-2xl font-bold'> </h2> <h2 className='text-2xl font-bold'> </h2>
<ul> <ul>
{ {GuildPermissions.membership.map((el) => (
GuildPermissions.membership.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />) <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
} ))}
</ul> </ul>
</div> </div>
<div> <div>
<h2 className='text-2xl font-bold'> </h2> <h2 className='text-2xl font-bold'> </h2>
<ul> <ul>
{ {GuildPermissions.channel.map((el) => (
GuildPermissions.channel.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />) <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
} ))}
</ul> </ul>
</div> </div>
<div> <div>
<h2 className='text-2xl font-bold'> </h2> <h2 className='text-2xl font-bold'> </h2>
<ul> <ul>
{ {GuildPermissions.voice.map((el) => (
GuildPermissions.voice.map(el => <Perm key={el.name} name={el.name} perm={el.flag} />) <Perm key={el.name} name={el.name} perm={el.flag} />
} ))}
</ul> </ul>
</div> </div>
<div> <div>
<h2 className='text-2xl font-bold'> </h2> <h2 className='text-2xl font-bold'> </h2>
<ul> <ul>
{ {GuildPermissions.advanced.map((el) => (
GuildPermissions.advanced.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />) <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
} ))}
</ul> </ul>
</div> </div>
</div> </div>
<div className='py-10'> <div className='py-10'>
<span className='text-amber-500'> = 2 , <a href='https://support.discord.com/hc/ko/articles/219576828-2단계-인증-설정하기'>2 </a> .</span> <span className='text-amber-500'>
= 2 , {' '}
<a href='https://support.discord.com/hc/ko/articles/219576828-2단계-인증-설정하기'>
2
</a>
.
</span>
</div> </div>
<Formik onSubmit={()=> console.log('Pong?')} initialValues={{ <Formik
onSubmit={() => console.log('Pong?')}
initialValues={{
id: query.id?.toString() || '', id: query.id?.toString() || '',
scope: 'bot', scope: 'bot',
redirect: '' redirect: '',
}}> }}
{ >
({ values, setFieldValue }) => ( {({ values, setFieldValue }) => (
<Form> <Form>
<div className='grid gap-3 lg:grid-cols-4'> <div className='grid gap-3 lg:grid-cols-4'>
<div> <div>
@ -94,7 +125,12 @@ const Calculator:NextPage<CalculatorProps> = ({ query }) => {
</div> </div>
<div> <div>
<h6> (Scope)</h6> <h6> (Scope)</h6>
<button onClick={() => setFieldValue('scope', 'bot applications.commands')} className='text-blue-500 hover:text-blue-400'> (Slash Command) ?</button> <button
onClick={() => setFieldValue('scope', 'bot applications.commands')}
className='text-blue-500 hover:text-blue-400'
>
(Slash Command) ?
</button>
<Input name='scope' placeholder='bot' /> <Input name='scope' placeholder='bot' />
</div> </div>
<div> <div>
@ -104,20 +140,37 @@ const Calculator:NextPage<CalculatorProps> = ({ query }) => {
</div> </div>
</div> </div>
<div className='mt-2'> <div className='mt-2'>
: <a rel='noreferrer' target='_blank' href={values.id ? DiscordEnpoints.InviteApplication(values.id, value, values.scope, values.redirect) : null} className='cursor-pointer text-blue-500 hover:text-blue-400'>{DiscordEnpoints.InviteApplication(values.id, value, values.scope, values.redirect)}</a> :{' '}
<a
rel='noreferrer'
target='_blank'
href={
values.id
? DiscordEnpoints.InviteApplication(
values.id,
value,
values.scope,
values.redirect
)
: null
}
className='cursor-pointer text-blue-500 hover:text-blue-400'
>
{DiscordEnpoints.InviteApplication(values.id, value, values.scope, values.redirect)}
</a>
</div> </div>
</Form> </Form>
) )}
}
</Formik> </Formik>
</Container> </Container>
)
} }
export const getServerSideProps: GetServerSideProps = async (ctx) => { export const getServerSideProps: GetServerSideProps = async (ctx) => {
return { return {
props: { props: {
query: ctx.query query: ctx.query,
} },
} }
} }

View File

@ -20,9 +20,13 @@ const DiscordCallback:NextPage = () => {
} }
}, [router]) }, [router])
return <> return (
<Loader text={notRedirecting ? '해당 창을 닫고 원래 앱으로 돌아가주세요.' : '리다이렉트 중 입니다.'} /> <>
<Loader
text={notRedirecting ? '해당 창을 닫고 원래 앱으로 돌아가주세요.' : '리다이렉트 중 입니다.'}
/>
</> </>
)
} }
export default DiscordCallback export default DiscordCallback

View File

@ -3,17 +3,12 @@ import dynamic from 'next/dynamic'
import { SpecialEndPoints } from '@utils/Constants' import { SpecialEndPoints } from '@utils/Constants'
const Docs = dynamic(() => import('@components/Docs')) const Docs = dynamic(() => import('@components/Docs'))
const Markdown = dynamic(() => import('@components/Markdown')) const Markdown = dynamic(() => import('@components/Markdown'))
const CommunityRule: NextPage<CommunityRuleProps> = ({ content }) => { const CommunityRule: NextPage<CommunityRuleProps> = ({ content }) => {
return ( return (
<Docs <Docs header='커뮤니티 규칙' description='한국 디스코드 리스트 커뮤니티 규칙입니다.'>
header='커뮤니티 규칙'
description='한국 디스코드 리스트 커뮤니티 규칙입니다.'
>
<Markdown text={content} /> <Markdown text={content} />
</Docs> </Docs>
) )
@ -24,14 +19,15 @@ interface CommunityRuleProps {
} }
export const getStaticProps: GetStaticProps<CommunityRuleProps> = async () => { export const getStaticProps: GetStaticProps<CommunityRuleProps> = async () => {
const res = await fetch(SpecialEndPoints.Github.Content('koreanbots', 'terms', 'community-rule.md')) const res = await fetch(
SpecialEndPoints.Github.Content('koreanbots', 'terms', 'community-rule.md')
)
const json = await res.json() const json = await res.json()
return { return {
props: { props: {
content: Buffer.from(json.content, 'base64').toString('utf-8') content: Buffer.from(json.content, 'base64').toString('utf-8'),
} },
} }
} }
export default CommunityRule export default CommunityRule

View File

@ -24,38 +24,52 @@ const ClientInfo = ():JSX.Element => {
[](https://twitter.com/koreanbots) [](https://twitter.com/koreanbots)
https://github.com/koreanbots https://github.com/koreanbots
` `,
},
onSubmit: () => {
alert('Pong')
}, },
onSubmit: ()=>{ alert('Pong') }
}) })
return <Container paddingTop className='pb-10'> return (
<h1 className='text-4xl font-bold mb-3 mt-3'></h1> <Container paddingTop className='pb-10'>
<h2 className='text-3xl font-semibold mb-4'></h2> <h1 className='mb-3 mt-3 text-4xl font-bold'></h1>
<h2 className='mb-4 text-3xl font-semibold'></h2>
<Segment> <Segment>
<div className='markdown-body text-black dark:text-white'> <div className='markdown-body text-black dark:text-white'>
<h1></h1> <h1></h1>
<ul className='list-disc'> <ul className='list-disc'>
<li>Tag: <code>{parseDockerhubTag(process.env.NEXT_PUBLIC_TAG)}</code></li> <li>
<li>Version: <code>v{Package.version}</code></li> Tag: <code>{parseDockerhubTag(process.env.NEXT_PUBLIC_TAG)}</code>
<li>Hash: <code>{process.env.NEXT_PUBLIC_SOURCE_COMMIT}</code></li> </li>
<li>
Version: <code>v{Package.version}</code>
</li>
<li>
Hash: <code>{process.env.NEXT_PUBLIC_SOURCE_COMMIT}</code>
</li>
</ul> </ul>
</div> </div>
</Segment> </Segment>
<Divider /> <Divider />
<h2 className='text-3xl font-semibold mb-2'></h2> <h2 className='mb-2 text-3xl font-semibold'></h2>
<h3 className='text-2xl font-semibold mb-2'></h3> <h3 className='mb-2 text-2xl font-semibold'></h3>
<Segment> <Segment>
<div className='lg:flex'> <div className='lg:flex'>
<div className='w-full lg:w-1/2 min-h-48'> <div className='min-h-48 w-full lg:w-1/2'>
<textarea className='resize-none w-full h-full dark:bg-discord-dark outline-none p-5' name='markdown' value={formik.values.markdown} onChange={formik.handleChange}/> <textarea
className='h-full w-full resize-none p-5 outline-none dark:bg-discord-dark'
name='markdown'
value={formik.values.markdown}
onChange={formik.handleChange}
/>
</div> </div>
<div className='w-full lg:w-1/2 p-10'> <div className='w-full p-10 lg:w-1/2'>
<Markdown text={formik.values.markdown} /> <Markdown text={formik.values.markdown} />
</div> </div>
</div> </div>
</Segment> </Segment>
</Container> </Container>
)
} }
export default ClientInfo export default ClientInfo

Some files were not shown because too many files have changed in this diff Show More