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

View File

@ -7,17 +7,16 @@ const ServerIcon = dynamic(() => import('@components/ServerIcon'))
const Application: React.FC<ApplicationProps> = ({ type, id, name }) => {
return (
<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'>
{
type === 'bot' ?
<DiscordAvatar userID={id} className='px-2 w-full rounded-xl' /> :
<ServerIcon id={id} className='px-2 w-full rounded-xl' />
}
<h2 className='pt-2 whitespace-nowrap text-xl font-medium truncate'>{name}</h2>
<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' ? (
<DiscordAvatar userID={id} className='w-full rounded-xl px-2' />
) : (
<ServerIcon id={id} className='w-full rounded-xl px-2' />
)}
<h2 className='truncate whitespace-nowrap pt-2 text-xl font-medium'>{name}</h2>
</div>
</Link>
)
}
interface ApplicationProps {

View File

@ -11,18 +11,18 @@ const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
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='container mx-auto'>
<div className='h-full'>
<div
className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl'
className='relative mx-auto h-full rounded-2xl bg-little-white text-black shadow-xl dark:bg-discord-black dark:text-white'
style={
checkBotFlag(bot.flags, 'trusted') && bot.banner
? {
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${bot.banner}") center top / cover no-repeat`,
color: 'white',
}
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${bot.banner}") center top / cover no-repeat`,
color: 'white',
}
: {}
}
>
@ -30,15 +30,15 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
<div>
<div className='flex flex-col'>
<div className='flex'>
<div className='w-3/5 flex justify-start'>
<div className='flex w-3/5 justify-start'>
<DiscordAvatar
size={128}
userID={bot.id}
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 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
text={
<>
@ -55,19 +55,19 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
</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'>
<i className={`fas fa-circle text-${Status[bot.status]?.color}`} />
{Status[bot.status]?.text}
</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>
<p className='mb-10 px-4 h-6 text-left text-gray-400 text-sm'>
{bot.intro}
</p>
<p className='mb-10 h-6 px-4 text-left text-sm text-gray-400'>{bot.intro}</p>
<div>
<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 />
))}{' '}
{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'>
<Link
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>
{manage ? (
<Link
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'
>
</a> :
) : 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
href={
makeBotURL(bot) + '/invite'
}
href={makeBotURL(bot) + '/invite'}
rel='noopener noreferrer'
target='_blank'
className='py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'
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>
}
)}
</div>
</div>
</div>
@ -117,7 +113,6 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
</div>
</div>
)
}
interface BotCardProps {
@ -125,4 +120,4 @@ interface BotCardProps {
bot: Bot
}
export default BotCard
export default BotCard

View File

@ -6,33 +6,41 @@ const Button: React.FC<ButtonProps> = ({
className,
children,
href,
disabled=false,
disabled = false,
onClick,
}) => {
return href ? <Link
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 ??
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}>
{children}
</Link>
: onClick ? <button
return href ? (
<Link
href={!disabled && href}
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}
</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'}`}
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>
:
<button
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 ??
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}
>
{children}
</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 {

View File

@ -1,15 +1,20 @@
import { Ref } from 'react'
import HCaptcha from '@hcaptcha/react-hcaptcha'
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 {
dark: boolean
onVerify(token: string, eKey?: string): void
ref?: Ref<HCaptcha>
dark: boolean
onVerify(token: string, eKey?: string): void
ref?: Ref<HCaptcha>
}
export default Captcha
export default Captcha

View File

@ -3,7 +3,7 @@ const ColorCard: React.FC<ColorCardProps> = ({ header, first, second, className
<div className={`rounded-lg p-10 ${className} shadow-lg`}>
<h2 className='text-2xl font-bold'>{header}</h2>
<p className='opacity-80'>
{first}
{first}
<br />
{second}
</p>

View File

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

View File

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

View File

@ -8,26 +8,28 @@ const Docs: React.FC<DocsProps> = ({ title, header, description, subheader, chil
const d = description || subheader
return (
<>
<NextSeo title={t} description={d}
<NextSeo
title={t}
description={d}
openGraph={{
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>
<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}
</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}
</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}
</h2>
</Container>
</div>
<Container className='pt-10 pb-20'>
<Container className='pb-20 pt-10'>
<div>{children}</div>
</Container>
</>

View File

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

View File

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

View File

@ -1,7 +1,14 @@
import { Field } from 'formik'
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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -8,64 +8,93 @@ import useOutsideClick from '@utils/useOutsideClick'
import 'emoji-mart/css/emoji-mart.css'
const TextArea: React.FC<TextAreaProps> = ({ name, placeholder, theme='auto', max, setValue, value }) => {
const TextArea: React.FC<TextAreaProps> = ({
name,
placeholder,
theme = 'auto',
max,
setValue,
value,
}) => {
const ref = useRef()
const [ emojiPickerHidden, setEmojiPickerHidden ] = useState(true)
const [emojiPickerHidden, setEmojiPickerHidden] = useState(true)
useOutsideClick(ref, () => {
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'>
<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 ref={ref}>
<div className='absolute bottom-12 left-10 z-30'>
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
!emojiPickerHidden && <Picker title='선택해주세요' emoji='sunglasses' set='twitter' enableFrequentEmojiSort theme={theme} showSkinTones={false} onSelect={(e) => {
setEmojiPickerHidden(true)
setValue(value + ' ' + ((e as { native: string }).native || e.colons))
}} i18n={{
search: '검색',
notfound: '검색 결과가 없습니다.',
categories: {
search: '검색 결과',
recent: '최근 사용',
people: '사람',
nature: '자연',
foods: '음식',
activity: '활동',
places: '장소',
objects: '사물',
symbols: '기호',
flags: '국기',
custom: '커스텀'
}
}} custom={KoreanbotsEmoji}/>
}
return (
<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 className='absolute bottom-12 left-10 z-30'>
{!emojiPickerHidden && (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<Picker
title='선택해주세요'
emoji='sunglasses'
set='twitter'
enableFrequentEmojiSort
theme={theme}
showSkinTones={false}
onSelect={(e) => {
setEmojiPickerHidden(true)
setValue(value + ' ' + ((e as { native: string }).native || e.colons))
}}
i18n={{
search: '검색',
notfound: '검색 결과가 없습니다.',
categories: {
search: '검색 결과',
recent: '최근 사용',
people: '사람',
nature: '자연',
foods: '음식',
activity: '활동',
places: '장소',
objects: '사물',
symbols: '기호',
flags: '국기',
custom: '커스텀',
},
}}
custom={KoreanbotsEmoji}
/>
)}
</div>
<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>
{max && (
<span
className={`absolute bottom-2 right-4 ${max < value.length ? ' text-red-400' : ''}`}
>
{max - value.length}
</span>
)}
</div>
<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>
{
max && <span className={`absolute bottom-2 right-4 ${max < value.length ? ' text-red-400' : ''}`}>
{max-value.length}
</span>
}
</div>
</div>
)
}
interface TextAreaProps {
name: string
placeholder?: string
theme?: 'auto' | 'dark' | 'light'
name: string
placeholder?: string
theme?: 'auto' | 'dark' | 'light'
max?: number
value: string
setValue(value: string): void
value: string
setValue(value: string): void
}
export default TextArea

View File

@ -1,71 +1,154 @@
import dynamic from 'next/dynamic'
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 Tag = dynamic(()=> import('@components/Tag'))
const Search = dynamic(()=> import('@components/Search'))
const Container = dynamic(() => import('@components/Container'))
const Tag = dynamic(() => import('@components/Tag'))
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`
return <>
<NextSeo title={header} description={description} openGraph={{
title: header,
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>
<h1 className='hidden md:block text-left text-3xl font-bold'>
{ header && `${header} - `}
</h1>
<h1 className='md:hidden text-center text-3xl font-semibold'>
{ header && <span className='text-4xl'>{header}<br/></span>}
</h1>
<p className='text-center sm:text-left text-xl font-base mt-2'>{description || `${type !== 'all' ? '다양한 ' : ''}국내 디스코드${{ all: '의 모든 것을', bots: ' 봇들을', servers: ' 서버들을' }[type]} 한 곳에서 확인하세요!`}</p>
<Search />
<div className='flex flex-wrap mt-5'>
{
type === 'all' ? <>
<Tag text={
<>
<i className='fas fa-robot text-koreanbots-blue'/>
</>
} dark bigger href='/bots' />
<Tag text={
<>
<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}`} />)
}
{
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>
</Container>
</div>
</>
return (
<>
<NextSeo
title={header}
description={description}
openGraph={{
title: header,
description,
}}
/>
<div className='mb-8 bg-discord-blurple text-gray-100 dark:bg-discord-black md:p-0'>
<Container className='pb-16 pt-24 md:pb-20' ignoreColor>
<h1 className='hidden text-left text-3xl font-bold md:block'>
{header && `${header} - `}
</h1>
<h1 className='text-center text-3xl font-semibold md:hidden'>
{header && (
<span className='text-4xl'>
{header}
<br />
</span>
)}
</h1>
<p className='font-base mt-2 text-center text-xl sm:text-left'>
{description ||
`${type !== 'all' ? '다양한 ' : ''}국내 디스코드${
{ all: '의 모든 것을', bots: ' 봇들을', servers: ' 서버들을' }[type]
} !`}
</p>
<Search />
<div className='mt-5 flex flex-wrap'>
{type === 'all' ? (
<>
<Tag
text={
<>
<i className='fas fa-robot text-koreanbots-blue' />
</>
}
dark
bigger
href='/bots'
/>
<Tag
text={
<>
<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}`}
/>
))}
{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>
</Container>
</div>
</>
)
}
interface HeroProps {
type?: 'all' | 'bots' | 'servers'
header?: string
description?: string
header?: string
description?: string
}
export default Hero
export default Hero

View File

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

View File

@ -3,9 +3,9 @@ const Loader: React.FC<LoaderProps> = ({ text, visible = true }) => {
<div
className={`${
visible ? '' : 'hidden '
} w-full h-full fixed block top-0 left-0 bg-gray-500 bg-opacity-75 z-50 dark:text-black`}
} 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}
</h1>
</div>

View File

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

View File

@ -1,42 +1,68 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
import Link from 'next/link'
const LongButton: React.FC<LongButtonProps> = ({ children, newTab=false, href, onClick, center=false }) => {
if(href) {
if(newTab) return <a href={href} rel='noopener noreferrer'
target='_blank'>
<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`}>
const LongButton: React.FC<LongButtonProps> = ({
children,
newTab = false,
href,
onClick,
center = false,
}) => {
if (href) {
if (newTab)
return (
<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}
</div>
</a>
)
else
return (
<Link
href={href}
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}
</Link>
)
}
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}
</div>
</a>
else return (
<Link
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`}>
{children}
</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`}>
{children}
</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`}>
{children}
</a>
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}
</a>
)
}
export default LongButton
interface LongButtonProps {
newTab?: boolean
onClick?: (event: React.KeyboardEvent<HTMLDivElement>|React.MouseEvent<HTMLDivElement>) => void
children: string | JSX.Element | JSX.Element[]
href?: string
center?: boolean
}
newTab?: boolean
onClick?: (event: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement>) => void
children: string | JSX.Element | JSX.Element[]
href?: string
center?: boolean
}

View File

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

View File

@ -4,9 +4,9 @@ import Markdown from './Markdown'
const Message: React.FC<MessageProps> = ({ type, children }) => {
return (
<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>
)
}

View File

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

View File

@ -4,30 +4,43 @@ const Button = dynamic(() => import('@components/Button'))
const Container = dynamic(() => import('@components/Container'))
const NSFW: React.FC<NSFWProps> = ({ onClick, onDisableClick }) => {
return <Container>
<div className='flex items-center h-screen select-none'>
<div className='px-10'>
<h1 className='text-2xl font-bold flex'>
<img draggable='false' 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='text-lg mb-3'>?</p>
<Button onClick={onClick}>
<i className='fas fa-arrow-right' />
</Button>
<div className='mt-1'>
<button className='text-blue-500 hover:text-blue-600' onClick={() => {
onClick()
onDisableClick()
}}> .</button>
return (
<Container>
<div className='flex h-screen select-none items-center'>
<div className='px-10'>
<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'
/>
19 .
</h1>
<p className='mb-3 text-lg'>?</p>
<Button onClick={onClick}>
<i className='fas fa-arrow-right' />
</Button>
<div className='mt-1'>
<button
className='text-blue-500 hover:text-blue-600'
onClick={() => {
onClick()
onDisableClick()
}}
>
.
</button>
</div>
</div>
</div>
</div>
</Container>
</Container>
)
}
interface NSFWProps {
onClick(): void
onDisableClick(): void
onClick(): void
onDisableClick(): void
}
export default NSFW
export default NSFW

View File

@ -20,309 +20,355 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
const [mobileAddDropdownOpen, setMobileAddDropdownOpen] = useState<boolean>(false)
const router = useRouter()
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')
useEffect(() => {
try {
if(localStorage.userCache) {
if (localStorage.userCache) {
setUserCache(token ? JSON.parse(localStorage.userCache) : null)
}
Fetch<User>('/users/@me').then(data => {
if(data.code !== 200) return
setUserCache(JSON.parse(localStorage.userCache = JSON.stringify({
id: data.data.id,
username: data.data.globalName,
tag: data.data.tag,
version: 2
})))
Fetch<User>('/users/@me').then((data) => {
if (data.code !== 200) return
setUserCache(
JSON.parse(
(localStorage.userCache = JSON.stringify({
id: data.data.id,
username: data.data.globalName,
tag: data.data.tag,
version: 2,
}))
)
)
})
} catch {
setUserCache(null)
}
}, [ token ])
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'>
<div className='relative flex justify-between w-full lg:justify-start lg:w-auto'>
<Link
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`}>
{ dev ? <><i className='fas fa-tools mr-1'/> DEVELOPERS</> : 'KOREANLIST'}
</Link>
<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'
type='button'
onClick={() => setNavbarOpen(!navbarOpen)}
>
<i className={`fas ${!navbarOpen ? 'fa-bars' : 'fa-times'}`}></i>
</button>
<ul className='hidden lg:flex flex-col list-none lg:flex-row lg:ml-auto'>
<li className='flex items-center'>
<Link
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'>
{dev ? '홈' : '개발자'}
</Link>
</li>
{
type !== 'bot' && <li className='flex items-center'>
}, [token])
return (
<>
<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='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
href={dev ? '/developers' : '/'}
className={`${
dev ? 'dark:text-koreanbots-blue ' : ''
}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>
<button
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'
onClick={() => setNavbarOpen(!navbarOpen)}
>
<i className={`fas ${!navbarOpen ? 'fa-bars' : 'fa-times'}`}></i>
</button>
<ul className='hidden list-none flex-col lg:ml-auto lg:flex lg:flex-row'>
<li className='flex items-center'>
<Link
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'>
href={dev ? '/' : '/developers'}
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 ? '홈' : '개발자'}
</Link>
</li>
}
{
type !== 'server' && <li className='flex items-center'>
{type !== 'bot' && (
<li className='flex items-center'>
<Link
href='/bots'
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>
</li>
)}
{type !== 'server' && (
<li className='flex items-center'>
<Link
href='/servers'
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>
</li>
)}
<li className='flex items-center'>
<Link
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'>
href='/discord'
target='_blank'
rel='noreferrer'
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>
</li>
}
<li className='flex items-center'>
<Link
href='/discord'
target='_blank'
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'>
</Link>
</li>
<li className='flex items-center'>
<Link
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'>
</Link>
</li>
<li className='flex items-center' onFocus={() => setAddDropdownOpen(true)} onMouseOver={() => setAddDropdownOpen(true)} onMouseOut={() => setAddDropdownOpen(false)} onBlur={() => setAddDropdownOpen(false)}>
<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'>
</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'}`}>
<ul className='relative'>
<li>
<Link
href='/addbot'
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-t'>
<i className='fas fa-robot' />
</Link>
</li>
<li>
<Link
href='/addserver'
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-b'>
<i className='fas fa-users' />
</Link>
</li>
</ul>
</div>
</li>
</ul>
</div>
<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'>
<li className='flex items-center outline-none' onFocus={() => setDropdownOpen(true)} onMouseOver={() => setDropdownOpen(true)} onMouseOut={() => setDropdownOpen(false)} onBlur={() => setDropdownOpen(false)}>
{
logged ?
<li className='flex items-center'>
<Link
href='/about'
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>
</li>
<li
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>
<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'>
<li>
<Link
href='/addbot'
className='block rounded-t px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
>
<i className='fas fa-robot' />
</Link>
</li>
<li>
<Link
href='/addserver'
className='block rounded-b px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
>
<i className='fas fa-users' />
</Link>
</li>
</ul>
</div>
</li>
</ul>
</div>
<div className='hidden grow items-center bg-white lg:flex lg:bg-transparent lg:shadow-none'>
<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)}
>
{logged ? (
<>
<a
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 userID={userCache.id} className='w-8 h-8 rounded-full mr-1.5' size={128}/>
{userCache.username} <i className='ml-2 fas fa-sort-down' />
<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'>
<DiscordAvatar
userID={userCache.id}
className='mr-1.5 h-8 w-8 rounded-full'
size={128}
/>
{userCache.username} <i className='fas fa-sort-down ml-2' />
</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'>
<li>
<Link
href={`/users/${userCache.id}`}
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-t'>
<i className='fas fa-user' />
className='block rounded-t px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
>
<i className='fas fa-user' />
</Link>
</li>
<li>
<Link
href='/panel'
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover'>
<i className='fas fa-cogs' />
className='block px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
>
<i className='fas fa-cogs' />
</Link>
</li>
{/* <li><hr className='border-t mx-2'/></li> */}
<li>
<a onKeyPress={() => {
localStorage.removeItem('userCache')
redirectTo(router, 'logout')
}
} onClick={() => {
localStorage.removeItem('userCache')
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>
<a
onKeyPress={() => {
localStorage.removeItem('userCache')
redirectTo(router, 'logout')
}}
onClick={() => {
localStorage.removeItem('userCache')
redirectTo(router, 'logout')
}}
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>
</ul>
</div>
</> :
<a tabIndex={0} onClick={()=> {
localStorage.redirectTo = window.location.href
setNavbarOpen(false)
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'>
</>
) : (
<a
tabIndex={0}
onClick={() => {
localStorage.redirectTo = window.location.href
setNavbarOpen(false)
redirectTo(router, 'login')
}}
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>
}
</li>
</ul>
</div>
</div>
</nav>
<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 ${
navbarOpen ? 'block' : 'hidden'
}`}
>
<nav className='mt-20'>
<Link
href={dev ? '/' : '/developers'}
onClick={()=> setNavbarOpen(false)}
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' />
}
<span className='px-2 font-medium'>
{dev ? '홈' : '개발자'}
</span>
</Link>
{
type !== 'bot' && <Link
href='/bots'
onClick={()=> setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
<i className='fas fa-robot' />
<span className='px-2 font-medium'> </span>
</Link>
}
{
type !== 'server' && <Link
href='/servers'
onClick={()=> setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
<i className='fas fa-users' />
<span className='px-2 font-medium'> </span>
</Link>
}
<Link
href='/discord'
target='_blank'
rel='noreferrer'
onClick={()=> setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
<i className='fab fa-discord' />
<span className='px-2 font-medium'> </span>
</Link>
<Link
href='/about'
onClick={()=> setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
<i className='fas fa-layer-group' />
<span className='px-2 font-medium'></span>
</Link>
<a
onClick={()=> {
setMobileAddDropdownOpen(!mobileAddDropdownOpen)
}}
className='flex items-center px-8 py-2 text-gray-100'
>
<i className='fas fa-plus' />
<span className='px-2 font-medium'></span>
</a>
<div className={mobileAddDropdownOpen ? 'px-4 flex flex-col' : 'px-4 hidden'}>
<Link
href='/addbot'
onClick={()=> setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
<i className='fas fa-robot' />
<span className='px-2 font-medium'> </span>
</Link>
<Link
href='/addserver'
onClick={()=> setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
<i className='fas fa-users' />
<span className='px-2 font-medium'> </span>
</Link>
)}
</li>
</ul>
</div>
</div>
</nav>
<div className='my-10'>
{
logged ? <>
<div
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'
}`}
>
<nav className='mt-20'>
<Link
href={dev ? '/' : '/developers'}
onClick={() => setNavbarOpen(false)}
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' />}
<span className='px-2 font-medium'>{dev ? '홈' : '개발자'}</span>
</Link>
{type !== 'bot' && (
<Link
href={`/users/${userCache.id}`}
href='/bots'
onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
onClick={() => setNavbarOpen(!navbarOpen)}>
<i className='far fa-user' />
<span className='px-2 font-medium'>{userCache.username}</span>
>
<i className='fas fa-robot' />
<span className='px-2 font-medium'> </span>
</Link>
)}
{type !== 'server' && (
<Link
href='/panel'
href='/servers'
onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
onClick={() => setNavbarOpen(!navbarOpen)}>
<i className='fas fa-cogs' />
<span className='px-2 font-medium'></span>
>
<i className='fas fa-users' />
<span className='px-2 font-medium'> </span>
</Link>
<a onClick={()=> {
setNavbarOpen(!navbarOpen)
localStorage.removeItem('userCache')
redirectTo(router, 'logout')
}} className='flex items-center px-8 py-2 text-red-500 hover:text-red-400'>
<i className='fas fa-sign-out-alt' />
<span className='px-2 font-medium'></span>
</a>
</> : <a onClick={() => {
localStorage.redirectTo = window.location.href
setNavbarOpen(false)
redirectTo(router, 'login')
}} className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
<i className='far fa-user' />
<span className='px-2 font-medium'></span>
)}
<Link
href='/discord'
target='_blank'
rel='noreferrer'
onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='fab fa-discord' />
<span className='px-2 font-medium'> </span>
</Link>
<Link
href='/about'
onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='fas fa-layer-group' />
<span className='px-2 font-medium'></span>
</Link>
<a
onClick={() => {
setMobileAddDropdownOpen(!mobileAddDropdownOpen)
}}
className='flex items-center px-8 py-2 text-gray-100'
>
<i className='fas fa-plus' />
<span className='px-2 font-medium'></span>
</a>
}
<div className={mobileAddDropdownOpen ? 'flex flex-col px-4' : 'hidden px-4'}>
<Link
href='/addbot'
onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='fas fa-robot' />
<span className='px-2 font-medium'> </span>
</Link>
<Link
href='/addserver'
onClick={() => setNavbarOpen(false)}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='fas fa-users' />
<span className='px-2 font-medium'> </span>
</Link>
</div>
</nav>
<div className='my-10'>
{logged ? (
<>
<Link
href={`/users/${userCache.id}`}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
onClick={() => setNavbarOpen(!navbarOpen)}
>
<i className='far fa-user' />
<span className='px-2 font-medium'>{userCache.username}</span>
</Link>
<Link
href='/panel'
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
onClick={() => setNavbarOpen(!navbarOpen)}
>
<i className='fas fa-cogs' />
<span className='px-2 font-medium'></span>
</Link>
<a
onClick={() => {
setNavbarOpen(!navbarOpen)
localStorage.removeItem('userCache')
redirectTo(router, 'logout')
}}
className='flex items-center px-8 py-2 text-red-500 hover:text-red-400'
>
<i className='fas fa-sign-out-alt' />
<span className='px-2 font-medium'></span>
</a>
</>
) : (
<a
onClick={() => {
localStorage.redirectTo = window.location.href
setNavbarOpen(false)
redirectTo(router, 'login')
}}
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
>
<i className='far fa-user' />
<span className='px-2 font-medium'></span>
</a>
)}
</div>
</div>
</div>
</>
</>
)
}
interface NavbarProps {

View File

@ -1,6 +1,6 @@
const Notice: React.FC<NoticeProps> = ({ header, desc }) => {
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>
<br />
<div>

View File

@ -1,21 +1,23 @@
import Link from 'next/link'
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 (
(<Link
<Link
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'>
<div className='relative shrink-0 mr-3 mt-1 w-8 h-8 rounded-full shadow-inner overflow-hidden'>
<DiscordAvatar userID={id} className='z-negative absolute inset-0 w-full h-full' />
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 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 h-full w-full' />
</div>
<div className='flex-1 w-0 leading-snug'>
<h4 className='whitespace-nowrap truncate'>{ crown && <i className='fas fa-crown text-amber-300 text-xs' /> }{tag === '0' ? globalName : username}</h4>
<span className='text-gray-600 text-sm'>{tag === '0' ? '@' + username : '#' + tag}</span>
<div className='w-0 flex-1 leading-snug'>
<h4 className='truncate whitespace-nowrap'>
{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>
</Link>)
</Link>
)
}

View File

@ -1,5 +1,10 @@
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 = []
if (currentPage < 4)
pages = [
@ -25,7 +30,7 @@ const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname,
currentPage + 1 > totalPage ? null : currentPage + 1,
currentPage + 2 > totalPage ? null : currentPage + 2,
]
pages = pages.filter(el => el)
pages = pages.filter((el) => el)
return (
<div className='flex flex-col items-center justify-center py-4 text-center'>
<div className='flex'>
@ -33,41 +38,38 @@ const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname,
href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage - 1 } }}
className={`${
currentPage === 1 ? 'invisible' : ''
} h-12 w-12 mr-1 flex justify-center items-center rounded-full transition duration-150 ease-in bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover cursor-pointer text-center`}>
} 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>
</Link>
{pages.map((el, i) => (
(<Link
<Link
key={i}
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
? 'rounded-full'
: i === 0
? 'rounded-l-full'
: i === pages.length - 1
? 'rounded-r-full'
: ''
? 'rounded-l-full'
: i === pages.length - 1
? 'rounded-r-full'
: ''
} ${
currentPage === el
? 'bg-gray-300 dark:bg-discord-dark-hover'
: 'bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover'
}`}>
: 'bg-gray-200 hover:bg-gray-300 dark:bg-discord-black dark:hover:bg-discord-dark-hover'
}`}
>
{el}
</Link>)
</Link>
))}
<Link
href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage + 1 } }}
className={`${
currentPage === totalPage ? 'invisible' : ''
} h-12 w-12 ml-1 flex justify-center items-center rounded-full transition duration-150 ease-in bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover cursor-pointer text-center`}>
} 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>
</Link>
</div>
</div>
@ -78,7 +80,7 @@ interface PaginatorProps {
pathname: string
currentPage: number
totalPage: number
searchParams?: Record<string, string|string[]>
searchParams?: Record<string, string | string[]>
}
export default Paginator

View File

@ -1,13 +1,16 @@
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)
return <>{isOSX ? osx ?? children : children}</>
}
interface PlatformDisplayProps {
osx?: ReactNode
children: ReactNode
osx?: ReactNode
children: ReactNode
}
export default PlatformDisplay
export default PlatformDisplay

View File

@ -6,26 +6,28 @@ import { redirectTo } from '@utils/Tools'
const Container = dynamic(() => import('@components/Container'))
const Redirect: React.FC<RedirectProps> = ({ to, text=true, children }) => {
const Redirect: React.FC<RedirectProps> = ({ to, text = true, children }) => {
const router = useRouter()
if(!to) throw new Error('No Link')
if (!to) throw new Error('No Link')
useEffect(() => {
redirectTo(router, to)
})
if(children) return <>
{children}
</>
return <Container paddingTop>
<div>
<a href={to} className='text-blue-400'>{text && '자동으로 리다이렉트되지 않는다면 클릭하세요.'}</a>
</div>
</Container>
if (children) return <>{children}</>
return (
<Container paddingTop>
<div>
<a href={to} className='text-blue-400'>
{text && '자동으로 리다이렉트되지 않는다면 클릭하세요.'}
</a>
</div>
</Container>
)
}
interface RedirectProps {
to: string
to: string
text?: boolean
children?: ReactNode
}
export default Redirect
export default Redirect

View File

@ -5,74 +5,123 @@ import { FormikErrors, FormikTouched } from 'formik'
const Button = dynamic(() => import('@components/Button'))
const TextArea = dynamic(() => import('@components/Form/TextArea'))
export const Check: FC<{ checked: boolean, text: string }> = ({ checked, text }) => <>
{checked && <i className='text-emerald-400 fas fa-check-circle mr-1' />}
{text}
</>
export const Check: FC<{ checked: boolean; text: string }> = ({ checked, text }) => (
<>
{checked && <i className='fas fa-check-circle mr-1 text-emerald-400' />}
{text}
</>
)
export const SubmitButton: FC = () => <div className='text-right'>
<Button type='submit'></Button>
</div>
export const SubmitButton: FC = () => (
<div className='text-right'>
<Button type='submit'></Button>
</div>
)
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>
<SubmitButton />
</>
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-xs font-light text-red-500'>
{errors.description && touched.description ? errors.description : null}
</div>
<SubmitButton />
</>
)
export const DMCA: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => {
const [ isOwner, setOwner ] = useState(null)
const [ contacted, setContacted ] = useState(null)
return <div>
<h3 className='font-bold my-2'> ?</h3>
<Button onClick={() => setOwner(true)}>
<Check checked={isOwner} text='권리자 본인 혹은 대리인입니다.' />
</Button>
<Button onClick={() => setOwner(false)}>
<Check checked={isOwner === false} text='권리자가 아닙니다.' />
</Button>
{
isOwner === true ? <>
<h3 className='font-bold my-2'> ?</h3>
<Button onClick={() => setContacted(true)}>
<Check checked={contacted} text='최대한 연락을 시도하였지만 개선되지 않았습니다.' />
</Button>
<Button onClick={() => setContacted(false)}>
<Check checked={contacted === false} text='아니요, 아직 연락하지 않았습니다.' />
</Button>
{
contacted ? <div>
<h3 className='font-bold mt-2'></h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
<ul className='text-gray-400 text-sm mb-1 list-disc list-inside'>
<li> ( )</li>
<li> ( , )</li>
</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>
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />
</div>
: contacted === false ? <>
<h2 className='font-bold mt-4 text-xl'> .</h2>
<p> , .</p>
</> : ''
}
</>
: isOwner === false ? <>
<h2 className='font-bold mt-4 text-xl'>, .</h2>
const [isOwner, setOwner] = useState(null)
const [contacted, setContacted] = useState(null)
return (
<div>
<h3 className='my-2 font-bold'> ?</h3>
<Button onClick={() => setOwner(true)}>
<Check checked={isOwner} text='권리자 본인 혹은 대리인입니다.' />
</Button>
<Button onClick={() => setOwner(false)}>
<Check checked={isOwner === false} text='권리자가 아닙니다.' />
</Button>
{isOwner === true ? (
<>
<h3 className='my-2 font-bold'>
?
</h3>
<Button onClick={() => setContacted(true)}>
<Check checked={contacted} text='최대한 연락을 시도하였지만 개선되지 않았습니다.' />
</Button>
<Button onClick={() => setContacted(false)}>
<Check checked={contacted === false} text='아니요, 아직 연락하지 않았습니다.' />
</Button>
{contacted ? (
<div>
<h3 className='mt-2 font-bold'></h3>
<p className='mb-1 text-sm text-gray-400'> .</p>
<ul className='mb-1 list-inside list-disc text-sm text-gray-400'>
<li>
(
)
</li>
<li> ( , )</li>
</ul>
<p className='mb-1 text-sm text-gray-400'>
{' '}
<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>
) : contacted === false ? (
<>
<h2 className='mt-4 text-xl font-bold'> .</h2>
<p>
,
.
</p>
</>
) : (
''
)}
</>
) : isOwner === false ? (
<>
<h2 className='mt-4 text-xl font-bold'>
, .
</h2>
<p> , !</p>
</> : ''
}
</div>
</>
) : (
''
)}
</div>
)
}
interface ReportValues {
category: string | null
description: string
_csrf: string
category: string | null
description: string
_csrf: string
}
interface ReportTemplateProps {
values?: ReportValues
errors?: FormikErrors<ReportValues>
touched?: FormikTouched<ReportValues>
setFieldValue?(field: string, value: unknown): void
}
values?: ReportValues
errors?: FormikErrors<ReportValues>
touched?: FormikTouched<ReportValues>
setFieldValue?(field: string, value: unknown): void
}

View File

@ -1,7 +1,9 @@
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'>
{children}
</div>
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}
</div>
)
}
export default ResponsiveGrid
export default ResponsiveGrid

View File

@ -46,7 +46,7 @@ const Search: React.FC = () => {
const res = await Fetch<ListAll>(`/search/all?q=${encodeURIComponent(value)}`, {
signal: controller.signal,
}).catch((e) => {
if(e.name !== 'AbortError') throw e
if (e.name !== 'AbortError') throw e
else return
})
setData(res || {})
@ -54,23 +54,29 @@ const Search: React.FC = () => {
}
const onSubmit = () => {
if(query.length < 2) return
if(!localStorage.recentSearch) localStorage.recentSearch = '[]'
if (query.length < 2) return
if (!localStorage.recentSearch) localStorage.recentSearch = '[]'
try {
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({
value: query,
date: Date.now()
date: Date.now(),
})
d.reverse()
setRecentSearch(d.slice(0, 10))
localStorage.recentSearch = JSON.stringify(d.slice(0, 10))
} catch {
setRecentSearch([{
value: query,
date: Date.now()
}])
setRecentSearch([
{
value: query,
date: Date.now(),
},
])
localStorage.recentSearch = JSON.stringify(recentSearch)
} finally {
redirectTo(router, `/search/?q=${encodeURIComponent(query)}`)
@ -79,19 +85,17 @@ const Search: React.FC = () => {
return (
<div onFocus={() => setHidden(false)} ref={ref}>
<div
className='relative z-10 flex mt-5 w-full text-black dark:text-gray-100 dark:bg-very-black bg-white rounded-lg'
>
<div className='relative z-10 mt-5 flex w-full rounded-lg bg-white text-black dark:bg-very-black dark:text-gray-100'>
<input
type='search'
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='검색...'
value={query}
onChange={e => {
onChange={(e) => {
SearchResults(e.target.value)
}}
onKeyDown={e => {
onKeyDown={(e) => {
if (e.key === 'Enter') {
onSubmit()
}
@ -101,103 +105,116 @@ const Search: React.FC = () => {
className='cusor-pointer absolute right-0 top-0 mr-5 mt-5 outline-none'
onClick={onSubmit}
>
<i className='fas fa-search text-gray-600 hover:text-gray-700 text-2xl' />
<i className='fas fa-search text-2xl text-gray-600 hover:text-gray-700' />
</button>
</div>
<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>
{(data && data.code === 200) ? (
{data && data.code === 200 ? (
<div className='grid lg:grid-cols-2'>
<ul>
<li className='px-3 py-3.5 font-bold'></li>
{
data.data.bots.length === 0 ?
<li className='px-3 py-3.5'> .</li> :
data.data.bots.map(el => (
<Link key={el.id} href={makeBotURL(el)} legacyBehavior>
<li className='h-15 flex px-3 py-2 cursor-pointer'>
<DiscordAvatar className='mt-1 w-12 h-12' size={128} userID={el.id} />
<div className='ml-2'>
<h1 className='text-black dark:text-gray-100 text-lg'>{el.name}</h1>
<p className='text-gray-400 text-sm'>{el.intro}</p>
</div>
</li>
</Link>
))
}
</ul>
<ul>
<li className='px-3 py-3.5 font-bold'></li>
{
data.data.servers.length === 0 ?
<li className='px-3 py-3.5'> .</li> :
data.data.servers.map(el => (
<Link key={el.id} href={makeServerURL(el)} legacyBehavior>
<li className='h-15 flex px-3 py-2 cursor-pointer'>
<ServerIcon className='mt-1 w-12 h-12' size={128} id={el.id} />
<div className='ml-2'>
<h1 className='text-black dark:text-gray-100 text-lg'>{el.name}</h1>
<p className='text-gray-400 text-sm'>{el.intro}</p>
</div>
</li>
</Link>
))
}
</ul>
</div>
) : loading ? <ul>
<li className='px-3 py-3.5'>...</li>
</ul> : <ul>
{query && data ? (
data.message?.includes('문법') ? (
<li className='px-3 py-3.5'>
.
<br />
<a
className='hover:text-blue-400 text-blue-500'
href='https://docs.koreanbots.dev/bots/usage/search'
target='_blank'
rel='noreferrer'
>
</a>
</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='h-15 px-3 py-2 cursor-pointer font-semibold'>
<button className='absolute right-0 pr-10 text-sm text-red-500 hover:opacity-90' onClick={() => {
setRecentSearch([])
localStorage.recentSearch = '[]'
}}>
</button>
</li>
{
recentSearch.slice(0, 10).map((el, n) => (
<Link
key={n}
href={`/search?q=${encodeURIComponent(el?.value)}`}
legacyBehavior>
<li className='h-15 px-3 py-2 cursor-pointer'>
<i className='fas fa-history' /> {el?.value}
<span className='absolute right-0 pr-10 text-gray-400 text-sm'>
{Day(el?.date).format('MM.DD.')}
</span>
{data.data.bots.length === 0 ? (
<li className='px-3 py-3.5'> .</li>
) : (
data.data.bots.map((el) => (
<Link key={el.id} href={makeBotURL(el)} legacyBehavior>
<li className='h-15 flex cursor-pointer px-3 py-2'>
<DiscordAvatar className='mt-1 h-12 w-12' size={128} userID={el.id} />
<div className='ml-2'>
<h1 className='text-lg text-black dark:text-gray-100'>{el.name}</h1>
<p className='text-sm text-gray-400'>{el.intro}</p>
</div>
</li>
</Link>
))
}
</> :
query.length < 3 ? (
)}
</ul>
<ul>
<li className='px-3 py-3.5 font-bold'></li>
{data.data.servers.length === 0 ? (
<li className='px-3 py-3.5'> .</li>
) : (
data.data.servers.map((el) => (
<Link key={el.id} href={makeServerURL(el)} legacyBehavior>
<li className='h-15 flex cursor-pointer px-3 py-2'>
<ServerIcon className='mt-1 h-12 w-12' size={128} id={el.id} />
<div className='ml-2'>
<h1 className='text-lg text-black dark:text-gray-100'>{el.name}</h1>
<p className='text-sm text-gray-400'>{el.intro}</p>
</div>
</li>
</Link>
))
)}
</ul>
</div>
) : loading ? (
<ul>
<li className='px-3 py-3.5'>...</li>
</ul>
) : (
<ul>
{query && data ? (
data.message?.includes('문법') ? (
<li className='px-3 py-3.5'>
.
<br />
<a
className='text-blue-500 hover:text-blue-400'
href='https://docs.koreanbots.dev/bots/usage/search'
target='_blank'
rel='noreferrer'
>
</a>
</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='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={() => {
setRecentSearch([])
localStorage.recentSearch = '[]'
}}
>
</button>
</li>
{recentSearch.slice(0, 10).map((el, n) => (
<Link
key={n}
href={`/search?q=${encodeURIComponent(el?.value)}`}
legacyBehavior
>
<li className='h-15 cursor-pointer px-3 py-2'>
<i className='fas fa-history' /> {el?.value}
<span className='absolute right-0 pr-10 text-sm text-gray-400'>
{Day(el?.date).format('MM.DD.')}
</span>
</li>
</Link>
))}
</>
)
) : query.length < 3 ? (
'최소 2글자 이상 입력해주세요.'
) : (
'검색어를 입력해주세요.'
)}
</ul>
}
</ul>
)}
</ul>
</div>
</div>
@ -208,6 +225,6 @@ const Search: React.FC = () => {
export default Search
interface ListAll {
bots: Bot[],
bots: Bot[]
servers: Server[]
}
}

View File

@ -3,7 +3,7 @@ import { ReactNode } from 'react'
const Segment: React.FC<SegmentProps> = ({ children, className = '' }) => {
return (
<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}
</div>

View File

@ -10,43 +10,50 @@ const Tag = dynamic(() => import('@components/Tag'))
const ServerIcon = dynamic(() => import('@components/ServerIcon'))
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 (
<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='container mx-auto'>
<div className='h-full'>
<div
className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl'
className='relative mx-auto h-full rounded-2xl bg-little-white text-black shadow-xl dark:bg-discord-black dark:text-white'
style={
checkServerFlag(server.flags, 'trusted') && server.banner
? {
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${server.banner}") center top / cover no-repeat`,
color: 'white',
}
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${server.banner}") center top / cover no-repeat`,
color: 'white',
}
: {}
}
>
<Link
href={type !== 'add' ? makeServerURL(server) : newServerLink}
legacyBehavior>
<Link href={type !== 'add' ? makeServerURL(server) : newServerLink} legacyBehavior>
<div>
<div className='flex flex-col'>
<div className='flex'>
<div className='w-3/5 flex justify-start'>
<div className='flex w-3/5 justify-start'>
<ServerIcon
size={128}
id={server.id}
hash={type === 'add' && server.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 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
text={
<>
<i className='fas fa-heart text-red-600' /> {formatNumber(server.votes)}
<i className='fas fa-heart text-red-600' />{' '}
{formatNumber(server.votes)}
</>
}
dark
@ -58,26 +65,38 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
/>
</div>
</div>
<div className='mt-3 px-4 h-16'>
<h2 className={`px-1 text-sm ${server.state !== 'unreachable' ? ' invisible' : ''}`}>
<i className='fas fa-ban text-red-600' />
<div className='mt-3 h-16 px-4'>
<h2
className={`px-1 text-sm ${
server.state !== 'unreachable' ? ' invisible' : ''
}`}
>
<i className='fas fa-ban text-red-600' />
</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>
<p className='mb-10 px-4 h-6 text-left text-gray-400 text-sm font'>
{type === 'add' ?
server.data ? '지금 바로 서버를 등록할 수 있습니다.' : '봇을 초대해야 서버를 등록할 수 있습니다.'
: server.intro
}
<p className='font mb-10 h-6 px-4 text-left text-sm text-gray-400'>
{type === 'add'
? server.data
? '지금 바로 서버를 등록할 수 있습니다.'
: '봇을 초대해야 서버를 등록할 수 있습니다.'
: server.intro}
</p>
<div>
<div className='category flex flex-wrap px-2'>
{server.category?.slice(0, 3).map(el => (
<Tag key={el} text={el} href={`/servers/categories/${el}`} dark />
))}{' '}
{server.category?.length > 3 && <Tag text={`+${server.category.length - 3}`} dark />}
{server.category
?.slice(0, 3)
.map((el) => (
<Tag key={el} text={el} href={`/servers/categories/${el}`} dark />
))}{' '}
{server.category?.length > 3 && (
<Tag text={`+${server.category.length - 3}`} dark />
)}
</div>
</div>
</div>
@ -85,59 +104,55 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
<Divider />
<div className='w-full'>
<div className='flex justify-evenly'>
{
type === 'add' ?
server.data ? <Link
{type === 'add' ? (
server.data ? (
<Link
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'>
</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'>
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='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
href={makeServerURL(server)}
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>
{type === 'manage' ? (
<Link
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'>
</Link>
{type === 'manage' ? (
<Link
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'>
</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'
href={`/servers/${server.id}/edit`}
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'
>
</a> :
<a
href={
makeServerURL(server) + '/join'
}
rel='noopener noreferrer'
target='_blank'
className='py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'
>
</a>
}
</>
}
</Link>
) : !['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
href={makeServerURL(server) + '/join'}
rel='noopener noreferrer'
target='_blank'
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>
)}
</>
)}
</div>
</div>
</div>
@ -146,28 +161,26 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
</div>
</div>
)
}
interface BotCardProps {
type: 'list' | 'manage' | 'add'
server: {
id: string,
name: string,
id: string
name: string
intro?: string
desc?: string,
desc?: string
flags?: number
state?: ServerState
icon: string | null,
banner?: string | null,
bg?: string | null,
icon: string | null
banner?: string | null
bg?: string | null
vanity?: string | null
category?: string[]
votes?: number | null
members?: number | null,
members?: number | null
data?: ServerData
}
}
export default ServerCard
export default ServerCard

View File

@ -5,13 +5,22 @@ import { DiscordEnpoints, KoreanbotsEndPoints } from '@utils/Constants'
const Image = dynamic(() => import('@components/Image'))
const ServerIcon: React.FC<ServerIconProps> = ({ id, size, className, alt, hash }) => {
return <Image
className={className}
alt={alt}
src={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})}
/>
return (
<Image
className={className}
alt={alt}
src={
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 {
@ -20,7 +29,7 @@ interface ServerIconProps {
hash?: string
fromDiscord?: boolean
className?: string
size? : 128 | 256 | 512
size?: 128 | 256 | 512
}
interface ImageEvent extends Event {

View File

@ -7,23 +7,23 @@ import Link from 'next/link'
const SubmittedBotCard: React.FC<SubmittedBotProps> = ({ href, submit }) => {
return (
(<Link
<Link
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='flex'>
<div className='grow w-full'>
<div className='w-full grow'>
<h2 className='text-lg'>{submit.id}</h2>
</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
text={
<>
<i
className={`fas fa-circle text-${
[Status.offline, Status.online, Status.dnd][submit.state]?.color
}`}
className={`fas fa-circle text-${[Status.offline, Status.online, Status.dnd][
submit.state
]?.color}`}
/>{' '}
{['대기중', '승인됨', '거부됨'][submit.state]}
</>
@ -32,13 +32,12 @@ const SubmittedBotCard: React.FC<SubmittedBotProps> = ({ href, submit }) => {
/>
</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.length > 25 && '...'}
</p>
</div>
</Link>)
</Link>
)
}

View File

@ -26,18 +26,18 @@ const Tag: React.FC<LabelProps> = ({
? 'bg-discord-blurple text-white'
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
: github
? 'bg-gray-900 text-white hover:bg-gray-700'
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
? 'bg-gray-900 text-white hover:bg-gray-700'
: 'bg-little-white hover:bg-little-white-hover dark:bg-discord-black'
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
circular
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}
} mr-1 mb-${marginBottom} transition duration-100 ease-in dark:hover:bg-discord-dark-hover`}
>
{text}
</a>
) : (
(<Link
<Link
href={href}
className={`${className ?? ''} text-center text-base ${
dark
@ -45,19 +45,18 @@ const Tag: React.FC<LabelProps> = ({
? 'bg-discord-blurple text-white'
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
: github
? 'bg-gray-900 text-white hover:bg-gray-700'
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
? 'bg-gray-900 text-white hover:bg-gray-700'
: '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'
} ${
circular
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}>
} mr-1 mb-${marginBottom} transition duration-100 ease-in dark:hover:bg-discord-dark-hover`}
>
{text}
</Link>)
</Link>
)
) : (
<a
@ -67,17 +66,17 @@ const Tag: React.FC<LabelProps> = ({
? blurple
? 'font-bg bg-discord-blurple text-white'
: github
? 'bg-gray-900 text-white hover:bg-gray-700'
: `bg-little-white-hover dark:bg-very-black ${
props.onClick
? 'hover:bg-little-white dark:hover:bg-discord-dark-hover transition duration-100 ease-in'
: ''
}`
? 'bg-gray-900 text-white hover:bg-gray-700'
: `bg-little-white-hover dark:bg-very-black ${
props.onClick
? 'transition duration-100 ease-in hover:bg-little-white dark:hover:bg-discord-dark-hover'
: ''
}`
: `bg-little-white dark:bg-discord-black ${
props.onClick
? 'hover:bg-little-white-hover dark:hover:bg-discord-dark-hover transition duration-100 ease-in'
: ''
}`
props.onClick
? 'transition duration-100 ease-in hover:bg-little-white-hover dark:hover:bg-discord-dark-hover'
: ''
}`
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
circular
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`

View File

@ -1,18 +1,18 @@
const Toggle: React.FC<ToggleProps> = ({ checked, onChange }: ToggleProps) => {
return (
<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}
onKeyPress={onChange}
>
<input
type='checkbox'
checked={checked}
className='absolute checked:right-0 block w-6 h-6 bg-white border-4 border-transparent rounded-full outline-none appearance-none cursor-pointer'
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
/>
<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' : ''
}`}
></span>

View File

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

View File

@ -3,20 +3,19 @@ import { ErrorText } from '@utils/Constants'
const NotFound: NextPage<{ message?: string }> = ({ message }) => {
return (
<div
className='flex items-center justify-center h-screen select-none text-center'
>
<div className='flex h-screen select-none items-center justify-center text-center'>
<div>
<div className='flex flex-row justify-center text-9xl'>
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
</div>
<h2 className='text-2xl font-semibold'>
{message || ErrorText[404]}
</h2>
<h2 className='text-2xl font-semibold'>{message || ErrorText[404]}</h2>
</div>
</div>
)
}

View File

@ -34,9 +34,9 @@ Router.events.on('routeChangeError', NProgress.done)
ReactGA.initialize('UA-165454387-1')
const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps): JSX.Element => {
const [ shortcutModal, setShortcutModal ] = useState(false)
const [ theme, setTheme ] = useState<Theme>('system')
const [ standalone, setStandalone ] = useState(false)
const [shortcutModal, setShortcutModal] = useState(false)
const [theme, setTheme] = useState<Theme>('system')
const [standalone, setStandalone] = useState(false)
const router = useRouter()
useEffect(() => {
@ -44,7 +44,11 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
'%c' + 'KOREANBOTS',
'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(
'%c' + '이곳에 코드를 붙여넣으면 공격자에게 엑세스 토큰을 넘겨줄 수 있습니다!!',
'color: #ff0000; font-size: 20px; font-weight: bold;'
@ -52,127 +56,147 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
if (!localStorage.theme) {
Logger.debug(`[THEME] ${systemTheme().toUpperCase()} THEME DETECTED`)
setTheme(systemTheme())
}
else setTheme(localStorage.theme)
} else setTheme(localStorage.theme)
setStandalone(handlePWA())
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}>
<DefaultSeo
titleTemplate='%s - 한국 디스코드 리스트'
defaultTitle={TITLE}
description={DESCRIPTION}
openGraph={{
type: 'website',
title: TITLE,
url: 'https://koreanbots.dev',
site_name: TITLE,
description: DESCRIPTION,
images: [
{
url: '/logo.png',
width: 300,
height: 300,
alt: 'Logo'
}
]
}}
twitter={{
site: '@koreanbots',
handle: '@koreanbots',
cardType: 'summary'
}}
/>
<Head>
{/* META */}
<meta charSet='utf-8' />
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
<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' />
return (
<div className={theme}>
<DefaultSeo
titleTemplate='%s - 한국 디스코드 리스트'
defaultTitle={TITLE}
description={DESCRIPTION}
openGraph={{
type: 'website',
title: TITLE,
url: 'https://koreanbots.dev',
site_name: TITLE,
description: DESCRIPTION,
images: [
{
url: '/logo.png',
width: 300,
height: 300,
alt: 'Logo',
},
],
}}
twitter={{
site: '@koreanbots',
handle: '@koreanbots',
cardType: 'summary',
}}
/>
<Head>
{/* META */}
<meta charSet='utf-8' />
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
<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'
/>
{/* Android */}
<meta name='theme-color' content={THEME_COLOR} />
<meta name='mobile-web-app-capable' content='yes' />
{/* iOS */}
<meta name='apple-mobile-web-app-title' content='Application Title' />
<meta name='apple-mobile-web-app-capable' content='yes' />
<meta name='apple-mobile-web-app-status-bar-style' content='default' />
{/* Windows */}
<meta name='msapplication-navbutton-color' content={THEME_COLOR} />
<meta name='msapplication-TileColor' content={THEME_COLOR} />
<meta name='msapplication-TileImage' content='/static/ms-icon-144x144.png' />
<meta name='msapplication-config' content='browserconfig.xml' />
{/* Android */}
<meta name='theme-color' content={THEME_COLOR} />
<meta name='mobile-web-app-capable' content='yes' />
{/* Pinned Sites */}
<meta name='application-name' content={TITLE} />
<meta name='msapplication-tooltip' content={DESCRIPTION} />
<meta name='msapplication-starturl' content='/' />
{/* iOS */}
<meta name='apple-mobile-web-app-title' content='Application Title' />
<meta name='apple-mobile-web-app-capable' content='yes' />
<meta name='apple-mobile-web-app-status-bar-style' content='default' />
{/* Tap highlighting */}
<meta name='msapplication-tap-highlight' content='no' />
{/* Windows */}
<meta name='msapplication-navbutton-color' content={THEME_COLOR} />
<meta name='msapplication-TileColor' content={THEME_COLOR} />
<meta name='msapplication-TileImage' content='/static/ms-icon-144x144.png' />
<meta name='msapplication-config' content='browserconfig.xml' />
{/* UC Mobile Browser */}
<meta name='full-screen' content='yes' />
<meta name='browsermode' content='application' />
{/* Pinned Sites */}
<meta name='application-name' content={TITLE} />
<meta name='msapplication-tooltip' content={DESCRIPTION} />
<meta name='msapplication-starturl' content='/' />
<meta name='nightmode' content='disable' />
<meta name='layoutmode' content='fitscreen' />
<meta name='imagemode' content='force' />
<meta name='screen-orientation' content='portrait' />
</Head>
<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'>
<Component {...pageProps} err={err} theme={theme} setTheme={setTheme} pwa={standalone} />
</div>
{
!(router.pathname.startsWith('/developers')) && <Footer theme={theme} setTheme={setTheme} />
}
<Modal full isOpen={shortcutModal} onClose={() => setShortcutModal(false)} dark={theme === 'dark'} header='단축키 안내'>
<div className='px-3 h-80'>
<h3 className='text-md font-semibold'></h3>
<ul>
<li className='pt-2'>
<h4 className='text-gray-500 dark:text-gray-400 text-xs'> </h4>
<kbd>
<PlatformDisplay osx='CMD'>
Ctrl
</PlatformDisplay>
</kbd> <kbd>/</kbd>
</li>
<li className='pt-2'>
<h4 className='text-gray-500 dark:text-gray-400 text-xs'> </h4>
<kbd>
<PlatformDisplay osx='CMD'>
Ctrl
</PlatformDisplay>
</kbd>
<kbd>Shift</kbd> <kbd>D</kbd>
</li>
</ul>
{/* Tap highlighting */}
<meta name='msapplication-tap-highlight' content='no' />
{/* UC Mobile Browser */}
<meta name='full-screen' content='yes' />
<meta name='browsermode' content='application' />
<meta name='nightmode' content='disable' />
<meta name='layoutmode' content='fitscreen' />
<meta name='imagemode' content='force' />
<meta name='screen-orientation' content='portrait' />
</Head>
<Navbar token={cookie.token} />
<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} />
</div>
</Modal>
<GlobalHotKeys keyMap={shortcutKeyMap} handlers={{
SHORTCUT_HELP: (event) => {
event.preventDefault()
setShortcutModal(value => !value)
return
},
CHANGE_THEME: (event) => {
event.preventDefault()
const overwrite = (localStorage.theme || systemTheme()) === 'dark' ? 'light' : 'dark'
setTheme(overwrite)
localStorage.setItem('theme', overwrite)
return false
}
}} />
</div>
{!router.pathname.startsWith('/developers') && <Footer theme={theme} setTheme={setTheme} />}
<Modal
full
isOpen={shortcutModal}
onClose={() => setShortcutModal(false)}
dark={theme === 'dark'}
header='단축키 안내'
>
<div className='h-80 px-3'>
<h3 className='text-md font-semibold'></h3>
<ul>
<li className='pt-2'>
<h4 className='text-xs text-gray-500 dark:text-gray-400'> </h4>
<kbd>
<PlatformDisplay osx='CMD'>Ctrl</PlatformDisplay>
</kbd>{' '}
<kbd>/</kbd>
</li>
<li className='pt-2'>
<h4 className='text-xs text-gray-500 dark:text-gray-400'> </h4>
<kbd>
<PlatformDisplay osx='CMD'>Ctrl</PlatformDisplay>
</kbd>
<kbd>Shift</kbd> <kbd>D</kbd>
</li>
</ul>
</div>
</Modal>
<GlobalHotKeys
keyMap={shortcutKeyMap}
handlers={{
SHORTCUT_HELP: (event) => {
event.preventDefault()
setShortcutModal((value) => !value)
return
},
CHANGE_THEME: (event) => {
event.preventDefault()
const overwrite = (localStorage.theme || systemTheme()) === 'dark' ? 'light' : 'dark'
setTheme(overwrite)
localStorage.setItem('theme', overwrite)
return false
},
}}
/>
</div>
)
}
KoreanbotsApp.getInitialProps = async (appCtx: AppContext) => {
@ -180,7 +204,7 @@ KoreanbotsApp.getInitialProps = async (appCtx: AppContext) => {
const parsed = parseCookie(appCtx.ctx.req)
return {
...appProps,
cookie: parsed
cookie: parsed,
}
}

View File

@ -13,7 +13,12 @@ class MyDocument extends Document {
<Head>
{/* LINK */}
<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
rel='stylesheet'
href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.6.0/styles/solarized-dark.min.css'
@ -21,8 +26,13 @@ class MyDocument extends Document {
<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='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 */}
<link rel='apple-touch-icon' sizes='57x57' href='/static/apple-icon-57x57.png' />
<link rel='apple-touch-icon' sizes='60x60' href='/static/apple-icon-60x60.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='256x256' href='/static/apple-icon-256x256.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 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)' />
<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)' />
<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)' />
<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 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)' />
<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
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)'
/>
<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)'
/>
<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)'
/>
<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
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 */}
<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 */}
<link rel='shortcut icon' href='/favicon.ico' />
{/* 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 data-cfasync='false' async src='//www.googletagmanager.com/gtag/js?id=UA-165454387-1'></script>
<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
dangerouslySetInnerHTML={{
__html: `
@ -91,7 +218,7 @@ class MyDocument extends Document {
}}
/>
</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 />
<NextScript />
</body>

View File

@ -8,24 +8,35 @@ const Container = dynamic(() => import('@components/Container'))
const MyError: NextPage = () => {
return (
<div
className='flex items-center h-screen select-none px-20'
>
<div className='flex h-screen select-none items-center px-20'>
<Container>
<h2 className='text-4xl font-semibold'>{getRandom(ErrorMessage)}</h2>
<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>
<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>
<div>
<Link
href='/discord'
target='_blank'
rel='noreferrer'
className='text-lg hover:opacity-80 cursor-pointer'>
className='cursor-pointer text-lg hover:opacity-80'
>
<i className='fab fa-discord' />
</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' />
</a>
</div>
@ -34,4 +45,4 @@ const MyError: NextPage = () => {
)
}
export default MyError
export default MyError

View File

@ -6,24 +6,33 @@ const Container = dynamic(() => import('@components/Container'))
const MyError: NextPage = () => {
return (
<div
className='flex items-center h-screen select-none px-20'
>
<div className='flex h-screen select-none items-center px-20'>
<Container>
<h2 className='text-4xl font-semibold'> ...</h2>
<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>
<Link
href='/discord'
target='_blank'
rel='noreferrer'
className='text-lg hover:opacity-80 cursor-pointer'>
className='cursor-pointer text-lg hover:opacity-80'
>
<i className='fab fa-discord' />
</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' />
</a>
</div>
@ -32,4 +41,4 @@ const MyError: NextPage = () => {
)
}
export default MyError
export default MyError

View File

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

View File

@ -31,10 +31,10 @@ const Container = dynamic(() => import('@components/Container'))
const Message = dynamic(() => import('@components/Message'))
const Captcha = dynamic(() => import('@components/Captcha'))
const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
const [ data, setData ] = useState<ResponseProps<SubmittedBot>>(null)
const [ captcha, setCaptcha ] = useState(false)
const [ touchedSumbit, setTouched ] = useState(false)
const AddBot: NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
const [data, setData] = useState<ResponseProps<SubmittedBot>>(null)
const [captcha, setCaptcha] = useState(false)
const [touchedSumbit, setTouched] = useState(false)
const captchaRef = useRef<HCaptcha>()
const router = useRouter()
const initialValues: AddBotSubmit = {
@ -58,159 +58,334 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
-
- ?`,
_csrf: csrfToken,
_captcha: 'captcha'
_captcha: 'captcha',
}
function toLogin() {
localStorage.redirectTo = window.location.href
redirectTo(router, 'login')
}
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)
}
if(!logged) return <Login>
<NextSeo title='새로운 봇 추가하기' description='자신의 봇을 한국 디스코드 리스트에 등록하세요.' openGraph={{
title:'새로운 봇 추가하기', description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.'
}} />
</Login>
if(data?.data && data.code === 200) {
setTimeout(
() => redirectTo(router, `/pendingBots/${data.data.id}/${data.data.date}`),
1_000
if (!logged)
return (
<Login>
<NextSeo
title='새로운 봇 추가하기'
description='자신의 봇을 한국 디스코드 리스트에 등록하세요.'
openGraph={{
title: '새로운 봇 추가하기',
description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.',
}}
/>
</Login>
)
if (data?.data && data.code === 200) {
setTimeout(() => redirectTo(router, `/pendingBots/${data.data.id}/${data.data.date}`), 1_000)
}
return (
<Container paddingTop className='py-5'>
<NextSeo title='새로운 봇 추가하기' description='자신의 봇을 한국 디스코드 리스트에 등록하세요.' openGraph={{
title:'새로운 봇 추가하기', description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.'
}} />
<NextSeo
title='새로운 봇 추가하기'
description='자신의 봇을 한국 디스코드 리스트에 등록하세요.'
openGraph={{
title: '새로운 봇 추가하기',
description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.',
}}
/>
<h1 className='text-3xl font-bold'> </h1>
<div className='mt-1 mb-5'>
, <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>
<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='cursor-pointer text-discord-blurple outline-none'
>
?
</a>
</div>
{
data ? data.code == 200 && data.data ? <Message type='success'>
<h2 className='text-lg font-extrabold'> !</h2>
<p> ! .</p>
</Message> : <Message type='error'>
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
<ul className='list-disc list-inside'>
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message> : <></>
}
<Formik initialValues={initialValues}
{data ? (
data.code == 200 && data.data ? (
<Message type='success'>
<h2 className='text-lg font-extrabold'> !</h2>
<p> ! .</p>
</Message>
) : (
<Message type='error'>
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
<ul className='list-inside list-disc'>
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message>
)
) : (
<></>
)}
<Formik
initialValues={initialValues}
validationSchema={AddBotSubmitSchema}
onSubmit={() => setCaptcha(true)}>
onSubmit={() => setCaptcha(true)}
>
{({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => (
<Form>
<div className='py-3'>
<Message type='warning'>
<h2 className='text-lg font-extrabold'> !</h2>
<ul className='list-disc list-inside'>
<li><Link
href='/discord'
rel='noreferrer'
target='_blank'
className='text-blue-500 hover:text-blue-600'> </Link> ?</li>
<li> <Link
href='/guidelines'
rel='noreferrer'
target='_blank'
className='text-blue-500 hover:text-blue-600'></Link> ?</li>
<li> ? , .</li>
<h2 className='text-lg font-extrabold'>
!
</h2>
<ul className='list-inside list-disc'>
<li>
<Link
href='/discord'
rel='noreferrer'
target='_blank'
className='text-blue-500 hover:text-blue-600'
>
</Link>
?
</li>
<li>
{' '}
<Link
href='/guidelines'
rel='noreferrer'
target='_blank'
className='text-blue-500 hover:text-blue-600'
>
</Link>
?
</li>
<li>
? ,
.
</li>
<li>, API에 .</li>
</ul>
</Message>
</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'>
<CheckBox name='agree' />
<strong className='text-sm ml-2'> , .</strong>
<strong className='ml-2 text-sm'>
,
.
</strong>
</div>
</Label>
<Divider />
<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' />
</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='!' />
</Label>
<Label 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
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 For='category' label='카테고리' labelDesc='봇에 해당되는 카테고리를 선택해주세요' 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)} />
<p className='text-gray-400 mt-1 text-sm'> 3 . . <strong> .</strong><br/>
<a className='text-blue-500 hover:text-blue-400' href='https://contents.koreanbots.dev/categories'></a> !</p>
<Label
For='category'
label='카테고리'
labelDesc='봇에 해당되는 카테고리를 선택해주세요'
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)}
/>
<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>
<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' />
</Label>
<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'/>
<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' />
</Label>
<Label For='inviteLink' 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='text-gray-400 mt-1 text-sm'>
<Label
For='inviteLink'
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
href='/calculator'
rel='noreferrer'
target='_blank'
className='text-blue-500 hover:text-blue-400'>
</Link> !
className='text-blue-500 hover:text-blue-400'
>
</Link>
!
</span>
</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'>
discord.gg/<Input name='discord' placeholder='JEh53MQ' />
discord.gg/
<Input name='discord' placeholder='JEh53MQ' />
</div>
</Label>
<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='국내 봇을 한 곳에서.' />
</Label>
<Label 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
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 For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다.'>
<Label
For='preview'
label='설명 미리보기'
labelDesc='다음 결과는 실제와 다를 수 있습니다.'
>
<Segment>
<Markdown text={values.desc} />
</Segment>
</Label>
<Divider />
<p className='text-base mt-2 mb-5'>
<span className='text-red-500 font-semibold'> *</span> =
<p className='mb-5 mt-2 text-base'>
<span className='font-semibold text-red-500'> *</span> =
</p>
{
captcha ? <Captcha ref={captchaRef} dark={theme === 'dark'} onVerify={(token) => {
submitBot(values, token)
window.scrollTo({ top: 0 })
setCaptcha(false)
captchaRef?.current?.resetCaptcha()
}} /> : <>
{
touchedSumbit && !isValid && <div className='my-1 text-red-500 text-xs font-light'> . .</div>
}
<Button type='submit' onClick={() => {
setTouched(true)
if(!isValid) window.scrollTo({ top: 0 })
} }>
{captcha ? (
<Captcha
ref={captchaRef}
dark={theme === 'dark'}
onVerify={(token) => {
submitBot(values, token)
window.scrollTo({ top: 0 })
setCaptcha(false)
captchaRef?.current?.resetCaptcha()
}}
/>
) : (
<>
{touchedSumbit && !isValid && (
<div className='my-1 text-xs font-light text-red-500'>
. .
</div>
)}
<Button
type='submit'
onClick={() => {
setTouched(true)
if (!isValid) window.scrollTo({ top: 0 })
}}
>
<>
<i className='far fa-paper-plane'/>
<i className='far fa-paper-plane' />
</>
</Button>
</>
}
)}
</Form>
)}
</Formik>
@ -222,7 +397,13 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
export const getServerSideProps = async (ctx: NextPageContext) => {
const parsed = parseCookie(ctx.req)
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 {

View File

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

View File

@ -3,7 +3,7 @@ import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo'
import { get } from '@utils/Query'
import { parseCookie} from '@utils/Tools'
import { parseCookie } from '@utils/Tools'
import { RawGuild, ServerData, Theme, User } from '@types'
const Advertisement = dynamic(() => import('@components/Advertisement'))
@ -12,40 +12,71 @@ const ServerCard = dynamic(() => import('@components/ServerCard'))
const Login = dynamic(() => import('@components/Login'))
const Container = dynamic(() => import('@components/Container'))
const AddBot:NextPage<AddBotProps> = ({ logged, guilds }) => {
if(!logged) return <Login>
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.'
}} />
</Login>
return <Container paddingTop className='py-5'>
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.'
}} />
<h1 className='text-3xl font-bold'> </h1>
<p className='text-gray-400'> .</p>
<p className='text-gray-400 pb-5'> . , 1 .</p>
<Advertisement />
<ResponsiveGrid>
{
guilds.sort((a ,b) => (+!!b.data || 0) - (+!!a.data || 0)).map(g => (
<ServerCard type={g.exists ? 'manage' : 'add'} server={g} key={g.id} />
))
}
</ResponsiveGrid>
<Advertisement />
</Container>
const AddBot: NextPage<AddBotProps> = ({ logged, guilds }) => {
if (!logged)
return (
<Login>
<NextSeo
title='새로운 서버 추가하기'
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
openGraph={{
title: '새로운 서버 추가하기',
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
}}
/>
</Login>
)
return (
<Container paddingTop className='py-5'>
<NextSeo
title='새로운 서버 추가하기'
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
openGraph={{
title: '새로운 서버 추가하기',
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
}}
/>
<h1 className='text-3xl font-bold'> </h1>
<p className='text-gray-400'> .</p>
<p className='pb-5 text-gray-400'>
. , 1 .
</p>
<Advertisement />
<ResponsiveGrid>
{guilds
.sort((a, b) => (+!!b.data || 0) - (+!!a.data || 0))
.map((g) => (
<ServerCard type={g.exists ? 'manage' : 'add'} server={g} key={g.id} />
))}
</ResponsiveGrid>
<Advertisement />
</Container>
)
}
export const getServerSideProps = async (ctx: NextPageContext) => {
const parsed = parseCookie(ctx.req)
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 server = (await get.server.load(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 { props: { logged: !!user || !!guilds, user: await get.user.load(user || ''), guilds: guilds ? (await Promise.all(guilds)).filter(g => !g?.exists) : null } }
const guilds = (await get.userGuilds.load(user || ''))
?.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)
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,
},
}
}
interface AddBotProps {
@ -53,7 +84,7 @@ interface AddBotProps {
user: User
csrfToken: string
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 validate = await OauthCallbackSchema.validate(req.query)
.then(r => r)
.catch(e => {
.then((r) => r)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
@ -35,7 +35,7 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}).then(r => r.json())
}).then((r) => r.json())
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
const user: DiscordUserInfo = await fetch(DiscordEnpoints.Me, {
@ -43,7 +43,7 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
headers: {
Authorization: `${token.token_type} ${token.access_token}`,
},
}).then(r => r.json())
}).then((r) => r.json())
const userToken = await update.assignToken({
id: user.id,
@ -53,11 +53,13 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
email: user.email,
username: user.username,
discriminator: user.discriminator,
verified: user.verified
verified: user.verified,
})
if(userToken === 1) 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')
if (userToken === 1)
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)
res.setHeader(
'set-cookie',

View File

@ -10,8 +10,8 @@ import RequestHandler from '@utils/RequestHandler'
const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
const validate = await OauthCallbackSchema.validate(req.query)
.then(r => r)
.catch(e => {
.then((r) => r)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
@ -20,21 +20,29 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
const user = await get.Authorization(req.cookies.token)
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), {
method: 'POST',
headers: {
Accept: 'application/json'
},
}).then(r => r.json())
const token: GithubTokenInfo = await fetch(
SpecialEndPoints.Github.Token(
process.env.GITHUB_CLIENT_ID,
process.env.GITHUB_CLIENT_SECRET,
req.query.code
),
{
method: 'POST',
headers: {
Accept: 'application/json',
},
}
).then((r) => r.json())
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
const github: { login: string } = await fetch(SpecialEndPoints.Github.Me, {
headers: {
Authorization: `token ${token.access_token}`
}
}).then(r => r.json())
Authorization: `token ${token.access_token}`,
},
}).then((r) => r.json())
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)
res.redirect(301, '/panel')
})

View File

@ -5,17 +5,15 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import { get, update } from '@utils/Query'
import { checkToken } from '@utils/Csrf'
const Github = RequestHandler().get(async (_req: NextApiRequest, res: NextApiResponse) => {
res.redirect(
301,
generateOauthURL('github', process.env.GITHUB_CLIENT_ID)
)
})
const Github = RequestHandler()
.get(async (_req: NextApiRequest, res: NextApiResponse) => {
res.redirect(301, generateOauthURL('github', process.env.GITHUB_CLIENT_ID))
})
.delete(async (req: DeleteApiRequest, res) => {
const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 })
const csrfValidated = checkToken(req, res, req.body._csrf)
if(!csrfValidated) return
if (!csrfValidated) return
await update.Github(user, null)
get.user.clear(user)
return ResponseWrapper(res, { code: 200 })

View File

@ -11,7 +11,9 @@ const rateLimiter = rateLimit({
windowMs: 60 * 1000,
max: 150,
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('Cache-Control', 'no-cache')
res.send(img)
@ -20,45 +22,57 @@ const rateLimiter = rateLimit({
skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global')
return false
}
},
})
const Avatar = RequestHandler()
.get(rateLimiter)
.get(async(req: ApiRequest, res) => {
.get(async (req: ApiRequest, res) => {
res.setHeader('Access-Control-Allow-Origin', process.env.KOREANBOTS_URL)
const { id: param, size='256' } = req.query
const { id: param, size = '256' } = req.query
const splitted = param.split('.')
let ext = splitted[1]
const id = splitted[0]
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false }).then(el=> el).catch(e=> {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if(!validated) return
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false })
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if (!validated) return
const user = await get.discord.user.load(id)
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 }))
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) {
img = await get.images.user.load(DiscordEnpoints.CDN.default(user.discriminator, { format: 'png', size: validated.size }))
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 }
)
)
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) {
img = await get.images.user.load(
DiscordEnpoints.CDN.default(user.discriminator, { format: 'png', size: validated.size })
)
ext = 'png'
}
res.setHeader('Content-Type', `image/${ext}`)
res.setHeader('Cache-Control', 'public, max-age=86400')
res.send(img)
})
interface ApiRequest extends NextApiRequest {
query: {
id: string
size?: '128' | '256' | '512'
size?: '128' | '256' | '512'
}
}
export default Avatar
export default Avatar

View File

@ -11,7 +11,9 @@ const rateLimiter = rateLimit({
windowMs: 60 * 1000,
max: 150,
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('Cache-Control', 'no-cache')
res.send(img)
@ -20,45 +22,51 @@ const rateLimiter = rateLimit({
skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global')
return false
}
},
})
const Icon = RequestHandler()
.get(rateLimiter)
.get(async(req: ApiRequest, res) => {
.get(async (req: ApiRequest, res) => {
res.setHeader('Access-Control-Allow-Origin', process.env.KOREANBOTS_URL)
const { id: param, size='256' } = req.query
const { id: param, size = '256' } = req.query
const splitted = param.split('.')
let ext = splitted[1]
const id = splitted[0]
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false }).then(el=> el).catch(e=> {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if(!validated) return
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false })
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if (!validated) return
const guild = await get.server.load(id)
let img: Buffer
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 }))
if(!img) {
img = await get.images.server.load(DiscordEnpoints.CDN.default(+id % 4, { format: 'png', size: validated.size }))
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,
})
)
if (!img) {
img = await get.images.server.load(
DiscordEnpoints.CDN.default(+id % 4, { format: 'png', size: validated.size })
)
ext = 'png'
}
res.setHeader('Content-Type', `image/${ext}`)
res.setHeader('Cache-Control', 'public, max-age=86400')
res.send(img)
})
interface ApiRequest extends NextApiRequest {
query: {
id: string
size?: '128' | '256' | '512'
size?: '128' | '256' | '512'
}
}
export default Icon
export default Icon

View File

@ -1 +1 @@
export { default } from './[...404]'
export { default } from './[...404]'

View File

@ -1,4 +1,4 @@
import { NextApiRequest} from 'next'
import { NextApiRequest } from 'next'
import rateLimit from 'express-rate-limit'
import { EmbedBuilder } from 'discord.js'
@ -18,43 +18,65 @@ const limiter = rateLimit({
keyGenerator: (req) => req.headers.authorization,
skip: (req, res) => {
res.removeHeader('X-RateLimit-Global')
if(!req.headers.authorization) return true
if (!req.headers.authorization) return true
else return false
}
},
})
const BotStats = RequestHandler()
.post(limiter)
.post(async (req: PostApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.token)
if(!bot) return ResponseWrapper(res, { code: 401, version: 1 })
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, { abortEarly: false })
.then(el => el)
.catch(e => {
if (!bot) return ResponseWrapper(res, { code: 401, version: 1 })
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, {
abortEarly: false,
})
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if(!validated) return
if (!validated) return
const botInfo = await get.bot.load(bot)
if(!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.', version: 1 })
if(botInfo.id !== bot) return ResponseWrapper(res, { code: 403, version: 1 })
if (!botInfo)
return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.', version: 1 })
if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403, version: 1 })
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)
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')}`,
embeds: [new EmbedBuilder().setDescription(`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(botInfo.id)}`)]
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'
)}`,
embeds: [
new EmbedBuilder().setDescription(
`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(
botInfo.id
)}`
),
],
})
return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.', version: 1 })
})
interface PostApiRequest extends NextApiRequest {
headers: {
token: string
}
body: BotStatUpdate
headers: {
token: string
}
body: BotStatUpdate
}
export default BotStats
export default BotStats

View File

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

View File

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

View File

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

View File

@ -17,8 +17,8 @@ const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) =
const csrfValidated = checkToken(req, res, req.body._csrf)
if (!csrfValidated) return
const validated = await DeveloperServerSchema.validate(req.body, { abortEarly: false })
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
@ -27,16 +27,22 @@ const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) =
const server = await get.serverData(req.query.id)
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
if (![server.owner, ...server.admins].includes(user)) return ResponseWrapper(res, { code: 403 })
if(validated.webhookURL) {
if (validated.webhookURL) {
const key = await verifyWebhook(validated.webhookURL)
if(key === false) {
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] })
if (key === false) {
return ResponseWrapper(res, {
code: 400,
message: '웹후크 주소를 검증할 수 없습니다.',
errors: [
'웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.',
],
})
}
const client = webhookClients.server.get(req.query.id)
if(client && validated.webhookURL !== client.url) {
if (client && validated.webhookURL !== client.url) {
destroyWebhookClient(req.query.id, 'server')
}
await update.webhook(req.query.id, 'servers', {
await update.webhook(req.query.id, 'servers', {
url: validated.webhookURL,
status: parseWebhookURL(validated.webhookURL) ? WebhookStatus.Discord : WebhookStatus.HTTP,
failedSince: null,
@ -44,7 +50,7 @@ const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) =
})
} else {
destroyWebhookClient(req.query.id, 'server')
await update.webhook(req.query.id, 'servers', {
await update.webhook(req.query.id, 'servers', {
url: null,
status: WebhookStatus.None,
failedSince: null,

View File

@ -12,8 +12,8 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
const csrfValidated = checkToken(req, res, req.body._csrf)
if (!csrfValidated) return
const validated = await ResetTokenSchema.validate(req.body, { abortEarly: false })
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
@ -21,8 +21,14 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
if (!validated) return
const server = await get.server.load(req.query.id)
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
if(server.state === 'unreachable') return ResponseWrapper(res, { code: 400, message: '서버 정보를 불러올 수 없습니다.', errors: ['서버에서 봇이 추방되었거나, 봇이 오프라인이여서 서버 정보를 갱신할 수 없습니다.'] })
if (!(await get.serverOwners(server.id)).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
if (server.state === 'unreachable')
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)
if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
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 ResponseWrapper from '@utils/ResponseWrapper'
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 { 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 { KoreanbotsEndPoints } from '@utils/Constants'
@ -21,7 +34,7 @@ const patchLimiter = rateLimit({
skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global')
return false
}
},
})
const Bots = RequestHandler()
.get(async (req: GetApiRequest, res) => {
@ -38,8 +51,8 @@ const Bots = RequestHandler()
if (!csrfValidated) return
const validated = await AddBotSubmitSchema.validate(req.body, { abortEarly: false })
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
@ -48,7 +61,7 @@ const Bots = RequestHandler()
if (validated.id !== req.query.id)
return ResponseWrapper(res, { code: 400, errors: ['요청 주소와 Body의 정보가 다릅니다.'] })
const captcha = await CaptchaVerify(validated._captcha)
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
const result = await put.submitBot(user, validated)
if (result === 1)
return ResponseWrapper(res, {
@ -82,30 +95,53 @@ const Bots = RequestHandler()
return ResponseWrapper(res, {
code: 403,
message: '더 이상 해당 봇에 대한 심사 요청을 하실 수 없습니다.',
errors: ['해당 봇은 심사에서 3회 이상 거부되었습니다. 더 이상의 심사를 요청하실 수 없습니다.', '이의 제기를 원하시는 경우 디스코드 서버를 통해 문의해주세요.'],
errors: [
'해당 봇은 심사에서 3회 이상 거부되었습니다. 더 이상의 심사를 요청하실 수 없습니다.',
'이의 제기를 원하시는 경우 디스코드 서버를 통해 문의해주세요.',
],
})
get.botSubmits.clear(user)
await discordLog('BOT/SUBMIT', user, new EmbedBuilder().setDescription(`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(result.id, result.date)})`), {
content: inspect(serialize(result)),
format: 'js'
})
await discordLog(
'BOT/SUBMIT',
user,
new EmbedBuilder().setDescription(
`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(
result.id,
result.date
)})`
),
{
content: inspect(serialize(result)),
format: 'js',
}
)
const userinfo = await get.user.load(user)
await webhookClients.internal.reviewLog.send({
embeds: [
new EmbedBuilder()
.setAuthor({
name: userinfo.tag === '0' ? `${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)
name:
userinfo.tag === '0'
? `${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('대기 중')
.setColor(Colors.Grey)
.setDescription(`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(result.id, result.date)})`)
.setTimestamp()
]
.setDescription(
`[${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('date', result.date)
span.setTag('user', userinfo.id)
@ -116,68 +152,114 @@ const Bots = RequestHandler()
const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 })
const bot = await get.bot.load(req.query.id)
if(!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
if((bot.owners as User[])[0].id !== user) return ResponseWrapper(res, { code: 403 })
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
if ((bot.owners as User[])[0].id !== user) return ResponseWrapper(res, { code: 403 })
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)
if (!csrfValidated) return
const captcha = await CaptchaVerify(req.body._captcha)
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
if(req.body.name !== bot.name) 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: '봇 이름을 입력해주세요.' })
await remove.bot(bot.id)
await getMainGuild().members.cache.get(bot.id)?.kick('봇 삭제됨.')
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),
format: 'js'
format: 'js',
}
)
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)
if(!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 })
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(!(bot.owners as User[]).find(el => el.id === user) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403 })
if (
['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)
if (!csrfValidated) return
const validated = await ManageBotSchema.validate(req.body, { abortEarly: false })
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if (!validated) return
const result = await update.bot(req.query.id, validated)
if(result === 0) return ResponseWrapper(res, { code: 400 })
if (result === 0) return ResponseWrapper(res, { code: 400 })
else {
get.bot.clear(req.query.id)
const embed = new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`)
const diffData = objectDiff(
{ 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) }
const embed = new EmbedBuilder().setDescription(
`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`
)
diffData.forEach(d => {
embed.addFields({name: d[0], value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff')
})
})
await discordLog('BOT/EDIT', user, embed,
const diffData = objectDiff(
{
content: `--- 설명\n${diff(bot.desc, validated.desc, true)}`,
format: 'diff'
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) => {
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 })
}
})
interface GetApiRequest extends NextApiRequest {
@ -195,7 +277,7 @@ interface PatchApiRequest extends GetApiRequest {
}
interface DeleteApiRequest extends GetApiRequest {
body: CsrfCaptcha & { name: string } | null
body: (CsrfCaptcha & { name: string }) | null
}
export default Bots

View File

@ -11,40 +11,69 @@ import { discordLog } from '@utils/DiscordBot'
import { EmbedBuilder } from 'discord.js'
import { KoreanbotsEndPoints } from '@utils/Constants'
const BotOwners = RequestHandler()
.patch(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 })
const userinfo = await get.user.load(user)
const bot = await get.bot.load(req.query.id)
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(['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 })
.then(el => el)
.catch(e => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if(!validated) return
const csrfValidated = checkToken(req, res, validated._csrf)
if (!csrfValidated) return
const captcha = await CaptchaVerify(validated._captcha)
if(!captcha) return
const userFetched: User[] = await Promise.all(validated.owners.map((u: string) => get.user.load(u)))
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)
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'))
return ResponseWrapper(res, { code: 200 })
})
const BotOwners = RequestHandler().patch(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 })
const userinfo = await get.user.load(user)
const bot = await get.bot.load(req.query.id)
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 (
['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 })
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if (!validated) return
const csrfValidated = checkToken(req, res, validated._csrf)
if (!csrfValidated) return
const captcha = await CaptchaVerify(validated._captcha)
if (!captcha) return
const userFetched: User[] = await Promise.all(
validated.owners.map((u: string) => get.user.load(u))
)
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)
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'
)
)
return ResponseWrapper(res, { code: 200 })
})
interface PostApiRequest extends NextApiRequest {
query: {
id: string
},
body: EditBotOwner
query: {
id: string
}
body: EditBotOwner
}
export default BotOwners
export default BotOwners

View File

@ -4,7 +4,7 @@ import rateLimit from 'express-rate-limit'
import { get } from '@utils/Query'
import RequestHandler from '@utils/RequestHandler'
import ResponseWrapper from '@utils/ResponseWrapper'
import { ReportSchema, Report} from '@utils/Yup'
import { ReportSchema, Report } from '@utils/Yup'
import { webhookClients } from '@utils/DiscordBot'
import { checkToken } from '@utils/Csrf'
@ -18,36 +18,40 @@ const limiter = rateLimit({
skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global')
return false
}
},
})
const BotReport = RequestHandler().post(limiter)
const BotReport = RequestHandler()
.post(limiter)
.post(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token)
if(!user) return ResponseWrapper(res, { code: 401 })
if (!user) return ResponseWrapper(res, { code: 401 })
const 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 csrfValidated = checkToken(req, res, req.body._csrf)
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 })
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
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'] }})
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'] },
})
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
})
interface PostApiRequest extends NextApiRequest {
body: Report | null
query: {
id: string
}
body: Report | null
query: {
id: string
}
}
export default BotReport
export default BotReport

View File

@ -21,9 +21,9 @@ const limiter = rateLimit({
keyGenerator: (req) => req.headers.authorization,
skip: (req, res) => {
res.removeHeader('X-RateLimit-Global')
if(!req.headers.authorization) return true
if (!req.headers.authorization) return true
else return false
}
},
})
const patchLimiter = rateLimit({
@ -36,27 +36,36 @@ const patchLimiter = rateLimit({
skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global')
return false
}
},
})
const BotStats = RequestHandler().post(limiter)
const BotStats = RequestHandler()
.post(limiter)
.post(async (req: PostApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization)
if(!bot) return ResponseWrapper(res, { code: 401 })
if(!req.body) return ResponseWrapper(res, { code: 400 })
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, { abortEarly: false })
.then(el => el)
.catch(e => {
if (!bot) return ResponseWrapper(res, { code: 401 })
if (!req.body) return ResponseWrapper(res, { code: 400 })
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, {
abortEarly: false,
})
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if(!validated) return
if (!validated) return
const botInfo = await get.bot.load(req.query.id)
if(!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
if(botInfo.id !== bot) return ResponseWrapper(res, { code: 403 })
if (!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403 })
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)
if (validated.servers && botInfo.servers !== validated.servers) {
sendWebhook(botInfo, {
@ -67,37 +76,54 @@ const BotStats = RequestHandler().post(limiter)
before: botInfo.servers,
after: validated.servers,
},
timestamp: Date.now()
timestamp: Date.now(),
})
}
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')}`,
embeds: [new EmbedBuilder().setDescription(`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(botInfo.id)}))`)]
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'
)}`,
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)
if (!user) return ResponseWrapper(res, { code: 401 })
const userinfo = await get.user.load(user)
const bot = await get.bot.load(req.query.id)
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) return ResponseWrapper(res, { code: 404 })
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)
return ResponseWrapper(res, { code: 200 })
})
interface ApiRequest extends NextApiRequest {
query: {
id: string
}
}
interface PostApiRequest extends ApiRequest {
query: {
id: string
}
body: BotStatUpdate
query: {
id: string
}
body: BotStatUpdate
}
export default BotStats
export default BotStats

View File

@ -12,29 +12,37 @@ import { WebhookType } from '@types'
const BotVote = RequestHandler()
.get(async (req: GetApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization)
if(!bot) return ResponseWrapper(res, { code: 401 })
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 => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if(!userID) return ResponseWrapper(res, { code: 400 })
if (!bot) return ResponseWrapper(res, { code: 401 })
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) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if (!userID) return ResponseWrapper(res, { code: 400 })
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) => {
const user = await get.Authorization(req.cookies.token)
if(!user) return ResponseWrapper(res, { code: 401 })
if (!user) return ResponseWrapper(res, { code: 401 })
const bot = await get.bot.load(req.query.id)
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
const csrfValidated = checkToken(req, res, req.body._csrf)
if (!csrfValidated) return
const captcha = await CaptchaVerify(req.body._captcha)
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
const vote = await put.voteBot(user, bot.id)
if(vote === null) return ResponseWrapper(res, { code: 401 })
else if(vote === true) {
if (vote === null) return ResponseWrapper(res, { code: 401 })
else if (vote === true) {
sendWebhook(bot, {
type: 'bot',
data: {
@ -42,19 +50,18 @@ const BotVote = RequestHandler()
type: WebhookType.HeartChange,
before: bot.votes,
after: bot.votes + 1,
userId: user
userId: user,
},
timestamp: Date.now()
timestamp: Date.now(),
})
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 {
query: {
id: string
}
query: {
id: string
}
}
interface GetApiRequest extends ApiRequest {
@ -65,8 +72,8 @@ interface GetApiRequest extends ApiRequest {
}
interface PostApiRequest extends ApiRequest {
body: {
_captcha: string
_csrf: string
}
_captcha: string
_csrf: string
}
}
export default BotVote
export default BotVote

View File

@ -9,4 +9,4 @@ const NewList = RequestHandler().get(async (_req, res) => {
return ResponseWrapper<List<Bot>>(res, { code: 200, data: result })
})
export default NewList
export default NewList

View File

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

View File

@ -8,36 +8,47 @@ import { get, update } from '@utils/Query'
import { DiscordBot, webhookClients } from '@utils/DiscordBot'
import { KoreanbotsEndPoints } from '@utils/Constants'
const ApproveBotSubmit = RequestHandler()
.post(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization)
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 }))
if(!submit) return ResponseWrapper(res, { code: 404 })
if(submit.state !== 0) return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
const result = await update.approveBotSubmission(submit.id, submit.date)
if(!result) return ResponseWrapper(res, { code: 400 })
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
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()
if(req.body.reviewer) embed.addFields({name: '📃 정보', value: `심사자: ${req.body.reviewer}`})
await webhookClients.internal.reviewLog.send({embeds: [embed]})
tracer.trace('botSubmits.approve', span => {
span.setTag('id', submit.id)
span.setTag('date', submit.date)
span.setTag('reviewer', req.body.reviewer)
})
return ResponseWrapper(res, { code: 200 })
const ApproveBotSubmit = RequestHandler().post(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization)
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 })
)
if (!submit) return ResponseWrapper(res, { code: 404 })
if (submit.state !== 0)
return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
const result = await update.approveBotSubmission(submit.id, submit.date)
if (!result) return ResponseWrapper(res, { code: 400 })
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
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()
if (req.body.reviewer) embed.addFields({ name: '📃 정보', value: `심사자: ${req.body.reviewer}` })
await webhookClients.internal.reviewLog.send({ embeds: [embed] })
tracer.trace('botSubmits.approve', (span) => {
span.setTag('id', submit.id)
span.setTag('date', submit.date)
span.setTag('reviewer', req.body.reviewer)
})
return ResponseWrapper(res, { code: 200 })
})
interface ApiRequest extends NextApiRequest {
query: {
id: string
date: string
}
body: {
query: {
id: string
date: string
}
body: {
reviewer?: string
}
}
}
export default ApproveBotSubmit
export default ApproveBotSubmit

View File

@ -8,40 +8,71 @@ import { get, update } from '@utils/Query'
import { DiscordBot, webhookClients } from '@utils/DiscordBot'
import { BotSubmissionDenyReasonPresetsName, KoreanbotsEndPoints } from '@utils/Constants'
const DenyBotSubmit = RequestHandler()
.post(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization)
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 }))
if(!submit) return ResponseWrapper(res, { code: 404 })
if(submit.state !== 0) return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
await update.denyBotSubmission(submit.id, submit.date, req.body.reason)
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()
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]})
const openEmbed = new EmbedBuilder().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]})
tracer.trace('botSubmits.deny', span => {
span.setTag('id', submit.id)
span.setTag('date', submit.date)
span.setTag('reviewer', req.body.reviewer)
span.setTag('reason', BotSubmissionDenyReasonPresetsName[req.body.reason] || 'OTHER')
span.setTag('_raw_reason', req.body.reason)
const DenyBotSubmit = RequestHandler().post(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization)
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 })
)
if (!submit) return ResponseWrapper(res, { code: 404 })
if (submit.state !== 0)
return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
await update.denyBotSubmission(submit.id, submit.date, req.body.reason)
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()
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}` : ''}`,
})
return ResponseWrapper(res, { code: 200 })
await webhookClients.internal.reviewLog.send({ embeds: [embed] })
const openEmbed = new EmbedBuilder()
.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] })
tracer.trace('botSubmits.deny', (span) => {
span.setTag('id', submit.id)
span.setTag('date', submit.date)
span.setTag('reviewer', req.body.reviewer)
span.setTag('reason', BotSubmissionDenyReasonPresetsName[req.body.reason] || 'OTHER')
span.setTag('_raw_reason', req.body.reason)
})
return ResponseWrapper(res, { code: 200 })
})
interface ApiRequest extends NextApiRequest {
query: {
id: string
date: string
}
body: {
reason?: string
query: {
id: string
date: string
}
body: {
reason?: string
reviewer: string
}
}
}
export default DenyBotSubmit

View File

@ -5,20 +5,21 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import { get } from '@utils/Query'
import { DiscordBot } from '@utils/DiscordBot'
const BotSubmit = RequestHandler()
.get(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization)
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 }))
if(!submit) return ResponseWrapper(res, { code: 404 })
return ResponseWrapper(res, { code: 200, data: submit })
})
const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization)
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 })
)
if (!submit) return ResponseWrapper(res, { code: 404 })
return ResponseWrapper(res, { code: 200, data: submit })
})
interface ApiRequest extends NextApiRequest {
query: {
id: string
query: {
id: string
date: string
}
}
}
export default BotSubmit
export default BotSubmit

View File

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

View File

@ -5,19 +5,18 @@ import ResponseWrapper from '@utils/ResponseWrapper'
import { get } from '@utils/Query'
import { DiscordBot } from '@utils/DiscordBot'
const BotSubmit = RequestHandler()
.get(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization)
if(bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
const submits = await get.botSubmitList()
const submit = submits.find((el, n) => el.id === req.query.id || n+1 === Number(req.query.id))
if(!submit) return ResponseWrapper(res, { code: 404 })
return ResponseWrapper(res, { code: 200, data: submit })
})
const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
const bot = await get.BotAuthorization(req.headers.authorization)
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
const submits = await get.botSubmitList()
const submit = submits.find((el, n) => el.id === req.query.id || n + 1 === Number(req.query.id))
if (!submit) return ResponseWrapper(res, { code: 404 })
return ResponseWrapper(res, { code: 200, data: submit })
})
interface ApiRequest extends NextApiRequest {
query: {
id: string
}
query: {
id: string
}
}
export default BotSubmit
export default BotSubmit

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import { NextApiHandler } from 'next'
import { getMainGuild } from '@utils/DiscordBot'
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 validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: 1 })
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
})
if (!validated) return
@ -27,7 +27,10 @@ const Search = RequestHandler().get(async (req: ApiRequest, res) => {
} catch {
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 {

View File

@ -8,9 +8,12 @@ import { SearchQuerySchema } from '@utils/Yup'
import { Bot, List } from '@types'
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 })
.then(el => el)
.catch(e => {
const validated = await SearchQuerySchema.validate({
q: req.query.q || req.query.query,
page: req.query.page,
})
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
})
if (!validated) return

View File

@ -8,9 +8,12 @@ import { SearchQuerySchema } from '@utils/Yup'
import { Server, List } from '@types'
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 })
.then(el => el)
.catch(e => {
const validated = await SearchQuerySchema.validate({
q: req.query.q || req.query.query,
page: req.query.page,
})
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
})
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 ResponseWrapper from '@utils/ResponseWrapper'
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 { 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 { KoreanbotsEndPoints } from '@utils/Constants'
@ -19,14 +32,14 @@ const patchLimiter = rateLimit({
skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global')
return false
}
},
})
const Servers = RequestHandler()
.get(async (req: GetApiRequest, res) => {
const server = await get.server.load(req.query.id)
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버 입니다.' })
else {
return ResponseWrapper(res, { code: 200, data: server })
return ResponseWrapper(res, { code: 200, data: server })
}
})
.post(async (req: PostApiRequest, res) => {
@ -36,20 +49,20 @@ const Servers = RequestHandler()
if (!csrfValidated) return
const validated = await AddServerSubmitSchema.validate(req.body, { abortEarly: false })
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if (!validated) return
const captcha = await CaptchaVerify(validated._captcha)
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
const result = await put.submitServer(user, req.query.id, validated)
if (result === 1)
return ResponseWrapper(res, {
code: 400,
message: '이미 등록된 서버 입니다.'
message: '이미 등록된 서버 입니다.',
})
else if (result === 2)
return ResponseWrapper(res, {
@ -57,7 +70,7 @@ const Servers = RequestHandler()
message: '봇이 초대되지 않았습니다.',
errors: [
'서버에 봇이 초대되지 않았습니다.',
'이미 봇을 초대하셨다면, 잠시 후 다시 시도해주세요.'
'이미 봇을 초대하셨다면, 잠시 후 다시 시도해주세요.',
],
})
else if (result === 3)
@ -66,7 +79,7 @@ const Servers = RequestHandler()
message: '서버의 관리자가 아닙니다.',
errors: [
'해당 서버를 등록할 권한이 없습니다.',
'서버에서 관리자 권한이 있으신지 확인해주세요.'
'서버에서 관리자 권한이 있으신지 확인해주세요.',
],
})
else if (result === 4)
@ -75,14 +88,21 @@ const Servers = RequestHandler()
message: '올바르지 않은 초대 코드 입니다.',
errors: [
'올바른 초대코드를 입력하셨는지 확인해주세요.',
'만료되지 않는 초대코드인지 확인해주세요.'
'만료되지 않는 초대코드인지 확인해주세요.',
],
})
get.user.clear(user)
await discordLog('SERVER/SUBMIT', user, new EmbedBuilder().setDescription(`[${req.query.id}](${KoreanbotsEndPoints.URL.server(req.query.id)})`), {
content: inspect(serialize(validated)),
format: 'js'
})
await discordLog(
'SERVER/SUBMIT',
user,
new EmbedBuilder().setDescription(
`[${req.query.id}](${KoreanbotsEndPoints.URL.server(req.query.id)})`
),
{
content: inspect(serialize(validated)),
format: 'js',
}
)
return ResponseWrapper(res, { code: 200, data: result })
})
@ -90,74 +110,120 @@ const Servers = RequestHandler()
const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 })
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)
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 || 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 })
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)
if (!csrfValidated) return
const captcha = await CaptchaVerify(req.body._captcha)
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
if(req.body.name !== server.name) 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: '봇 이름을 입력해주세요.' })
await remove.server(server.id)
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),
format: 'js'
format: 'js',
}
)
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)
if(!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
const user = await get.Authorization(req.cookies.token)
if (!user) return ResponseWrapper(res, { code: 401 })
const userInfo = await get.user.load(user)
const data = await get.serverData(req.query.id)
if(!data || server.state === 'unreachable') return ResponseWrapper(res, { 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: ['오류라고 생각되면 문의해주세요.'] })
if (!data || server.state === 'unreachable')
return ResponseWrapper(res, {
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)
if (!csrfValidated) return
const validated = await ManageServerSchema.validate(req.body, { abortEarly: false })
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if (!validated) return
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)
if(result === 0) return ResponseWrapper(res, { code: 400 })
if (result === 0) return ResponseWrapper(res, { code: 400 })
else {
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(
{ 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)}`,
format: 'diff'
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)}`,
format: 'diff',
})
return ResponseWrapper(res, { code: 200 })
}
})
interface GetApiRequest extends NextApiRequest {
@ -175,7 +241,7 @@ interface PatchApiRequest extends GetApiRequest {
}
interface DeleteApiRequest extends GetApiRequest {
body: CsrfCaptcha & { name: string } | null
body: (CsrfCaptcha & { name: string }) | null
}
export default Servers

View File

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

View File

@ -4,7 +4,7 @@ import rateLimit from 'express-rate-limit'
import { get } from '@utils/Query'
import RequestHandler from '@utils/RequestHandler'
import ResponseWrapper from '@utils/ResponseWrapper'
import { ReportSchema, Report} from '@utils/Yup'
import { ReportSchema, Report } from '@utils/Yup'
import { webhookClients } from '@utils/DiscordBot'
import { checkToken } from '@utils/Csrf'
@ -18,36 +18,40 @@ const limiter = rateLimit({
skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global')
return false
}
},
})
const ServerReport = RequestHandler().post(limiter)
const ServerReport = RequestHandler()
.post(limiter)
.post(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token)
if(!user) return ResponseWrapper(res, { code: 401 })
if (!user) return ResponseWrapper(res, { code: 401 })
const 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 csrfValidated = checkToken(req, res, req.body._csrf)
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 })
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
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'] }})
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'] },
})
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
})
interface PostApiRequest extends NextApiRequest {
body: Report | null
query: {
id: string
}
body: Report | null
query: {
id: string
}
}
export default ServerReport
export default ServerReport

View File

@ -12,49 +12,56 @@ import { WebhookType } from '@types'
const ServerVote = RequestHandler()
.get(async (req: GetApiRequest, res) => {
const server = await get.ServerAuthorization(req.headers.authorization)
if(!server) return ResponseWrapper(res, { code: 401 })
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 => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if(!userID) return ResponseWrapper(res, { code: 400 })
if (!server) return ResponseWrapper(res, { code: 401 })
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) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
if (!userID) return ResponseWrapper(res, { code: 400 })
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) => {
const user = await get.Authorization(req.cookies.token)
if(!user) return ResponseWrapper(res, { code: 401 })
if (!user) return ResponseWrapper(res, { code: 401 })
const server = await get.server.load(req.query.id)
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
const csrfValidated = checkToken(req, res, req.body._csrf)
if (!csrfValidated) return
const captcha = await CaptchaVerify(req.body._captcha)
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
const vote = await put.voteServer(user, server.id)
if(vote === null) return ResponseWrapper(res, { code: 401 })
else if(vote === true) {
if (vote === null) return ResponseWrapper(res, { code: 401 })
else if (vote === true) {
sendWebhook(server, {
type: 'server',
type: 'server',
data: {
guildId: server.id,
type: WebhookType.HeartChange,
before: server.votes,
after: server.votes + 1,
userId: user
userId: user,
},
timestamp: Date.now()
timestamp: Date.now(),
})
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 {
query: {
id: string
}
query: {
id: string
}
}
interface GetApiRequest extends ApiRequest {
@ -65,8 +72,8 @@ interface GetApiRequest extends ApiRequest {
}
interface PostApiRequest extends ApiRequest {
body: {
_captcha: string
_csrf: string
}
_captcha: string
_csrf: string
}
}
export default ServerVote
export default ServerVote

View File

@ -1,10 +1,10 @@
import { NextApiRequest} from 'next'
import { NextApiRequest } from 'next'
import rateLimit from 'express-rate-limit'
import { get } from '@utils/Query'
import RequestHandler from '@utils/RequestHandler'
import ResponseWrapper from '@utils/ResponseWrapper'
import { ReportSchema, Report} from '@utils/Yup'
import { ReportSchema, Report } from '@utils/Yup'
import { webhookClients } from '@utils/DiscordBot'
import { checkToken } from '@utils/Csrf'
@ -17,36 +17,46 @@ const limiter = rateLimit({
skip: (_req, res) => {
res.removeHeader('X-RateLimit-Global')
return false
}
},
})
const UserReport = RequestHandler().post(limiter)
const UserReport = RequestHandler()
.post(limiter)
.post(async (req: PostApiRequest, res) => {
const user = await get.Authorization(req.cookies.token)
if(!user) return ResponseWrapper(res, { code: 401 })
if (!user) return ResponseWrapper(res, { code: 401 })
const userInfo = await get.user.load(req.query.id)
if(!userInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 유저입니다.' })
if (!userInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 유저입니다.' })
const csrfValidated = checkToken(req, res, req.body._csrf)
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 })
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
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'] }})
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'] },
})
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
})
interface PostApiRequest extends NextApiRequest {
body: Report | null
query: {
id: string
}
body: Report | null
query: {
id: string
}
}
export default UserReport
export default UserReport

View File

@ -20,8 +20,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
scale,
icon,
})
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
@ -34,8 +34,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
const userImage = !data.avatar
? null
: await get.images.user.load(
DiscordEnpoints.CDN.user(data.id, data.avatar, { format: 'png', size: 128 })
)
DiscordEnpoints.CDN.user(data.id, data.avatar, { format: 'png', size: 128 })
)
const img =
userImage ||
(await get.images.user.load(

View File

@ -20,8 +20,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
scale,
icon,
})
.then(el => el)
.catch(e => {
.then((el) => el)
.catch((e) => {
ResponseWrapper(res, { code: 400, errors: e.errors })
return null
})
@ -34,8 +34,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
const userImage = !data.icon
? null
: await get.images.user.load(
DiscordEnpoints.CDN.guild(data.id, data.icon, { format: 'png', size: 128 })
)
DiscordEnpoints.CDN.guild(data.id, data.icon, { format: 'png', size: 128 })
)
const img =
userImage ||
(await get.images.user.load(

View File

@ -37,290 +37,535 @@ const Modal = dynamic(() => import('@components/Modal'))
const Captcha = dynamic(() => import('@components/Captcha'))
const Login = dynamic(() => import('@components/Login'))
const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }) => {
const [ data, setData ] = useState(null)
const [ adminModal, setAdminModal ] = useState(false)
const [ transferModal, setTransferModal ] = useState(false)
const [ deleteModal, setDeleteModal ] = useState(false)
const ManageBotPage: NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }) => {
const [data, setData] = useState(null)
const [adminModal, setAdminModal] = useState(false)
const [transferModal, setTransferModal] = useState(false)
const [deleteModal, setDeleteModal] = useState(false)
const router = useRouter()
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)
}
async function getUser(id: string) {
const u = await Fetch<User>(`/users/${encodeURIComponent(id)}`)
if(u.code === 200 && u.data) return u.data
if (u.code === 200 && u.data) return u.data
else return null
}
if(!bot) return <NotFound />
if(!user) return <Login>
<NextSeo title='봇 정보 수정하기' description='봇의 정보를 수정합니다.'/>
</Login>
if(!(bot.owners as User[]).find(el => el.id === user.id) && !checkUserFlag(user.flags, 'staff')) return <Forbidden />
if (!bot) return <NotFound />
if (!user)
return (
<Login>
<NextSeo title='봇 정보 수정하기' description='봇의 정보를 수정합니다.' />
</Login>
)
if (
!(bot.owners as User[]).find((el) => el.id === user.id) &&
!checkUserFlag(user.flags, 'staff')
)
return <Forbidden />
return (
<Container paddingTop className='pt-5 pb-10'>
<NextSeo title={`${bot.name} 수정하기`} description='봇의 정보를 수정합니다.'/>
<h1 className='text-3xl font-bold mb-8'> </h1>
<Formik initialValues={cleanObject({
agree: false,
id: bot.id,
prefix: bot.prefix,
library: bot.lib,
category: bot.category,
intro: bot.intro,
desc: bot.desc,
website: bot.web,
url: bot.url,
git: bot.git,
discord: bot.discord,
_csrf: csrfToken
})}
validationSchema={ManageBotSchema}
onSubmit={submitBot}>
<Container paddingTop className='pb-10 pt-5'>
<NextSeo title={`${bot.name} 수정하기`} description='봇의 정보를 수정합니다.' />
<h1 className='mb-8 text-3xl font-bold'> </h1>
<Formik
initialValues={cleanObject({
agree: false,
id: bot.id,
prefix: bot.prefix,
library: bot.lib,
category: bot.category,
intro: bot.intro,
desc: bot.desc,
website: bot.web,
url: bot.url,
git: bot.git,
discord: bot.discord,
_csrf: csrfToken,
})}
validationSchema={ManageBotSchema}
onSubmit={submitBot}
>
{({ errors, touched, values, setFieldTouched, setFieldValue }) => (
<Form>
<div className='md:flex text-center md:text-left'>
<DiscordAvatar userID={bot.id} className='md:mx-1 mx-auto rounded-full'/>
<div className='md:w-2/3 px-8 py-6'>
<h1 className='text-3xl font-bold'>{bot.name}#{bot.tag}</h1>
<div className='text-center md:flex md:text-left'>
<DiscordAvatar userID={bot.id} className='mx-auto rounded-full md:mx-1' />
<div className='px-8 py-6 md:w-2/3'>
<h1 className='text-3xl font-bold'>
{bot.name}#{bot.tag}
</h1>
<h2>ID: {bot.id}</h2>
</div>
</div>
{
data ? data.code === 200 ? <div className='mt-4'>
<Redirect to={makeBotURL(bot)}>
<Message type='success'>
<h2 className='text-lg font-extrabold'> .</h2>
<p> !</p>
{data ? (
data.code === 200 ? (
<div className='mt-4'>
<Redirect to={makeBotURL(bot)}>
<Message type='success'>
<h2 className='text-lg font-extrabold'> .</h2>
<p> !</p>
</Message>
</Redirect>
</div>
) : (
<div className='mt-4'>
<Message type='error'>
<h2 className='text-lg font-extrabold'>
{data.message || '오류가 발생했습니다.'}
</h2>
<ul className='list-inside list-disc'>
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message>
</Redirect>
</div> : <div className='mt-4'>
<Message type='error'>
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
<ul className='list-disc list-inside'>
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message>
</div> : ''
}
<Label For='prefix' label='접두사' labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)' error={errors.prefix && touched.prefix ? errors.prefix : null} short required>
</div>
)
) : (
''
)}
<Label
For='prefix'
label='접두사'
labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)'
error={errors.prefix && touched.prefix ? errors.prefix : null}
short
required
>
<Input name='prefix' placeholder='!' />
</Label>
<Label 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
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 For='category' label='카테고리' labelDesc='봇에 해당되는 카테고리를 선택해주세요' 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='text-gray-400 mt-1 text-sm'> 3 . . <strong> .</strong></span>
<Label
For='category'
label='카테고리'
labelDesc='봇에 해당되는 카테고리를 선택해주세요'
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>
<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' />
</Label>
<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'/>
<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' />
</Label>
<Label For='inviteLink' 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='text-gray-400 mt-1 text-sm'>
<Label
For='inviteLink'
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
href='/calculator'
rel='noreferrer'
target='_blank'
className='text-blue-500 hover:text-blue-400'>
</Link> !
className='text-blue-500 hover:text-blue-400'
>
</Link>
!
</span>
</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'>
discord.gg/<Input name='discord' placeholder='JEh53MQ' />
discord.gg/
<Input name='discord' placeholder='JEh53MQ' />
</div>
</Label>
<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='국내 봇을 한 곳에서.' />
</Label>
<Label 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
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 For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다'>
<Label
For='preview'
label='설명 미리보기'
labelDesc='다음 결과는 실제와 다를 수 있습니다'
>
<Segment>
<Markdown text={values.desc} />
</Segment>
</Label>
<Divider />
<p className='text-base mt-2 mb-5'>
<span className='text-red-500 font-semibold'> *</span> =
<p className='mb-5 mt-2 text-base'>
<span className='font-semibold text-red-500'> *</span> =
</p>
<Button type='submit' onClick={() => window.scrollTo({ top: 0 })}>
<>
<i className='far fa-save'/>
<i className='far fa-save' />
</>
</Button>
</Form>
)}
</Formik>
{
(checkUserFlag(user.flags, 'staff') || (bot.owners as User[])[0].id === user.id) && <div className='py-4'>
{(checkUserFlag(user.flags, 'staff') || (bot.owners as User[])[0].id === user.id) && (
<div className='py-4'>
<Divider />
<h2 className='text-2xl font-semibold pb-2'></h2>
<h2 className='pb-2 text-2xl font-semibold'></h2>
<Segment>
<div className='lg:flex items-center'>
<div className='items-center lg:flex'>
<div className='grow py-1'>
<h3 className='text-lg font-semibold'> </h3>
<p className='text-gray-400'> .</p>
</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>
<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,
_csrf: csrfToken,
owners: v.owners.map(el => el.id)
}) })
if(res.code === 200) {
alert('성공적으로 수정했습니다.')
router.push(makeBotURL(bot))
} else {
alert(res.message)
setAdminModal(false)
}
}}>
{
({ values, setFieldValue }) => <Form>
<Button
onClick={() => setAdminModal(true)}
className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
>
<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,
_csrf: csrfToken,
owners: v.owners.map((el) => el.id),
}),
})
if (res.code === 200) {
alert('성공적으로 수정했습니다.')
router.push(makeBotURL(bot))
} else {
alert(res.message)
setAdminModal(false)
}
}}
>
{({ values, setFieldValue }) => (
<Form>
<Message type='warning'>
<p> . .</p>
<p>
.
.
</p>
</Message>
<div className='py-4'>
<h2 className='text-md my-1'> ID를 .</h2>
<div className='flex flex-wrap'>
{
(values.owners as User[]).map((el, n) => <Tag className='flex items-center' text={<>
<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}`}
{
n !== 0 && <button className='ml-0.5 hover:text-red-500' onClick={() => {
setFieldValue('owners', (() => {
const arr = [...values.owners]
arr.splice(n, 1)
return arr
})())
}}>
<i className='fas fa-times' />
</button>
{(values.owners as User[]).map((el, n) => (
<Tag
className='flex items-center'
text={
<>
<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]
arr.splice(n, 1)
return arr
})()
)
}}
>
<i className='fas fa-times' />
</button>
)}
</>
}
</>} key={el.id} />)
}
key={el.id}
/>
))}
</div>
<div className='flex'>
<div className='grow pr-2'>
<Input name='id' placeholder='추가할 유저 ID' />
</div>
<Button 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 arr = [...values.owners]
if(!user) return alert('올바르지 않은 유저입니다.')
else {
arr.push(user)
setFieldValue('owners', arr)
setFieldValue('id', '')
}
}}>
<Button
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 arr = [...values.owners]
if (!user) return alert('올바르지 않은 유저입니다.')
else {
arr.push(user)
setFieldValue('owners', arr)
setFieldValue('id', '')
}
}}
>
<i className='fas fa-user-plus text-white' />
</Button>
</div>
</div>
<Captcha 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>
<Captcha
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>
}
)}
</Formik>
</Modal>
</div>
<Divider />
<div className='lg:flex items-center'>
<div className='items-center lg:flex'>
<div className='grow py-1'>
<h3 className='text-lg font-semibold'> </h3>
<p className='text-gray-400'> . .</p>
<p className='text-gray-400'>
. .
</p>
</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>
<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,
_csrf: csrfToken,
owners: [ v.ownerID ]
}) })
if(res.code === 200) {
alert('성공적으로 소유권을 이전했습니다.')
router.push('/')
} else {
alert(res.message)
setTransferModal(false)
}
}}>
{
({ values, setFieldValue }) => <Form>
<Button
onClick={() => setTransferModal(true)}
className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
>
<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,
_csrf: csrfToken,
owners: [v.ownerID],
}),
})
if (res.code === 200) {
alert('성공적으로 소유권을 이전했습니다.')
router.push('/')
} else {
alert(res.message)
setTransferModal(false)
}
}}
>
{({ values, setFieldValue }) => (
<Form>
<Message type='warning'>
<h2 className='text-2xl font-bold'>!</h2>
<p> , .</p>
<p>
,
.
</p>
</Message>
<div className='py-4'>
<h2 className='text-md my-1'> ID를 .</h2>
<Input name='ownerID' placeholder='이전할 유저 ID' />
<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} />
</div>
<Captcha 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>
<Captcha
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>
}
)}
</Formik>
</Modal>
</div>
<Divider />
<div className='lg:flex items-center'>
<div className='items-center lg:flex'>
<div className='grow py-1'>
<h3 className='text-lg font-semibold'> </h3>
<p className='text-gray-400'> .</p>
</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>
<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) {
alert('성공적으로 삭제하였습니다.')
redirectTo(router, '/')
}
else alert(res.message)
}}>
{
({ values, setFieldValue }) => <Form>
<Button
onClick={() => setDeleteModal(true)}
className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
>
<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) {
alert('성공적으로 삭제하였습니다.')
redirectTo(router, '/')
} else alert(res.message)
}}
>
{({ values, setFieldValue }) => (
<Form>
<Message type='warning'>
<p> .<br/> .</p>
<p> <strong>{bot.name}</strong>{getJosaPicker('을')(bot.name)} .</p>
<p>
.
<br />
.
</p>
<p>
<strong>{bot.name}</strong>
{getJosaPicker('을')(bot.name)} .
</p>
</Message>
<div className='py-4'>
<Input name='name' placeholder={bot.name} />
</div>
<Captcha 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>
<Captcha
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>
}
)}
</Formik>
</Modal>
</div>
</Segment>
</div>
}
)}
</Container>
)
}
@ -328,7 +573,13 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
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 {
@ -346,4 +597,4 @@ interface Query extends ParsedUrlQuery {
id: string
}
export default ManageBotPage
export default ManageBotPage

View File

@ -29,7 +29,7 @@ const Owner = dynamic(() => import('@components/Owner'))
const Segment = dynamic(() => import('@components/Segment'))
const LongButton = dynamic(() => import('@components/LongButton'))
const Advertisement = dynamic(() => import('@components/Advertisement'))
const Markdown = dynamic(() => import ('@components/Markdown'))
const Markdown = dynamic(() => import('@components/Markdown'))
const Message = dynamic(() => import('@components/Message'))
const Button = dynamic(() => import('@components/Button'))
const TextArea = dynamic(() => import('@components/Form/TextArea'))
@ -39,302 +39,411 @@ const NSFW = dynamic(() => import('@components/NSFW'))
const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken }) => {
const bg = checkBotFlag(data?.flags, 'trusted') && data?.banner
const router = useRouter()
const [ nsfw, setNSFW ] = useState<boolean>()
const [ reportModal, setReportModal ] = useState(false)
const [ reportRes, setReportRes ] = useState<ResponseProps<unknown>>(null)
const [nsfw, setNSFW] = useState<boolean>()
const [reportModal, setReportModal] = useState(false)
const [reportRes, setReportRes] = useState<ResponseProps<unknown>>(null)
useEffect(() => {
setNSFW(localStorage.nsfw)
}, [])
if (!data?.id) return <NotFound />
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'>
<NextSeo
title={data.name}
description={data.intro}
twitter={{
cardType: 'summary_large_image'
cardType: 'summary_large_image',
}}
openGraph={{
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,
height: 1170,
alt: 'Bot Preview Image'
}
]
alt: 'Bot Preview Image',
},
],
}}
/>
{
data.state === 'blocked' ? <div className='pb-40'>
{data.state === 'blocked' ? (
<div className='pb-40'>
<Message type='error'>
<h2 className='text-lg font-extrabold'> .</h2>
</Message>
</div>
: data.category.includes('NSFW') && !nsfw ? <NSFW onClick={() => setNSFW(true)} onDisableClick={() => localStorage.nsfw = true} />
: <>
<div className='w-full pb-2'>
{
data.state === 'private' ? <Message type='info'>
<h2 className='text-lg font-extrabold'> .</h2>
<p> . .</p>
</Message> :
data.state === 'reported' ?
<Message type='error'>
<h2 className='text-lg font-extrabold'> , .</h2>
<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>
</Message> : ''
}
</div>
<div className='lg:flex w-full'>
<div className='w-full text-center lg:w-2/12'>
<DiscordAvatar
userID={data.id}
size={256}
className='w-full rounded-full'
/>
</div>
<div className='grow px-5 py-12 w-full text-center lg:w-5/12 lg:text-left'>
<Tag
circular
text={
<>
<i className={`fas fa-circle text-${Status[data.status]?.color}`} />{' '}
{Status[data.status]?.text}
</>
}
/>
<h1 className='mb-2 mt-3 text-4xl font-bold' style={bg ? { color: 'white' } : {}}>
{data.name}{' '}
{checkBotFlag(data.flags, 'trusted') ? (
<Tooltip placement='bottom' overlay='해당 봇은 한국 디스코드 리스트에서 엄격한 기준을 통과한 봇입니다!'>
<span className='text-koreanbots-blue text-3xl'>
<i className='fas fa-award' />
</span>
</Tooltip>
) : ''}
</h1>
<p className={`${bg ? 'text-gray-300' : 'dark:text-gray-300 text-gray-800'} text-base`}>{data.intro}</p>
</div>
<div className='w-full lg:w-1/4'>
{
data.state === 'ok' && <LongButton
newTab
href={`/bots/${router.query.id}/invite`}
>
<h4 className='whitespace-nowrap'>
<i className='fas fa-user-plus text-discord-blurple' />
</h4>
</LongButton>
}
<Link href={`/bots/${router.query.id}/vote`} legacyBehavior>
<LongButton>
<h4>
<i className='fas fa-heart text-red-600' />
</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'>
{formatNumber(data.votes)}
</span>
</LongButton>
) : data.category.includes('NSFW') && !nsfw ? (
<NSFW onClick={() => setNSFW(true)} onDisableClick={() => (localStorage.nsfw = true)} />
) : (
<>
<div className='w-full pb-2'>
{data.state === 'private' ? (
<Message type='info'>
<h2 className='text-lg font-extrabold'>
.
</h2>
<p>
.
.
</p>
</Message>
) : data.state === 'reported' ? (
<Message type='error'>
<h2 className='text-lg font-extrabold'>
, .
</h2>
<p> .</p>
<p>
{' '}
<Link href='/guidelines' className='text-blue-500 hover:text-blue-400'>
</Link>
{
((data.owners as User[]).find(el => el.id === user?.id) || checkUserFlag(user?.flags, 'staff')) && <LongButton href={`/bots/${data.id}/edit`}>
<h4>
<i className='fas fa-cogs' />
</h4>
</LongButton>
}
{
((data.owners as User[]).find(el => el.id === user?.id) || checkUserFlag(user?.flags, 'staff')) && <LongButton onClick={async() => {
const res = await Fetch(`/bots/${data.id}/stats`, { method: 'PATCH'} )
if(res.code !== 200) return alert(res.message)
else window.location.reload()
}}>
<h4>
<i className='fas fa-sync' />
</h4>
</LongButton>
}
{' '}
<Link href='/discord' className='text-blue-500 hover:text-blue-400'>
</Link>
.
</p>
</Message>
) : (
''
)}
</div>
<div className='w-full lg:flex'>
<div className='w-full text-center lg:w-2/12'>
<DiscordAvatar userID={data.id} size={256} className='w-full rounded-full' />
</div>
<div className='w-full grow px-5 py-12 text-center lg:w-5/12 lg:text-left'>
<Tag
circular
text={
<>
<i className={`fas fa-circle text-${Status[data.status]?.color}`} />{' '}
{Status[data.status]?.text}
</>
}
/>
<h1 className='mb-2 mt-3 text-4xl font-bold' style={bg ? { color: 'white' } : {}}>
{data.name}{' '}
{checkBotFlag(data.flags, 'trusted') ? (
<Tooltip
placement='bottom'
overlay='해당 봇은 한국 디스코드 리스트에서 엄격한 기준을 통과한 봇입니다!'
>
<span className='text-3xl text-koreanbots-blue'>
<i className='fas fa-award' />
</span>
</Tooltip>
) : (
''
)}
</h1>
<p
className={`${
bg ? 'text-gray-300' : 'text-gray-800 dark:text-gray-300'
} text-base`}
>
{data.intro}
</p>
</div>
<div className='w-full lg:w-1/4'>
{data.state === 'ok' && (
<LongButton newTab href={`/bots/${router.query.id}/invite`}>
<h4 className='whitespace-nowrap'>
<i className='fas fa-user-plus text-discord-blurple' />
</h4>
</LongButton>
)}
<Link href={`/bots/${router.query.id}/vote`} legacyBehavior>
<LongButton>
<h4>
<i className='fas fa-heart text-red-600' />
</h4>
<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)}
</span>
</LongButton>
</Link>
{((data.owners as User[]).find((el) => el.id === user?.id) ||
checkUserFlag(user?.flags, 'staff')) && (
<LongButton href={`/bots/${data.id}/edit`}>
<h4>
<i className='fas fa-cogs' />
</h4>
</LongButton>
)}
{((data.owners as User[]).find((el) => el.id === user?.id) ||
checkUserFlag(user?.flags, 'staff')) && (
<LongButton
onClick={async () => {
const res = await Fetch(`/bots/${data.id}/stats`, { method: 'PATCH' })
if (res.code !== 200) return alert(res.message)
else window.location.reload()
}}
>
<h4>
<i className='fas fa-sync' />
</h4>
</LongButton>
)}
</div>
</div>
<Divider className='px-5' />
<div className='hidden lg:block'>
<Advertisement />
</div>
<div className='lg:flex lg:flex-row-reverse' style={bg ? { color: 'white' } : {}}>
<div className='mb-1 w-full lg:w-1/4'>
<h2 className='3xl mb-2 font-bold'></h2>
<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>
<i className='far fa-flag' />
</div>
<div className='markdown-body text-black dark:text-gray-400'>
<code>{data.prefix}</code>
</div>
<div>
<i className='fas fa-users' />
</div>
<div>{data.servers || 'N/A'}</div>
{data.shards && data.servers > 1500 && (
<>
<div>
<i className='fas fa-sitemap' />
</div>
<div>{data.shards}</div>
</>
)}
<div>
<i className='fas fa-calendar-day' />
</div>
<div>{Day(date).fromNow(false)}</div>
{checkBotFlag(data.flags, 'verified') ? (
<Tooltip overlay='해당 봇은 디스코드측에서 인증된 봇입니다.'>
<div className='col-span-2'>
<i className='fas fa-check text-discord-blurple' />
</div>
</Tooltip>
) : (
''
)}
</div>
<Divider className='px-5' />
<div className='hidden lg:block'>
<Advertisement />
<h2 className='3xl mb-2 mt-2 font-bold'></h2>
<div className='flex flex-wrap'>
{data.category.map((el) => (
<Tag key={el} text={el} href={`/bots/categories/${el}`} />
))}
</div>
<div className='lg:flex lg:flex-row-reverse' style={bg ? { color: 'white' } : {}}>
<div className='mb-1 w-full lg:w-1/4'>
<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>
<i className='far fa-flag' />
</div>
<div className='markdown-body text-black dark:text-gray-400'>
<code>{data.prefix}</code>
</div>
<div>
<i className='fas fa-users' />
</div>
<div>{data.servers || 'N/A'}</div>
{
data.shards && data.servers > 1500 && <>
<div>
<i className='fas fa-sitemap' />
</div>
<div>{data.shards}</div>
</>
}
<div>
<i className='fas fa-calendar-day' />
</div>
<div>{Day(date).fromNow(false)}</div>
{
checkBotFlag(data.flags, 'verified') ?
<Tooltip overlay='해당 봇은 디스코드측에서 인증된 봇입니다.'>
<div className='col-span-2'>
<i className='fas fa-check text-discord-blurple' />
<h2 className='3xl mb-2 mt-2 font-bold'></h2>
{(data.owners as User[]).map((el) => (
<Owner
key={el.id}
id={el.id}
tag={el.tag}
globalName={el.globalName}
username={el.username}
/>
))}
<div className='list grid'>
<Link
href={`/bots/${router.query.id}/report`}
className='cursor-pointer text-red-600 hover:underline'
aria-hidden='true'
>
<i className='far fa-flag' />
</Link>
<Modal
header={`${data.name}#${data.tag} 신고하기`}
closeIcon
isOpen={reportModal}
onClose={() => {
setReportModal(false)
setReportRes(null)
}}
full
dark={theme === 'dark'}
>
{reportRes?.code === 200 ? (
<Message type='success'>
<h2 className='text-lg font-semibold'> !</h2>
<p>
!{' '}
<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)
}}
validationSchema={ReportSchema}
initialValues={{
category: null,
description: '',
_csrf: csrfToken,
}}
>
{({ errors, touched, values, setFieldValue }) => (
<Form>
<div className='mb-5'>
{reportRes && (
<div className='my-5'>
<Message type='error'>
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
<ul className='list-disc'>
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message>
</div>
)}
<h3 className='font-bold'> </h3>
<p className='mb-1 text-sm text-gray-400'>
.
</p>
{reportCats.map((el) => (
<div key={el}>
<label>
<Field
type='radio'
name='category'
value={el}
className='mr-1.5 py-2'
/>
{el}
</label>
</div>
))}
<div className='mt-1 text-xs font-light text-red-500'>
{errors.category && touched.category
? (errors.category as string)
: 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>
</Tooltip>
: ''
}
</div>
<h2 className='3xl mb-2 mt-2 font-bold'></h2>
<div className='flex flex-wrap'>
{data.category.map(el => (
<Tag key={el} text={el} href={`/bots/categories/${el}`} />
))}
</div>
<h2 className='3xl mb-2 mt-2 font-bold'></h2>
{(data.owners as User[]).map(el => (
<Owner
key={el.id}
id={el.id}
tag={el.tag}
globalName={el.globalName}
username={el.username}
<div className='text-right'>
<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>
</Form>
)}
</Formik>
)}
</Modal>
{data.discord && (
<a
rel='noopener noreferrer'
target='_blank'
className='text-discord-blurple hover:underline'
href={`https://discord.gg/${data.discord}`}
>
<i className='fab fa-discord' />
</a>
)}
{data.web && (
<a
rel='noopener noreferrer'
target='_blank'
className='text-blue-500 hover:underline'
href={data.web}
>
<i className='fas fa-globe' />
</a>
)}
{data.git && (
<a
rel='noopener noreferrer'
target='_blank'
className='hover:underline'
href={data.git}
>
<i
className={`fab fa-${git[new URL(data.git).hostname]?.icon ?? 'git-alt'}`}
/>
))}
<div className='list grid'>
<Link
href={`/bots/${router.query.id}/report`}
className='text-red-600 hover:underline cursor-pointer'
aria-hidden='true'>
<i className='far fa-flag' />
</Link>
<Modal header={`${data.name}#${data.tag} 신고하기`} closeIcon isOpen={reportModal} onClose={() => {
setReportModal(false)
setReportRes(null)
}} full dark={theme === 'dark'}>
{
reportRes?.code === 200 ? <Message type='success'>
<h2 className='text-lg font-semibold'> !</h2>
<p> ! <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)
}} validationSchema={ReportSchema} initialValues={{
category: null,
description: '',
_csrf: csrfToken
}}>
{
({ errors, touched, values, setFieldValue }) => (
<Form>
<div className='mb-5'>
{
reportRes && <div className='my-5'>
<Message type='error'>
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
<ul className='list-disc'>
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message>
</div>
}
<h3 className='font-bold'> </h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
{
reportCats.map(el =>
<div key={el}>
<label>
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
{el}
</label>
</div>
)
}
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category as string: null}</div>
<h3 className='font-bold mt-2'></h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
<TextArea name='description' placeholder='최대한 자세하게 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} 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>
</div>
<div className='text-right'>
<Button className='bg-gray-500 hover:opacity-90 text-white' onClick={()=> setReportModal(false)}></Button>
<Button type='submit' className='bg-red-500 hover:opacity-90 text-white'></Button>
</div>
</Form>
)
}
</Formik>
}
</Modal>
{data.discord && (
<a
rel='noopener noreferrer'
target='_blank'
className='text-discord-blurple hover:underline'
href={`https://discord.gg/${data.discord}`}
>
<i className='fab fa-discord' />
</a>
)}
{data.web && (
<a
rel='noopener noreferrer'
target='_blank'
className='text-blue-500 hover:underline'
href={data.web}
>
<i className='fas fa-globe' />
</a>
)}
{data.git && (
<a
rel='noopener noreferrer'
target='_blank'
className='hover:underline'
href={data.git}
>
<i className={`fab fa-${git[new URL(data.git).hostname]?.icon ?? 'git-alt'}`} />
{git[new URL(data.git).hostname]?.text ?? 'Git'}
</a>
)}
</div>
<Advertisement size='tall' />
</div>
<div className='w-full lg:pr-5 lg:w-3/4'>
{
checkBotFlag(data.flags, 'hackerthon') ? <Segment className='mt-10'>
<h1 className='text-3xl font-semibold'>
<i className='fas fa-trophy mr-4 my-2 text-amber-300' /> !
</h1>
<p> "한국 디스코드 리스트 제1회 해커톤" .</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'>
<Markdown text={desc}/>
</Segment>
<Advertisement />
</div>
{git[new URL(data.git).hostname]?.text ?? 'Git'}
</a>
)}
</div>
</>
}
<Advertisement size='tall' />
</div>
<div className='w-full lg:w-3/4 lg:pr-5'>
{checkBotFlag(data.flags, 'hackerthon') ? (
<Segment className='mt-10'>
<h1 className='text-3xl font-semibold'>
<i className='fas fa-trophy my-2 mr-4 text-amber-300' />
!
</h1>
<p>
" 1
" .
</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'>
<Markdown text={desc} />
</Segment>
<Advertisement />
</div>
</div>
</>
)}
</Container>
</div>
)
@ -343,20 +452,25 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
const data = await get.bot.load(ctx.query.id)
if(!data) return {
props: {
data
if (!data)
return {
props: {
data,
},
}
}
const desc = await get.botDescSafe(data.id)
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 {
redirect: {
destination: `/bots/${data.vanity}`,
permanent: true
permanent: true,
},
props: {}
props: {},
}
}
return {
@ -365,7 +479,7 @@ export const getServerSideProps = async (ctx: Context) => {
desc,
date: Number(SnowflakeUtil.deconstruct(data.id ?? '0').timestamp),
user: await get.user.load(user || ''),
csrfToken: getToken(ctx.req, ctx.res)
csrfToken: getToken(ctx.req, ctx.res),
},
}
}

View File

@ -7,14 +7,26 @@ const Invite: NextPage = () => <NotFound />
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const data = await get.bot.load(ctx.query.id as string)
if(!data) return { props: {} }
const record = await Bots.updateOne({ _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 })
if (!data) return { props: {} }
const record = await Bots.updateOne(
{ _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.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 {
props: {}
props: {},
}
}
export default Invite
export default Invite

View File

@ -19,94 +19,144 @@ import { getJosaPicker } from 'josa'
import { reportCats } from '@utils/Constants'
import { NextSeo } from 'next-seo'
const Container = dynamic(() => import('@components/Container'))
const Message = dynamic(() => import('@components/Message'))
const Login = dynamic(() => import('@components/Login'))
const ReportBot: NextPage<ReportBotProps> = ({ data, user, csrfToken }) => {
const [ reportRes, setReportRes ] = useState<ResponseProps<unknown>>(null)
if(!data?.id) return <NotFound />
if(!user) return <Login>
<NextSeo title='신고하기' />
</Login>
const [reportRes, setReportRes] = useState<ResponseProps<unknown>>(null)
if (!data?.id) return <NotFound />
if (!user)
return (
<Login>
<NextSeo title='신고하기' />
</Login>
)
return (
<Container paddingTop className='py-10'>
<NextSeo title={`${data.name} 신고하기`} />
<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>
{
reportRes?.code === 200 ? <Message type='success'>
{reportRes?.code === 200 ? (
<Message type='success'>
<h2 className='text-lg font-semibold'> !</h2>
<p> . <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)
}} validationSchema={ReportSchema} initialValues={{
category: null,
description: '',
_csrf: csrfToken
}}>
{
({ errors, touched, values, setFieldValue }) => (
<Form>
<div className='mb-5'>
{
reportRes && <div className='my-5'>
<Message type='error'>
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
<ul className='list-disc'>
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message>
</div>
}
<h3 className='font-bold'> </h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
{
reportCats.map(el =>
<div key={el}>
<label>
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
{el}
</label>
</div>
)
}
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category as string: null}</div>
{
values.category && <>
{
{
[reportCats[2]]: <Message type='info'>
<h3 className='font-bold text-xl'> ?</h3>
<p> .</p>
<p className='list-disc list-item list-inside'> 1393 | 1388</p>
</Message>,
[reportCats[5]]: <DMCA values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />,
[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>
}[values.category]
}
{
!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].includes(values.category) && <>
<h3 className='font-bold mt-2'></h3>
<p className='text-gray-400 text-sm mb-1'> .</p>
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />
</>
}
</>
}
<p>
.{' '}
<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)
}}
validationSchema={ReportSchema}
initialValues={{
category: null,
description: '',
_csrf: csrfToken,
}}
>
{({ errors, touched, values, setFieldValue }) => (
<Form>
<div className='mb-5'>
{reportRes && (
<div className='my-5'>
<Message type='error'>
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
<ul className='list-disc'>
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
</ul>
</Message>
</div>
)}
<h3 className='font-bold'> </h3>
<p className='mb-1 text-sm text-gray-400'> .</p>
{reportCats.map((el) => (
<div key={el}>
<label>
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
{el}
</label>
</div>
))}
<div className='mt-1 text-xs font-light text-red-500'>
{errors.category && touched.category ? (errors.category as string) : null}
</div>
</Form>
)
}
{values.category && (
<>
{
{
[reportCats[2]]: (
<Message type='info'>
<h3 className='text-xl font-bold'>
?
</h3>
<p> .</p>
<p className='list-item list-inside list-disc'>
1393 | 1388
</p>
</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]
}
{!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].includes(values.category) && (
<>
<h3 className='mt-2 font-bold'></h3>
<p className='mb-1 text-sm text-gray-400'> .</p>
<TextField
values={values}
errors={errors}
touched={touched}
setFieldValue={setFieldValue}
/>
</>
)}
</>
)}
</div>
</Form>
)}
</Formik>
}
)}
</Container>
)
}
@ -115,28 +165,28 @@ export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
const data = await get.bot.load(ctx.query.id)
const user = await get.Authorization(parsed?.token)
return {
props: {
csrfToken: getToken(ctx.req, ctx.res),
data,
user: await get.user.load(user || '')
user: await get.user.load(user || ''),
},
}
}
interface ReportBotProps {
csrfToken: string
data: Bot
user: User
data: Bot
user: User
}
interface Context extends CsrfContext {
query: URLQuery
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
id: string
}
export default ReportBot
export default ReportBot

View File

@ -19,7 +19,6 @@ import { getJosaPicker } from 'josa'
import { KoreanbotsEndPoints } from '@utils/Constants'
import { NextSeo } from 'next-seo'
const Container = dynamic(() => import('@components/Container'))
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
const Button = dynamic(() => import('@components/Button'))
@ -30,76 +29,122 @@ const Login = dynamic(() => import('@components/Login'))
const Message = dynamic(() => import('@components/Message'))
const VoteBot: NextPage<VoteBotProps> = ({ data, user, theme, csrfToken }) => {
const [ votingStatus, setVotingStatus ] = useState(0)
const [ result, setResult ] = useState<ResponseProps<{retryAfter?: number}>>(null)
const [votingStatus, setVotingStatus] = useState(0)
const [result, setResult] = useState<ResponseProps<{ retryAfter?: number }>>(null)
const router = useRouter()
if(!data?.id) return <NotFound />
if(!user) return <Login>
<NextSeo title={data.name} description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`} openGraph={{
images: [
{
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
width: 256,
height: 256,
alt: 'Bot Avatar'
}
]
}} />
</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 (!data?.id) return <NotFound />
if (!user)
return (
<Login>
<NextSeo
title={data.name}
description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`}
openGraph={{
images: [
{
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
width: 256,
height: 256,
alt: 'Bot Avatar',
},
],
}}
/>
</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}`)
return (
<Container paddingTop className='py-10'>
<NextSeo title={data.name} description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`} openGraph={{
images: [
{
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
width: 256,
height: 256,
alt: 'Bot Avatar'
}
]
}} />
{
data.state === 'blocked' ? <div className='pb-40'>
<NextSeo
title={data.name}
description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`}
openGraph={{
images: [
{
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
width: 256,
height: 256,
alt: 'Bot Avatar',
},
],
}}
/>
{data.state === 'blocked' ? (
<div className='pb-40'>
<Message type='error'>
<h2 className='text-lg font-extrabold'> .</h2>
</Message>
</div> : <>
</div>
) : (
<>
<Advertisement />
<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>
<Segment className='mb-16 py-8'>
<div className='text-center'>
<DiscordAvatar userID={data.id} className='mx-auto w-52 h-52 bg-white mb-4 rounded-full' />
<Tag text={<span><i className='fas fa-heart text-red-600' /> {data.votes}</span>} dark />
<h1 className='text-3xl font-bold mt-3'>{data.name}</h1>
<DiscordAvatar
userID={data.id}
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>
<div className='inline-block mt-2'>
{
votingStatus === 0 ? <Button onClick={()=> setVotingStatus(1)}>
<><i className='far fa-heart text-red-600'/> </>
</Button>
: votingStatus === 1 ? <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 }) })
<div className='mt-2 inline-block'>
{votingStatus === 0 ? (
<Button onClick={() => setVotingStatus(1)}>
<>
<i className='far fa-heart text-red-600' />
</>
</Button>
) : votingStatus === 1 ? (
<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)
setVotingStatus(2)
}}
/>
: result.code === 200 ? <h2 className='text-2xl font-bold'> !</h2>
: result.code === 429 ? <>
<h2 className='text-2xl font-bold'> .</h2>
<h4 className='text-md mt-1'>{Day(+new Date() + result.data?.retryAfter).fromNow()} .</h4>
</>
: <p>{result.message}</p>
}
/>
) : result.code === 200 ? (
<h2 className='text-2xl font-bold'> !</h2>
) : result.code === 429 ? (
<>
<h2 className='text-2xl font-bold'> .</h2>
<h4 className='text-md mt-1'>
{Day(+new Date() + result.data?.retryAfter).fromNow()}
.
</h4>
</>
) : (
<p>{result.message}</p>
)}
</div>
</div>
</Segment>
<Advertisement /></>
}
<Advertisement />
</>
)}
</Container>
)
}
@ -108,30 +153,30 @@ export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
const data = await get.bot.load(ctx.query.id)
const user = await get.Authorization(parsed?.token)
return {
props: {
csrfToken: getToken(ctx.req, ctx.res),
data,
user: await get.user.load(user || '')
user: await get.user.load(user || ''),
},
}
}
interface VoteBotProps {
csrfToken: string
vote: boolean
data: Bot
user: User
vote: boolean
data: Bot
user: User
theme: Theme
}
interface Context extends CsrfContext {
query: URLQuery
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
id: string
}
export default VoteBot
export default VoteBot

View File

@ -20,62 +20,88 @@ const Markdown = dynamic(() => import('@components/Markdown'))
const NSFW = dynamic(() => import('@components/NSFW'))
const Category: NextPage<CategoryProps> = ({ data, query }) => {
const [ nsfw, setNSFW ] = useState<boolean>()
const [nsfw, setNSFW] = useState<boolean>()
const router = useRouter()
useEffect(() => {
setNSFW(localStorage.nsfw)
}, [])
if(!data || data.data.length === 0 || data.totalPage < Number(query.page)) return <NotFound message={data?.data.length === 0 ? '해당 카테고리에 해당되는 봇이 존재하지 않습니다.' : null} />
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='text-2xl font-bold pt-3.5 pb-1'> </h1>
<Markdown text={'빗금 명렁어는 디스코드 채팅창에 `/` 를 입력하여 사용할 수 있습니다.'} />
if (!data || data.data.length === 0 || data.totalPage < Number(query.page))
return (
<NotFound
message={
data?.data.length === 0 ? '해당 카테고리에 해당되는 봇이 존재하지 않습니다.' : null
}
/>
)
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 />
<ResponsiveGrid>
{
data.data.map(bot => <BotCard key={bot.id} bot={bot} /> )
}
{data.data.map((bot) => (
<BotCard key={bot.id} bot={bot} />
))}
</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 />
</Container>
}
</>
)}
</>
)
}
export const getServerSideProps = async (ctx: Context) => {
let data: List<Bot>
if(!ctx.query.page) ctx.query.page = '1'
const validate = await botCategoryListArgumentSchema.validate(ctx.query).then(el => el).catch(() => 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 }))
if (!ctx.query.page) ctx.query.page = '1'
const validate = await botCategoryListArgumentSchema
.validate(ctx.query)
.then((el) => el)
.catch(() => 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 })
)
return {
props: {
data,
query: ctx.query
}
query: ctx.query,
},
}
}
interface CategoryProps {
data: List<Bot>
query: URLQuery
data: List<Bot>
query: URLQuery
}
interface Context extends NextPageContext {
query: URLQuery
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
category: string
page?: string
category: string
page?: string
}
export default Category
export default Category

View File

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

View File

@ -18,49 +18,54 @@ const Index: NextPage<IndexProps> = ({ votes, newBots, trusted }) => {
<Hero type='bots' />
<Container className='pb-10'>
<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' />
</h1>
<p className='text-base'> !</p>
<ResponsiveGrid>
{
votes.data.map(bot=> <BotCard key={bot.id} bot={bot} />)
}
{votes.data.map((bot) => (
<BotCard key={bot.id} bot={bot} />
))}
</ResponsiveGrid>
<Paginator totalPage={votes.totalPage} currentPage={votes.currentPage} pathname='/bots/list/votes' />
<Paginator
totalPage={votes.totalPage}
currentPage={votes.currentPage}
pathname='/bots/list/votes'
/>
<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' />
</h1>
<p className='text-base'> !!</p>
<ResponsiveGrid>
{
trusted.data.slice(0, 4).map(bot=> <BotCard key={bot.id} bot={bot} />)
}
{trusted.data.slice(0, 4).map((bot) => (
<BotCard key={bot.id} bot={bot} />
))}
</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' />
</h1>
<p className='text-base'> .</p>
<ResponsiveGrid>
{
newBots.data.slice(0, 4).map(bot=> <BotCard key={bot.id} bot={bot} />)
}
{newBots.data.slice(0, 4).map((bot) => (
<BotCard key={bot.id} bot={bot} />
))}
</ResponsiveGrid>
<LongButton href='/bots/list/new' center></LongButton>
<LongButton href='/bots/list/new' center>
</LongButton>
<Advertisement />
</Container>
</>
)
}
export const getServerSideProps = async() => {
export const getServerSideProps = async () => {
const votes = await Query.get.list.votes.load(1)
const newBots = await Query.get.list.new.load(1)
const trusted = await Query.get.list.trusted.load(1)
return { props: { votes, newBots, trusted }}
return { props: { votes, newBots, trusted } }
}
interface IndexProps {

View File

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

View File

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

View File

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

View File

@ -10,81 +10,112 @@ import { DiscordEnpoints, GuildPermissions } from '@utils/Constants'
const Container = dynamic(() => import('@components/Container'))
const Input = dynamic(() => import('@components/Form/Input'))
const Calculator:NextPage<CalculatorProps> = ({ query }) => {
const [ value, setValue ] = useState<{[perm: string]: boolean}>({})
const Perm = ({ name, perm, yellow }:{ name: string, perm: number, yellow?: boolean }) => {
return <li>
<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={() => {
setValue({ ...value, [perm]: !value[perm] })
}} />
<span className={`ml-2.5 text-lg ${yellow ? 'text-amber-500' : ''}`}>{name}</span>
</label>
</li>
const Calculator: NextPage<CalculatorProps> = ({ query }) => {
const [value, setValue] = useState<{ [perm: string]: boolean }>({})
const Perm = ({ name, perm, yellow }: { name: string; perm: number; yellow?: boolean }) => {
return (
<li>
<label className='inline-flex items-center py-1'>
<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] })
}}
/>
<span className={`ml-2.5 text-lg ${yellow ? 'text-amber-500' : ''}`}>{name}</span>
</label>
</li>
)
}
return <Container paddingTop className='pb-10'>
<NextSeo title='봇 초대링크 생성기' description='디스코드 봇 초대링크를 간편하게 생성하세요' openGraph={{
title:'봇 초대링크 생성기',
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)))}
<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 className='grid gap-2 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 mt-2'>
<div>
<h2 className='text-2xl font-bold'> </h2>
<ul>
{
GuildPermissions.general.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
}
</ul>
return (
<Container paddingTop className='pb-10'>
<NextSeo
title='봇 초대링크 생성기'
description='디스코드 봇 초대링크를 간편하게 생성하세요'
openGraph={{
title: '봇 초대링크 생성기',
description: '디스코드 봇 초대링크를 간편하게 생성하세요',
}}
/>
<h1 className='mb-4 mt-2 text-4xl font-bold'> </h1>
<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>
<h2 className='text-2xl font-bold'> </h2>
<ul>
{
GuildPermissions.membership.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
}
</ul>
<div className='mt-2 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3'>
<div>
<h2 className='text-2xl font-bold'> </h2>
<ul>
{GuildPermissions.general.map((el) => (
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
))}
</ul>
</div>
<div>
<h2 className='text-2xl font-bold'> </h2>
<ul>
{GuildPermissions.membership.map((el) => (
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
))}
</ul>
</div>
<div>
<h2 className='text-2xl font-bold'> </h2>
<ul>
{GuildPermissions.channel.map((el) => (
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
))}
</ul>
</div>
<div>
<h2 className='text-2xl font-bold'> </h2>
<ul>
{GuildPermissions.voice.map((el) => (
<Perm key={el.name} name={el.name} perm={el.flag} />
))}
</ul>
</div>
<div>
<h2 className='text-2xl font-bold'> </h2>
<ul>
{GuildPermissions.advanced.map((el) => (
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
))}
</ul>
</div>
</div>
<div>
<h2 className='text-2xl font-bold'> </h2>
<ul>
{
GuildPermissions.channel.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
}
</ul>
<div className='py-10'>
<span className='text-amber-500'>
= 2 , {' '}
<a href='https://support.discord.com/hc/ko/articles/219576828-2단계-인증-설정하기'>
2
</a>
.
</span>
</div>
<div>
<h2 className='text-2xl font-bold'> </h2>
<ul>
{
GuildPermissions.voice.map(el => <Perm key={el.name} name={el.name} perm={el.flag} />)
}
</ul>
</div>
<div>
<h2 className='text-2xl font-bold'> </h2>
<ul>
{
GuildPermissions.advanced.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
}
</ul>
</div>
</div>
<div className='py-10'>
<span className='text-amber-500'> = 2 , <a href='https://support.discord.com/hc/ko/articles/219576828-2단계-인증-설정하기'>2 </a> .</span>
</div>
<Formik onSubmit={()=> console.log('Pong?')} initialValues={{
id: query.id?.toString() || '',
scope: 'bot',
redirect: ''
}}>
{
({ values, setFieldValue }) => (
<Formik
onSubmit={() => console.log('Pong?')}
initialValues={{
id: query.id?.toString() || '',
scope: 'bot',
redirect: '',
}}
>
{({ values, setFieldValue }) => (
<Form>
<div className='grid gap-3 lg:grid-cols-4'>
<div>
@ -94,7 +125,12 @@ const Calculator:NextPage<CalculatorProps> = ({ query }) => {
</div>
<div>
<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' />
</div>
<div>
@ -104,20 +140,37 @@ const Calculator:NextPage<CalculatorProps> = ({ query }) => {
</div>
</div>
<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>
</Form>
)
}
</Formik>
</Container>
)}
</Formik>
</Container>
)
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
return {
props: {
query: ctx.query
}
query: ctx.query,
},
}
}

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