mirror of
https://github.com/koreanbots/core.git
synced 2025-12-16 06:20:24 +00:00
chore: apply prettier (#637)
* chore: apply prettier * chore: edit ready comment * chore: move ts comment
This commit is contained in:
parent
a324106f9f
commit
b421d1ab64
@ -1,16 +1,21 @@
|
|||||||
import AdSense from 'react-adsense'
|
import AdSense from 'react-adsense'
|
||||||
|
|
||||||
const Advertisement: React.FC<AdvertisementProps> = ({ size = 'short' }) => {
|
const Advertisement: React.FC<AdvertisementProps> = ({ size = 'short' }) => {
|
||||||
return <div className='py-5'>
|
return (
|
||||||
|
<div className='py-5'>
|
||||||
<div
|
<div
|
||||||
className={`z-0 mx-auto w-full text-center text-white ${
|
className={`z-0 mx-auto w-full text-center text-white ${
|
||||||
process.env.NODE_ENV === 'production' ? '' : 'py-12 bg-gray-700'
|
process.env.NODE_ENV === 'production' ? '' : 'bg-gray-700 py-12'
|
||||||
}`}
|
}`}
|
||||||
style={size === 'short' ? { height: '90px' } : { height: '330px' }}
|
style={size === 'short' ? { height: '90px' } : { height: '330px' }}
|
||||||
>
|
>
|
||||||
{process.env.NODE_ENV === 'production' ? (
|
{process.env.NODE_ENV === 'production' ? (
|
||||||
<AdSense.Google
|
<AdSense.Google
|
||||||
style={{ display: 'inline-block', width: '100%', height: size === 'short' ? '90px' : '330px'}}
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '100%',
|
||||||
|
height: size === 'short' ? '90px' : '330px',
|
||||||
|
}}
|
||||||
client='ca-pub-4856582423981759'
|
client='ca-pub-4856582423981759'
|
||||||
slot='3250141451'
|
slot='3250141451'
|
||||||
format=''
|
format=''
|
||||||
@ -20,6 +25,7 @@ const Advertisement: React.FC<AdvertisementProps> = ({ size = 'short' }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@ -7,17 +7,16 @@ const ServerIcon = dynamic(() => import('@components/ServerIcon'))
|
|||||||
const Application: React.FC<ApplicationProps> = ({ type, id, name }) => {
|
const Application: React.FC<ApplicationProps> = ({ type, id, name }) => {
|
||||||
return (
|
return (
|
||||||
<Link href={`/developers/applications/${type + 's'}/${id}`} legacyBehavior>
|
<Link href={`/developers/applications/${type + 's'}/${id}`} legacyBehavior>
|
||||||
<div className='relative px-2 py-4 text-center dark:bg-discord-black bg-little-white rounded-lg cursor-pointer transform hover:-translate-y-1 transition duration-100 ease-in'>
|
<div className='relative transform cursor-pointer rounded-lg bg-little-white px-2 py-4 text-center transition duration-100 ease-in hover:-translate-y-1 dark:bg-discord-black'>
|
||||||
{
|
{type === 'bot' ? (
|
||||||
type === 'bot' ?
|
<DiscordAvatar userID={id} className='w-full rounded-xl px-2' />
|
||||||
<DiscordAvatar userID={id} className='px-2 w-full rounded-xl' /> :
|
) : (
|
||||||
<ServerIcon id={id} className='px-2 w-full rounded-xl' />
|
<ServerIcon id={id} className='w-full rounded-xl px-2' />
|
||||||
}
|
)}
|
||||||
<h2 className='pt-2 whitespace-nowrap text-xl font-medium truncate'>{name}</h2>
|
<h2 className='truncate whitespace-nowrap pt-2 text-xl font-medium'>{name}</h2>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApplicationProps {
|
interface ApplicationProps {
|
||||||
|
|||||||
@ -11,12 +11,12 @@ const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
|
|||||||
|
|
||||||
const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
||||||
return (
|
return (
|
||||||
<div className='min-w-0 container mb-16 transform hover:-translate-y-1 transition duration-100 ease-in cursor-pointer'>
|
<div className='container mb-16 min-w-0 transform cursor-pointer transition duration-100 ease-in hover:-translate-y-1'>
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='h-full'>
|
<div className='h-full'>
|
||||||
<div
|
<div
|
||||||
className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl'
|
className='relative mx-auto h-full rounded-2xl bg-little-white text-black shadow-xl dark:bg-discord-black dark:text-white'
|
||||||
style={
|
style={
|
||||||
checkBotFlag(bot.flags, 'trusted') && bot.banner
|
checkBotFlag(bot.flags, 'trusted') && bot.banner
|
||||||
? {
|
? {
|
||||||
@ -30,15 +30,15 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<div className='w-3/5 flex justify-start'>
|
<div className='flex w-3/5 justify-start'>
|
||||||
<DiscordAvatar
|
<DiscordAvatar
|
||||||
size={128}
|
size={128}
|
||||||
userID={bot.id}
|
userID={bot.id}
|
||||||
alt='Avatar'
|
alt='Avatar'
|
||||||
className='absolute -left-2 -top-8 mx-auto w-32 h-32 bg-white rounded-full'
|
className='absolute -left-2 -top-8 mx-auto h-32 w-32 rounded-full bg-white'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid grid-cols-1 pr-5 pt-5 w-2/5'>
|
<div className='grid w-2/5 grid-cols-1 pr-5 pt-5'>
|
||||||
<Tag
|
<Tag
|
||||||
text={
|
text={
|
||||||
<>
|
<>
|
||||||
@ -55,19 +55,19 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 px-4 h-16'>
|
<div className='mt-3 h-16 px-4'>
|
||||||
<h2 className='px-1 text-sm'>
|
<h2 className='px-1 text-sm'>
|
||||||
<i className={`fas fa-circle text-${Status[bot.status]?.color}`} />
|
<i className={`fas fa-circle text-${Status[bot.status]?.color}`} />
|
||||||
{Status[bot.status]?.text}
|
{Status[bot.status]?.text}
|
||||||
</h2>
|
</h2>
|
||||||
<h1 className='mb-3 text-left text-xl sm:text-2xl font-bold truncate'>{bot.name}</h1>
|
<h1 className='mb-3 truncate text-left text-xl font-bold sm:text-2xl'>
|
||||||
|
{bot.name}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className='mb-10 px-4 h-6 text-left text-gray-400 text-sm'>
|
<p className='mb-10 h-6 px-4 text-left text-sm text-gray-400'>{bot.intro}</p>
|
||||||
{bot.intro}
|
|
||||||
</p>
|
|
||||||
<div>
|
<div>
|
||||||
<div className='category flex flex-wrap px-2'>
|
<div className='category flex flex-wrap px-2'>
|
||||||
{bot.category.slice(0, 3).map(el => (
|
{bot.category.slice(0, 3).map((el) => (
|
||||||
<Tag key={el} text={el} href={`/bots/categories/${el}`} dark />
|
<Tag key={el} text={el} href={`/bots/categories/${el}`} dark />
|
||||||
))}{' '}
|
))}{' '}
|
||||||
{bot.category.length > 3 && <Tag text={`+${bot.category.length - 3}`} dark />}
|
{bot.category.length > 3 && <Tag text={`+${bot.category.length - 3}`} dark />}
|
||||||
@ -80,35 +80,31 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
|||||||
<div className='flex justify-evenly'>
|
<div className='flex justify-evenly'>
|
||||||
<Link
|
<Link
|
||||||
href={makeBotURL(bot)}
|
href={makeBotURL(bot)}
|
||||||
className='py-3 w-full text-center text-koreanbots-blue hover:text-white text-sm font-bold hover:bg-koreanbots-blue rounded-bl-2xl hover:shadow-lg transition duration-100 ease-in'>
|
className='w-full rounded-bl-2xl py-3 text-center text-sm font-bold text-koreanbots-blue transition duration-100 ease-in hover:bg-koreanbots-blue hover:text-white hover:shadow-lg'
|
||||||
|
>
|
||||||
보기
|
보기
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
{manage ? (
|
{manage ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/bots/${bot.id}/edit`}
|
href={`/bots/${bot.id}/edit`}
|
||||||
className='py-3 w-full text-center text-emerald-500 hover:text-white text-sm font-bold hover:bg-emerald-500 rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'>
|
className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-emerald-500 transition duration-100 ease-in hover:bg-emerald-500 hover:text-white hover:shadow-lg'
|
||||||
|
|
||||||
관리하기
|
|
||||||
|
|
||||||
</Link>
|
|
||||||
) : bot.state !== 'ok' ? <a
|
|
||||||
className='py-3 w-full text-center text-discord-blurple text-sm font-bold rounded-br-2xl hover:shadow-lg transition duration-100 ease-in opacity-50 cursor-default select-none'
|
|
||||||
>
|
>
|
||||||
|
관리하기
|
||||||
|
</Link>
|
||||||
|
) : bot.state !== 'ok' ? (
|
||||||
|
<a className='w-full cursor-default select-none rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple opacity-50 transition duration-100 ease-in hover:shadow-lg'>
|
||||||
초대하기
|
초대하기
|
||||||
</a> :
|
</a>
|
||||||
|
) : (
|
||||||
<a
|
<a
|
||||||
href={
|
href={makeBotURL(bot) + '/invite'}
|
||||||
makeBotURL(bot) + '/invite'
|
|
||||||
}
|
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
className='py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'
|
className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple transition duration-100 ease-in hover:bg-discord-blurple hover:text-white hover:shadow-lg'
|
||||||
>
|
>
|
||||||
초대하기
|
초대하기
|
||||||
</a>
|
</a>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -117,7 +113,6 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BotCardProps {
|
interface BotCardProps {
|
||||||
|
|||||||
@ -9,30 +9,38 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
return href ? <Link
|
return href ? (
|
||||||
|
<Link
|
||||||
href={!disabled && href}
|
href={!disabled && href}
|
||||||
className={`cursor-pointer rounded-md px-4 py-2 transition duration-300 ease select-none outline-none foucs:outline-none mr-1.5 ${className ??
|
className={`ease foucs:outline-none mr-1.5 cursor-pointer select-none rounded-md px-4 py-2 outline-none transition duration-300 ${
|
||||||
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}>
|
className ??
|
||||||
|
'bg-discord-blurple text-white hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover'
|
||||||
{children}
|
}`}
|
||||||
|
|
||||||
</Link>
|
|
||||||
: onClick ? <button
|
|
||||||
type={disabled ? 'button' : type}
|
|
||||||
onClick={disabled ? null : onClick}
|
|
||||||
className={`cursor-pointer rounded-md px-4 py-2 transition duration-300 ease select-none outline-none foucs:outline-none mr-1.5 ${className ??
|
|
||||||
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</Link>
|
||||||
:
|
) : onClick ? (
|
||||||
<button
|
<button
|
||||||
type={disabled ? 'button' : type}
|
type={disabled ? 'button' : type}
|
||||||
className={`cursor-pointer rounded-md px-4 py-2 transition duration-300 ease select-none outline-none foucs:outline-none mr-1.5 ${className ??
|
onClick={disabled ? null : onClick}
|
||||||
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}
|
className={`ease foucs:outline-none mr-1.5 cursor-pointer select-none rounded-md px-4 py-2 outline-none transition duration-300 ${
|
||||||
|
className ??
|
||||||
|
'bg-discord-blurple text-white hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type={disabled ? 'button' : type}
|
||||||
|
className={`ease foucs:outline-none mr-1.5 cursor-pointer select-none rounded-md px-4 py-2 outline-none transition duration-300 ${
|
||||||
|
className ??
|
||||||
|
'bg-discord-blurple text-white hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { Ref } from 'react'
|
import { Ref } from 'react'
|
||||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||||
|
|
||||||
|
|
||||||
const Captcha: React.FC<CaptchaProps> = ({ dark, onVerify }) => {
|
const Captcha: React.FC<CaptchaProps> = ({ dark, onVerify }) => {
|
||||||
return <HCaptcha sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITEKEY} theme={dark ? 'dark' : 'light'} onVerify={onVerify}/>
|
return (
|
||||||
|
<HCaptcha
|
||||||
|
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITEKEY}
|
||||||
|
theme={dark ? 'dark' : 'light'}
|
||||||
|
onVerify={onVerify}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CaptchaProps {
|
interface CaptchaProps {
|
||||||
|
|||||||
@ -9,98 +9,161 @@ import { NextSeo } from 'next-seo'
|
|||||||
const Container = dynamic(() => import('@components/Container'))
|
const Container = dynamic(() => import('@components/Container'))
|
||||||
const Divider = dynamic(() => import('@components/Divider'))
|
const Divider = dynamic(() => import('@components/Divider'))
|
||||||
|
|
||||||
const DeveloperLayout: React.FC<DeveloperLayout> = ({ children, enabled, docs, currentDoc }:DeveloperLayout) => {
|
const DeveloperLayout: React.FC<DeveloperLayout> = ({
|
||||||
|
children,
|
||||||
|
enabled,
|
||||||
|
docs,
|
||||||
|
currentDoc,
|
||||||
|
}: DeveloperLayout) => {
|
||||||
const [navbarEnabled, setNavbarOpen] = useState(false)
|
const [navbarEnabled, setNavbarOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen'>
|
<div className='min-h-screen flex'>
|
||||||
<NextSeo title='한디리 개발자' description='한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.' openGraph={{
|
<NextSeo
|
||||||
|
title='한디리 개발자'
|
||||||
|
description='한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.'
|
||||||
|
openGraph={{
|
||||||
title: '한디리 개발자',
|
title: '한디리 개발자',
|
||||||
description:'한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.'
|
description: '한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.',
|
||||||
}} />
|
}}
|
||||||
<div className='block lg:hidden h-screen relative'>
|
/>
|
||||||
<div className='w-18 pt-20 px-2 h-full text-center bg-little-white dark:bg-discord-black fixed'>
|
<div className='relative block h-screen lg:hidden'>
|
||||||
|
<div className='w-18 fixed h-full bg-little-white px-2 pt-20 text-center dark:bg-discord-black'>
|
||||||
<ul className='text-gray-600 dark:text-gray-300'>
|
<ul className='text-gray-600 dark:text-gray-300'>
|
||||||
<li className={`cursor-pointer py-2 px-4 mb-2 rounded-md ${enabled === 'applications' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
<li
|
||||||
<Link href='/developers/applications' legacyBehavior><i className='fas fa-robot'/></Link>
|
className={`mb-2 cursor-pointer rounded-md px-4 py-2 ${
|
||||||
|
enabled === 'applications'
|
||||||
|
? 'bg-discord-blurple text-white'
|
||||||
|
: 'hover:text-gray-500 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Link href='/developers/applications' legacyBehavior>
|
||||||
|
<i className='fas fa-robot' />
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className={`cursor-pointer py-2 px-4 my-2 rounded-md ${enabled === 'docs' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
<li
|
||||||
<Link href='/developers/docs' legacyBehavior><i className='fas fa-book'/></Link>
|
className={`my-2 cursor-pointer rounded-md px-4 py-2 ${
|
||||||
|
enabled === 'docs'
|
||||||
|
? 'bg-discord-blurple text-white'
|
||||||
|
: 'hover:text-gray-500 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Link href='/developers/docs' legacyBehavior>
|
||||||
|
<i className='fas fa-book' />
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{
|
{enabled === 'docs' && (
|
||||||
enabled === 'docs' && <>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<li className='cursor-pointer py-2 px-4 my-2 rounded-md hover:text-gray-500 dark:hover:text-white' onKeyDown={() => setNavbarOpen(true)} onClick={() => setNavbarOpen(true)}>
|
<li
|
||||||
|
className='my-2 cursor-pointer rounded-md px-4 py-2 hover:text-gray-500 dark:hover:text-white'
|
||||||
|
onKeyDown={() => setNavbarOpen(true)}
|
||||||
|
onClick={() => setNavbarOpen(true)}
|
||||||
|
>
|
||||||
<i className='fas fa-bars' />
|
<i className='fas fa-bars' />
|
||||||
</li></>
|
</li>
|
||||||
}
|
</>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${navbarEnabled ? 'block' : 'hidden'} lg:block relative`}>
|
<div className={`${navbarEnabled ? 'block' : 'hidden'} relative lg:block`}>
|
||||||
<div className='bg-little-white dark:bg-discord-black pt-20 px-6 fixed h-screen w-screen lg:w-60 overflow-y-auto'>
|
<div className='fixed h-screen w-screen overflow-y-auto bg-little-white px-6 pt-20 dark:bg-discord-black lg:w-60'>
|
||||||
<ul className='text-base text-gray-600 dark:text-gray-300 mb-6 hidden lg:block'>
|
<ul className='mb-6 hidden text-base text-gray-600 dark:text-gray-300 lg:block'>
|
||||||
<li className='cursor-pointer py-2 px-4 rounded-md hover:text-gray-500 dark:hover:text-white lg:hidden' onKeyDown={() => setNavbarOpen(false)} onClick={() => setNavbarOpen(false)}>닫기</li>
|
<li
|
||||||
|
className='cursor-pointer rounded-md px-4 py-2 hover:text-gray-500 dark:hover:text-white lg:hidden'
|
||||||
|
onKeyDown={() => setNavbarOpen(false)}
|
||||||
|
onClick={() => setNavbarOpen(false)}
|
||||||
|
>
|
||||||
|
닫기
|
||||||
|
</li>
|
||||||
<Divider className='lg:hidden' />
|
<Divider className='lg:hidden' />
|
||||||
<Link href='/developers/applications' legacyBehavior>
|
<Link href='/developers/applications' legacyBehavior>
|
||||||
<li className={`cursor-pointer py-2 px-4 rounded-md ${enabled === 'applications' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
<li
|
||||||
|
className={`cursor-pointer rounded-md px-4 py-2 ${
|
||||||
|
enabled === 'applications'
|
||||||
|
? 'bg-discord-blurple text-white'
|
||||||
|
: 'hover:text-gray-500 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
나의 리스트
|
나의 리스트
|
||||||
</li>
|
</li>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href='/developers/docs' legacyBehavior>
|
<Link href='/developers/docs' legacyBehavior>
|
||||||
<li className={`cursor-pointer py-2 px-4 rounded-md ${enabled === 'docs' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
<li
|
||||||
|
className={`cursor-pointer rounded-md px-4 py-2 ${
|
||||||
|
enabled === 'docs'
|
||||||
|
? 'bg-discord-blurple text-white'
|
||||||
|
: 'hover:text-gray-500 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
문서
|
문서
|
||||||
</li>
|
</li>
|
||||||
</Link>
|
</Link>
|
||||||
</ul>
|
</ul>
|
||||||
{
|
{enabled === 'docs' && (
|
||||||
enabled === 'docs' && <>
|
<>
|
||||||
<Divider className='hidden lg:block' />
|
<Divider className='hidden lg:block' />
|
||||||
<ul className='text-sm text-gray-600 dark:text-gray-300 px-0.5 lg:mt-6'>
|
<ul className='px-0.5 text-sm text-gray-600 dark:text-gray-300 lg:mt-6'>
|
||||||
<li onClick={() => setNavbarOpen(false)} className='lg:hidden cursor-pointer py-1 px-4 rounded-md mb-2'>
|
<li
|
||||||
|
onClick={() => setNavbarOpen(false)}
|
||||||
|
className='mb-2 cursor-pointer rounded-md px-4 py-1 lg:hidden'
|
||||||
|
>
|
||||||
<i className='fas fa-times' /> 닫기
|
<i className='fas fa-times' /> 닫기
|
||||||
</li>
|
</li>
|
||||||
<Divider className='lg:hidden' />
|
<Divider className='lg:hidden' />
|
||||||
{
|
{docs?.map((el) => {
|
||||||
docs?.map(el => {
|
if (el.list)
|
||||||
if(el.list) return (
|
return (
|
||||||
<div key={el.name} className='mt-2'>
|
<div key={el.name} className='mt-2'>
|
||||||
<span className='text-gray-600 dark:text-gray-100 font-bold mb-1'>{el.name}</span>
|
<span className='mb-1 font-bold text-gray-600 dark:text-gray-100'>
|
||||||
<ul className='text-sm py-3'>
|
{el.name}
|
||||||
{
|
</span>
|
||||||
el.list.map(e =>
|
<ul className='py-3 text-sm'>
|
||||||
|
{el.list.map((e) => (
|
||||||
<Link
|
<Link
|
||||||
key={e.name}
|
key={e.name}
|
||||||
href={`/developers/docs/${el.name}/${e.name}`}
|
href={`/developers/docs/${el.name}/${e.name}`}
|
||||||
legacyBehavior>
|
legacyBehavior
|
||||||
<li onClick={() => setNavbarOpen(false)} className={`cursor-pointer px-4 py-2 rounded-md ${currentDoc === e.name ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
>
|
||||||
|
<li
|
||||||
|
onClick={() => setNavbarOpen(false)}
|
||||||
|
className={`cursor-pointer rounded-md px-4 py-2 ${
|
||||||
|
currentDoc === e.name
|
||||||
|
? 'bg-discord-blurple text-white'
|
||||||
|
: 'hover:text-gray-500 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{e.name}
|
{e.name}
|
||||||
</li>
|
</li>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
))}
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={el.name} href={`/developers/docs/${el.name}`} legacyBehavior>
|
<Link key={el.name} href={`/developers/docs/${el.name}`} legacyBehavior>
|
||||||
<li onClick={() => setNavbarOpen(false)} className={`cursor-pointer py-2 px-4 rounded-md ${currentDoc === el.name ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
<li
|
||||||
|
onClick={() => setNavbarOpen(false)}
|
||||||
|
className={`cursor-pointer rounded-md px-4 py-2 ${
|
||||||
|
currentDoc === el.name
|
||||||
|
? 'bg-discord-blurple text-white'
|
||||||
|
: 'hover:text-gray-500 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{el.name}
|
{el.name}
|
||||||
</li>
|
</li>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full py-28 lg:pl-60 pl-16'>
|
<div className='w-full py-28 pl-16 lg:pl-60'>
|
||||||
<Container>
|
<Container>{children}</Container>
|
||||||
{children}
|
|
||||||
</Container>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,13 +4,20 @@ import { KoreanbotsEndPoints } from '@utils/Constants'
|
|||||||
|
|
||||||
const Image = dynamic(() => import('@components/Image'))
|
const Image = dynamic(() => import('@components/Image'))
|
||||||
|
|
||||||
const DiscordAvatar: React.FC<DiscordAvatarProps> = props => {
|
const DiscordAvatar: React.FC<DiscordAvatarProps> = (props) => {
|
||||||
return <Image
|
return (
|
||||||
|
<Image
|
||||||
{...props}
|
{...props}
|
||||||
src={KoreanbotsEndPoints.CDN.avatar(props.userID, { format: 'webp', size: props.size ?? 256})}
|
src={KoreanbotsEndPoints.CDN.avatar(props.userID, {
|
||||||
fallbackSrc={KoreanbotsEndPoints.CDN.avatar(props.userID, { format: 'png', size: props.size ?? 256})}
|
format: 'webp',
|
||||||
|
size: props.size ?? 256,
|
||||||
|
})}
|
||||||
|
fallbackSrc={KoreanbotsEndPoints.CDN.avatar(props.userID, {
|
||||||
|
format: 'png',
|
||||||
|
size: props.size ?? 256,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DiscordAvatarProps {
|
interface DiscordAvatarProps {
|
||||||
|
|||||||
@ -8,26 +8,28 @@ const Docs: React.FC<DocsProps> = ({ title, header, description, subheader, chil
|
|||||||
const d = description || subheader
|
const d = description || subheader
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NextSeo title={t} description={d}
|
<NextSeo
|
||||||
|
title={t}
|
||||||
|
description={d}
|
||||||
openGraph={{
|
openGraph={{
|
||||||
title: t,
|
title: t,
|
||||||
description: d
|
description: d,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className='dark:bg-discord-black bg-discord-blurple z-20'>
|
<div className='z-20 bg-discord-blurple dark:bg-discord-black'>
|
||||||
<Container className='py-20' ignoreColor>
|
<Container className='py-20' ignoreColor>
|
||||||
<h1 className='mt-10 text-center text-gray-100 text-4xl font-bold sm:text-left'>
|
<h1 className='mt-10 text-center text-4xl font-bold text-gray-100 sm:text-left'>
|
||||||
{header}
|
{header}
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className='mt-5 text-center text-gray-200 text-xl font-medium sm:text-left'>
|
<h2 className='mt-5 text-center text-xl font-medium text-gray-200 sm:text-left'>
|
||||||
{description}
|
{description}
|
||||||
</h2>
|
</h2>
|
||||||
<h2 className='mt-5 text-center text-gray-200 text-xl font-medium sm:text-left'>
|
<h2 className='mt-5 text-center text-xl font-medium text-gray-200 sm:text-left'>
|
||||||
{subheader}
|
{subheader}
|
||||||
</h2>
|
</h2>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
<Container className='pt-10 pb-20'>
|
<Container className='pb-20 pt-10'>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -9,16 +9,16 @@ const Toggle = dynamic(() => import('@components/Toggle'))
|
|||||||
const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
||||||
return (
|
return (
|
||||||
<div className='releative z-30'>
|
<div className='releative z-30'>
|
||||||
<div className='bottom-0 text-white bg-discord-black py-24'>
|
<div className='bottom-0 bg-discord-black py-24 text-white'>
|
||||||
<Container className='w-11/12 lg:flex lg:pt-0 lg:w-4/5' ignoreColor>
|
<Container className='w-11/12 lg:flex lg:w-4/5 lg:pt-0' ignoreColor>
|
||||||
<div className='w-full lg:w-2/5'>
|
<div className='w-full lg:w-2/5'>
|
||||||
<h1 className='text-koreanbots-blue text-2xl font-bold'>국내 디스코드의 모든 것을 한 곳에서.</h1>
|
<h1 className='text-2xl font-bold text-koreanbots-blue'>
|
||||||
|
국내 디스코드의 모든 것을 한 곳에서.
|
||||||
|
</h1>
|
||||||
<span className='text-base'>2020-2023 한국 디스코드 리스트, All rights reserved.</span>
|
<span className='text-base'>2020-2023 한국 디스코드 리스트, All rights reserved.</span>
|
||||||
<div className='text-2xl flex space-x-1'>
|
<div className='flex space-x-1 text-2xl'>
|
||||||
<Link href='/discord'>
|
<Link href='/discord'>
|
||||||
|
|
||||||
<i className='fab fa-discord inline-block w-full' />
|
<i className='fab fa-discord inline-block w-full' />
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
<a href='https://github.com/koreanbots'>
|
<a href='https://github.com/koreanbots'>
|
||||||
<i className='fab fa-github inline-block w-full' />
|
<i className='fab fa-github inline-block w-full' />
|
||||||
@ -28,9 +28,9 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid grow gap-2 grid-cols-2 md:grid-cols-7'>
|
<div className='grid grow grid-cols-2 gap-2 md:grid-cols-7'>
|
||||||
<div className='col-span-2 mb-2'>
|
<div className='col-span-2 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>한국 디스코드 리스트</h2>
|
<h2 className='text-base font-bold text-koreanbots-blue'>한국 디스코드 리스트</h2>
|
||||||
<ul className='text-sm'>
|
<ul className='text-sm'>
|
||||||
<li>
|
<li>
|
||||||
<Link href='/about' className='hover:text-gray-300'>
|
<Link href='/about' className='hover:text-gray-300'>
|
||||||
@ -50,7 +50,7 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2 mb-2'>
|
<div className='col-span-2 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>정책</h2>
|
<h2 className='text-base font-bold text-koreanbots-blue'>정책</h2>
|
||||||
<ul className='text-sm'>
|
<ul className='text-sm'>
|
||||||
<li>
|
<li>
|
||||||
<Link href='/tos' className='hover:text-gray-300'>
|
<Link href='/tos' className='hover:text-gray-300'>
|
||||||
@ -75,7 +75,7 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-1 mb-2'>
|
<div className='col-span-1 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>커뮤니티</h2>
|
<h2 className='text-base font-bold text-koreanbots-blue'>커뮤니티</h2>
|
||||||
<ul className='text-sm'>
|
<ul className='text-sm'>
|
||||||
{/* <li>
|
{/* <li>
|
||||||
<Link href='/partners'>
|
<Link href='/partners'>
|
||||||
@ -90,7 +90,7 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2 mb-2'>
|
<div className='col-span-2 mb-2'>
|
||||||
<h2 className='text-koreanbots-blue text-base font-bold'>기타</h2>
|
<h2 className='text-base font-bold text-koreanbots-blue'>기타</h2>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<a className='mr-2 hover:text-gray-300'>다크모드</a>
|
<a className='mr-2 hover:text-gray-300'>다크모드</a>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
|||||||
@ -8,19 +8,21 @@ const Button = dynamic(() => import('@components/Button'))
|
|||||||
|
|
||||||
const Forbidden: React.FC = () => {
|
const Forbidden: React.FC = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
return <>
|
return (
|
||||||
|
<>
|
||||||
<NextSeo title='권한이 없습니다' />
|
<NextSeo title='권한이 없습니다' />
|
||||||
<div className='flex items-center justify-center h-screen select-none'>
|
<div className='flex h-screen select-none items-center justify-center'>
|
||||||
<div className='container mx-auto px-20 md:text-left text-center'>
|
<div className='container mx-auto px-20 text-center md:text-left'>
|
||||||
<h1 className='text-8xl font-semibold'>403</h1>
|
<h1 className='text-8xl font-semibold'>403</h1>
|
||||||
<h2 className='text-2xl font-semibold py-2'>
|
<h2 className='py-2 text-2xl font-semibold'>{ErrorText[403]}</h2>
|
||||||
{ErrorText[403]}
|
|
||||||
</h2>
|
|
||||||
<Button onClick={router.back}>뒤로 가기</Button>
|
<Button onClick={router.back}>뒤로 가기</Button>
|
||||||
<p className='text-gray-400 text-sm mt-2'>해당 작업을 수행할 수 있는 권한이 없습니다. 무언가 잘못된 것 같다면 문의해주세요.</p>
|
<p className='mt-2 text-sm text-gray-400'>
|
||||||
|
해당 작업을 수행할 수 있는 권한이 없습니다. 무언가 잘못된 것 같다면 문의해주세요.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Forbidden
|
export default Forbidden
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
import { Field } from 'formik'
|
import { Field } from 'formik'
|
||||||
|
|
||||||
const CheckBox: React.FC<CheckBoxProps> = ({ name, ...props }) => {
|
const CheckBox: React.FC<CheckBoxProps> = ({ name, ...props }) => {
|
||||||
return <Field type='checkbox' name={name} className='form-checkbox text-koreanbots-blue bg-gray-300 h-4 w-4 rounded' {...props} />
|
return (
|
||||||
|
<Field
|
||||||
|
type='checkbox'
|
||||||
|
name={name}
|
||||||
|
className='form-checkbox h-4 w-4 rounded bg-gray-300 text-koreanbots-blue'
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CheckBoxProps {
|
interface CheckBoxProps {
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import { Field } from 'formik'
|
import { Field } from 'formik'
|
||||||
|
|
||||||
const Input: React.FC<InputProps> = ({ name, placeholder, ...props }) => {
|
const Input: React.FC<InputProps> = ({ name, placeholder, ...props }) => {
|
||||||
return <Field
|
return (
|
||||||
|
<Field
|
||||||
{...props}
|
{...props}
|
||||||
name={name}
|
name={name}
|
||||||
className={'border-grey-light relative px-3 w-full h-10 text-black dark:text-white dark:bg-very-black border dark:border-transparent rounded outline-none'}
|
className={
|
||||||
|
'border-grey-light relative h-10 w-full rounded border px-3 text-black outline-none dark:border-transparent dark:bg-very-black dark:text-white'
|
||||||
|
}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputProps {
|
interface InputProps {
|
||||||
|
|||||||
@ -6,18 +6,19 @@ const Label: React.FC<LabelProps> = ({
|
|||||||
error = null,
|
error = null,
|
||||||
grid = true,
|
grid = true,
|
||||||
short = false,
|
short = false,
|
||||||
required = false
|
required = false,
|
||||||
}) => {
|
}) => {
|
||||||
return <label
|
return (
|
||||||
className={grid ? 'grid grid-cols-1 xl:grid-cols-4 gap-2 my-4' : 'inline-flex items-center'}
|
<label
|
||||||
|
className={grid ? 'my-4 grid grid-cols-1 gap-2 xl:grid-cols-4' : 'inline-flex items-center'}
|
||||||
htmlFor={For}
|
htmlFor={For}
|
||||||
>
|
>
|
||||||
{label && (
|
{label && (
|
||||||
<div className='col-span-1 text-sm'>
|
<div className='col-span-1 text-sm'>
|
||||||
<h3 className='text-koreanbots-blue text-lg font-bold'>
|
<h3 className='text-lg font-bold text-koreanbots-blue'>
|
||||||
{label}
|
{label}
|
||||||
{required && (
|
{required && (
|
||||||
<span className='align-text-top text-red-500 text-base font-semibold'> *</span>
|
<span className='align-text-top text-base font-semibold text-red-500'> *</span>
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
{labelDesc}
|
{labelDesc}
|
||||||
@ -25,9 +26,10 @@ const Label: React.FC<LabelProps> = ({
|
|||||||
)}
|
)}
|
||||||
<div className={short ? 'col-span-1' : 'col-span-3'}>
|
<div className={short ? 'col-span-1' : 'col-span-3'}>
|
||||||
{children}
|
{children}
|
||||||
<div className='mt-1 text-red-500 text-xs font-light'>{error}</div>
|
<div className='mt-1 text-xs font-light text-red-500'>{error}</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LabelProps {
|
interface LabelProps {
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
import ReactSelect from 'react-select'
|
import ReactSelect from 'react-select'
|
||||||
|
|
||||||
const Select: React.FC<SelectProps> = ({ placeholder, options, handleChange, handleTouch, value }) => {
|
const Select: React.FC<SelectProps> = ({
|
||||||
return <ReactSelect
|
placeholder,
|
||||||
|
options,
|
||||||
|
handleChange,
|
||||||
|
handleTouch,
|
||||||
|
value,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ReactSelect
|
||||||
styles={{
|
styles={{
|
||||||
control: provided => {
|
control: (provided) => {
|
||||||
return { ...provided, border: 'none' }
|
return { ...provided, border: 'none' }
|
||||||
},
|
},
|
||||||
option: provided => {
|
option: (provided) => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@ -15,20 +22,20 @@ const Select: React.FC<SelectProps> = ({ placeholder, options, handleChange, han
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
placeholder: provided => {
|
placeholder: (provided) => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
position: 'absolute'
|
position: 'absolute',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
singleValue: provided => {
|
singleValue: (provided) => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
position: 'absolute'
|
position: 'absolute',
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
className='border-grey-light border dark:border-transparent rounded'
|
className='border-grey-light rounded border dark:border-transparent'
|
||||||
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white '
|
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white '
|
||||||
placeholder={placeholder || '선택해주세요.'}
|
placeholder={placeholder || '선택해주세요.'}
|
||||||
options={options}
|
options={options}
|
||||||
@ -37,6 +44,7 @@ const Select: React.FC<SelectProps> = ({ placeholder, options, handleChange, han
|
|||||||
noOptionsMessage={() => '검색 결과가 없습니다.'}
|
noOptionsMessage={() => '검색 결과가 없습니다.'}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectProps {
|
interface SelectProps {
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
import React, { MouseEventHandler } from 'react'
|
import React, { MouseEventHandler } from 'react'
|
||||||
import ReactSelect, {
|
import ReactSelect, { components, MultiValueProps, MultiValueRemoveProps } from 'react-select'
|
||||||
components,
|
|
||||||
MultiValueProps,
|
|
||||||
MultiValueRemoveProps,
|
|
||||||
} from 'react-select'
|
|
||||||
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core'
|
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core'
|
||||||
import { restrictToParentElement } from '@dnd-kit/modifiers'
|
import { restrictToParentElement } from '@dnd-kit/modifiers'
|
||||||
import {
|
import {
|
||||||
@ -20,8 +16,9 @@ const MultiValue = (props: MultiValueProps<Option>) => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}
|
}
|
||||||
const innerProps = { ...props.innerProps, onMouseDown }
|
const innerProps = { ...props.innerProps, onMouseDown }
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||||
useSortable({ id: props.data.value })
|
id: props.data.value,
|
||||||
|
})
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
@ -46,16 +43,30 @@ const MultiValueRemove = (props: MultiValueRemoveProps<Option>) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Select: React.FC<SelectProps> = ({ placeholder, options, values, setValues, handleChange, handleTouch }) => {
|
const Select: React.FC<SelectProps> = ({
|
||||||
|
placeholder,
|
||||||
|
options,
|
||||||
|
values,
|
||||||
|
setValues,
|
||||||
|
handleChange,
|
||||||
|
handleTouch,
|
||||||
|
}) => {
|
||||||
const onSortEnd = (event: DragEndEvent) => {
|
const onSortEnd = (event: DragEndEvent) => {
|
||||||
const { active, over } = event
|
const { active, over } = event
|
||||||
const newValue = arrayMove(values, values.findIndex(i => i === active.id), values.findIndex(i => i === over.id))
|
const newValue = arrayMove(
|
||||||
|
values,
|
||||||
|
values.findIndex((i) => i === active.id),
|
||||||
|
values.findIndex((i) => i === over.id)
|
||||||
|
)
|
||||||
setValues(newValue)
|
setValues(newValue)
|
||||||
}
|
}
|
||||||
return <DndContext modifiers={[restrictToParentElement]} onDragEnd={onSortEnd} collisionDetection={closestCenter}>
|
return (
|
||||||
<SortableContext
|
<DndContext
|
||||||
items={values}
|
modifiers={[restrictToParentElement]}
|
||||||
strategy={horizontalListSortingStrategy}>
|
onDragEnd={onSortEnd}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
>
|
||||||
|
<SortableContext items={values} strategy={horizontalListSortingStrategy}>
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
styles={{
|
styles={{
|
||||||
placeholder: (provided) => {
|
placeholder: (provided) => {
|
||||||
@ -65,20 +76,24 @@ const Select: React.FC<SelectProps> = ({ placeholder, options, values, setValues
|
|||||||
return { ...provided, border: 'none' }
|
return { ...provided, border: 'none' }
|
||||||
},
|
},
|
||||||
option: (provided) => {
|
option: (provided) => {
|
||||||
return { ...provided, cursor: 'pointer', ':hover': {
|
return {
|
||||||
opacity: '0.7'
|
...provided,
|
||||||
} }
|
cursor: 'pointer',
|
||||||
|
':hover': {
|
||||||
|
opacity: '0.7',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
isMulti
|
isMulti
|
||||||
className='border border-grey-light dark:border-transparent rounded'
|
className='border-grey-light rounded border dark:border-transparent'
|
||||||
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white cursor-pointer '
|
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white cursor-pointer '
|
||||||
placeholder={placeholder || '선택해주세요.'}
|
placeholder={placeholder || '선택해주세요.'}
|
||||||
options={options}
|
options={options}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleTouch}
|
onBlur={handleTouch}
|
||||||
noOptionsMessage={() => '검색 결과가 없습니다.'}
|
noOptionsMessage={() => '검색 결과가 없습니다.'}
|
||||||
value={values.map(el => ({ label: el, value: el}))}
|
value={values.map((el) => ({ label: el, value: el }))}
|
||||||
components={{
|
components={{
|
||||||
MultiValue,
|
MultiValue,
|
||||||
MultiValueRemove,
|
MultiValueRemove,
|
||||||
@ -87,6 +102,7 @@ const Select: React.FC<SelectProps> = ({ placeholder, options, values, setValues
|
|||||||
/>
|
/>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectProps {
|
interface SelectProps {
|
||||||
|
|||||||
@ -8,26 +8,45 @@ import useOutsideClick from '@utils/useOutsideClick'
|
|||||||
|
|
||||||
import 'emoji-mart/css/emoji-mart.css'
|
import 'emoji-mart/css/emoji-mart.css'
|
||||||
|
|
||||||
|
const TextArea: React.FC<TextAreaProps> = ({
|
||||||
|
name,
|
||||||
const TextArea: React.FC<TextAreaProps> = ({ name, placeholder, theme='auto', max, setValue, value }) => {
|
placeholder,
|
||||||
|
theme = 'auto',
|
||||||
|
max,
|
||||||
|
setValue,
|
||||||
|
value,
|
||||||
|
}) => {
|
||||||
const ref = useRef()
|
const ref = useRef()
|
||||||
const [emojiPickerHidden, setEmojiPickerHidden] = useState(true)
|
const [emojiPickerHidden, setEmojiPickerHidden] = useState(true)
|
||||||
useOutsideClick(ref, () => {
|
useOutsideClick(ref, () => {
|
||||||
setEmojiPickerHidden(true)
|
setEmojiPickerHidden(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
return <div className='border border-grey-light dark:border-transparent h-96 text-black dark:bg-very-black dark:text-white rounded px-4 py-3 inline-block relative w-full'>
|
return (
|
||||||
<Field as='textarea' name={name} className='dark:border-transparent text-black dark:bg-very-black dark:text-white w-full relative h-full resize-none outline-none' placeholder={placeholder} />
|
<div className='border-grey-light relative inline-block h-96 w-full rounded border px-4 py-3 text-black dark:border-transparent dark:bg-very-black dark:text-white'>
|
||||||
|
<Field
|
||||||
|
as='textarea'
|
||||||
|
name={name}
|
||||||
|
className='relative h-full w-full resize-none text-black outline-none dark:border-transparent dark:bg-very-black dark:text-white'
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<div className='absolute bottom-12 left-10 z-30'>
|
<div className='absolute bottom-12 left-10 z-30'>
|
||||||
{
|
{!emojiPickerHidden && (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
!emojiPickerHidden && <Picker title='선택해주세요' emoji='sunglasses' set='twitter' enableFrequentEmojiSort theme={theme} showSkinTones={false} onSelect={(e) => {
|
<Picker
|
||||||
|
title='선택해주세요'
|
||||||
|
emoji='sunglasses'
|
||||||
|
set='twitter'
|
||||||
|
enableFrequentEmojiSort
|
||||||
|
theme={theme}
|
||||||
|
showSkinTones={false}
|
||||||
|
onSelect={(e) => {
|
||||||
setEmojiPickerHidden(true)
|
setEmojiPickerHidden(true)
|
||||||
setValue(value + ' ' + ((e as { native: string }).native || e.colons))
|
setValue(value + ' ' + ((e as { native: string }).native || e.colons))
|
||||||
}} i18n={{
|
}}
|
||||||
|
i18n={{
|
||||||
search: '검색',
|
search: '검색',
|
||||||
notfound: '검색 결과가 없습니다.',
|
notfound: '검색 결과가 없습니다.',
|
||||||
categories: {
|
categories: {
|
||||||
@ -41,21 +60,32 @@ const TextArea: React.FC<TextAreaProps> = ({ name, placeholder, theme='auto', ma
|
|||||||
objects: '사물',
|
objects: '사물',
|
||||||
symbols: '기호',
|
symbols: '기호',
|
||||||
flags: '국기',
|
flags: '국기',
|
||||||
custom: '커스텀'
|
custom: '커스텀',
|
||||||
}
|
},
|
||||||
}} custom={KoreanbotsEmoji}/>
|
}}
|
||||||
}
|
custom={KoreanbotsEmoji}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='absolute bottom-2 left-4 hidden sm:block'>
|
<div className='absolute bottom-2 left-4 hidden sm:block'>
|
||||||
<div className='emoji-selector-button outline-none' onClick={() => setEmojiPickerHidden(false)} onKeyPress={() => setEmojiPickerHidden(false)} role='button' tabIndex={0} />
|
<div
|
||||||
|
className='emoji-selector-button outline-none'
|
||||||
|
onClick={() => setEmojiPickerHidden(false)}
|
||||||
|
onKeyPress={() => setEmojiPickerHidden(false)}
|
||||||
|
role='button'
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{
|
{max && (
|
||||||
max && <span className={`absolute bottom-2 right-4 ${max < value.length ? ' text-red-400' : ''}`}>
|
<span
|
||||||
|
className={`absolute bottom-2 right-4 ${max < value.length ? ' text-red-400' : ''}`}
|
||||||
|
>
|
||||||
{max - value.length}
|
{max - value.length}
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextAreaProps {
|
interface TextAreaProps {
|
||||||
@ -68,4 +98,3 @@ interface TextAreaProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default TextArea
|
export default TextArea
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
|
|
||||||
import { botCategories, botCategoryIcon, serverCategories, serverCategoryIcon } from '@utils/Constants'
|
import {
|
||||||
|
botCategories,
|
||||||
|
botCategoryIcon,
|
||||||
|
serverCategories,
|
||||||
|
serverCategoryIcon,
|
||||||
|
} from '@utils/Constants'
|
||||||
|
|
||||||
const Container = dynamic(() => import('@components/Container'))
|
const Container = dynamic(() => import('@components/Container'))
|
||||||
const Tag = dynamic(() => import('@components/Tag'))
|
const Tag = dynamic(() => import('@components/Tag'))
|
||||||
@ -9,57 +14,135 @@ const Search = dynamic(()=> import('@components/Search'))
|
|||||||
|
|
||||||
const Hero: React.FC<HeroProps> = ({ type = 'all', header, description }) => {
|
const Hero: React.FC<HeroProps> = ({ type = 'all', header, description }) => {
|
||||||
const link = `/${type}/categories`
|
const link = `/${type}/categories`
|
||||||
return <>
|
return (
|
||||||
<NextSeo title={header} description={description} openGraph={{
|
<>
|
||||||
|
<NextSeo
|
||||||
|
title={header}
|
||||||
|
description={description}
|
||||||
|
openGraph={{
|
||||||
title: header,
|
title: header,
|
||||||
description
|
description,
|
||||||
}} />
|
}}
|
||||||
<div className='dark:bg-discord-black bg-discord-blurple text-gray-100 md:p-0 mb-8'>
|
/>
|
||||||
<Container className='pt-24 pb-16 md:pb-20' ignoreColor>
|
<div className='mb-8 bg-discord-blurple text-gray-100 dark:bg-discord-black md:p-0'>
|
||||||
<h1 className='hidden md:block text-left text-3xl font-bold'>
|
<Container className='pb-16 pt-24 md:pb-20' ignoreColor>
|
||||||
|
<h1 className='hidden text-left text-3xl font-bold md:block'>
|
||||||
{header && `${header} - `}한국 디스코드 리스트
|
{header && `${header} - `}한국 디스코드 리스트
|
||||||
</h1>
|
</h1>
|
||||||
<h1 className='md:hidden text-center text-3xl font-semibold'>
|
<h1 className='text-center text-3xl font-semibold md:hidden'>
|
||||||
{ header && <span className='text-4xl'>{header}<br/></span>}한국 디스코드 리스트
|
{header && (
|
||||||
|
<span className='text-4xl'>
|
||||||
|
{header}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
한국 디스코드 리스트
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-center sm:text-left text-xl font-base mt-2'>{description || `${type !== 'all' ? '다양한 ' : ''}국내 디스코드${{ all: '의 모든 것을', bots: ' 봇들을', servers: ' 서버들을' }[type]} 한 곳에서 확인하세요!`}</p>
|
<p className='font-base mt-2 text-center text-xl sm:text-left'>
|
||||||
|
{description ||
|
||||||
|
`${type !== 'all' ? '다양한 ' : ''}국내 디스코드${
|
||||||
|
{ all: '의 모든 것을', bots: ' 봇들을', servers: ' 서버들을' }[type]
|
||||||
|
} 한 곳에서 확인하세요!`}
|
||||||
|
</p>
|
||||||
<Search />
|
<Search />
|
||||||
<div className='flex flex-wrap mt-5'>
|
<div className='mt-5 flex flex-wrap'>
|
||||||
{
|
{type === 'all' ? (
|
||||||
type === 'all' ? <>
|
<>
|
||||||
<Tag text={
|
<Tag
|
||||||
|
text={
|
||||||
<>
|
<>
|
||||||
<i className='fas fa-robot text-koreanbots-blue' /> 봇 리스트
|
<i className='fas fa-robot text-koreanbots-blue' /> 봇 리스트
|
||||||
</>
|
</>
|
||||||
} dark bigger href='/bots' />
|
}
|
||||||
<Tag text={
|
dark
|
||||||
|
bigger
|
||||||
|
href='/bots'
|
||||||
|
/>
|
||||||
|
<Tag
|
||||||
|
text={
|
||||||
<>
|
<>
|
||||||
<i className='fas fa-users text-koreanbots-blue' /> 서버 리스트
|
<i className='fas fa-users text-koreanbots-blue' /> 서버 리스트
|
||||||
</>
|
</>
|
||||||
} dark bigger href='/servers' />
|
|
||||||
{
|
|
||||||
botCategories.slice(0, 2).map(t => <Tag key={t} text={<><i className={botCategoryIcon[t]} /> {t} 봇</>} dark bigger href={`/bots/categories/${t}`} />)
|
|
||||||
}
|
}
|
||||||
|
dark
|
||||||
{
|
bigger
|
||||||
serverCategories.slice(0, 2).map(t => <Tag key={t} text={<><i className={serverCategoryIcon[t]} /> {t} 서버</>} dark bigger href={`/servers/categories/${t}`} />)
|
href='/servers'
|
||||||
}
|
/>
|
||||||
</>: <>
|
{botCategories.slice(0, 2).map((t) => (
|
||||||
<Tag key='list' text={<>
|
<Tag
|
||||||
<i className='fas fa-heart text-red-600'/> 하트 랭킹
|
key={t}
|
||||||
</>} dark bigger href={type === 'bots' ? '/bots/list/votes' : '/servers/list/votes'} />
|
text={
|
||||||
{ (type === 'bots' ? botCategories : serverCategories).slice(0, 4).map(t=> <Tag key={t} text={<>
|
<>
|
||||||
<i className={(type === 'bots' ? botCategoryIcon : serverCategoryIcon)[t]} /> {t}
|
<i className={botCategoryIcon[t]} /> {t} 봇
|
||||||
</>} dark bigger href={`${link}/${t}`} />) }
|
|
||||||
<Tag key='tag' text={<>
|
|
||||||
<i className='fas fa-tag'/> 카테고리 더보기
|
|
||||||
</>} dark bigger href={link} />
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
dark
|
||||||
|
bigger
|
||||||
|
href={`/bots/categories/${t}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{serverCategories.slice(0, 2).map((t) => (
|
||||||
|
<Tag
|
||||||
|
key={t}
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
<i className={serverCategoryIcon[t]} /> {t} 서버
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
dark
|
||||||
|
bigger
|
||||||
|
href={`/servers/categories/${t}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Tag
|
||||||
|
key='list'
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
<i className='fas fa-heart text-red-600' /> 하트 랭킹
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
dark
|
||||||
|
bigger
|
||||||
|
href={type === 'bots' ? '/bots/list/votes' : '/servers/list/votes'}
|
||||||
|
/>
|
||||||
|
{(type === 'bots' ? botCategories : serverCategories).slice(0, 4).map((t) => (
|
||||||
|
<Tag
|
||||||
|
key={t}
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
<i
|
||||||
|
className={(type === 'bots' ? botCategoryIcon : serverCategoryIcon)[t]}
|
||||||
|
/>{' '}
|
||||||
|
{t}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
dark
|
||||||
|
bigger
|
||||||
|
href={`${link}/${t}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Tag
|
||||||
|
key='tag'
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
<i className='fas fa-tag' /> 카테고리 더보기
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
dark
|
||||||
|
bigger
|
||||||
|
href={link}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HeroProps {
|
interface HeroProps {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { SyntheticEvent, useEffect, useState } from 'react'
|
|||||||
import { supportsWebP } from '@utils/Tools'
|
import { supportsWebP } from '@utils/Tools'
|
||||||
import Logger from '@utils/Logger'
|
import Logger from '@utils/Logger'
|
||||||
|
|
||||||
const BaseImage: React.FC<ImageProps> = props => {
|
const BaseImage: React.FC<ImageProps> = (props) => {
|
||||||
const fallback = '/img/default.png'
|
const fallback = '/img/default.png'
|
||||||
const [webpUnavailable, setWebpUnavailable] = useState<boolean>()
|
const [webpUnavailable, setWebpUnavailable] = useState<boolean>()
|
||||||
|
|
||||||
@ -10,37 +10,38 @@ const BaseImage: React.FC<ImageProps> = props => {
|
|||||||
setWebpUnavailable(localStorage.webp === 'false')
|
setWebpUnavailable(localStorage.webp === 'false')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <img
|
return (
|
||||||
|
<img
|
||||||
alt={props.alt ?? 'Image'}
|
alt={props.alt ?? 'Image'}
|
||||||
loading='lazy'
|
loading='lazy'
|
||||||
className={props.className}
|
className={props.className}
|
||||||
src={
|
src={(webpUnavailable && props.fallbackSrc) || props.src}
|
||||||
webpUnavailable && props.fallbackSrc || props.src
|
|
||||||
}
|
|
||||||
onError={(e: SyntheticEvent<HTMLImageElement, ImageEvent>) => {
|
onError={(e: SyntheticEvent<HTMLImageElement, ImageEvent>) => {
|
||||||
if (webpUnavailable) {
|
if (webpUnavailable) {
|
||||||
(e.target as ImageTarget).onerror = (event) => {
|
;(e.target as ImageTarget).onerror = (event) => {
|
||||||
// All Fails
|
// All Fails
|
||||||
(event.target as ImageTarget).onerror = () => { Logger.warn('FALLBACK IMAGE LOAD FAIL') }
|
;(event.target as ImageTarget).onerror = () => {
|
||||||
(event.target as ImageTarget).src = fallback
|
Logger.warn('FALLBACK IMAGE LOAD FAIL')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
;(event.target as ImageTarget).src = fallback
|
||||||
}
|
}
|
||||||
else if (props.fallbackSrc) {
|
} else if (props.fallbackSrc) {
|
||||||
(e.target as ImageTarget).onerror = (event) => {
|
;(e.target as ImageTarget).onerror = (event) => {
|
||||||
// All Fails
|
// All Fails
|
||||||
(event.target as ImageTarget).onerror = () => { Logger.warn('FALLBACK IMAGE LOAD FAIL') }
|
;(event.target as ImageTarget).onerror = () => {
|
||||||
(event.target as ImageTarget).src = fallback
|
Logger.warn('FALLBACK IMAGE LOAD FAIL')
|
||||||
|
}
|
||||||
|
;(event.target as ImageTarget).src = fallback
|
||||||
}
|
}
|
||||||
// Webp Load Fail
|
// Webp Load Fail
|
||||||
(e.target as ImageTarget).src = props.fallbackSrc
|
;(e.target as ImageTarget).src = props.fallbackSrc
|
||||||
if (!supportsWebP()) localStorage.setItem('webp', 'false')
|
if (!supportsWebP()) localStorage.setItem('webp', 'false')
|
||||||
}
|
} else {
|
||||||
else {
|
;(e.target as ImageTarget).src = fallback
|
||||||
(e.target as ImageTarget).src = fallback
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImageProps {
|
interface ImageProps {
|
||||||
|
|||||||
@ -3,9 +3,9 @@ const Loader: React.FC<LoaderProps> = ({ text, visible = true }) => {
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
visible ? '' : 'hidden '
|
visible ? '' : 'hidden '
|
||||||
} w-full h-full fixed block top-0 left-0 bg-gray-500 bg-opacity-75 z-50 dark:text-black`}
|
} fixed left-0 top-0 z-50 block h-full w-full bg-gray-500 bg-opacity-75 dark:text-black`}
|
||||||
>
|
>
|
||||||
<h1 className='relative top-1/2 block mx-auto my-0 text-center text-2xl font-semibold opacity-100'>
|
<h1 className='relative top-1/2 mx-auto my-0 block text-center text-2xl font-semibold opacity-100'>
|
||||||
{text}
|
{text}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,9 +9,7 @@ const Login: React.FC<React.PropsWithChildren> = ({ children }) => {
|
|||||||
localStorage.redirectTo = window.location.href
|
localStorage.redirectTo = window.location.href
|
||||||
redirectTo(router, 'login')
|
redirectTo(router, 'login')
|
||||||
})
|
})
|
||||||
return <>
|
return <>{children}</>
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Login
|
export default Login
|
||||||
@ -1,34 +1,60 @@
|
|||||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
const LongButton: React.FC<LongButtonProps> = ({ children, newTab=false, href, onClick, center=false }) => {
|
const LongButton: React.FC<LongButtonProps> = ({
|
||||||
|
children,
|
||||||
|
newTab = false,
|
||||||
|
href,
|
||||||
|
onClick,
|
||||||
|
center = false,
|
||||||
|
}) => {
|
||||||
if (href) {
|
if (href) {
|
||||||
if(newTab) return <a href={href} rel='noopener noreferrer'
|
if (newTab)
|
||||||
target='_blank'>
|
return (
|
||||||
<div className={`${center ? 'justify-center ': '' }text-base bg-little-white dark:bg-discord-black text-black dark:text-gray-400 rounded flex hover:bg-little-white-hover dark:hover:bg-discord-dark-hover cursor-pointer px-4 py-4 mb-1`}>
|
<a href={href} rel='noopener noreferrer' target='_blank'>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
center ? 'justify-center ' : ''
|
||||||
|
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
else return (
|
)
|
||||||
|
else
|
||||||
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className={`${center ? 'justify-center ': '' }text-base bg-little-white dark:bg-discord-black text-black dark:text-gray-400 rounded flex hover:bg-little-white-hover dark:hover:bg-discord-dark-hover cursor-pointer px-4 py-4 mb-1`}>
|
className={`${
|
||||||
|
center ? 'justify-center ' : ''
|
||||||
|
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if(onClick) return <div onKeyPress={onClick} onClick={onClick} className={`${center ? 'justify-center ': '' }text-base bg-little-white dark:bg-discord-black text-black dark:text-gray-400 rounded flex hover:bg-little-white-hover dark:hover:bg-discord-dark-hover cursor-pointer px-4 py-4 mb-1`}>
|
if (onClick)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onKeyPress={onClick}
|
||||||
|
onClick={onClick}
|
||||||
|
className={`${
|
||||||
|
center ? 'justify-center ' : ''
|
||||||
|
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return <a className={`${center ? 'justify-center ': '' }text-base bg-little-white dark:bg-discord-black text-black dark:text-gray-400 rounded flex hover:bg-little-white-hover dark:hover:bg-discord-dark-hover cursor-pointer px-4 py-4 mb-1`}>
|
return (
|
||||||
|
<a
|
||||||
|
className={`${
|
||||||
|
center ? 'justify-center ' : ''
|
||||||
|
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LongButton
|
export default LongButton
|
||||||
|
|||||||
@ -5,7 +5,12 @@ import * as Emoji from 'node-emoji'
|
|||||||
|
|
||||||
import { anchorHeader, customEmoji, twemoji } from '@utils/Tools'
|
import { anchorHeader, customEmoji, twemoji } from '@utils/Tools'
|
||||||
|
|
||||||
const Markdown: React.FC<MarkdownProps> = ({ text, options={}, allowedTag=[], components={} }) => {
|
const Markdown: React.FC<MarkdownProps> = ({
|
||||||
|
text,
|
||||||
|
options = {},
|
||||||
|
allowedTag = [],
|
||||||
|
components = {},
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className='markdown-body w-full'>
|
<div className='markdown-body w-full'>
|
||||||
<MarkdownView
|
<MarkdownView
|
||||||
@ -23,10 +28,10 @@ const Markdown: React.FC<MarkdownProps> = ({ text, options={}, allowedTag=[], co
|
|||||||
tasklists: true,
|
tasklists: true,
|
||||||
ghCompatibleHeaderId: true,
|
ghCompatibleHeaderId: true,
|
||||||
encodeEmails: true,
|
encodeEmails: true,
|
||||||
...options
|
...options,
|
||||||
}}
|
}}
|
||||||
components={components}
|
components={components}
|
||||||
sanitizeHtml={html =>
|
sanitizeHtml={(html) =>
|
||||||
sanitizeHtml(html, {
|
sanitizeHtml(html, {
|
||||||
allowedTags: [
|
allowedTags: [
|
||||||
'addr',
|
'addr',
|
||||||
@ -98,16 +103,16 @@ const Markdown: React.FC<MarkdownProps> = ({ text, options={}, allowedTag=[], co
|
|||||||
'svg',
|
'svg',
|
||||||
'path',
|
'path',
|
||||||
'input',
|
'input',
|
||||||
...allowedTag
|
...allowedTag,
|
||||||
],
|
],
|
||||||
allowedAttributes: false,
|
allowedAttributes: false,
|
||||||
allowedClasses: {
|
allowedClasses: {
|
||||||
'*': ['align-middle'],
|
'*': ['align-middle'],
|
||||||
a: ['anchor', 'mr-1'],
|
a: ['anchor', 'mr-1'],
|
||||||
svg: ['octicon-link'],
|
svg: ['octicon-link'],
|
||||||
img: ['emoji', 'special']
|
img: ['emoji', 'special'],
|
||||||
},
|
},
|
||||||
allowedStyles: {}
|
allowedStyles: {},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import Markdown from './Markdown'
|
|||||||
const Message: React.FC<MessageProps> = ({ type, children }) => {
|
const Message: React.FC<MessageProps> = ({ type, children }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${MessageColor[type]} px-6 py-4 rounded-md text-base mx-auto w-full text-left`}
|
className={`${MessageColor[type]} mx-auto w-full rounded-md px-6 py-4 text-left text-base`}
|
||||||
>
|
>
|
||||||
{typeof children === 'string' ? <Markdown text={children} /> : children}
|
{typeof children === 'string' ? <Markdown text={children} /> : children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,7 +2,15 @@ import { ReactNode } from 'react'
|
|||||||
import { Modal as ReactModal } from 'react-responsive-modal'
|
import { Modal as ReactModal } from 'react-responsive-modal'
|
||||||
import 'react-responsive-modal/styles.css'
|
import 'react-responsive-modal/styles.css'
|
||||||
|
|
||||||
const Modal: React.FC<ModalProps> = ({ children, isOpen, onClose, closeIcon=false, dark, header, full=false }) => {
|
const Modal: React.FC<ModalProps> = ({
|
||||||
|
children,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
closeIcon = false,
|
||||||
|
dark,
|
||||||
|
header,
|
||||||
|
full = false,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
@ -12,13 +20,13 @@ const Modal: React.FC<ModalProps> = ({ children, isOpen, onClose, closeIcon=fals
|
|||||||
showCloseIcon={closeIcon}
|
showCloseIcon={closeIcon}
|
||||||
styles={{
|
styles={{
|
||||||
closeButton: {
|
closeButton: {
|
||||||
color: dark ? 'white' : 'black'
|
color: dark ? 'white' : 'black',
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
background: dark ? '#2C2F33' : '#fbfbfb',
|
background: dark ? '#2C2F33' : '#fbfbfb',
|
||||||
color: dark ? 'white' : 'black',
|
color: dark ? 'white' : 'black',
|
||||||
width: full ? '90%' : 'inherit'
|
width: full ? '90%' : 'inherit',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -4,25 +4,38 @@ const Button = dynamic(() => import('@components/Button'))
|
|||||||
const Container = dynamic(() => import('@components/Container'))
|
const Container = dynamic(() => import('@components/Container'))
|
||||||
|
|
||||||
const NSFW: React.FC<NSFWProps> = ({ onClick, onDisableClick }) => {
|
const NSFW: React.FC<NSFWProps> = ({ onClick, onDisableClick }) => {
|
||||||
return <Container>
|
return (
|
||||||
<div className='flex items-center h-screen select-none'>
|
<Container>
|
||||||
|
<div className='flex h-screen select-none items-center'>
|
||||||
<div className='px-10'>
|
<div className='px-10'>
|
||||||
<h1 className='text-2xl font-bold flex'>
|
<h1 className='flex text-2xl font-bold'>
|
||||||
<img draggable='false' alt='⚠' src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/26a0.svg' className='emoji mr-2 w-8' />
|
<img
|
||||||
해당 컨텐츠는 만19세 이상의 성인만 열람할 수 있습니다.</h1>
|
draggable='false'
|
||||||
<p className='text-lg mb-3'>계속하시겠습니까?</p>
|
alt='⚠'
|
||||||
|
src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/26a0.svg'
|
||||||
|
className='emoji mr-2 w-8'
|
||||||
|
/>
|
||||||
|
해당 컨텐츠는 만19세 이상의 성인만 열람할 수 있습니다.
|
||||||
|
</h1>
|
||||||
|
<p className='mb-3 text-lg'>계속하시겠습니까?</p>
|
||||||
<Button onClick={onClick}>
|
<Button onClick={onClick}>
|
||||||
<i className='fas fa-arrow-right' /> 계속하기
|
<i className='fas fa-arrow-right' /> 계속하기
|
||||||
</Button>
|
</Button>
|
||||||
<div className='mt-1'>
|
<div className='mt-1'>
|
||||||
<button className='text-blue-500 hover:text-blue-600' onClick={() => {
|
<button
|
||||||
|
className='text-blue-500 hover:text-blue-600'
|
||||||
|
onClick={() => {
|
||||||
onClick()
|
onClick()
|
||||||
onDisableClick()
|
onDisableClick()
|
||||||
}}>다시 표시하지 않기.</button>
|
}}
|
||||||
|
>
|
||||||
|
다시 표시하지 않기.
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NSFWProps {
|
interface NSFWProps {
|
||||||
|
|||||||
@ -20,7 +20,11 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
|
|||||||
const [mobileAddDropdownOpen, setMobileAddDropdownOpen] = useState<boolean>(false)
|
const [mobileAddDropdownOpen, setMobileAddDropdownOpen] = useState<boolean>(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const logged = userCache?.id && userCache.version === 2
|
const logged = userCache?.id && userCache.version === 2
|
||||||
const type: Nullable<'bot'|'server'> = router.pathname.startsWith('/bots') ? 'bot' : router.pathname.startsWith('/servers') ? 'server' : null
|
const type: Nullable<'bot' | 'server'> = router.pathname.startsWith('/bots')
|
||||||
|
? 'bot'
|
||||||
|
: router.pathname.startsWith('/servers')
|
||||||
|
? 'server'
|
||||||
|
: null
|
||||||
const dev = router.pathname.startsWith('/developers')
|
const dev = router.pathname.startsWith('/developers')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -28,107 +32,127 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
|
|||||||
if (localStorage.userCache) {
|
if (localStorage.userCache) {
|
||||||
setUserCache(token ? JSON.parse(localStorage.userCache) : null)
|
setUserCache(token ? JSON.parse(localStorage.userCache) : null)
|
||||||
}
|
}
|
||||||
Fetch<User>('/users/@me').then(data => {
|
Fetch<User>('/users/@me').then((data) => {
|
||||||
if (data.code !== 200) return
|
if (data.code !== 200) return
|
||||||
setUserCache(JSON.parse(localStorage.userCache = JSON.stringify({
|
setUserCache(
|
||||||
|
JSON.parse(
|
||||||
|
(localStorage.userCache = JSON.stringify({
|
||||||
id: data.data.id,
|
id: data.data.id,
|
||||||
username: data.data.globalName,
|
username: data.data.globalName,
|
||||||
tag: data.data.tag,
|
tag: data.data.tag,
|
||||||
version: 2
|
version: 2,
|
||||||
})))
|
}))
|
||||||
|
)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
setUserCache(null)
|
setUserCache(null)
|
||||||
}
|
}
|
||||||
}, [token])
|
}, [token])
|
||||||
return <>
|
return (
|
||||||
<nav className='fixed z-40 top-0 flex flex-wrap items-center justify-between px-2 py-3 w-full text-gray-100 dark:bg-discord-black bg-discord-blurple lg:absolute'>
|
<>
|
||||||
<div className='container flex flex-wrap items-center justify-between mx-auto px-4'>
|
<nav className='fixed top-0 z-40 flex w-full flex-wrap items-center justify-between bg-discord-blurple px-2 py-3 text-gray-100 dark:bg-discord-black lg:absolute'>
|
||||||
<div className='relative flex justify-between w-full lg:justify-start lg:w-auto'>
|
<div className='container mx-auto flex flex-wrap items-center justify-between px-4'>
|
||||||
|
<div className='relative flex w-full justify-between lg:w-auto lg:justify-start'>
|
||||||
<Link
|
<Link
|
||||||
href={dev ? '/developers' : '/'}
|
href={dev ? '/developers' : '/'}
|
||||||
className={`${dev ? 'dark:text-koreanbots-blue ' : ''}logofont text-large whitespace-no-wrap inline-block mr-4 py-2 hover:text-gray-300 font-semibold leading-relaxed uppercase sm:text-2xl`}>
|
className={`${
|
||||||
|
dev ? 'dark:text-koreanbots-blue ' : ''
|
||||||
{ dev ? <><i className='fas fa-tools mr-1'/> DEVELOPERS</> : 'KOREANLIST'}
|
}logofont text-large whitespace-no-wrap mr-4 inline-block py-2 font-semibold uppercase leading-relaxed hover:text-gray-300 sm:text-2xl`}
|
||||||
|
>
|
||||||
|
{dev ? (
|
||||||
|
<>
|
||||||
|
<i className='fas fa-tools mr-1' /> DEVELOPERS
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'KOREANLIST'
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
className='block px-3 py-1 dark:text-gray-200 text-xl leading-none bg-transparent border border-solid border-transparent rounded outline-none focus:outline-none cursor-pointer lg:hidden'
|
className='block cursor-pointer rounded border border-solid border-transparent bg-transparent px-3 py-1 text-xl leading-none outline-none focus:outline-none dark:text-gray-200 lg:hidden'
|
||||||
type='button'
|
type='button'
|
||||||
onClick={() => setNavbarOpen(!navbarOpen)}
|
onClick={() => setNavbarOpen(!navbarOpen)}
|
||||||
>
|
>
|
||||||
<i className={`fas ${!navbarOpen ? 'fa-bars' : 'fa-times'}`}></i>
|
<i className={`fas ${!navbarOpen ? 'fa-bars' : 'fa-times'}`}></i>
|
||||||
</button>
|
</button>
|
||||||
<ul className='hidden lg:flex flex-col list-none lg:flex-row lg:ml-auto'>
|
<ul className='hidden list-none flex-col lg:ml-auto lg:flex lg:flex-row'>
|
||||||
<li className='flex items-center'>
|
<li className='flex items-center'>
|
||||||
<Link
|
<Link
|
||||||
href={dev ? '/' : '/developers'}
|
href={dev ? '/' : '/developers'}
|
||||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'>
|
className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||||
|
>
|
||||||
{dev ? '홈' : '개발자'}
|
{dev ? '홈' : '개발자'}
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{
|
{type !== 'bot' && (
|
||||||
type !== 'bot' && <li className='flex items-center'>
|
<li className='flex items-center'>
|
||||||
<Link
|
<Link
|
||||||
href='/bots'
|
href='/bots'
|
||||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'>
|
className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||||
|
>
|
||||||
봇 리스트
|
봇 리스트
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
{
|
{type !== 'server' && (
|
||||||
type !== 'server' && <li className='flex items-center'>
|
<li className='flex items-center'>
|
||||||
<Link
|
<Link
|
||||||
href='/servers'
|
href='/servers'
|
||||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'>
|
className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||||
|
>
|
||||||
서버 리스트
|
서버 리스트
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
}
|
)}
|
||||||
<li className='flex items-center'>
|
<li className='flex items-center'>
|
||||||
<Link
|
<Link
|
||||||
href='/discord'
|
href='/discord'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'>
|
className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||||
|
>
|
||||||
디스코드
|
디스코드
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className='flex items-center'>
|
<li className='flex items-center'>
|
||||||
<Link
|
<Link
|
||||||
href='/about'
|
href='/about'
|
||||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'>
|
className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||||
|
>
|
||||||
소개
|
소개
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className='flex items-center' onFocus={() => setAddDropdownOpen(true)} onMouseOver={() => setAddDropdownOpen(true)} onMouseOut={() => setAddDropdownOpen(false)} onBlur={() => setAddDropdownOpen(false)}>
|
<li
|
||||||
<span className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100 cursor-pointer'>
|
className='flex items-center'
|
||||||
|
onFocus={() => setAddDropdownOpen(true)}
|
||||||
|
onMouseOver={() => setAddDropdownOpen(true)}
|
||||||
|
onMouseOut={() => setAddDropdownOpen(false)}
|
||||||
|
onBlur={() => setAddDropdownOpen(false)}
|
||||||
|
>
|
||||||
|
<span className='flex w-full cursor-pointer items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'>
|
||||||
추가하기
|
추가하기
|
||||||
</span>
|
</span>
|
||||||
<div className={`rounded shadow-md absolute mt-11 top-0 w-40 bg-white text-black dark:bg-very-black dark:text-gray-300 text-sm ${addDropdownOpen ? 'block' : 'hidden'}`}>
|
<div
|
||||||
|
className={`absolute top-0 mt-11 w-40 rounded bg-white text-sm text-black shadow-md dark:bg-very-black dark:text-gray-300 ${
|
||||||
|
addDropdownOpen ? 'block' : 'hidden'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<ul className='relative'>
|
<ul className='relative'>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href='/addbot'
|
href='/addbot'
|
||||||
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-t'>
|
className='block rounded-t px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
|
||||||
|
>
|
||||||
<i className='fas fa-robot' />봇 추가하기
|
<i className='fas fa-robot' />봇 추가하기
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href='/addserver'
|
href='/addserver'
|
||||||
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-b'>
|
className='block rounded-b px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
|
||||||
<i className='fas fa-users' />서버 추가하기
|
>
|
||||||
|
<i className='fas fa-users' />
|
||||||
|
서버 추가하기
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -137,61 +161,87 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='hidden grow items-center bg-white lg:flex lg:bg-transparent lg:shadow-none'>
|
<div className='hidden grow items-center bg-white lg:flex lg:bg-transparent lg:shadow-none'>
|
||||||
<ul className='flex flex-col list-none lg:flex-row lg:ml-auto'>
|
<ul className='flex list-none flex-col lg:ml-auto lg:flex-row'>
|
||||||
<li className='flex items-center outline-none' onFocus={() => setDropdownOpen(true)} onMouseOver={() => setDropdownOpen(true)} onMouseOut={() => setDropdownOpen(false)} onBlur={() => setDropdownOpen(false)}>
|
<li
|
||||||
{
|
className='flex items-center outline-none'
|
||||||
logged ?
|
onFocus={() => setDropdownOpen(true)}
|
||||||
|
onMouseOver={() => setDropdownOpen(true)}
|
||||||
|
onMouseOut={() => setDropdownOpen(false)}
|
||||||
|
onBlur={() => setDropdownOpen(false)}
|
||||||
|
>
|
||||||
|
{logged ? (
|
||||||
<>
|
<>
|
||||||
<a
|
<a className='flex w-full cursor-pointer items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'>
|
||||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100 cursor-pointer'>
|
<DiscordAvatar
|
||||||
<DiscordAvatar userID={userCache.id} className='w-8 h-8 rounded-full mr-1.5' size={128}/>
|
userID={userCache.id}
|
||||||
{userCache.username} <i className='ml-2 fas fa-sort-down' />
|
className='mr-1.5 h-8 w-8 rounded-full'
|
||||||
|
size={128}
|
||||||
|
/>
|
||||||
|
{userCache.username} <i className='fas fa-sort-down ml-2' />
|
||||||
</a>
|
</a>
|
||||||
<div className={`rounded shadow-md absolute mt-14 top-0 w-48 bg-white text-black dark:bg-very-black dark:text-gray-300 text-sm ${dropdownOpen ? 'block' : 'hidden'}`}>
|
<div
|
||||||
|
className={`absolute top-0 mt-14 w-48 rounded bg-white text-sm text-black shadow-md dark:bg-very-black dark:text-gray-300 ${
|
||||||
|
dropdownOpen ? 'block' : 'hidden'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<ul className='relative'>
|
<ul className='relative'>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href={`/users/${userCache.id}`}
|
href={`/users/${userCache.id}`}
|
||||||
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-t'>
|
className='block rounded-t px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
|
||||||
<i className='fas fa-user' />프로필
|
>
|
||||||
|
<i className='fas fa-user' />
|
||||||
|
프로필
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href='/panel'
|
href='/panel'
|
||||||
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover'>
|
className='block px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
|
||||||
<i className='fas fa-cogs' />관리패널
|
>
|
||||||
|
<i className='fas fa-cogs' />
|
||||||
|
관리패널
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{/* <li><hr className='border-t mx-2'/></li> */}
|
{/* <li><hr className='border-t mx-2'/></li> */}
|
||||||
<li>
|
<li>
|
||||||
<a onKeyPress={() => {
|
<a
|
||||||
|
onKeyPress={() => {
|
||||||
localStorage.removeItem('userCache')
|
localStorage.removeItem('userCache')
|
||||||
redirectTo(router, 'logout')
|
redirectTo(router, 'logout')
|
||||||
}
|
}}
|
||||||
} onClick={() => {
|
onClick={() => {
|
||||||
localStorage.removeItem('userCache')
|
localStorage.removeItem('userCache')
|
||||||
redirectTo(router, 'logout')
|
redirectTo(router, 'logout')
|
||||||
}} className='px-4 py-2 block text-red-500 hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-b cursor-pointer'><i className='fas fa-sign-out-alt' /> 로그아웃</a>
|
}}
|
||||||
|
className='block cursor-pointer rounded-b px-4 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
|
||||||
|
>
|
||||||
|
<i className='fas fa-sign-out-alt' /> 로그아웃
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</> :
|
</>
|
||||||
<a tabIndex={0} onClick={()=> {
|
) : (
|
||||||
|
<a
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
localStorage.redirectTo = window.location.href
|
localStorage.redirectTo = window.location.href
|
||||||
setNavbarOpen(false)
|
setNavbarOpen(false)
|
||||||
redirectTo(router, 'login')
|
redirectTo(router, 'login')
|
||||||
}} className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100 cursor-pointer outline-none'>
|
}}
|
||||||
|
className='flex w-full cursor-pointer items-center px-3 py-4 text-sm font-semibold text-gray-700 outline-none hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||||
|
>
|
||||||
로그인
|
로그인
|
||||||
</a>
|
</a>
|
||||||
}
|
)}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div
|
<div
|
||||||
className={`z-30 w-full h-full fixed bg-discord-blurple dark:bg-discord-black mt-8 sm:mt-0 lg:hidden overflow-y-scroll lg:scroll-none ${
|
className={`lg:scroll-none fixed z-30 mt-8 h-full w-full overflow-y-scroll bg-discord-blurple dark:bg-discord-black sm:mt-0 lg:hidden ${
|
||||||
navbarOpen ? 'block' : 'hidden'
|
navbarOpen ? 'block' : 'hidden'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -199,57 +249,48 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
|
|||||||
<Link
|
<Link
|
||||||
href={dev ? '/' : '/developers'}
|
href={dev ? '/' : '/developers'}
|
||||||
onClick={() => setNavbarOpen(false)}
|
onClick={() => setNavbarOpen(false)}
|
||||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||||
|
>
|
||||||
{
|
{dev ? <i className='fas fa-home' /> : <i className='fas fa-tools' />}
|
||||||
dev ? <i className='fas fa-home' /> : <i className='fas fa-tools' />
|
<span className='px-2 font-medium'>{dev ? '홈' : '개발자'}</span>
|
||||||
}
|
|
||||||
<span className='px-2 font-medium'>
|
|
||||||
{dev ? '홈' : '개발자'}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
{
|
{type !== 'bot' && (
|
||||||
type !== 'bot' && <Link
|
<Link
|
||||||
href='/bots'
|
href='/bots'
|
||||||
onClick={() => setNavbarOpen(false)}
|
onClick={() => setNavbarOpen(false)}
|
||||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||||
|
>
|
||||||
<i className='fas fa-robot' />
|
<i className='fas fa-robot' />
|
||||||
<span className='px-2 font-medium'>봇 리스트</span>
|
<span className='px-2 font-medium'>봇 리스트</span>
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
}
|
)}
|
||||||
{
|
{type !== 'server' && (
|
||||||
type !== 'server' && <Link
|
<Link
|
||||||
href='/servers'
|
href='/servers'
|
||||||
onClick={() => setNavbarOpen(false)}
|
onClick={() => setNavbarOpen(false)}
|
||||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||||
|
>
|
||||||
<i className='fas fa-users' />
|
<i className='fas fa-users' />
|
||||||
<span className='px-2 font-medium'>서버 리스트</span>
|
<span className='px-2 font-medium'>서버 리스트</span>
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
}
|
)}
|
||||||
<Link
|
<Link
|
||||||
href='/discord'
|
href='/discord'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
onClick={() => setNavbarOpen(false)}
|
onClick={() => setNavbarOpen(false)}
|
||||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||||
|
>
|
||||||
<i className='fab fa-discord' />
|
<i className='fab fa-discord' />
|
||||||
<span className='px-2 font-medium'>디스코드 서버</span>
|
<span className='px-2 font-medium'>디스코드 서버</span>
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href='/about'
|
href='/about'
|
||||||
onClick={() => setNavbarOpen(false)}
|
onClick={() => setNavbarOpen(false)}
|
||||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||||
|
>
|
||||||
<i className='fas fa-layer-group' />
|
<i className='fas fa-layer-group' />
|
||||||
<span className='px-2 font-medium'>소개</span>
|
<span className='px-2 font-medium'>소개</span>
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -260,69 +301,74 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
|
|||||||
<i className='fas fa-plus' />
|
<i className='fas fa-plus' />
|
||||||
<span className='px-2 font-medium'>추가하기</span>
|
<span className='px-2 font-medium'>추가하기</span>
|
||||||
</a>
|
</a>
|
||||||
<div className={mobileAddDropdownOpen ? 'px-4 flex flex-col' : 'px-4 hidden'}>
|
<div className={mobileAddDropdownOpen ? 'flex flex-col px-4' : 'hidden px-4'}>
|
||||||
<Link
|
<Link
|
||||||
href='/addbot'
|
href='/addbot'
|
||||||
onClick={() => setNavbarOpen(false)}
|
onClick={() => setNavbarOpen(false)}
|
||||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||||
|
>
|
||||||
<i className='fas fa-robot' />
|
<i className='fas fa-robot' />
|
||||||
<span className='px-2 font-medium'>봇 추가하기</span>
|
<span className='px-2 font-medium'>봇 추가하기</span>
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href='/addserver'
|
href='/addserver'
|
||||||
onClick={() => setNavbarOpen(false)}
|
onClick={() => setNavbarOpen(false)}
|
||||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||||
|
>
|
||||||
<i className='fas fa-users' />
|
<i className='fas fa-users' />
|
||||||
<span className='px-2 font-medium'>서버 추가하기</span>
|
<span className='px-2 font-medium'>서버 추가하기</span>
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className='my-10'>
|
<div className='my-10'>
|
||||||
{
|
{logged ? (
|
||||||
logged ? <>
|
<>
|
||||||
<Link
|
<Link
|
||||||
href={`/users/${userCache.id}`}
|
href={`/users/${userCache.id}`}
|
||||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||||
onClick={() => setNavbarOpen(!navbarOpen)}>
|
onClick={() => setNavbarOpen(!navbarOpen)}
|
||||||
|
>
|
||||||
<i className='far fa-user' />
|
<i className='far fa-user' />
|
||||||
<span className='px-2 font-medium'>{userCache.username}</span>
|
<span className='px-2 font-medium'>{userCache.username}</span>
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href='/panel'
|
href='/panel'
|
||||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||||
onClick={() => setNavbarOpen(!navbarOpen)}>
|
onClick={() => setNavbarOpen(!navbarOpen)}
|
||||||
|
>
|
||||||
<i className='fas fa-cogs' />
|
<i className='fas fa-cogs' />
|
||||||
<span className='px-2 font-medium'>관리패널</span>
|
<span className='px-2 font-medium'>관리패널</span>
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
<a onClick={()=> {
|
<a
|
||||||
|
onClick={() => {
|
||||||
setNavbarOpen(!navbarOpen)
|
setNavbarOpen(!navbarOpen)
|
||||||
localStorage.removeItem('userCache')
|
localStorage.removeItem('userCache')
|
||||||
redirectTo(router, 'logout')
|
redirectTo(router, 'logout')
|
||||||
}} className='flex items-center px-8 py-2 text-red-500 hover:text-red-400'>
|
}}
|
||||||
|
className='flex items-center px-8 py-2 text-red-500 hover:text-red-400'
|
||||||
|
>
|
||||||
<i className='fas fa-sign-out-alt' />
|
<i className='fas fa-sign-out-alt' />
|
||||||
<span className='px-2 font-medium'>로그아웃</span>
|
<span className='px-2 font-medium'>로그아웃</span>
|
||||||
</a>
|
</a>
|
||||||
</> : <a onClick={() => {
|
</>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
localStorage.redirectTo = window.location.href
|
localStorage.redirectTo = window.location.href
|
||||||
setNavbarOpen(false)
|
setNavbarOpen(false)
|
||||||
redirectTo(router, 'login')
|
redirectTo(router, 'login')
|
||||||
}} className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
}}
|
||||||
|
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||||
|
>
|
||||||
<i className='far fa-user' />
|
<i className='far fa-user' />
|
||||||
<span className='px-2 font-medium'>로그인</span>
|
<span className='px-2 font-medium'>로그인</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const Notice: React.FC<NoticeProps> = ({ header, desc }) => {
|
const Notice: React.FC<NoticeProps> = ({ header, desc }) => {
|
||||||
return (
|
return (
|
||||||
<div className='mx-auto my-auto px-10 py-48 h-screen text-center'>
|
<div className='mx-auto my-auto h-screen px-10 py-48 text-center'>
|
||||||
<h1 className='text-4xl font-bold'>KOREANBOTS</h1>
|
<h1 className='text-4xl font-bold'>KOREANBOTS</h1>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -3,19 +3,21 @@ import DiscordAvatar from '@components/DiscordAvatar'
|
|||||||
|
|
||||||
const Owner: React.FC<OwnerProps> = ({ id, globalName, username, tag, crown = false }) => {
|
const Owner: React.FC<OwnerProps> = ({ id, globalName, username, tag, crown = false }) => {
|
||||||
return (
|
return (
|
||||||
(<Link
|
<Link
|
||||||
href={`/users/${id}`}
|
href={`/users/${id}`}
|
||||||
className='dark:hover:bg-discord-dark-hover flex mb-1 px-4 py-4 text-black dark:text-gray-400 text-base dark:bg-discord-black bg-little-white hover:bg-little-white-hover rounded cursor-pointer'>
|
className='mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-base text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover'
|
||||||
|
>
|
||||||
<div className='relative shrink-0 mr-3 mt-1 w-8 h-8 rounded-full shadow-inner overflow-hidden'>
|
<div className='relative mr-3 mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full shadow-inner'>
|
||||||
<DiscordAvatar userID={id} className='z-negative absolute inset-0 w-full h-full' />
|
<DiscordAvatar userID={id} className='z-negative absolute inset-0 h-full w-full' />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-1 w-0 leading-snug'>
|
<div className='w-0 flex-1 leading-snug'>
|
||||||
<h4 className='whitespace-nowrap truncate'>{ crown && <i className='fas fa-crown text-amber-300 text-xs' /> }{tag === '0' ? globalName : username}</h4>
|
<h4 className='truncate whitespace-nowrap'>
|
||||||
<span className='text-gray-600 text-sm'>{tag === '0' ? '@' + username : '#' + tag}</span>
|
{crown && <i className='fas fa-crown text-xs text-amber-300' />}
|
||||||
|
{tag === '0' ? globalName : username}
|
||||||
|
</h4>
|
||||||
|
<span className='text-sm text-gray-600'>{tag === '0' ? '@' + username : '#' + tag}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
</Link>)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname, searchParams }) => {
|
const Paginator: React.FC<PaginatorProps> = ({
|
||||||
|
currentPage,
|
||||||
|
totalPage,
|
||||||
|
pathname,
|
||||||
|
searchParams,
|
||||||
|
}) => {
|
||||||
let pages = []
|
let pages = []
|
||||||
if (currentPage < 4)
|
if (currentPage < 4)
|
||||||
pages = [
|
pages = [
|
||||||
@ -25,7 +30,7 @@ const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname,
|
|||||||
currentPage + 1 > totalPage ? null : currentPage + 1,
|
currentPage + 1 > totalPage ? null : currentPage + 1,
|
||||||
currentPage + 2 > totalPage ? null : currentPage + 2,
|
currentPage + 2 > totalPage ? null : currentPage + 2,
|
||||||
]
|
]
|
||||||
pages = pages.filter(el => el)
|
pages = pages.filter((el) => el)
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center justify-center py-4 text-center'>
|
<div className='flex flex-col items-center justify-center py-4 text-center'>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
@ -33,16 +38,15 @@ const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname,
|
|||||||
href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage - 1 } }}
|
href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage - 1 } }}
|
||||||
className={`${
|
className={`${
|
||||||
currentPage === 1 ? 'invisible' : ''
|
currentPage === 1 ? 'invisible' : ''
|
||||||
} h-12 w-12 mr-1 flex justify-center items-center rounded-full transition duration-150 ease-in bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover cursor-pointer text-center`}>
|
} mr-1 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-gray-200 text-center transition duration-150 ease-in hover:bg-gray-300 dark:bg-discord-black dark:hover:bg-discord-dark-hover`}
|
||||||
|
>
|
||||||
<i className='fas fa-chevron-left'></i>
|
<i className='fas fa-chevron-left'></i>
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
{pages.map((el, i) => (
|
{pages.map((el, i) => (
|
||||||
(<Link
|
<Link
|
||||||
key={i}
|
key={i}
|
||||||
href={{ pathname, hash: 'list', query: { ...searchParams, page: el } }}
|
href={{ pathname, hash: 'list', query: { ...searchParams, page: el } }}
|
||||||
className={`w-12 flex justify-center items-center cursor-pointer leading-5 transition duration-150 ease-in ${
|
className={`flex w-12 cursor-pointer items-center justify-center leading-5 transition duration-150 ease-in ${
|
||||||
i === 0 && i === pages.length - 1
|
i === 0 && i === pages.length - 1
|
||||||
? 'rounded-full'
|
? 'rounded-full'
|
||||||
: i === 0
|
: i === 0
|
||||||
@ -53,21 +57,19 @@ const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname,
|
|||||||
} ${
|
} ${
|
||||||
currentPage === el
|
currentPage === el
|
||||||
? 'bg-gray-300 dark:bg-discord-dark-hover'
|
? 'bg-gray-300 dark:bg-discord-dark-hover'
|
||||||
: 'bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover'
|
: 'bg-gray-200 hover:bg-gray-300 dark:bg-discord-black dark:hover:bg-discord-dark-hover'
|
||||||
}`}>
|
}`}
|
||||||
|
>
|
||||||
{el}
|
{el}
|
||||||
|
</Link>
|
||||||
</Link>)
|
|
||||||
))}
|
))}
|
||||||
<Link
|
<Link
|
||||||
href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage + 1 } }}
|
href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage + 1 } }}
|
||||||
className={`${
|
className={`${
|
||||||
currentPage === totalPage ? 'invisible' : ''
|
currentPage === totalPage ? 'invisible' : ''
|
||||||
} h-12 w-12 ml-1 flex justify-center items-center rounded-full transition duration-150 ease-in bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover cursor-pointer text-center`}>
|
} ml-1 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-gray-200 text-center transition duration-150 ease-in hover:bg-gray-300 dark:bg-discord-black dark:hover:bg-discord-dark-hover`}
|
||||||
|
>
|
||||||
<i className='fas fa-chevron-right'></i>
|
<i className='fas fa-chevron-right'></i>
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
const PlatformDisplay: React.FC<PlatformDisplayProps> = ({ osx, children }:PlatformDisplayProps) => {
|
const PlatformDisplay: React.FC<PlatformDisplayProps> = ({
|
||||||
|
osx,
|
||||||
|
children,
|
||||||
|
}: PlatformDisplayProps) => {
|
||||||
const isOSX = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)
|
const isOSX = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)
|
||||||
return <>{isOSX ? osx ?? children : children}</>
|
return <>{isOSX ? osx ?? children : children}</>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,14 +12,16 @@ const Redirect: React.FC<RedirectProps> = ({ to, text=true, children }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
redirectTo(router, to)
|
redirectTo(router, to)
|
||||||
})
|
})
|
||||||
if(children) return <>
|
if (children) return <>{children}</>
|
||||||
{children}
|
return (
|
||||||
</>
|
<Container paddingTop>
|
||||||
return <Container paddingTop>
|
|
||||||
<div>
|
<div>
|
||||||
<a href={to} className='text-blue-400'>{text && '자동으로 리다이렉트되지 않는다면 클릭하세요.'}</a>
|
<a href={to} className='text-blue-400'>
|
||||||
|
{text && '자동으로 리다이렉트되지 않는다면 클릭하세요.'}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RedirectProps {
|
interface RedirectProps {
|
||||||
|
|||||||
@ -5,64 +5,113 @@ import { FormikErrors, FormikTouched } from 'formik'
|
|||||||
const Button = dynamic(() => import('@components/Button'))
|
const Button = dynamic(() => import('@components/Button'))
|
||||||
const TextArea = dynamic(() => import('@components/Form/TextArea'))
|
const TextArea = dynamic(() => import('@components/Form/TextArea'))
|
||||||
|
|
||||||
export const Check: FC<{ checked: boolean, text: string }> = ({ checked, text }) => <>
|
export const Check: FC<{ checked: boolean; text: string }> = ({ checked, text }) => (
|
||||||
{checked && <i className='text-emerald-400 fas fa-check-circle mr-1' />}
|
<>
|
||||||
|
{checked && <i className='fas fa-check-circle mr-1 text-emerald-400' />}
|
||||||
{text}
|
{text}
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export const SubmitButton: FC = () => <div className='text-right'>
|
export const SubmitButton: FC = () => (
|
||||||
|
<div className='text-right'>
|
||||||
<Button type='submit'>제출</Button>
|
<Button type='submit'>제출</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
export const TextField: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => <>
|
export const TextField: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => (
|
||||||
<TextArea name='description' placeholder='최대한 자세하게 설명해주세요!' value={values.description} setValue={(value) => setFieldValue('description', value)} />
|
<>
|
||||||
<div className='mt-1 text-red-500 text-xs font-light'>{errors.description && touched.description ? errors.description : null}</div>
|
<TextArea
|
||||||
|
name='description'
|
||||||
|
placeholder='최대한 자세하게 설명해주세요!'
|
||||||
|
value={values.description}
|
||||||
|
setValue={(value) => setFieldValue('description', value)}
|
||||||
|
/>
|
||||||
|
<div className='mt-1 text-xs font-light text-red-500'>
|
||||||
|
{errors.description && touched.description ? errors.description : null}
|
||||||
|
</div>
|
||||||
<SubmitButton />
|
<SubmitButton />
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export const DMCA: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => {
|
export const DMCA: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => {
|
||||||
const [isOwner, setOwner] = useState(null)
|
const [isOwner, setOwner] = useState(null)
|
||||||
const [contacted, setContacted] = useState(null)
|
const [contacted, setContacted] = useState(null)
|
||||||
return <div>
|
return (
|
||||||
<h3 className='font-bold my-2'>권리자와는 어떤 관계인가요?</h3>
|
<div>
|
||||||
|
<h3 className='my-2 font-bold'>권리자와는 어떤 관계인가요?</h3>
|
||||||
<Button onClick={() => setOwner(true)}>
|
<Button onClick={() => setOwner(true)}>
|
||||||
<Check checked={isOwner} text='권리자 본인 혹은 대리인입니다.' />
|
<Check checked={isOwner} text='권리자 본인 혹은 대리인입니다.' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setOwner(false)}>
|
<Button onClick={() => setOwner(false)}>
|
||||||
<Check checked={isOwner === false} text='권리자가 아닙니다.' />
|
<Check checked={isOwner === false} text='권리자가 아닙니다.' />
|
||||||
</Button>
|
</Button>
|
||||||
{
|
{isOwner === true ? (
|
||||||
isOwner === true ? <>
|
<>
|
||||||
<h3 className='font-bold my-2'>권리 침해자에게 연락하여 라이선스 위반사항을 고지하셨나요?</h3>
|
<h3 className='my-2 font-bold'>
|
||||||
|
권리 침해자에게 연락하여 라이선스 위반사항을 고지하셨나요?
|
||||||
|
</h3>
|
||||||
<Button onClick={() => setContacted(true)}>
|
<Button onClick={() => setContacted(true)}>
|
||||||
<Check checked={contacted} text='최대한 연락을 시도하였지만 개선되지 않았습니다.' />
|
<Check checked={contacted} text='최대한 연락을 시도하였지만 개선되지 않았습니다.' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setContacted(false)}>
|
<Button onClick={() => setContacted(false)}>
|
||||||
<Check checked={contacted === false} text='아니요, 아직 연락하지 않았습니다.' />
|
<Check checked={contacted === false} text='아니요, 아직 연락하지 않았습니다.' />
|
||||||
</Button>
|
</Button>
|
||||||
{
|
{contacted ? (
|
||||||
contacted ? <div>
|
<div>
|
||||||
<h3 className='font-bold mt-2'>설명</h3>
|
<h3 className='mt-2 font-bold'>설명</h3>
|
||||||
<p className='text-gray-400 text-sm mb-1'>반드시 아래 항목들을 포함해야합니다.</p>
|
<p className='mb-1 text-sm text-gray-400'>반드시 아래 항목들을 포함해야합니다.</p>
|
||||||
<ul className='text-gray-400 text-sm mb-1 list-disc list-inside'>
|
<ul className='mb-1 list-inside list-disc text-sm text-gray-400'>
|
||||||
<li>권리자 본인임을 증명 (단체 소속인 경우 어떤 자격으로 단체를 대표하여 신고하는지 설명)</li>
|
<li>
|
||||||
|
권리자 본인임을 증명 (단체 소속인 경우 어떤 자격으로 단체를 대표하여 신고하는지
|
||||||
|
설명)
|
||||||
|
</li>
|
||||||
<li>본인의 권리를 입증 (원본 컨텐츠의 주소, 라이선스 등을 포함)</li>
|
<li>본인의 권리를 입증 (원본 컨텐츠의 주소, 라이선스 등을 포함)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className='text-gray-400 text-sm mb-1'>컨텐츠를 추가로 첨부해야하는 경우 <a className='text-blue-400' target='_blank' rel='noreferrer' href={`mailto:dmca@koreanbots.dev?subject=${encodeURI('[DMCA] 추가 컨텐츠')}&body=${encodeURI('디스코드 태그:')}`}>dmca@koreanbots.dev</a>의 이메일로 첨부해주시고, 해당 이메일로 첨부했음을 아래 설명에 기재해주세요.</p>
|
<p className='mb-1 text-sm text-gray-400'>
|
||||||
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />
|
컨텐츠를 추가로 첨부해야하는 경우{' '}
|
||||||
|
<a
|
||||||
|
className='text-blue-400'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
href={`mailto:dmca@koreanbots.dev?subject=${encodeURI(
|
||||||
|
'[DMCA] 추가 컨텐츠'
|
||||||
|
)}&body=${encodeURI('디스코드 태그:')}`}
|
||||||
|
>
|
||||||
|
dmca@koreanbots.dev
|
||||||
|
</a>
|
||||||
|
의 이메일로 첨부해주시고, 해당 이메일로 첨부했음을 아래 설명에 기재해주세요.
|
||||||
|
</p>
|
||||||
|
<TextField
|
||||||
|
values={values}
|
||||||
|
errors={errors}
|
||||||
|
touched={touched}
|
||||||
|
setFieldValue={setFieldValue}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
: contacted === false ? <>
|
) : contacted === false ? (
|
||||||
<h2 className='font-bold mt-4 text-xl'>먼저 권리 침해자에게 연락을 시도해주세요.</h2>
|
<>
|
||||||
<p>본인의 권리를 침해하신 분께 먼저 연락을 시도하셔서 위반사항을 고지하시고, 연락이 불가하다면 신고 기능을 이용해주세요.</p>
|
<h2 className='mt-4 text-xl font-bold'>먼저 권리 침해자에게 연락을 시도해주세요.</h2>
|
||||||
</> : ''
|
<p>
|
||||||
}
|
본인의 권리를 침해하신 분께 먼저 연락을 시도하셔서 위반사항을 고지하시고, 연락이
|
||||||
|
불가하다면 신고 기능을 이용해주세요.
|
||||||
|
</p>
|
||||||
</>
|
</>
|
||||||
: isOwner === false ? <>
|
) : (
|
||||||
<h2 className='font-bold mt-4 text-xl'>아쉽지만, 권리자 본인 혹은 대리인만 신고하실 수 있습니다.</h2>
|
''
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : isOwner === false ? (
|
||||||
|
<>
|
||||||
|
<h2 className='mt-4 text-xl font-bold'>
|
||||||
|
아쉽지만, 권리자 본인 혹은 대리인만 신고하실 수 있습니다.
|
||||||
|
</h2>
|
||||||
<p>권리자 분께 말씀드려, 권리자 본인이 직접 신고하시도록 해주세요!</p>
|
<p>권리자 분께 말씀드려, 권리자 본인이 직접 신고하시도록 해주세요!</p>
|
||||||
</> : ''
|
</>
|
||||||
}
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReportValues {
|
interface ReportValues {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
const ResponsiveGrid: React.FC<React.PropsWithChildren> = ({ children }) => {
|
const ResponsiveGrid: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||||
return <div className='grid gap-x-4 grid-rows-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mt-10 -mb-10'>
|
return (
|
||||||
|
<div className='-mb-10 mt-10 grid grid-rows-1 gap-x-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResponsiveGrid
|
export default ResponsiveGrid
|
||||||
@ -58,19 +58,25 @@ const Search: React.FC = () => {
|
|||||||
if (!localStorage.recentSearch) localStorage.recentSearch = '[]'
|
if (!localStorage.recentSearch) localStorage.recentSearch = '[]'
|
||||||
try {
|
try {
|
||||||
const d = JSON.parse(localStorage.recentSearch).reverse()
|
const d = JSON.parse(localStorage.recentSearch).reverse()
|
||||||
if(d.findIndex(n => n.value === query) !== -1) d.splice(d.findIndex(n => n.value === query), 1)
|
if (d.findIndex((n) => n.value === query) !== -1)
|
||||||
|
d.splice(
|
||||||
|
d.findIndex((n) => n.value === query),
|
||||||
|
1
|
||||||
|
)
|
||||||
d.push({
|
d.push({
|
||||||
value: query,
|
value: query,
|
||||||
date: Date.now()
|
date: Date.now(),
|
||||||
})
|
})
|
||||||
d.reverse()
|
d.reverse()
|
||||||
setRecentSearch(d.slice(0, 10))
|
setRecentSearch(d.slice(0, 10))
|
||||||
localStorage.recentSearch = JSON.stringify(d.slice(0, 10))
|
localStorage.recentSearch = JSON.stringify(d.slice(0, 10))
|
||||||
} catch {
|
} catch {
|
||||||
setRecentSearch([{
|
setRecentSearch([
|
||||||
|
{
|
||||||
value: query,
|
value: query,
|
||||||
date: Date.now()
|
date: Date.now(),
|
||||||
}])
|
},
|
||||||
|
])
|
||||||
localStorage.recentSearch = JSON.stringify(recentSearch)
|
localStorage.recentSearch = JSON.stringify(recentSearch)
|
||||||
} finally {
|
} finally {
|
||||||
redirectTo(router, `/search/?q=${encodeURIComponent(query)}`)
|
redirectTo(router, `/search/?q=${encodeURIComponent(query)}`)
|
||||||
@ -79,19 +85,17 @@ const Search: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div onFocus={() => setHidden(false)} ref={ref}>
|
<div onFocus={() => setHidden(false)} ref={ref}>
|
||||||
<div
|
<div className='relative z-10 mt-5 flex w-full rounded-lg bg-white text-black dark:bg-very-black dark:text-gray-100'>
|
||||||
className='relative z-10 flex mt-5 w-full text-black dark:text-gray-100 dark:bg-very-black bg-white rounded-lg'
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type='search'
|
type='search'
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
className='grow pr-20 px-7 py-3 h-16 text-xl bg-transparent border-0 border-none outline-none shadow'
|
className='h-16 grow border-0 border-none bg-transparent px-7 py-3 pr-20 text-xl shadow outline-none'
|
||||||
placeholder='검색...'
|
placeholder='검색...'
|
||||||
value={query}
|
value={query}
|
||||||
onChange={e => {
|
onChange={(e) => {
|
||||||
SearchResults(e.target.value)
|
SearchResults(e.target.value)
|
||||||
}}
|
}}
|
||||||
onKeyDown={e => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
onSubmit()
|
onSubmit()
|
||||||
}
|
}
|
||||||
@ -101,61 +105,64 @@ const Search: React.FC = () => {
|
|||||||
className='cusor-pointer absolute right-0 top-0 mr-5 mt-5 outline-none'
|
className='cusor-pointer absolute right-0 top-0 mr-5 mt-5 outline-none'
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
>
|
>
|
||||||
<i className='fas fa-search text-gray-600 hover:text-gray-700 text-2xl' />
|
<i className='fas fa-search text-2xl text-gray-600 hover:text-gray-700' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={`relative ${hidden ? 'hidden' : 'block'} z-50`}>
|
<div className={`relative ${hidden ? 'hidden' : 'block'} z-50`}>
|
||||||
<div className='pin-t pin-l absolute my-2 w-full h-60 text-black dark:text-gray-100 dark:bg-very-black bg-white rounded shadow-md overflow-y-scroll md:h-80'>
|
<div className='pin-t pin-l absolute my-2 h-60 w-full overflow-y-scroll rounded bg-white text-black shadow-md dark:bg-very-black dark:text-gray-100 md:h-80'>
|
||||||
<ul>
|
<ul>
|
||||||
{(data && data.code === 200) ? (
|
{data && data.code === 200 ? (
|
||||||
<div className='grid lg:grid-cols-2'>
|
<div className='grid lg:grid-cols-2'>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='px-3 py-3.5 font-bold'>봇</li>
|
<li className='px-3 py-3.5 font-bold'>봇</li>
|
||||||
{
|
{data.data.bots.length === 0 ? (
|
||||||
data.data.bots.length === 0 ?
|
<li className='px-3 py-3.5'>검색 결과가 없습니다.</li>
|
||||||
<li className='px-3 py-3.5'>검색 결과가 없습니다.</li> :
|
) : (
|
||||||
data.data.bots.map(el => (
|
data.data.bots.map((el) => (
|
||||||
<Link key={el.id} href={makeBotURL(el)} legacyBehavior>
|
<Link key={el.id} href={makeBotURL(el)} legacyBehavior>
|
||||||
<li className='h-15 flex px-3 py-2 cursor-pointer'>
|
<li className='h-15 flex cursor-pointer px-3 py-2'>
|
||||||
<DiscordAvatar className='mt-1 w-12 h-12' size={128} userID={el.id} />
|
<DiscordAvatar className='mt-1 h-12 w-12' size={128} userID={el.id} />
|
||||||
<div className='ml-2'>
|
<div className='ml-2'>
|
||||||
<h1 className='text-black dark:text-gray-100 text-lg'>{el.name}</h1>
|
<h1 className='text-lg text-black dark:text-gray-100'>{el.name}</h1>
|
||||||
<p className='text-gray-400 text-sm'>{el.intro}</p>
|
<p className='text-sm text-gray-400'>{el.intro}</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</Link>
|
</Link>
|
||||||
))
|
))
|
||||||
}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='px-3 py-3.5 font-bold'>서버</li>
|
<li className='px-3 py-3.5 font-bold'>서버</li>
|
||||||
{
|
{data.data.servers.length === 0 ? (
|
||||||
data.data.servers.length === 0 ?
|
<li className='px-3 py-3.5'>검색 결과가 없습니다.</li>
|
||||||
<li className='px-3 py-3.5'>검색 결과가 없습니다.</li> :
|
) : (
|
||||||
data.data.servers.map(el => (
|
data.data.servers.map((el) => (
|
||||||
<Link key={el.id} href={makeServerURL(el)} legacyBehavior>
|
<Link key={el.id} href={makeServerURL(el)} legacyBehavior>
|
||||||
<li className='h-15 flex px-3 py-2 cursor-pointer'>
|
<li className='h-15 flex cursor-pointer px-3 py-2'>
|
||||||
<ServerIcon className='mt-1 w-12 h-12' size={128} id={el.id} />
|
<ServerIcon className='mt-1 h-12 w-12' size={128} id={el.id} />
|
||||||
<div className='ml-2'>
|
<div className='ml-2'>
|
||||||
<h1 className='text-black dark:text-gray-100 text-lg'>{el.name}</h1>
|
<h1 className='text-lg text-black dark:text-gray-100'>{el.name}</h1>
|
||||||
<p className='text-gray-400 text-sm'>{el.intro}</p>
|
<p className='text-sm text-gray-400'>{el.intro}</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</Link>
|
</Link>
|
||||||
))
|
))
|
||||||
}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
) : loading ? <ul>
|
) : loading ? (
|
||||||
|
<ul>
|
||||||
<li className='px-3 py-3.5'>검색중입니다...</li>
|
<li className='px-3 py-3.5'>검색중입니다...</li>
|
||||||
</ul> : <ul>
|
</ul>
|
||||||
|
) : (
|
||||||
|
<ul>
|
||||||
{query && data ? (
|
{query && data ? (
|
||||||
data.message?.includes('문법') ? (
|
data.message?.includes('문법') ? (
|
||||||
<li className='px-3 py-3.5'>
|
<li className='px-3 py-3.5'>
|
||||||
검색 문법이 잘못되었습니다.
|
검색 문법이 잘못되었습니다.
|
||||||
<br />
|
<br />
|
||||||
<a
|
<a
|
||||||
className='hover:text-blue-400 text-blue-500'
|
className='text-blue-500 hover:text-blue-400'
|
||||||
href='https://docs.koreanbots.dev/bots/usage/search'
|
href='https://docs.koreanbots.dev/bots/usage/search'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
@ -163,41 +170,51 @@ const Search: React.FC = () => {
|
|||||||
더 알아보기
|
더 알아보기
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
) : <li className='px-3 py-3.5'>{(data.errors && data.errors[0]) || data.message || '검색중입니다...'}</li>
|
) : (
|
||||||
) : query.length === 0 ? !recentSearch || !Array.isArray(recentSearch) || recentSearch.length === 0? <li className='px-3 py-3.5'>최근 검색 기록이 없습니다.</li>
|
<li className='px-3 py-3.5'>
|
||||||
: <>
|
{(data.errors && data.errors[0]) || data.message || '검색중입니다...'}
|
||||||
<li className='h-15 px-3 py-2 cursor-pointer font-semibold'>
|
</li>
|
||||||
|
)
|
||||||
|
) : query.length === 0 ? (
|
||||||
|
!recentSearch || !Array.isArray(recentSearch) || recentSearch.length === 0 ? (
|
||||||
|
<li className='px-3 py-3.5'>최근 검색 기록이 없습니다.</li>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<li className='h-15 cursor-pointer px-3 py-2 font-semibold'>
|
||||||
최근 검색어
|
최근 검색어
|
||||||
<button className='absolute right-0 pr-10 text-sm text-red-500 hover:opacity-90' onClick={() => {
|
<button
|
||||||
|
className='absolute right-0 pr-10 text-sm text-red-500 hover:opacity-90'
|
||||||
|
onClick={() => {
|
||||||
setRecentSearch([])
|
setRecentSearch([])
|
||||||
localStorage.recentSearch = '[]'
|
localStorage.recentSearch = '[]'
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
전체 삭제
|
전체 삭제
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{
|
{recentSearch.slice(0, 10).map((el, n) => (
|
||||||
recentSearch.slice(0, 10).map((el, n) => (
|
|
||||||
<Link
|
<Link
|
||||||
key={n}
|
key={n}
|
||||||
href={`/search?q=${encodeURIComponent(el?.value)}`}
|
href={`/search?q=${encodeURIComponent(el?.value)}`}
|
||||||
legacyBehavior>
|
legacyBehavior
|
||||||
<li className='h-15 px-3 py-2 cursor-pointer'>
|
>
|
||||||
|
<li className='h-15 cursor-pointer px-3 py-2'>
|
||||||
<i className='fas fa-history' /> {el?.value}
|
<i className='fas fa-history' /> {el?.value}
|
||||||
<span className='absolute right-0 pr-10 text-gray-400 text-sm'>
|
<span className='absolute right-0 pr-10 text-sm text-gray-400'>
|
||||||
{Day(el?.date).format('MM.DD.')}
|
{Day(el?.date).format('MM.DD.')}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</Link>
|
</Link>
|
||||||
))
|
))}
|
||||||
}
|
</>
|
||||||
</> :
|
)
|
||||||
query.length < 3 ? (
|
) : query.length < 3 ? (
|
||||||
'최소 2글자 이상 입력해주세요.'
|
'최소 2글자 이상 입력해주세요.'
|
||||||
) : (
|
) : (
|
||||||
'검색어를 입력해주세요.'
|
'검색어를 입력해주세요.'
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -208,6 +225,6 @@ const Search: React.FC = () => {
|
|||||||
export default Search
|
export default Search
|
||||||
|
|
||||||
interface ListAll {
|
interface ListAll {
|
||||||
bots: Bot[],
|
bots: Bot[]
|
||||||
servers: Server[]
|
servers: Server[]
|
||||||
}
|
}
|
||||||
@ -3,7 +3,7 @@ import { ReactNode } from 'react'
|
|||||||
const Segment: React.FC<SegmentProps> = ({ children, className = '' }) => {
|
const Segment: React.FC<SegmentProps> = ({ children, className = '' }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`py-3 px-7 text-black dark:text-white dark:bg-discord-black bg-little-white rounded ${className}`}
|
className={`rounded bg-little-white px-7 py-3 text-black dark:bg-discord-black dark:text-white ${className}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,14 +10,22 @@ const Tag = dynamic(() => import('@components/Tag'))
|
|||||||
const ServerIcon = dynamic(() => import('@components/ServerIcon'))
|
const ServerIcon = dynamic(() => import('@components/ServerIcon'))
|
||||||
|
|
||||||
const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
|
const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
|
||||||
const newServerLink = server.data ? `/addserver/${server.id}` : `${DiscordEnpoints.InviteApplication(DSKR_BOT_ID, {}, 'bot', null, server.id)}&disable_guild_select=true`
|
const newServerLink = server.data
|
||||||
|
? `/addserver/${server.id}`
|
||||||
|
: `${DiscordEnpoints.InviteApplication(
|
||||||
|
DSKR_BOT_ID,
|
||||||
|
{},
|
||||||
|
'bot',
|
||||||
|
null,
|
||||||
|
server.id
|
||||||
|
)}&disable_guild_select=true`
|
||||||
return (
|
return (
|
||||||
<div className='min-w-0 container mb-16 transform hover:-translate-y-1 transition duration-100 ease-in cursor-pointer'>
|
<div className='container mb-16 min-w-0 transform cursor-pointer transition duration-100 ease-in hover:-translate-y-1'>
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<div className='container mx-auto'>
|
<div className='container mx-auto'>
|
||||||
<div className='h-full'>
|
<div className='h-full'>
|
||||||
<div
|
<div
|
||||||
className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl'
|
className='relative mx-auto h-full rounded-2xl bg-little-white text-black shadow-xl dark:bg-discord-black dark:text-white'
|
||||||
style={
|
style={
|
||||||
checkServerFlag(server.flags, 'trusted') && server.banner
|
checkServerFlag(server.flags, 'trusted') && server.banner
|
||||||
? {
|
? {
|
||||||
@ -27,26 +35,25 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Link
|
<Link href={type !== 'add' ? makeServerURL(server) : newServerLink} legacyBehavior>
|
||||||
href={type !== 'add' ? makeServerURL(server) : newServerLink}
|
|
||||||
legacyBehavior>
|
|
||||||
<div>
|
<div>
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<div className='w-3/5 flex justify-start'>
|
<div className='flex w-3/5 justify-start'>
|
||||||
<ServerIcon
|
<ServerIcon
|
||||||
size={128}
|
size={128}
|
||||||
id={server.id}
|
id={server.id}
|
||||||
hash={type === 'add' && server.icon}
|
hash={type === 'add' && server.icon}
|
||||||
alt='Icon'
|
alt='Icon'
|
||||||
className='absolute -left-2 -top-8 mx-auto w-32 h-32 bg-white rounded-full'
|
className='absolute -left-2 -top-8 mx-auto h-32 w-32 rounded-full bg-white'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid grid-cols-1 pr-5 pt-5 w-2/5'>
|
<div className='grid w-2/5 grid-cols-1 pr-5 pt-5'>
|
||||||
<Tag
|
<Tag
|
||||||
text={
|
text={
|
||||||
<>
|
<>
|
||||||
<i className='fas fa-heart text-red-600' /> {formatNumber(server.votes)}
|
<i className='fas fa-heart text-red-600' />{' '}
|
||||||
|
{formatNumber(server.votes)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
dark
|
dark
|
||||||
@ -58,26 +65,38 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 px-4 h-16'>
|
<div className='mt-3 h-16 px-4'>
|
||||||
<h2 className={`px-1 text-sm ${server.state !== 'unreachable' ? ' invisible' : ''}`}>
|
<h2
|
||||||
<i className='fas fa-ban text-red-600' />정보 갱신 불가
|
className={`px-1 text-sm ${
|
||||||
|
server.state !== 'unreachable' ? ' invisible' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<i className='fas fa-ban text-red-600' />
|
||||||
|
정보 갱신 불가
|
||||||
</h2>
|
</h2>
|
||||||
<h1 className='mb-3 text-left text-xl sm:text-2xl font-bold truncate'>{server.name}</h1>
|
<h1 className='mb-3 truncate text-left text-xl font-bold sm:text-2xl'>
|
||||||
|
{server.name}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className='mb-10 px-4 h-6 text-left text-gray-400 text-sm font'>
|
<p className='font mb-10 h-6 px-4 text-left text-sm text-gray-400'>
|
||||||
{type === 'add' ?
|
{type === 'add'
|
||||||
server.data ? '지금 바로 서버를 등록할 수 있습니다.' : '봇을 초대해야 서버를 등록할 수 있습니다.'
|
? server.data
|
||||||
: server.intro
|
? '지금 바로 서버를 등록할 수 있습니다.'
|
||||||
}
|
: '봇을 초대해야 서버를 등록할 수 있습니다.'
|
||||||
|
: server.intro}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<div className='category flex flex-wrap px-2'>
|
<div className='category flex flex-wrap px-2'>
|
||||||
{server.category?.slice(0, 3).map(el => (
|
{server.category
|
||||||
|
?.slice(0, 3)
|
||||||
|
.map((el) => (
|
||||||
<Tag key={el} text={el} href={`/servers/categories/${el}`} dark />
|
<Tag key={el} text={el} href={`/servers/categories/${el}`} dark />
|
||||||
))}{' '}
|
))}{' '}
|
||||||
{server.category?.length > 3 && <Tag text={`+${server.category.length - 3}`} dark />}
|
{server.category?.length > 3 && (
|
||||||
|
<Tag text={`+${server.category.length - 3}`} dark />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -85,59 +104,55 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<div className='flex justify-evenly'>
|
<div className='flex justify-evenly'>
|
||||||
{
|
{type === 'add' ? (
|
||||||
type === 'add' ?
|
server.data ? (
|
||||||
server.data ? <Link
|
<Link
|
||||||
href={newServerLink}
|
href={newServerLink}
|
||||||
className='py-3 w-full text-center text-emerald-500 hover:text-white text-sm font-bold hover:bg-emerald-500 rounded-b-2xl hover:shadow-lg transition duration-100 ease-in'>
|
className='w-full rounded-b-2xl py-3 text-center text-sm font-bold text-emerald-500 transition duration-100 ease-in hover:bg-emerald-500 hover:text-white hover:shadow-lg'
|
||||||
|
>
|
||||||
등록하기
|
등록하기
|
||||||
|
|
||||||
</Link> : <Link
|
|
||||||
href={newServerLink}
|
|
||||||
className='py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple rounded-b-2xl hover:shadow-lg transition duration-100 ease-in'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'>
|
|
||||||
|
|
||||||
봇 초대하기
|
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
:
|
) : (
|
||||||
|
<Link
|
||||||
|
href={newServerLink}
|
||||||
|
className='w-full rounded-b-2xl py-3 text-center text-sm font-bold text-discord-blurple transition duration-100 ease-in hover:bg-discord-blurple hover:text-white hover:shadow-lg'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
봇 초대하기
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
href={makeServerURL(server)}
|
href={makeServerURL(server)}
|
||||||
className='py-3 w-full text-center text-koreanbots-blue hover:text-white text-sm font-bold hover:bg-koreanbots-blue rounded-bl-2xl hover:shadow-lg transition duration-100 ease-in'>
|
className='w-full rounded-bl-2xl py-3 text-center text-sm font-bold text-koreanbots-blue transition duration-100 ease-in hover:bg-koreanbots-blue hover:text-white hover:shadow-lg'
|
||||||
|
>
|
||||||
보기
|
보기
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
{type === 'manage' ? (
|
{type === 'manage' ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/servers/${server.id}/edit`}
|
href={`/servers/${server.id}/edit`}
|
||||||
className='py-3 w-full text-center text-emerald-500 hover:text-white text-sm font-bold hover:bg-emerald-500 rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'>
|
className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-emerald-500 transition duration-100 ease-in hover:bg-emerald-500 hover:text-white hover:shadow-lg'
|
||||||
|
|
||||||
관리하기
|
|
||||||
|
|
||||||
</Link>
|
|
||||||
) : !['ok', 'unreachable'].includes(server.state) ? <a
|
|
||||||
className='py-3 w-full text-center text-discord-blurple text-sm font-bold rounded-br-2xl hover:shadow-lg transition duration-100 ease-in opacity-50 cursor-default select-none'
|
|
||||||
>
|
>
|
||||||
|
관리하기
|
||||||
|
</Link>
|
||||||
|
) : !['ok', 'unreachable'].includes(server.state) ? (
|
||||||
|
<a className='w-full cursor-default select-none rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple opacity-50 transition duration-100 ease-in hover:shadow-lg'>
|
||||||
참가하기
|
참가하기
|
||||||
</a> :
|
</a>
|
||||||
|
) : (
|
||||||
<a
|
<a
|
||||||
href={
|
href={makeServerURL(server) + '/join'}
|
||||||
makeServerURL(server) + '/join'
|
|
||||||
}
|
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
className='py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'
|
className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple transition duration-100 ease-in hover:bg-discord-blurple hover:text-white hover:shadow-lg'
|
||||||
>
|
>
|
||||||
참가하기
|
참가하기
|
||||||
</a>
|
</a>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -146,27 +161,25 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BotCardProps {
|
interface BotCardProps {
|
||||||
type: 'list' | 'manage' | 'add'
|
type: 'list' | 'manage' | 'add'
|
||||||
server: {
|
server: {
|
||||||
id: string,
|
id: string
|
||||||
name: string,
|
name: string
|
||||||
intro?: string
|
intro?: string
|
||||||
desc?: string,
|
desc?: string
|
||||||
flags?: number
|
flags?: number
|
||||||
state?: ServerState
|
state?: ServerState
|
||||||
icon: string | null,
|
icon: string | null
|
||||||
banner?: string | null,
|
banner?: string | null
|
||||||
bg?: string | null,
|
bg?: string | null
|
||||||
vanity?: string | null
|
vanity?: string | null
|
||||||
category?: string[]
|
category?: string[]
|
||||||
votes?: number | null
|
votes?: number | null
|
||||||
members?: number | null,
|
members?: number | null
|
||||||
data?: ServerData
|
data?: ServerData
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,22 @@ import { DiscordEnpoints, KoreanbotsEndPoints } from '@utils/Constants'
|
|||||||
const Image = dynamic(() => import('@components/Image'))
|
const Image = dynamic(() => import('@components/Image'))
|
||||||
|
|
||||||
const ServerIcon: React.FC<ServerIconProps> = ({ id, size, className, alt, hash }) => {
|
const ServerIcon: React.FC<ServerIconProps> = ({ id, size, className, alt, hash }) => {
|
||||||
return <Image
|
return (
|
||||||
|
<Image
|
||||||
className={className}
|
className={className}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
src={hash ? DiscordEnpoints.CDN.guild(id, hash, { format: 'webp', size: size ?? 256 }) : KoreanbotsEndPoints.CDN.icon(id, { format: 'webp', size: size ?? 256})}
|
src={
|
||||||
fallbackSrc={hash ? DiscordEnpoints.CDN.guild(id, hash, { format: 'png', size: size ?? 256 }) : KoreanbotsEndPoints.CDN.icon(id, { format: 'png', size: size ?? 256})}
|
hash
|
||||||
|
? DiscordEnpoints.CDN.guild(id, hash, { format: 'webp', size: size ?? 256 })
|
||||||
|
: KoreanbotsEndPoints.CDN.icon(id, { format: 'webp', size: size ?? 256 })
|
||||||
|
}
|
||||||
|
fallbackSrc={
|
||||||
|
hash
|
||||||
|
? DiscordEnpoints.CDN.guild(id, hash, { format: 'png', size: size ?? 256 })
|
||||||
|
: KoreanbotsEndPoints.CDN.icon(id, { format: 'png', size: size ?? 256 })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerIconProps {
|
interface ServerIconProps {
|
||||||
|
|||||||
@ -7,23 +7,23 @@ import Link from 'next/link'
|
|||||||
|
|
||||||
const SubmittedBotCard: React.FC<SubmittedBotProps> = ({ href, submit }) => {
|
const SubmittedBotCard: React.FC<SubmittedBotProps> = ({ href, submit }) => {
|
||||||
return (
|
return (
|
||||||
(<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className='relative mx-auto px-4 py-5 w-full h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl transform hover:-translate-y-1 transition duration-100 ease-in'>
|
className='relative mx-auto h-full w-full transform rounded-2xl bg-little-white px-4 py-5 text-black shadow-xl transition duration-100 ease-in hover:-translate-y-1 dark:bg-discord-black dark:text-white'
|
||||||
|
>
|
||||||
<div className='h-18'>
|
<div className='h-18'>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<div className='grow w-full'>
|
<div className='w-full grow'>
|
||||||
<h2 className='text-lg'>{submit.id}</h2>
|
<h2 className='text-lg'>{submit.id}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className='absolute right-0 grid grid-cols-1 px-4 w-2/5 h-0'>
|
<div className='absolute right-0 grid h-0 w-2/5 grid-cols-1 px-4'>
|
||||||
<Tag
|
<Tag
|
||||||
text={
|
text={
|
||||||
<>
|
<>
|
||||||
<i
|
<i
|
||||||
className={`fas fa-circle text-${
|
className={`fas fa-circle text-${[Status.offline, Status.online, Status.dnd][
|
||||||
[Status.offline, Status.online, Status.dnd][submit.state]?.color
|
submit.state
|
||||||
}`}
|
]?.color}`}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{['대기중', '승인됨', '거부됨'][submit.state]}
|
{['대기중', '승인됨', '거부됨'][submit.state]}
|
||||||
</>
|
</>
|
||||||
@ -32,13 +32,12 @@ const SubmittedBotCard: React.FC<SubmittedBotProps> = ({ href, submit }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className='mt-1.5 w-full h-6 text-left text-gray-400 text-sm font-medium truncate'>
|
<p className='mt-1.5 h-6 w-full truncate text-left text-sm font-medium text-gray-400'>
|
||||||
{submit.intro.slice(0, 25)}
|
{submit.intro.slice(0, 25)}
|
||||||
{submit.intro.length > 25 && '...'}
|
{submit.intro.length > 25 && '...'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
</Link>)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,17 +27,17 @@ const Tag: React.FC<LabelProps> = ({
|
|||||||
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
|
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
|
||||||
: github
|
: github
|
||||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||||
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
: 'bg-little-white hover:bg-little-white-hover dark:bg-discord-black'
|
||||||
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
||||||
circular
|
circular
|
||||||
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
||||||
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
||||||
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}
|
} mr-1 mb-${marginBottom} transition duration-100 ease-in dark:hover:bg-discord-dark-hover`}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
(<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className={`${className ?? ''} text-center text-base ${
|
className={`${className ?? ''} text-center text-base ${
|
||||||
dark
|
dark
|
||||||
@ -46,18 +46,17 @@ const Tag: React.FC<LabelProps> = ({
|
|||||||
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
|
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
|
||||||
: github
|
: github
|
||||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||||
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
: 'bg-little-white hover:bg-little-white-hover dark:bg-discord-black'
|
||||||
} ${
|
} ${
|
||||||
!blurple && !github ? 'text-black dark:text-gray-400' : 'hover:bg-little-white-hover'
|
!blurple && !github ? 'text-black dark:text-gray-400' : 'hover:bg-little-white-hover'
|
||||||
} ${
|
} ${
|
||||||
circular
|
circular
|
||||||
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
||||||
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
||||||
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}>
|
} mr-1 mb-${marginBottom} transition duration-100 ease-in dark:hover:bg-discord-dark-hover`}
|
||||||
|
>
|
||||||
{text}
|
{text}
|
||||||
|
</Link>
|
||||||
</Link>)
|
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
@ -70,12 +69,12 @@ const Tag: React.FC<LabelProps> = ({
|
|||||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||||
: `bg-little-white-hover dark:bg-very-black ${
|
: `bg-little-white-hover dark:bg-very-black ${
|
||||||
props.onClick
|
props.onClick
|
||||||
? 'hover:bg-little-white dark:hover:bg-discord-dark-hover transition duration-100 ease-in'
|
? 'transition duration-100 ease-in hover:bg-little-white dark:hover:bg-discord-dark-hover'
|
||||||
: ''
|
: ''
|
||||||
}`
|
}`
|
||||||
: `bg-little-white dark:bg-discord-black ${
|
: `bg-little-white dark:bg-discord-black ${
|
||||||
props.onClick
|
props.onClick
|
||||||
? 'hover:bg-little-white-hover dark:hover:bg-discord-dark-hover transition duration-100 ease-in'
|
? 'transition duration-100 ease-in hover:bg-little-white-hover dark:hover:bg-discord-dark-hover'
|
||||||
: ''
|
: ''
|
||||||
}`
|
}`
|
||||||
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
const Toggle: React.FC<ToggleProps> = ({ checked, onChange }: ToggleProps) => {
|
const Toggle: React.FC<ToggleProps> = ({ checked, onChange }: ToggleProps) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className='relative inline-block align-middle mr-2 w-10 outline-none select-none'
|
className='relative mr-2 inline-block w-10 select-none align-middle outline-none'
|
||||||
onClick={onChange}
|
onClick={onChange}
|
||||||
onKeyPress={onChange}
|
onKeyPress={onChange}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
checked={checked}
|
checked={checked}
|
||||||
className='absolute checked:right-0 block w-6 h-6 bg-white border-4 border-transparent rounded-full outline-none appearance-none cursor-pointer'
|
className='absolute block h-6 w-6 cursor-pointer appearance-none rounded-full border-4 border-transparent bg-white outline-none checked:right-0'
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer ${
|
className={`block h-6 cursor-pointer overflow-hidden rounded-full bg-gray-300 ${
|
||||||
checked ? 'bg-koreanbots-blue' : ''
|
checked ? 'bg-koreanbots-blue' : ''
|
||||||
}`}
|
}`}
|
||||||
></span>
|
></span>
|
||||||
|
|||||||
@ -8,15 +8,14 @@ const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
text,
|
text,
|
||||||
}) => {
|
}) => {
|
||||||
return href ? (
|
return href ? (
|
||||||
(<Link href={href} className='inline'>
|
<Link href={href} className='inline'>
|
||||||
|
|
||||||
<div className='relative inline py-3'>
|
<div className='relative inline py-3'>
|
||||||
<div className='group relative inline-block text-center cursor-pointer'>
|
<div className='group relative inline-block cursor-pointer text-center'>
|
||||||
{children}
|
{children}
|
||||||
<div
|
<div
|
||||||
className={`opacity-0 ${
|
className={`opacity-0 ${
|
||||||
size === 'small' ? 'w-44' : 'w-60'
|
size === 'small' ? 'w-44' : 'w-60'
|
||||||
} bg-black text-white text-center text-xs rounded-lg py-2 px-3 absolute z-10 group-hover:opacity-100 bottom-full -left-4 pointer-events-none`}
|
} pointer-events-none absolute -left-4 bottom-full z-10 rounded-lg bg-black px-3 py-2 text-center text-xs text-white group-hover:opacity-100`}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
{direction === 'left' ? (
|
{direction === 'left' ? (
|
||||||
@ -31,7 +30,7 @@ const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
</svg>
|
</svg>
|
||||||
) : direction === 'center' ? (
|
) : direction === 'center' ? (
|
||||||
<svg
|
<svg
|
||||||
className='absolute left-0 top-full w-full h-2 text-black'
|
className='absolute left-0 top-full h-2 w-full text-black'
|
||||||
x='0px'
|
x='0px'
|
||||||
y='0px'
|
y='0px'
|
||||||
viewBox='0 0 255 255'
|
viewBox='0 0 255 255'
|
||||||
@ -53,17 +52,16 @@ const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
</Link>)
|
|
||||||
) : (
|
) : (
|
||||||
<a className='inline'>
|
<a className='inline'>
|
||||||
<div className='relative inline py-3'>
|
<div className='relative inline py-3'>
|
||||||
<div className='group relative inline-block text-center cursor-pointer'>
|
<div className='group relative inline-block cursor-pointer text-center'>
|
||||||
{children}
|
{children}
|
||||||
<div
|
<div
|
||||||
className={`opacity-0 ${
|
className={`opacity-0 ${
|
||||||
size === 'small' ? 'w-44' : 'w-60'
|
size === 'small' ? 'w-44' : 'w-60'
|
||||||
} bg-black text-white text-center text-xs rounded-lg py-2 px-3 absolute z-10 group-hover:opacity-100 bottom-full -left-4 pointer-events-none`}
|
} pointer-events-none absolute -left-4 bottom-full z-10 rounded-lg bg-black px-3 py-2 text-center text-xs text-white group-hover:opacity-100`}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
{direction === 'left' ? (
|
{direction === 'left' ? (
|
||||||
@ -78,7 +76,7 @@ const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
</svg>
|
</svg>
|
||||||
) : direction === 'center' ? (
|
) : direction === 'center' ? (
|
||||||
<svg
|
<svg
|
||||||
className='absolute left-0 top-full w-full h-2 text-black'
|
className='absolute left-0 top-full h-2 w-full text-black'
|
||||||
x='0px'
|
x='0px'
|
||||||
y='0px'
|
y='0px'
|
||||||
viewBox='0 0 255 255'
|
viewBox='0 0 255 255'
|
||||||
|
|||||||
@ -3,20 +3,19 @@ import { ErrorText } from '@utils/Constants'
|
|||||||
|
|
||||||
const NotFound: NextPage<{ message?: string }> = ({ message }) => {
|
const NotFound: NextPage<{ message?: string }> = ({ message }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='flex h-screen select-none items-center justify-center text-center'>
|
||||||
className='flex items-center justify-center h-screen select-none text-center'
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<div className='flex flex-row justify-center text-9xl'>
|
<div className='flex flex-row justify-center text-9xl'>
|
||||||
4
|
4
|
||||||
<img alt='robot' src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f916.svg' className='w-24 mx-6 md:mx-12 rounded-full' />
|
<img
|
||||||
|
alt='robot'
|
||||||
|
src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f916.svg'
|
||||||
|
className='mx-6 w-24 rounded-full md:mx-12'
|
||||||
|
/>
|
||||||
4
|
4
|
||||||
</div>
|
</div>
|
||||||
<h2 className='text-2xl font-semibold'>
|
<h2 className='text-2xl font-semibold'>{message || ErrorText[404]}</h2>
|
||||||
{message || ErrorText[404]}
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,11 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
|
|||||||
'%c' + 'KOREANBOTS',
|
'%c' + 'KOREANBOTS',
|
||||||
'color: #3366FF; -webkit-text-stroke: 2px black; font-size: 72px; font-weight: bold;'
|
'color: #3366FF; -webkit-text-stroke: 2px black; font-size: 72px; font-weight: bold;'
|
||||||
)
|
)
|
||||||
Logger.debug(`[BUILD INFO] Tag: ${parseDockerhubTag(process.env.NEXT_PUBLIC_TAG)}, Version: v${Package.version}, Hash: ${process.env.NEXT_PUBLIC_SOURCE_COMMIT}`)
|
Logger.debug(
|
||||||
|
`[BUILD INFO] Tag: ${parseDockerhubTag(process.env.NEXT_PUBLIC_TAG)}, Version: v${
|
||||||
|
Package.version
|
||||||
|
}, Hash: ${process.env.NEXT_PUBLIC_SOURCE_COMMIT}`
|
||||||
|
)
|
||||||
console.log(
|
console.log(
|
||||||
'%c' + '이곳에 코드를 붙여넣으면 공격자에게 엑세스 토큰을 넘겨줄 수 있습니다!!',
|
'%c' + '이곳에 코드를 붙여넣으면 공격자에게 엑세스 토큰을 넘겨줄 수 있습니다!!',
|
||||||
'color: #ff0000; font-size: 20px; font-weight: bold;'
|
'color: #ff0000; font-size: 20px; font-weight: bold;'
|
||||||
@ -52,16 +56,29 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
|
|||||||
if (!localStorage.theme) {
|
if (!localStorage.theme) {
|
||||||
Logger.debug(`[THEME] ${systemTheme().toUpperCase()} THEME DETECTED`)
|
Logger.debug(`[THEME] ${systemTheme().toUpperCase()} THEME DETECTED`)
|
||||||
setTheme(systemTheme())
|
setTheme(systemTheme())
|
||||||
}
|
} else setTheme(localStorage.theme)
|
||||||
else setTheme(localStorage.theme)
|
|
||||||
setStandalone(handlePWA())
|
setStandalone(handlePWA())
|
||||||
|
|
||||||
const script = document.querySelector('script[src*=googlesyndication]')
|
const script = document.querySelector('script[src*=googlesyndication]')
|
||||||
|
|
||||||
if (script) script.addEventListener('error', () => {ReactGA.ga('send', 'event', 'adblock', 'adblock_' + (navigator.userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i) ? 'mobile' : 'pc'))})
|
if (script)
|
||||||
|
script.addEventListener('error', () => {
|
||||||
|
ReactGA.ga(
|
||||||
|
'send',
|
||||||
|
'event',
|
||||||
|
'adblock',
|
||||||
|
'adblock_' +
|
||||||
|
(navigator.userAgent.match(
|
||||||
|
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
|
||||||
|
)
|
||||||
|
? 'mobile'
|
||||||
|
: 'pc')
|
||||||
|
)
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div className={theme}>
|
return (
|
||||||
|
<div className={theme}>
|
||||||
<DefaultSeo
|
<DefaultSeo
|
||||||
titleTemplate='%s - 한국 디스코드 리스트'
|
titleTemplate='%s - 한국 디스코드 리스트'
|
||||||
defaultTitle={TITLE}
|
defaultTitle={TITLE}
|
||||||
@ -77,14 +94,14 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
|
|||||||
url: '/logo.png',
|
url: '/logo.png',
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
alt: 'Logo'
|
alt: 'Logo',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}}
|
}}
|
||||||
twitter={{
|
twitter={{
|
||||||
site: '@koreanbots',
|
site: '@koreanbots',
|
||||||
handle: '@koreanbots',
|
handle: '@koreanbots',
|
||||||
cardType: 'summary'
|
cardType: 'summary',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
@ -92,7 +109,10 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
|
|||||||
<meta charSet='utf-8' />
|
<meta charSet='utf-8' />
|
||||||
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
|
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
|
||||||
<meta name='keywords' content='Korea, Korean, Discord, Bot, 디스코드봇, 한디리' />
|
<meta name='keywords' content='Korea, Korean, Discord, Bot, 디스코드봇, 한디리' />
|
||||||
<meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' />
|
<meta
|
||||||
|
name='viewport'
|
||||||
|
content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no'
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Android */}
|
{/* Android */}
|
||||||
<meta name='theme-color' content={THEME_COLOR} />
|
<meta name='theme-color' content={THEME_COLOR} />
|
||||||
@ -125,43 +145,45 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
|
|||||||
<meta name='layoutmode' content='fitscreen' />
|
<meta name='layoutmode' content='fitscreen' />
|
||||||
<meta name='imagemode' content='force' />
|
<meta name='imagemode' content='force' />
|
||||||
<meta name='screen-orientation' content='portrait' />
|
<meta name='screen-orientation' content='portrait' />
|
||||||
|
|
||||||
</Head>
|
</Head>
|
||||||
<Navbar token={cookie.token} />
|
<Navbar token={cookie.token} />
|
||||||
<div className='iu-is-the-best min-h-screen text-black dark:text-gray-100 dark:bg-discord-dark bg-white'>
|
<div className='iu-is-the-best min-h-screen bg-white text-black dark:bg-discord-dark dark:text-gray-100'>
|
||||||
<Component {...pageProps} err={err} theme={theme} setTheme={setTheme} pwa={standalone} />
|
<Component {...pageProps} err={err} theme={theme} setTheme={setTheme} pwa={standalone} />
|
||||||
</div>
|
</div>
|
||||||
{
|
{!router.pathname.startsWith('/developers') && <Footer theme={theme} setTheme={setTheme} />}
|
||||||
!(router.pathname.startsWith('/developers')) && <Footer theme={theme} setTheme={setTheme} />
|
<Modal
|
||||||
}
|
full
|
||||||
<Modal full isOpen={shortcutModal} onClose={() => setShortcutModal(false)} dark={theme === 'dark'} header='단축키 안내'>
|
isOpen={shortcutModal}
|
||||||
<div className='px-3 h-80'>
|
onClose={() => setShortcutModal(false)}
|
||||||
|
dark={theme === 'dark'}
|
||||||
|
header='단축키 안내'
|
||||||
|
>
|
||||||
|
<div className='h-80 px-3'>
|
||||||
<h3 className='text-md font-semibold'>일반</h3>
|
<h3 className='text-md font-semibold'>일반</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li className='pt-2'>
|
<li className='pt-2'>
|
||||||
<h4 className='text-gray-500 dark:text-gray-400 text-xs'>단축키 도움말 표시</h4>
|
<h4 className='text-xs text-gray-500 dark:text-gray-400'>단축키 도움말 표시</h4>
|
||||||
<kbd>
|
<kbd>
|
||||||
<PlatformDisplay osx='CMD'>
|
<PlatformDisplay osx='CMD'>Ctrl</PlatformDisplay>
|
||||||
Ctrl
|
</kbd>{' '}
|
||||||
</PlatformDisplay>
|
<kbd>/</kbd>
|
||||||
</kbd> <kbd>/</kbd>
|
|
||||||
</li>
|
</li>
|
||||||
<li className='pt-2'>
|
<li className='pt-2'>
|
||||||
<h4 className='text-gray-500 dark:text-gray-400 text-xs'>다크모드 전환</h4>
|
<h4 className='text-xs text-gray-500 dark:text-gray-400'>다크모드 전환</h4>
|
||||||
<kbd>
|
<kbd>
|
||||||
<PlatformDisplay osx='CMD'>
|
<PlatformDisplay osx='CMD'>Ctrl</PlatformDisplay>
|
||||||
Ctrl
|
|
||||||
</PlatformDisplay>
|
|
||||||
</kbd>
|
</kbd>
|
||||||
<kbd>Shift</kbd> <kbd>D</kbd>
|
<kbd>Shift</kbd> <kbd>D</kbd>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<GlobalHotKeys keyMap={shortcutKeyMap} handlers={{
|
<GlobalHotKeys
|
||||||
|
keyMap={shortcutKeyMap}
|
||||||
|
handlers={{
|
||||||
SHORTCUT_HELP: (event) => {
|
SHORTCUT_HELP: (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setShortcutModal(value => !value)
|
setShortcutModal((value) => !value)
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
CHANGE_THEME: (event) => {
|
CHANGE_THEME: (event) => {
|
||||||
@ -170,9 +192,11 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
|
|||||||
setTheme(overwrite)
|
setTheme(overwrite)
|
||||||
localStorage.setItem('theme', overwrite)
|
localStorage.setItem('theme', overwrite)
|
||||||
return false
|
return false
|
||||||
}
|
},
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
KoreanbotsApp.getInitialProps = async (appCtx: AppContext) => {
|
KoreanbotsApp.getInitialProps = async (appCtx: AppContext) => {
|
||||||
@ -180,7 +204,7 @@ KoreanbotsApp.getInitialProps = async (appCtx: AppContext) => {
|
|||||||
const parsed = parseCookie(appCtx.ctx.req)
|
const parsed = parseCookie(appCtx.ctx.req)
|
||||||
return {
|
return {
|
||||||
...appProps,
|
...appProps,
|
||||||
cookie: parsed
|
cookie: parsed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,12 @@ class MyDocument extends Document {
|
|||||||
<Head>
|
<Head>
|
||||||
{/* LINK */}
|
{/* LINK */}
|
||||||
<link rel='manifest' href='/manifest.json' />
|
<link rel='manifest' href='/manifest.json' />
|
||||||
<link rel='search' type='application/opensearchdescription+xml' title={TITLE} href='/opensearch.xml' />
|
<link
|
||||||
|
rel='search'
|
||||||
|
type='application/opensearchdescription+xml'
|
||||||
|
title={TITLE}
|
||||||
|
href='/opensearch.xml'
|
||||||
|
/>
|
||||||
<link
|
<link
|
||||||
rel='stylesheet'
|
rel='stylesheet'
|
||||||
href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.6.0/styles/solarized-dark.min.css'
|
href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.6.0/styles/solarized-dark.min.css'
|
||||||
@ -21,7 +26,12 @@ class MyDocument extends Document {
|
|||||||
<link rel='icon' type='image/png' sizes='32x32' href='/favicon-32x32.png' />
|
<link rel='icon' type='image/png' sizes='32x32' href='/favicon-32x32.png' />
|
||||||
<link rel='icon' type='image/png' sizes='96x96' href='/favicon-96x96.png' />
|
<link rel='icon' type='image/png' sizes='96x96' href='/favicon-96x96.png' />
|
||||||
<link rel='icon' type='image/png' sizes='16x16' href='/favicon-16x16.png' />
|
<link rel='icon' type='image/png' sizes='16x16' href='/favicon-16x16.png' />
|
||||||
<link rel='stylesheet' as='style' crossOrigin='anonymous' href='https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.6/dist/web/variable/pretendardvariable-dynamic-subset.css' />
|
<link
|
||||||
|
rel='stylesheet'
|
||||||
|
as='style'
|
||||||
|
crossOrigin='anonymous'
|
||||||
|
href='https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.6/dist/web/variable/pretendardvariable-dynamic-subset.css'
|
||||||
|
/>
|
||||||
|
|
||||||
{/* iOS */}
|
{/* iOS */}
|
||||||
<link rel='apple-touch-icon' sizes='57x57' href='/static/apple-icon-57x57.png' />
|
<link rel='apple-touch-icon' sizes='57x57' href='/static/apple-icon-57x57.png' />
|
||||||
@ -35,43 +45,160 @@ class MyDocument extends Document {
|
|||||||
<link rel='apple-touch-icon' sizes='180x180' href='/static/apple-icon-180x180.png' />
|
<link rel='apple-touch-icon' sizes='180x180' href='/static/apple-icon-180x180.png' />
|
||||||
<link rel='apple-touch-icon' sizes='256x256' href='/static/apple-icon-256x256.png' />
|
<link rel='apple-touch-icon' sizes='256x256' href='/static/apple-icon-256x256.png' />
|
||||||
<link rel='apple-touch-icon' sizes='512x512' href='/static/apple-icon-512x512.png' />
|
<link rel='apple-touch-icon' sizes='512x512' href='/static/apple-icon-512x512.png' />
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2048-2732.jpg' media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
<link
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2732-2048.jpg' media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
rel='apple-touch-startup-image'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1668-2388.jpg' media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
href='/static/apple-splash-2048-2732.jpg'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2388-1668.jpg' media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1536-2048.jpg' media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
/>
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2048-1536.jpg' media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
<link
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1668-2224.jpg' media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
rel='apple-touch-startup-image'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2224-1668.jpg' media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
href='/static/apple-splash-2732-2048.jpg'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1620-2160.jpg' media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2160-1620.jpg' media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
/>
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1284-2778.jpg' media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' />
|
<link
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2778-1284.jpg' media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' />
|
rel='apple-touch-startup-image'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1170-2532.jpg' media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' />
|
href='/static/apple-splash-1668-2388.jpg'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2532-1170.jpg' media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' />
|
media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1125-2436.jpg' media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' />
|
/>
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2436-1125.jpg' media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' />
|
<link
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1242-2688.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' />
|
rel='apple-touch-startup-image'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2688-1242.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' />
|
href='/static/apple-splash-2388-1668.jpg'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-828-1792.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1792-828.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
/>
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1242-2208.jpg' media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' />
|
<link
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2208-1242.jpg' media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' />
|
rel='apple-touch-startup-image'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-750-1334.jpg' media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
href='/static/apple-splash-1536-2048.jpg'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1334-750.jpg' media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-640-1136.jpg' media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
/>
|
||||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1136-640.jpg' media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-2048-1536.jpg'
|
||||||
|
media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-1668-2224.jpg'
|
||||||
|
media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-2224-1668.jpg'
|
||||||
|
media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-1620-2160.jpg'
|
||||||
|
media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-2160-1620.jpg'
|
||||||
|
media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-1284-2778.jpg'
|
||||||
|
media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-2778-1284.jpg'
|
||||||
|
media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-1170-2532.jpg'
|
||||||
|
media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-2532-1170.jpg'
|
||||||
|
media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-1125-2436.jpg'
|
||||||
|
media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-2436-1125.jpg'
|
||||||
|
media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-1242-2688.jpg'
|
||||||
|
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-2688-1242.jpg'
|
||||||
|
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-828-1792.jpg'
|
||||||
|
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-1792-828.jpg'
|
||||||
|
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-1242-2208.jpg'
|
||||||
|
media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-2208-1242.jpg'
|
||||||
|
media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-750-1334.jpg'
|
||||||
|
media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-1334-750.jpg'
|
||||||
|
media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-640-1136.jpg'
|
||||||
|
media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='apple-touch-startup-image'
|
||||||
|
href='/static/apple-splash-1136-640.jpg'
|
||||||
|
media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Android */}
|
{/* Android */}
|
||||||
<link rel='icon' type='image/png' sizes='192x192' href='/static/android-icon-192x192.png' />
|
<link
|
||||||
|
rel='icon'
|
||||||
|
type='image/png'
|
||||||
|
sizes='192x192'
|
||||||
|
href='/static/android-icon-192x192.png'
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Others */}
|
{/* Others */}
|
||||||
<link rel='shortcut icon' href='/favicon.ico' />
|
<link rel='shortcut icon' href='/favicon.ico' />
|
||||||
|
|
||||||
{/* SCRIPT */}
|
{/* SCRIPT */}
|
||||||
<script src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js'></script>
|
<script src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js'></script>
|
||||||
<script data-ad-client='ca-pub-4856582423981759' async src='//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'></script>
|
<script
|
||||||
<script data-cfasync='false' async src='//www.googletagmanager.com/gtag/js?id=UA-165454387-1'></script>
|
data-ad-client='ca-pub-4856582423981759'
|
||||||
|
async
|
||||||
|
src='//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
data-cfasync='false'
|
||||||
|
async
|
||||||
|
src='//www.googletagmanager.com/gtag/js?id=UA-165454387-1'
|
||||||
|
></script>
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
@ -91,7 +218,7 @@ class MyDocument extends Document {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<body className='h-full text-black dark:text-gray-100 dark:bg-discord-dark bg-white overflow-x-hidden'>
|
<body className='h-full overflow-x-hidden bg-white text-black dark:bg-discord-dark dark:text-gray-100'>
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -8,24 +8,35 @@ const Container = dynamic(() => import('@components/Container'))
|
|||||||
|
|
||||||
const MyError: NextPage = () => {
|
const MyError: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='flex h-screen select-none items-center px-20'>
|
||||||
className='flex items-center h-screen select-none px-20'
|
|
||||||
>
|
|
||||||
<Container>
|
<Container>
|
||||||
<h2 className='text-4xl font-semibold'>{getRandom(ErrorMessage)}</h2>
|
<h2 className='text-4xl font-semibold'>{getRandom(ErrorMessage)}</h2>
|
||||||
<p className='text-md mt-1'>예상치 못한 오류가 발생하였습니다. 문제가 지속적으로 발생한다면 문의해주세요!</p>
|
<p className='text-md mt-1'>
|
||||||
<a className='text-sm text-blue-500 hover:text-blue-400' href='https://status.koreanbots.dev' target='_blank' rel='noreferrer'>상태 페이지</a>
|
예상치 못한 오류가 발생하였습니다. 문제가 지속적으로 발생한다면 문의해주세요!
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
className='text-sm text-blue-500 hover:text-blue-400'
|
||||||
|
href='https://status.koreanbots.dev'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
상태 페이지
|
||||||
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
href='/discord'
|
href='/discord'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
className='text-lg hover:opacity-80 cursor-pointer'>
|
className='cursor-pointer text-lg hover:opacity-80'
|
||||||
|
>
|
||||||
<i className='fab fa-discord' />
|
<i className='fab fa-discord' />
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
<a href='https://twitter.com/koreanbots' target='_blank' rel='noreferrer' className='text-lg ml-2 hover:opacity-80 cursor-pointer'>
|
<a
|
||||||
|
href='https://twitter.com/koreanbots'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
className='ml-2 cursor-pointer text-lg hover:opacity-80'
|
||||||
|
>
|
||||||
<i className='fab fa-twitter' />
|
<i className='fab fa-twitter' />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,24 +6,33 @@ const Container = dynamic(() => import('@components/Container'))
|
|||||||
|
|
||||||
const MyError: NextPage = () => {
|
const MyError: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='flex h-screen select-none items-center px-20'>
|
||||||
className='flex items-center h-screen select-none px-20'
|
|
||||||
>
|
|
||||||
<Container>
|
<Container>
|
||||||
<h2 className='text-4xl font-semibold'>인터넷이 끊어졌나봐요...</h2>
|
<h2 className='text-4xl font-semibold'>인터넷이 끊어졌나봐요...</h2>
|
||||||
<p className='text-md mt-1'>인터넷 연결을 확인하시고 다시 시도 해주세요!</p>
|
<p className='text-md mt-1'>인터넷 연결을 확인하시고 다시 시도 해주세요!</p>
|
||||||
<a className='text-sm text-blue-500 hover:text-blue-400' href='https://status.koreanbots.dev' target='_blank' rel='noreferrer'>상태 페이지</a>
|
<a
|
||||||
|
className='text-sm text-blue-500 hover:text-blue-400'
|
||||||
|
href='https://status.koreanbots.dev'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
상태 페이지
|
||||||
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
href='/discord'
|
href='/discord'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
className='text-lg hover:opacity-80 cursor-pointer'>
|
className='cursor-pointer text-lg hover:opacity-80'
|
||||||
|
>
|
||||||
<i className='fab fa-discord' />
|
<i className='fab fa-discord' />
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
<a href='https://twitter.com/koreanbots' target='_blank' rel='noreferrer' className='text-lg ml-2 hover:opacity-80 cursor-pointer'>
|
<a
|
||||||
|
href='https://twitter.com/koreanbots'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
className='ml-2 cursor-pointer text-lg hover:opacity-80'
|
||||||
|
>
|
||||||
<i className='fab fa-twitter' />
|
<i className='fab fa-twitter' />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,41 +10,64 @@ import { ThemeColors } from '@utils/Constants'
|
|||||||
const Container = dynamic(() => import('@components/Container'))
|
const Container = dynamic(() => import('@components/Container'))
|
||||||
|
|
||||||
const About: NextPage = () => {
|
const About: NextPage = () => {
|
||||||
return <div className='pb-10'>
|
return (
|
||||||
<Docs title='소개' header={<h1 className='font-black text-4xl dark:text-koreanbots-blue'>“국내 디스코드의 모든 것을 한 곳에서.”</h1>} subheader='한국 디스코드 리스트에서 자신에게 필요한 디스코드의 모든 것을 찾아보세요!'>
|
<div className='pb-10'>
|
||||||
|
<Docs
|
||||||
|
title='소개'
|
||||||
|
header={
|
||||||
|
<h1 className='text-4xl font-black dark:text-koreanbots-blue'>
|
||||||
|
“국내 디스코드의 모든 것을 한 곳에서.”
|
||||||
|
</h1>
|
||||||
|
}
|
||||||
|
subheader='한국 디스코드 리스트에서 자신에게 필요한 디스코드의 모든 것을 찾아보세요!'
|
||||||
|
>
|
||||||
<Container>
|
<Container>
|
||||||
<div className='py-1'>
|
<div className='py-1'>
|
||||||
<h1 className='font-bold text-5xl my-5'>소개</h1>
|
<h1 className='my-5 text-5xl font-bold'>소개</h1>
|
||||||
<p className='text-lg'><span className='text-koreanbots-blue font-bold'>한국 디스코드 리스트</span>는 본인의 봇과 서버를 직접 등록하고, 유저 분은 봇 또는 서버를 카테고리별로 확인할 수 있는 플랫폼입니다.</p>
|
<p className='text-lg'>
|
||||||
|
<span className='font-bold text-koreanbots-blue'>한국 디스코드 리스트</span>는 본인의
|
||||||
|
봇과 서버를 직접 등록하고, 유저 분은 봇 또는 서버를 카테고리별로 확인할 수 있는
|
||||||
|
플랫폼입니다.
|
||||||
|
</p>
|
||||||
<p className='text-lg'>자신에게 필요한 디스코드의 모든 것을 찾아보세요!</p>
|
<p className='text-lg'>자신에게 필요한 디스코드의 모든 것을 찾아보세요!</p>
|
||||||
<Divider />
|
<Divider />
|
||||||
<h1 className='font-bold text-5xl my-5'>특징</h1>
|
<h1 className='my-5 text-5xl font-bold'>특징</h1>
|
||||||
<div className='grid md:grid-cols-3 gap-12 px-4 pb-5'>
|
<div className='grid gap-12 px-4 pb-5 md:grid-cols-3'>
|
||||||
<div className='mx-auto font-normal'>
|
<div className='mx-auto font-normal'>
|
||||||
<h2 className='text-3xl mb-1 font-bold text-koreanbots-blue'>하트 시스템</h2>
|
<h2 className='mb-1 text-3xl font-bold text-koreanbots-blue'>하트 시스템</h2>
|
||||||
<p className='text-base'>마음에 드는 봇이나 서버에 투표하는 하트 시스템으로 유용한 봇 또는 서버가 상단에 노출될 수 있는 기회를 제공합니다.</p>
|
<p className='text-base'>
|
||||||
|
마음에 드는 봇이나 서버에 투표하는 하트 시스템으로 유용한 봇 또는 서버가 상단에
|
||||||
|
노출될 수 있는 기회를 제공합니다.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx-auto font-normal'>
|
<div className='mx-auto font-normal'>
|
||||||
<h2 className='text-3xl mb-1 font-bold text-koreanbots-blue'>인증 시스템</h2>
|
<h2 className='mb-1 text-3xl font-bold text-koreanbots-blue'>인증 시스템</h2>
|
||||||
<p className='text-base'>봇은 디스코드 봇 인증보다 한 단계 까다로운 기준을 적용하며 서버는 신뢰할 수 있는 서버를 정해, 이용자분들에게 신뢰감을 줍니다.</p>
|
<p className='text-base'>
|
||||||
|
봇은 디스코드 봇 인증보다 한 단계 까다로운 기준을 적용하며 서버는 신뢰할 수 있는
|
||||||
|
서버를 정해, 이용자분들에게 신뢰감을 줍니다.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx-auto font-normal'>
|
<div className='mx-auto font-normal'>
|
||||||
<h2 className='text-3xl mb-1 font-bold text-koreanbots-blue'>API 제공</h2>
|
<h2 className='mb-1 text-3xl font-bold text-koreanbots-blue'>API 제공</h2>
|
||||||
<p className='text-base'>정보부터, 유저 투표 여부 확인, 위젯까지.<br />다양한 API를 제공하여 커스텀할 수 있습니다!</p>
|
<p className='text-base'>
|
||||||
|
정보부터, 유저 투표 여부 확인, 위젯까지.
|
||||||
|
<br />
|
||||||
|
다양한 API를 제공하여 커스텀할 수 있습니다!
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<h1 className='font-bold text-5xl my-5'>브랜드</h1>
|
<h1 className='my-5 text-5xl font-bold'>브랜드</h1>
|
||||||
<h2 className='font-semibold text-3xl mb-7'>슬로건</h2>
|
<h2 className='mb-7 text-3xl font-semibold'>슬로건</h2>
|
||||||
<Segment>
|
<Segment>
|
||||||
<h2 className='font-semibold text-xl py-10 text-center'>
|
<h2 className='py-10 text-center text-xl font-semibold'>
|
||||||
<i className='fas fa-quote-left text-xs align-top' />
|
<i className='fas fa-quote-left align-top text-xs' />
|
||||||
국내 디스코드의 모든 것을 한 곳에서.
|
국내 디스코드의 모든 것을 한 곳에서.
|
||||||
<i className='fas fa-quote-right text-xs align-bottom' />
|
<i className='fas fa-quote-right align-bottom text-xs' />
|
||||||
</h2>
|
</h2>
|
||||||
</Segment>
|
</Segment>
|
||||||
<Divider className='mt-7' />
|
<Divider className='mt-7' />
|
||||||
<h2 className='font-semibold text-3xl my-7'>로고</h2>
|
<h2 className='my-7 text-3xl font-semibold'>로고</h2>
|
||||||
<Segment>
|
<Segment>
|
||||||
<>
|
<>
|
||||||
로고를 수정하거나, 변경, 왜곡 등 기타 다른 방법으로 로고를 수정하지 말아주세요.
|
로고를 수정하거나, 변경, 왜곡 등 기타 다른 방법으로 로고를 수정하지 말아주세요.
|
||||||
@ -52,27 +75,36 @@ const About:NextPage = () => {
|
|||||||
<div>
|
<div>
|
||||||
<img src='/logo.png' alt='Logo' />
|
<img src='/logo.png' alt='Logo' />
|
||||||
<div className='text-right text-blue-400'>
|
<div className='text-right text-blue-400'>
|
||||||
<a href='/logo.png' download='koreanbots.png'>.png</a>
|
<a href='/logo.png' download='koreanbots.png'>
|
||||||
|
.png
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className='font-bold text-xl my-1'>폰트</h3>
|
<h3 className='my-1 text-xl font-bold'>폰트</h3>
|
||||||
<p className='font-bold text-md my-1'>영문: Uni Sans Heavy | 한글: Gugi</p>
|
<p className='text-md my-1 font-bold'>영문: Uni Sans Heavy | 한글: Gugi</p>
|
||||||
</>
|
</>
|
||||||
</Segment>
|
</Segment>
|
||||||
<Divider className='mt-7' />
|
<Divider className='mt-7' />
|
||||||
<h2 className='font-semibold text-3xl my-5'>색상</h2>
|
<h2 className='my-5 text-3xl font-semibold'>색상</h2>
|
||||||
<div className='grid md:grid-cols-2 lg:grid-cols-4 gap-4'>
|
<div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4'>
|
||||||
{
|
{ThemeColors.map((el) => (
|
||||||
ThemeColors.map(el => (
|
<ColorCard
|
||||||
<ColorCard key={el.color} header={el.name} first={el.rgb} second={el.hex} className={`bg-${el.color} ${el.color.includes('white') ? 'text-black' : 'text-white'}`} />
|
key={el.color}
|
||||||
))
|
header={el.name}
|
||||||
}
|
first={el.rgb}
|
||||||
|
second={el.hex}
|
||||||
|
className={`bg-${el.color} ${
|
||||||
|
el.color.includes('white') ? 'text-black' : 'text-white'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</Docs>
|
</Docs>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default About
|
export default About
|
||||||
317
pages/addbot.tsx
317
pages/addbot.tsx
@ -58,7 +58,7 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
|
|||||||
- 기능
|
- 기능
|
||||||
- 있나요?`,
|
- 있나요?`,
|
||||||
_csrf: csrfToken,
|
_csrf: csrfToken,
|
||||||
_captcha: 'captcha'
|
_captcha: 'captcha',
|
||||||
}
|
}
|
||||||
|
|
||||||
function toLogin() {
|
function toLogin() {
|
||||||
@ -67,150 +67,325 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitBot(value: AddBotSubmit, token: string) {
|
async function submitBot(value: AddBotSubmit, token: string) {
|
||||||
const res = await Fetch<SubmittedBot>(`/bots/${value.id}`, { method: 'POST', body: JSON.stringify(cleanObject<AddBotSubmit>({ ...value, _captcha: token})) })
|
const res = await Fetch<SubmittedBot>(`/bots/${value.id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(cleanObject<AddBotSubmit>({ ...value, _captcha: token })),
|
||||||
|
})
|
||||||
setData(res)
|
setData(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!logged) return <Login>
|
if (!logged)
|
||||||
<NextSeo title='새로운 봇 추가하기' description='자신의 봇을 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
return (
|
||||||
title:'새로운 봇 추가하기', description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.'
|
<Login>
|
||||||
}} />
|
<NextSeo
|
||||||
|
title='새로운 봇 추가하기'
|
||||||
|
description='자신의 봇을 한국 디스코드 리스트에 등록하세요.'
|
||||||
|
openGraph={{
|
||||||
|
title: '새로운 봇 추가하기',
|
||||||
|
description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Login>
|
</Login>
|
||||||
if(data?.data && data.code === 200) {
|
|
||||||
setTimeout(
|
|
||||||
() => redirectTo(router, `/pendingBots/${data.data.id}/${data.data.date}`),
|
|
||||||
1_000
|
|
||||||
)
|
)
|
||||||
|
if (data?.data && data.code === 200) {
|
||||||
|
setTimeout(() => redirectTo(router, `/pendingBots/${data.data.id}/${data.data.date}`), 1_000)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Container paddingTop className='py-5'>
|
<Container paddingTop className='py-5'>
|
||||||
<NextSeo title='새로운 봇 추가하기' description='자신의 봇을 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
<NextSeo
|
||||||
title:'새로운 봇 추가하기', description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.'
|
title='새로운 봇 추가하기'
|
||||||
}} />
|
description='자신의 봇을 한국 디스코드 리스트에 등록하세요.'
|
||||||
|
openGraph={{
|
||||||
|
title: '새로운 봇 추가하기',
|
||||||
|
description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<h1 className='text-3xl font-bold'>새로운 봇 추가하기</h1>
|
<h1 className='text-3xl font-bold'>새로운 봇 추가하기</h1>
|
||||||
<div className='mt-1 mb-5'>
|
<div className='mb-5 mt-1'>
|
||||||
안녕하세요, <span className='font-semibold'>{user.tag === '0' ? `@${user.username}` : `${user.username}#${user.tag}`}</span>님! <a role='button' tabIndex={0} onKeyDown={toLogin} onClick={toLogin} className='text-discord-blurple cursor-pointer outline-none'>본인이 아니신가요?</a>
|
안녕하세요,{' '}
|
||||||
|
<span className='font-semibold'>
|
||||||
|
{user.tag === '0' ? `@${user.username}` : `${user.username}#${user.tag}`}
|
||||||
|
</span>
|
||||||
|
님!{' '}
|
||||||
|
<a
|
||||||
|
role='button'
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={toLogin}
|
||||||
|
onClick={toLogin}
|
||||||
|
className='cursor-pointer text-discord-blurple outline-none'
|
||||||
|
>
|
||||||
|
본인이 아니신가요?
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{
|
{data ? (
|
||||||
data ? data.code == 200 && data.data ? <Message type='success'>
|
data.code == 200 && data.data ? (
|
||||||
|
<Message type='success'>
|
||||||
<h2 className='text-lg font-extrabold'>봇 신청 성공!</h2>
|
<h2 className='text-lg font-extrabold'>봇 신청 성공!</h2>
|
||||||
<p>봇을 성공적으로 신청했습니다! 심사 페이지로 리다이렉트됩니다.</p>
|
<p>봇을 성공적으로 신청했습니다! 심사 페이지로 리다이렉트됩니다.</p>
|
||||||
</Message> : <Message type='error'>
|
</Message>
|
||||||
|
) : (
|
||||||
|
<Message type='error'>
|
||||||
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
|
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
|
||||||
<ul className='list-disc list-inside'>
|
<ul className='list-inside list-disc'>
|
||||||
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
|
</Message>
|
||||||
</Message> : <></>
|
)
|
||||||
}
|
) : (
|
||||||
<Formik initialValues={initialValues}
|
<></>
|
||||||
|
)}
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
validationSchema={AddBotSubmitSchema}
|
validationSchema={AddBotSubmitSchema}
|
||||||
onSubmit={() => setCaptcha(true)}>
|
onSubmit={() => setCaptcha(true)}
|
||||||
|
>
|
||||||
{({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => (
|
{({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<div className='py-3'>
|
<div className='py-3'>
|
||||||
<Message type='warning'>
|
<Message type='warning'>
|
||||||
<h2 className='text-lg font-extrabold'>신청하시기 전에 다음 사항을 확인해 주세요!</h2>
|
<h2 className='text-lg font-extrabold'>
|
||||||
<ul className='list-disc list-inside'>
|
신청하시기 전에 다음 사항을 확인해 주세요!
|
||||||
<li><Link
|
</h2>
|
||||||
|
<ul className='list-inside list-disc'>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
href='/discord'
|
href='/discord'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
className='text-blue-500 hover:text-blue-600'>디스코드 서버</Link>에 참가하셨나요?</li>
|
className='text-blue-500 hover:text-blue-600'
|
||||||
<li>봇이 <Link
|
>
|
||||||
|
디스코드 서버
|
||||||
|
</Link>
|
||||||
|
에 참가하셨나요?
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
봇이{' '}
|
||||||
|
<Link
|
||||||
href='/guidelines'
|
href='/guidelines'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
className='text-blue-500 hover:text-blue-600'>가이드라인</Link>을 지키고 있나요?</li>
|
className='text-blue-500 hover:text-blue-600'
|
||||||
<li>봇 소유자가 두 명 이상인가요? 봇 소유자는 봇이 승인된 뒤, 더 추가하실 수 있습니다.</li>
|
>
|
||||||
|
가이드라인
|
||||||
|
</Link>
|
||||||
|
을 지키고 있나요?
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
봇 소유자가 두 명 이상인가요? 봇 소유자는 봇이 승인된 뒤, 더 추가하실 수
|
||||||
|
있습니다.
|
||||||
|
</li>
|
||||||
<li>또한, 봇을 등록하게 되면 작성하신 모든 정보는 웹과 API에 공개됩니다.</li>
|
<li>또한, 봇을 등록하게 되면 작성하신 모든 정보는 웹과 API에 공개됩니다.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Message>
|
</Message>
|
||||||
</div>
|
</div>
|
||||||
<Label For='agree' error={errors.agree && touched.agree ? errors.agree : null} grid={false}>
|
<Label
|
||||||
|
For='agree'
|
||||||
|
error={errors.agree && touched.agree ? errors.agree : null}
|
||||||
|
grid={false}
|
||||||
|
>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<CheckBox name='agree' />
|
<CheckBox name='agree' />
|
||||||
<strong className='text-sm ml-2'>해당 내용을 숙지하였으며, 모두 이행하였고 위 내용에 해당하는 거부 사유는 답변받지 않는다는 점을 이해합니다.</strong>
|
<strong className='ml-2 text-sm'>
|
||||||
|
해당 내용을 숙지하였으며, 모두 이행하였고 위 내용에 해당하는 거부 사유는 답변받지
|
||||||
|
않는다는 점을 이해합니다.
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
<Label For='id' label='봇 ID' labelDesc='봇의 클라이언트 ID를 의미합니다.' error={errors.id && touched.id ? errors.id : null} short required>
|
<Label
|
||||||
|
For='id'
|
||||||
|
label='봇 ID'
|
||||||
|
labelDesc='봇의 클라이언트 ID를 의미합니다.'
|
||||||
|
error={errors.id && touched.id ? errors.id : null}
|
||||||
|
short
|
||||||
|
required
|
||||||
|
>
|
||||||
<Input name='id' placeholder='653534001742741552' />
|
<Input name='id' placeholder='653534001742741552' />
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='prefix' label='접두사' labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)' error={errors.prefix && touched.prefix ? errors.prefix : null} short required>
|
<Label
|
||||||
|
For='prefix'
|
||||||
|
label='접두사'
|
||||||
|
labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)'
|
||||||
|
error={errors.prefix && touched.prefix ? errors.prefix : null}
|
||||||
|
short
|
||||||
|
required
|
||||||
|
>
|
||||||
<Input name='prefix' placeholder='!' />
|
<Input name='prefix' placeholder='!' />
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='library' label='라이브러리' labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.' short required error={errors.library && touched.library ? errors.library : null}>
|
<Label
|
||||||
<Select options={library.map(el=> ({ label: el, value: el }))} handleChange={(value) => setFieldValue('library', value.value)} handleTouch={() => setFieldTouched('library', true)} />
|
For='library'
|
||||||
|
label='라이브러리'
|
||||||
|
labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.'
|
||||||
|
short
|
||||||
|
required
|
||||||
|
error={errors.library && touched.library ? errors.library : null}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={library.map((el) => ({ label: el, value: el }))}
|
||||||
|
handleChange={(value) => setFieldValue('library', value.value)}
|
||||||
|
handleTouch={() => setFieldTouched('library', true)}
|
||||||
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='category' label='카테고리' labelDesc='봇에 해당되는 카테고리를 선택해주세요' required error={errors.category && touched.category ? errors.category as string : null}>
|
<Label
|
||||||
<Selects options={botCategories.map(el=> ({ label: el, value: el }))} handleChange={(value) => {
|
For='category'
|
||||||
setFieldValue('category', value.map(v=> v.value))
|
label='카테고리'
|
||||||
}} handleTouch={() => setFieldTouched('category', true)} values={values.category as string[]} setValues={(value) => setFieldValue('category', value)} />
|
labelDesc='봇에 해당되는 카테고리를 선택해주세요'
|
||||||
<p className='text-gray-400 mt-1 text-sm'>봇 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요. <strong>반드시 해당되는 카테고리만 선택해주세요.</strong><br/>
|
required
|
||||||
<a className='text-blue-500 hover:text-blue-400' href='https://contents.koreanbots.dev/categories'>이곳</a>에서 카테고리에 관한 자세한 설명을 확인하실 수 있습니다!</p>
|
error={errors.category && touched.category ? (errors.category as string) : null}
|
||||||
|
>
|
||||||
|
<Selects
|
||||||
|
options={botCategories.map((el) => ({ label: el, value: el }))}
|
||||||
|
handleChange={(value) => {
|
||||||
|
setFieldValue(
|
||||||
|
'category',
|
||||||
|
value.map((v) => v.value)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
handleTouch={() => setFieldTouched('category', true)}
|
||||||
|
values={values.category as string[]}
|
||||||
|
setValues={(value) => setFieldValue('category', value)}
|
||||||
|
/>
|
||||||
|
<p className='mt-1 text-sm text-gray-400'>
|
||||||
|
봇 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요.{' '}
|
||||||
|
<strong>반드시 해당되는 카테고리만 선택해주세요.</strong>
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
className='text-blue-500 hover:text-blue-400'
|
||||||
|
href='https://contents.koreanbots.dev/categories'
|
||||||
|
>
|
||||||
|
이곳
|
||||||
|
</a>
|
||||||
|
에서 카테고리에 관한 자세한 설명을 확인하실 수 있습니다!
|
||||||
|
</p>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Label For='website' label='웹사이트' labelDesc='봇의 웹사이트를 작성해주세요.' error={errors.website && touched.website ? errors.website : null}>
|
<Label
|
||||||
|
For='website'
|
||||||
|
label='웹사이트'
|
||||||
|
labelDesc='봇의 웹사이트를 작성해주세요.'
|
||||||
|
error={errors.website && touched.website ? errors.website : null}
|
||||||
|
>
|
||||||
<Input name='website' placeholder='https://koreanbots.dev' />
|
<Input name='website' placeholder='https://koreanbots.dev' />
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='git' label='Git URL' labelDesc='봇 소스코드의 Git 주소를 입력해주세요. (오픈소스인 경우)' error={errors.git && touched.git ? errors.git : null}>
|
<Label
|
||||||
|
For='git'
|
||||||
|
label='Git URL'
|
||||||
|
labelDesc='봇 소스코드의 Git 주소를 입력해주세요. (오픈소스인 경우)'
|
||||||
|
error={errors.git && touched.git ? errors.git : null}
|
||||||
|
>
|
||||||
<Input name='git' placeholder='https://github.com/koreanbots/koreanbots' />
|
<Input name='git' placeholder='https://github.com/koreanbots/koreanbots' />
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='inviteLink' label='초대링크' labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.' error={errors.url && touched.url ? errors.url : null}>
|
<Label
|
||||||
<Input name='url' placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0' />
|
For='inviteLink'
|
||||||
<span className='text-gray-400 mt-1 text-sm'>
|
label='초대링크'
|
||||||
|
labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.'
|
||||||
|
error={errors.url && touched.url ? errors.url : null}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name='url'
|
||||||
|
placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0'
|
||||||
|
/>
|
||||||
|
<span className='mt-1 text-sm text-gray-400'>
|
||||||
<Link
|
<Link
|
||||||
href='/calculator'
|
href='/calculator'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
className='text-blue-500 hover:text-blue-400'>
|
className='text-blue-500 hover:text-blue-400'
|
||||||
|
>
|
||||||
이곳
|
이곳
|
||||||
</Link>에서 초대링크를 생성하실 수 있습니다!
|
</Link>
|
||||||
|
에서 초대링크를 생성하실 수 있습니다!
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='discord' label='지원 디스코드 서버' labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)' error={errors.discord && touched.discord ? errors.discord : null} short>
|
<Label
|
||||||
|
For='discord'
|
||||||
|
label='지원 디스코드 서버'
|
||||||
|
labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)'
|
||||||
|
error={errors.discord && touched.discord ? errors.discord : null}
|
||||||
|
short
|
||||||
|
>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
discord.gg/<Input name='discord' placeholder='JEh53MQ' />
|
discord.gg/
|
||||||
|
<Input name='discord' placeholder='JEh53MQ' />
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Label For='intro' label='봇 소개' labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required>
|
<Label
|
||||||
|
For='intro'
|
||||||
|
label='봇 소개'
|
||||||
|
labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)'
|
||||||
|
error={errors.intro && touched.intro ? errors.intro : null}
|
||||||
|
required
|
||||||
|
>
|
||||||
<Input name='intro' placeholder='국내 봇을 한 곳에서.' />
|
<Input name='intro' placeholder='국내 봇을 한 곳에서.' />
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='intro' label='봇 설명' labelDesc={<>봇을 자세하게 설명해주세요! (최대 1500자)<br/>마크다운을 지원합니다!</>} error={errors.desc && touched.desc ? errors.desc : null} required>
|
<Label
|
||||||
<TextArea max={1500} name='desc' placeholder='봇에 대해 최대한 자세히 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.desc} setValue={(value) => setFieldValue('desc', value)} />
|
For='intro'
|
||||||
|
label='봇 설명'
|
||||||
|
labelDesc={
|
||||||
|
<>
|
||||||
|
봇을 자세하게 설명해주세요! (최대 1500자)
|
||||||
|
<br />
|
||||||
|
마크다운을 지원합니다!
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
error={errors.desc && touched.desc ? errors.desc : null}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
max={1500}
|
||||||
|
name='desc'
|
||||||
|
placeholder='봇에 대해 최대한 자세히 설명해주세요!'
|
||||||
|
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||||
|
value={values.desc}
|
||||||
|
setValue={(value) => setFieldValue('desc', value)}
|
||||||
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다.'>
|
<Label
|
||||||
|
For='preview'
|
||||||
|
label='설명 미리보기'
|
||||||
|
labelDesc='다음 결과는 실제와 다를 수 있습니다.'
|
||||||
|
>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Markdown text={values.desc} />
|
<Markdown text={values.desc} />
|
||||||
</Segment>
|
</Segment>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
<p className='text-base mt-2 mb-5'>
|
<p className='mb-5 mt-2 text-base'>
|
||||||
<span className='text-red-500 font-semibold'> *</span> = 필수 항목
|
<span className='font-semibold text-red-500'> *</span> = 필수 항목
|
||||||
</p>
|
</p>
|
||||||
{
|
{captcha ? (
|
||||||
captcha ? <Captcha ref={captchaRef} dark={theme === 'dark'} onVerify={(token) => {
|
<Captcha
|
||||||
|
ref={captchaRef}
|
||||||
|
dark={theme === 'dark'}
|
||||||
|
onVerify={(token) => {
|
||||||
submitBot(values, token)
|
submitBot(values, token)
|
||||||
window.scrollTo({ top: 0 })
|
window.scrollTo({ top: 0 })
|
||||||
setCaptcha(false)
|
setCaptcha(false)
|
||||||
captchaRef?.current?.resetCaptcha()
|
captchaRef?.current?.resetCaptcha()
|
||||||
}} /> : <>
|
}}
|
||||||
{
|
/>
|
||||||
touchedSumbit && !isValid && <div className='my-1 text-red-500 text-xs font-light'>누락되거나 잘못된 항목이 있습니다. 다시 확인해주세요.</div>
|
) : (
|
||||||
}
|
<>
|
||||||
<Button type='submit' onClick={() => {
|
{touchedSumbit && !isValid && (
|
||||||
|
<div className='my-1 text-xs font-light text-red-500'>
|
||||||
|
누락되거나 잘못된 항목이 있습니다. 다시 확인해주세요.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
onClick={() => {
|
||||||
setTouched(true)
|
setTouched(true)
|
||||||
if (!isValid) window.scrollTo({ top: 0 })
|
if (!isValid) window.scrollTo({ top: 0 })
|
||||||
} }>
|
}}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
<i className='far fa-paper-plane' /> 제출
|
<i className='far fa-paper-plane' /> 제출
|
||||||
</>
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
@ -222,7 +397,13 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
|
|||||||
export const getServerSideProps = async (ctx: NextPageContext) => {
|
export const getServerSideProps = async (ctx: NextPageContext) => {
|
||||||
const parsed = parseCookie(ctx.req)
|
const parsed = parseCookie(ctx.req)
|
||||||
const user = await get.Authorization(parsed?.token)
|
const user = await get.Authorization(parsed?.token)
|
||||||
return { props: { logged: !!user, user: await get.user.load(user || ''), csrfToken: getToken(ctx.req, ctx.res) } }
|
return {
|
||||||
|
props: {
|
||||||
|
logged: !!user,
|
||||||
|
user: await get.user.load(user || ''),
|
||||||
|
csrfToken: getToken(ctx.req, ctx.res),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddBotProps {
|
interface AddBotProps {
|
||||||
|
|||||||
@ -30,7 +30,14 @@ const Container = dynamic(() => import('@components/Container'))
|
|||||||
const Message = dynamic(() => import('@components/Message'))
|
const Message = dynamic(() => import('@components/Message'))
|
||||||
const Captcha = dynamic(() => import('@components/Captcha'))
|
const Captcha = dynamic(() => import('@components/Captcha'))
|
||||||
|
|
||||||
const AddServer:NextPage<AddServerProps> = ({ logged, user, csrfToken, server, serverData, theme }) => {
|
const AddServer: NextPage<AddServerProps> = ({
|
||||||
|
logged,
|
||||||
|
user,
|
||||||
|
csrfToken,
|
||||||
|
server,
|
||||||
|
serverData,
|
||||||
|
theme,
|
||||||
|
}) => {
|
||||||
const [data, setData] = useState<ResponseProps<AddServerSubmit>>(null)
|
const [data, setData] = useState<ResponseProps<AddServerSubmit>>(null)
|
||||||
const [captcha, setCaptcha] = useState(false)
|
const [captcha, setCaptcha] = useState(false)
|
||||||
const [touchedSumbit, setTouched] = useState(false)
|
const [touchedSumbit, setTouched] = useState(false)
|
||||||
@ -58,7 +65,7 @@ const AddServer:NextPage<AddServerProps> = ({ logged, user, csrfToken, server, s
|
|||||||
- 있나요?`,
|
- 있나요?`,
|
||||||
category: [],
|
category: [],
|
||||||
_csrf: csrfToken,
|
_csrf: csrfToken,
|
||||||
_captcha: 'captcha'
|
_captcha: 'captcha',
|
||||||
}
|
}
|
||||||
|
|
||||||
function toLogin() {
|
function toLogin() {
|
||||||
@ -67,80 +74,146 @@ const AddServer:NextPage<AddServerProps> = ({ logged, user, csrfToken, server, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitServer(id: string, value: AddServerSubmit, token: string) {
|
async function submitServer(id: string, value: AddServerSubmit, token: string) {
|
||||||
const res = await Fetch<AddServerSubmit>(`/servers/${id}`, { method: 'POST', body: JSON.stringify(cleanObject<AddServerSubmit>({ ...value, _captcha: token })) })
|
const res = await Fetch<AddServerSubmit>(`/servers/${id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(cleanObject<AddServerSubmit>({ ...value, _captcha: token })),
|
||||||
|
})
|
||||||
setData(res)
|
setData(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!logged) return <Login>
|
if (!logged)
|
||||||
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
return (
|
||||||
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
<Login>
|
||||||
}} />
|
<NextSeo
|
||||||
|
title='새로운 서버 추가하기'
|
||||||
|
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||||
|
openGraph={{
|
||||||
|
title: '새로운 서버 추가하기',
|
||||||
|
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Login>
|
</Login>
|
||||||
if(data?.data && data.code == 200) {
|
|
||||||
setTimeout(
|
|
||||||
() => redirectTo(router, `/servers/${router.query.id}`),
|
|
||||||
1_000
|
|
||||||
)
|
)
|
||||||
|
if (data?.data && data.code == 200) {
|
||||||
|
setTimeout(() => redirectTo(router, `/servers/${router.query.id}`), 1_000)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Container paddingTop className='py-5'>
|
<Container paddingTop className='py-5'>
|
||||||
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
<NextSeo
|
||||||
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
title='새로운 서버 추가하기'
|
||||||
}} />
|
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||||
|
openGraph={{
|
||||||
|
title: '새로운 서버 추가하기',
|
||||||
|
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<h1 className='text-3xl font-bold'>새로운 서버 추가하기</h1>
|
<h1 className='text-3xl font-bold'>새로운 서버 추가하기</h1>
|
||||||
<div className='mt-1 mb-5'>
|
<div className='mb-5 mt-1'>
|
||||||
안녕하세요, <span className='font-semibold'>{user.username}#{user.tag}</span>님! <a role='button' tabIndex={0} onKeyDown={toLogin} onClick={toLogin} className='text-discord-blurple cursor-pointer outline-none'>본인이 아니신가요?</a>
|
안녕하세요,{' '}
|
||||||
|
<span className='font-semibold'>
|
||||||
|
{user.username}#{user.tag}
|
||||||
|
</span>
|
||||||
|
님!{' '}
|
||||||
|
<a
|
||||||
|
role='button'
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={toLogin}
|
||||||
|
onClick={toLogin}
|
||||||
|
className='cursor-pointer text-discord-blurple outline-none'
|
||||||
|
>
|
||||||
|
본인이 아니신가요?
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{
|
{data ? (
|
||||||
data ? data.code == 200 && data.data ? <Message type='success'>
|
data.code == 200 && data.data ? (
|
||||||
|
<Message type='success'>
|
||||||
<h2 className='text-lg font-extrabold'>서버 등록 성공!</h2>
|
<h2 className='text-lg font-extrabold'>서버 등록 성공!</h2>
|
||||||
<p>서버를 성공적으로 등록했습니다! 서버 페이지로 리다이렉트 됩니다!</p>
|
<p>서버를 성공적으로 등록했습니다! 서버 페이지로 리다이렉트 됩니다!</p>
|
||||||
</Message> : <Message type='error'>
|
</Message>
|
||||||
|
) : (
|
||||||
|
<Message type='error'>
|
||||||
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
|
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
|
||||||
<ul className='list-disc list-inside'>
|
<ul className='list-inside list-disc'>
|
||||||
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
|
</Message>
|
||||||
</Message> : <></>
|
)
|
||||||
}
|
) : (
|
||||||
{
|
<></>
|
||||||
server ? <Message type='warning'>
|
)}
|
||||||
|
{server ? (
|
||||||
|
<Message type='warning'>
|
||||||
<h2 className='text-lg font-extrabold'>이미 등록된 서버입니다.</h2>
|
<h2 className='text-lg font-extrabold'>이미 등록된 서버입니다.</h2>
|
||||||
</Message> :
|
</Message>
|
||||||
!serverData ? <Message type='info'>
|
) : !serverData ? (
|
||||||
|
<Message type='info'>
|
||||||
<h2 className='text-lg font-extrabold'>서버에 봇이 초대되지 않았습니다.</h2>
|
<h2 className='text-lg font-extrabold'>서버에 봇이 초대되지 않았습니다.</h2>
|
||||||
<p>서버를 등록하시려면 먼저 봇을 초대해야합니다.</p>
|
<p>서버를 등록하시려면 먼저 봇을 초대해야합니다.</p>
|
||||||
<p>서버에 이미 봇이 초대되었다면 반영까지 최대 1분이 소요될 수 있습니다.</p>
|
<p>서버에 이미 봇이 초대되었다면 반영까지 최대 1분이 소요될 수 있습니다.</p>
|
||||||
</Message>
|
</Message>
|
||||||
: serverData.admins.includes(user.id) || serverData.owner.includes(user.id) ? <Formik initialValues={initialValues}
|
) : serverData.admins.includes(user.id) || serverData.owner.includes(user.id) ? (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
validationSchema={AddServerSubmitSchema}
|
validationSchema={AddServerSubmitSchema}
|
||||||
onSubmit={() => setCaptcha(true)}>
|
onSubmit={() => setCaptcha(true)}
|
||||||
|
>
|
||||||
{({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => (
|
{({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<div className='py-3'>
|
<div className='py-3'>
|
||||||
<Message type='warning'>
|
<Message type='warning'>
|
||||||
<h2 className='text-lg font-extrabold'>등록하시기 전에 다음 사항을 확인해 주세요!</h2>
|
<h2 className='text-lg font-extrabold'>
|
||||||
<ul className='list-disc list-inside'>
|
등록하시기 전에 다음 사항을 확인해 주세요!
|
||||||
<li><Link
|
</h2>
|
||||||
|
<ul className='list-inside list-disc'>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
href='/discord'
|
href='/discord'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
className='text-blue-500 hover:text-blue-600'>디스코드 서버</Link>에 참여를 권장드립니다.</li>
|
className='text-blue-500 hover:text-blue-600'
|
||||||
<li>서버가 <Link
|
>
|
||||||
|
디스코드 서버
|
||||||
|
</Link>
|
||||||
|
에 참여를 권장드립니다.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
서버가{' '}
|
||||||
|
<Link
|
||||||
href='/guidelines'
|
href='/guidelines'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
className='text-blue-500 hover:text-blue-600'>가이드라인</Link>을 지키고 있나요?</li>
|
className='text-blue-500 hover:text-blue-600'
|
||||||
<li>서버에서 <strong>관리자</strong> 권한을 갖고 있는 모든 분은 삭제를 제외한 모든 행동을 할 수 있습니다.</li>
|
>
|
||||||
<li>서버를 등록한 이후 봇을 추방하시게 되면 서버 정보가 웹에 업데이트 되지 않습니다.</li>
|
가이드라인
|
||||||
<li>또한, 서버를 등록하게 되면 작성하신 모든 정보와 서버에서 수집된 정보는 웹과 API에 공개됩니다.</li>
|
</Link>
|
||||||
|
을 지키고 있나요?
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
서버에서 <strong>관리자</strong> 권한을 갖고 있는 모든 분은 삭제를 제외한 모든
|
||||||
|
행동을 할 수 있습니다.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
서버를 등록한 이후 봇을 추방하시게 되면 서버 정보가 웹에 업데이트 되지
|
||||||
|
않습니다.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
또한, 서버를 등록하게 되면 작성하신 모든 정보와 서버에서 수집된 정보는 웹과
|
||||||
|
API에 공개됩니다.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Message>
|
</Message>
|
||||||
</div>
|
</div>
|
||||||
<Label For='agree' error={errors.agree && touched.agree ? errors.agree : null} grid={false}>
|
<Label
|
||||||
|
For='agree'
|
||||||
|
error={errors.agree && touched.agree ? errors.agree : null}
|
||||||
|
grid={false}
|
||||||
|
>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<CheckBox name='agree' />
|
<CheckBox name='agree' />
|
||||||
<strong className='text-sm ml-2'>해당 내용을 숙지하였으며, 등록 이후에 가이드라인을 위반할시 서버가 웹에서 삭제될 수 있다는 점을 확인했습니다.</strong>
|
<strong className='ml-2 text-sm'>
|
||||||
|
해당 내용을 숙지하였으며, 등록 이후에 가이드라인을 위반할시 서버가 웹에서 삭제될
|
||||||
|
수 있다는 점을 확인했습니다.
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -151,58 +224,125 @@ const AddServer:NextPage<AddServerProps> = ({ logged, user, csrfToken, server, s
|
|||||||
</p>
|
</p>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Label For='category' label='카테고리' labelDesc='서버에 해당되는 카테고리를 선택해주세요' required error={errors.category && touched.category ? errors.category as string : null}>
|
<Label
|
||||||
<Selects options={serverCategories.map(el=> ({ label: el, value: el }))} handleChange={(value) => {
|
For='category'
|
||||||
setFieldValue('category', value.map(v=> v.value))
|
label='카테고리'
|
||||||
}} handleTouch={() => setFieldTouched('category', true)} values={values.category as string[]} setValues={(value) => setFieldValue('category', value)} />
|
labelDesc='서버에 해당되는 카테고리를 선택해주세요'
|
||||||
<span className='text-gray-400 mt-1 text-sm'>서버 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요. <strong>반드시 해당되는 카테고리만 선택해주세요.</strong></span>
|
required
|
||||||
|
error={errors.category && touched.category ? (errors.category as string) : null}
|
||||||
|
>
|
||||||
|
<Selects
|
||||||
|
options={serverCategories.map((el) => ({ label: el, value: el }))}
|
||||||
|
handleChange={(value) => {
|
||||||
|
setFieldValue(
|
||||||
|
'category',
|
||||||
|
value.map((v) => v.value)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
handleTouch={() => setFieldTouched('category', true)}
|
||||||
|
values={values.category as string[]}
|
||||||
|
setValues={(value) => setFieldValue('category', value)}
|
||||||
|
/>
|
||||||
|
<span className='mt-1 text-sm text-gray-400'>
|
||||||
|
서버 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요.{' '}
|
||||||
|
<strong>반드시 해당되는 카테고리만 선택해주세요.</strong>
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='invite' label='서버 초대코드' labelDesc='서버의 초대코드를 입력해주세요. (만료되지 않는 코드로 입력해주세요!)' error={errors.invite && touched.invite ? errors.invite : null} short required>
|
<Label
|
||||||
|
For='invite'
|
||||||
|
label='서버 초대코드'
|
||||||
|
labelDesc='서버의 초대코드를 입력해주세요. (만료되지 않는 코드로 입력해주세요!)'
|
||||||
|
error={errors.invite && touched.invite ? errors.invite : null}
|
||||||
|
short
|
||||||
|
required
|
||||||
|
>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
discord.gg/<Input name='invite' placeholder='JEh53MQ' />
|
discord.gg/
|
||||||
|
<Input name='invite' placeholder='JEh53MQ' />
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Label For='intro' label='서버 소개' labelDesc='서버를 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required>
|
<Label
|
||||||
|
For='intro'
|
||||||
|
label='서버 소개'
|
||||||
|
labelDesc='서버를 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)'
|
||||||
|
error={errors.intro && touched.intro ? errors.intro : null}
|
||||||
|
required
|
||||||
|
>
|
||||||
<Input name='intro' placeholder={getRandom(ServerIntroList)} />
|
<Input name='intro' placeholder={getRandom(ServerIntroList)} />
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='desc' label='서버 설명' labelDesc={<>서버를 자세하게 설명해주세요! (최대 1500자)<br/>마크다운을 지원합니다!</>} error={errors.desc && touched.desc ? errors.desc : null} required>
|
<Label
|
||||||
<TextArea max={1500} name='desc' placeholder='서버에 대해 최대한 자세히 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.desc} setValue={(value) => setFieldValue('desc', value)} />
|
For='desc'
|
||||||
|
label='서버 설명'
|
||||||
|
labelDesc={
|
||||||
|
<>
|
||||||
|
서버를 자세하게 설명해주세요! (최대 1500자)
|
||||||
|
<br />
|
||||||
|
마크다운을 지원합니다!
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
error={errors.desc && touched.desc ? errors.desc : null}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
max={1500}
|
||||||
|
name='desc'
|
||||||
|
placeholder='서버에 대해 최대한 자세히 설명해주세요!'
|
||||||
|
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||||
|
value={values.desc}
|
||||||
|
setValue={(value) => setFieldValue('desc', value)}
|
||||||
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다.'>
|
<Label
|
||||||
|
For='preview'
|
||||||
|
label='설명 미리보기'
|
||||||
|
labelDesc='다음 결과는 실제와 다를 수 있습니다.'
|
||||||
|
>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Markdown text={values.desc} />
|
<Markdown text={values.desc} />
|
||||||
</Segment>
|
</Segment>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
<p className='text-base mt-2 mb-5'>
|
<p className='mb-5 mt-2 text-base'>
|
||||||
<span className='text-red-500 font-semibold'> *</span> = 필수 항목
|
<span className='font-semibold text-red-500'> *</span> = 필수 항목
|
||||||
</p>
|
</p>
|
||||||
{
|
{captcha ? (
|
||||||
captcha ? <Captcha ref={captchaRef} dark={theme === 'dark'} onVerify={(token) => {
|
<Captcha
|
||||||
|
ref={captchaRef}
|
||||||
|
dark={theme === 'dark'}
|
||||||
|
onVerify={(token) => {
|
||||||
submitServer(router.query.id as string, values, token)
|
submitServer(router.query.id as string, values, token)
|
||||||
window.scrollTo({ top: 0 })
|
window.scrollTo({ top: 0 })
|
||||||
setCaptcha(false)
|
setCaptcha(false)
|
||||||
captchaRef?.current?.resetCaptcha()
|
captchaRef?.current?.resetCaptcha()
|
||||||
}} /> : <>
|
}}
|
||||||
{
|
/>
|
||||||
touchedSumbit && !isValid && <div className='my-1 text-red-500 text-xs font-light'>누락되거나 잘못된 항목이 있습니다. 다시 확인해주세요.</div>
|
) : (
|
||||||
}
|
<>
|
||||||
<Button type='submit' onClick={() => {
|
{touchedSumbit && !isValid && (
|
||||||
|
<div className='my-1 text-xs font-light text-red-500'>
|
||||||
|
누락되거나 잘못된 항목이 있습니다. 다시 확인해주세요.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
onClick={() => {
|
||||||
setTouched(true)
|
setTouched(true)
|
||||||
if (!isValid) window.scrollTo({ top: 0 })
|
if (!isValid) window.scrollTo({ top: 0 })
|
||||||
} }>
|
}}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
<i className='far fa-paper-plane' /> 등록
|
<i className='far fa-paper-plane' /> 등록
|
||||||
</>
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
: <Forbidden />
|
) : (
|
||||||
}
|
<Forbidden />
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -212,12 +352,16 @@ export const getServerSideProps = async (ctx: NextPageContext) => {
|
|||||||
const user = await get.Authorization(parsed?.token)
|
const user = await get.Authorization(parsed?.token)
|
||||||
const server = (await get.server.load(ctx.query.id as string)) || null
|
const server = (await get.server.load(ctx.query.id as string)) || null
|
||||||
const serverData = (await get.serverData(ctx.query.id as string)) || null
|
const serverData = (await get.serverData(ctx.query.id as string)) || null
|
||||||
return { props: {
|
return {
|
||||||
logged: !!user, user: await get.user.load(user || ''),
|
props: {
|
||||||
|
logged: !!user,
|
||||||
|
user: await get.user.load(user || ''),
|
||||||
csrfToken: getToken(ctx.req, ctx.res),
|
csrfToken: getToken(ctx.req, ctx.res),
|
||||||
server,
|
server,
|
||||||
serverData: (+new Date() - +new Date(serverData?.updatedAt)) < 2 * 60 * 1000 ? serverData : null
|
serverData:
|
||||||
} }
|
+new Date() - +new Date(serverData?.updatedAt) < 2 * 60 * 1000 ? serverData : null,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddServerProps {
|
interface AddServerProps {
|
||||||
|
|||||||
@ -13,39 +13,70 @@ const Login = dynamic(() => import('@components/Login'))
|
|||||||
const Container = dynamic(() => import('@components/Container'))
|
const Container = dynamic(() => import('@components/Container'))
|
||||||
|
|
||||||
const AddBot: NextPage<AddBotProps> = ({ logged, guilds }) => {
|
const AddBot: NextPage<AddBotProps> = ({ logged, guilds }) => {
|
||||||
if(!logged) return <Login>
|
if (!logged)
|
||||||
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
return (
|
||||||
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
<Login>
|
||||||
}} />
|
<NextSeo
|
||||||
|
title='새로운 서버 추가하기'
|
||||||
|
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||||
|
openGraph={{
|
||||||
|
title: '새로운 서버 추가하기',
|
||||||
|
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Login>
|
</Login>
|
||||||
return <Container paddingTop className='py-5'>
|
)
|
||||||
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
return (
|
||||||
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
<Container paddingTop className='py-5'>
|
||||||
}} />
|
<NextSeo
|
||||||
|
title='새로운 서버 추가하기'
|
||||||
|
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||||
|
openGraph={{
|
||||||
|
title: '새로운 서버 추가하기',
|
||||||
|
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<h1 className='text-3xl font-bold'>새로운 서버 추가하기</h1>
|
<h1 className='text-3xl font-bold'>새로운 서버 추가하기</h1>
|
||||||
<p className='text-gray-400'>관리자이신 서버 목록입니다.</p>
|
<p className='text-gray-400'>관리자이신 서버 목록입니다.</p>
|
||||||
<p className='text-gray-400 pb-5'>봇을 초대한 뒤 새로고침 해주세요. 또한, 반영까지 최대 1분이 소요될 수 있습니다.</p>
|
<p className='pb-5 text-gray-400'>
|
||||||
|
봇을 초대한 뒤 새로고침 해주세요. 또한, 반영까지 최대 1분이 소요될 수 있습니다.
|
||||||
|
</p>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
<ResponsiveGrid>
|
<ResponsiveGrid>
|
||||||
{
|
{guilds
|
||||||
guilds.sort((a ,b) => (+!!b.data || 0) - (+!!a.data || 0)).map(g => (
|
.sort((a, b) => (+!!b.data || 0) - (+!!a.data || 0))
|
||||||
|
.map((g) => (
|
||||||
<ServerCard type={g.exists ? 'manage' : 'add'} server={g} key={g.id} />
|
<ServerCard type={g.exists ? 'manage' : 'add'} server={g} key={g.id} />
|
||||||
))
|
))}
|
||||||
}
|
|
||||||
</ResponsiveGrid>
|
</ResponsiveGrid>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
</Container>
|
</Container>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (ctx: NextPageContext) => {
|
export const getServerSideProps = async (ctx: NextPageContext) => {
|
||||||
const parsed = parseCookie(ctx.req)
|
const parsed = parseCookie(ctx.req)
|
||||||
const user = await get.Authorization(parsed?.token)
|
const user = await get.Authorization(parsed?.token)
|
||||||
const guilds = (await get.userGuilds.load(user || ''))?.filter(g=> (g.permissions & 8) || g.owner).map(async g => {
|
const guilds = (await get.userGuilds.load(user || ''))
|
||||||
const server = (await get.server.load(g.id))
|
?.filter((g) => g.permissions & 8 || g.owner)
|
||||||
|
.map(async (g) => {
|
||||||
|
const server = await get.server.load(g.id)
|
||||||
const data = await get.serverData(g.id)
|
const data = await get.serverData(g.id)
|
||||||
return { ...g, ...(server || {}), ...((+new Date() - +new Date(data?.updatedAt)) < 2 * 60 * 1000 ? { data } : {}), members: data?.memberCount || null, exists: !!server }
|
return {
|
||||||
|
...g,
|
||||||
|
...(server || {}),
|
||||||
|
...(+new Date() - +new Date(data?.updatedAt) < 2 * 60 * 1000 ? { data } : {}),
|
||||||
|
members: data?.memberCount || null,
|
||||||
|
exists: !!server,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return { props: { logged: !!user || !!guilds, user: await get.user.load(user || ''), guilds: guilds ? (await Promise.all(guilds)).filter(g => !g?.exists) : null } }
|
return {
|
||||||
|
props: {
|
||||||
|
logged: !!user || !!guilds,
|
||||||
|
user: await get.user.load(user || ''),
|
||||||
|
guilds: guilds ? (await Promise.all(guilds)).filter((g) => !g?.exists) : null,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddBotProps {
|
interface AddBotProps {
|
||||||
@ -53,7 +84,7 @@ interface AddBotProps {
|
|||||||
user: User
|
user: User
|
||||||
csrfToken: string
|
csrfToken: string
|
||||||
theme: Theme
|
theme: Theme
|
||||||
guilds: (RawGuild & { data: ServerData, exists?: boolean })[]
|
guilds: (RawGuild & { data: ServerData; exists?: boolean })[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddBot
|
export default AddBot
|
||||||
@ -13,8 +13,8 @@ import RequestHandler from '@utils/RequestHandler'
|
|||||||
|
|
||||||
const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
const validate = await OauthCallbackSchema.validate(req.query)
|
const validate = await OauthCallbackSchema.validate(req.query)
|
||||||
.then(r => r)
|
.then((r) => r)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -35,7 +35,7 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
}).then(r => r.json())
|
}).then((r) => r.json())
|
||||||
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
|
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
|
||||||
|
|
||||||
const user: DiscordUserInfo = await fetch(DiscordEnpoints.Me, {
|
const user: DiscordUserInfo = await fetch(DiscordEnpoints.Me, {
|
||||||
@ -43,7 +43,7 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `${token.token_type} ${token.access_token}`,
|
Authorization: `${token.token_type} ${token.access_token}`,
|
||||||
},
|
},
|
||||||
}).then(r => r.json())
|
}).then((r) => r.json())
|
||||||
|
|
||||||
const userToken = await update.assignToken({
|
const userToken = await update.assignToken({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@ -53,11 +53,13 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
discriminator: user.discriminator,
|
discriminator: user.discriminator,
|
||||||
verified: user.verified
|
verified: user.verified,
|
||||||
})
|
})
|
||||||
|
|
||||||
if(userToken === 1) return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/unverified')
|
if (userToken === 1)
|
||||||
else if(userToken === 2) return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/blocked')
|
return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/unverified')
|
||||||
|
else if (userToken === 2)
|
||||||
|
return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/blocked')
|
||||||
const info = verify(userToken)
|
const info = verify(userToken)
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
'set-cookie',
|
'set-cookie',
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import RequestHandler from '@utils/RequestHandler'
|
|||||||
|
|
||||||
const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
const validate = await OauthCallbackSchema.validate(req.query)
|
const validate = await OauthCallbackSchema.validate(req.query)
|
||||||
.then(r => r)
|
.then((r) => r)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -20,21 +20,29 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
|||||||
|
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
const token: GithubTokenInfo = await fetch(SpecialEndPoints.Github.Token(process.env.GITHUB_CLIENT_ID, process.env.GITHUB_CLIENT_SECRET,req.query.code), {
|
const token: GithubTokenInfo = await fetch(
|
||||||
|
SpecialEndPoints.Github.Token(
|
||||||
|
process.env.GITHUB_CLIENT_ID,
|
||||||
|
process.env.GITHUB_CLIENT_SECRET,
|
||||||
|
req.query.code
|
||||||
|
),
|
||||||
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
},
|
||||||
}).then(r => r.json())
|
}
|
||||||
|
).then((r) => r.json())
|
||||||
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
|
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
|
||||||
|
|
||||||
const github: { login: string } = await fetch(SpecialEndPoints.Github.Me, {
|
const github: { login: string } = await fetch(SpecialEndPoints.Github.Me, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `token ${token.access_token}`
|
Authorization: `token ${token.access_token}`,
|
||||||
}
|
},
|
||||||
}).then(r => r.json())
|
}).then((r) => r.json())
|
||||||
const result = await update.Github(user, github.login)
|
const result = await update.Github(user, github.login)
|
||||||
if(result === 0) return ResponseWrapper(res, { code: 400, message: '이미 등록되어있는 깃허브 계정입니다.' })
|
if (result === 0)
|
||||||
|
return ResponseWrapper(res, { code: 400, message: '이미 등록되어있는 깃허브 계정입니다.' })
|
||||||
get.user.clear(user)
|
get.user.clear(user)
|
||||||
res.redirect(301, '/panel')
|
res.redirect(301, '/panel')
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,11 +5,9 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
|||||||
import { get, update } from '@utils/Query'
|
import { get, update } from '@utils/Query'
|
||||||
import { checkToken } from '@utils/Csrf'
|
import { checkToken } from '@utils/Csrf'
|
||||||
|
|
||||||
const Github = RequestHandler().get(async (_req: NextApiRequest, res: NextApiResponse) => {
|
const Github = RequestHandler()
|
||||||
res.redirect(
|
.get(async (_req: NextApiRequest, res: NextApiResponse) => {
|
||||||
301,
|
res.redirect(301, generateOauthURL('github', process.env.GITHUB_CLIENT_ID))
|
||||||
generateOauthURL('github', process.env.GITHUB_CLIENT_ID)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.delete(async (req: DeleteApiRequest, res) => {
|
.delete(async (req: DeleteApiRequest, res) => {
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
|
|||||||
@ -11,7 +11,9 @@ const rateLimiter = rateLimit({
|
|||||||
windowMs: 60 * 1000,
|
windowMs: 60 * 1000,
|
||||||
max: 150,
|
max: 150,
|
||||||
handler: async (_req, res) => {
|
handler: async (_req, res) => {
|
||||||
const img = await get.images.user.load(DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' }))
|
const img = await get.images.user.load(
|
||||||
|
DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' })
|
||||||
|
)
|
||||||
res.setHeader('Content-Type', 'image/png')
|
res.setHeader('Content-Type', 'image/png')
|
||||||
res.setHeader('Cache-Control', 'no-cache')
|
res.setHeader('Cache-Control', 'no-cache')
|
||||||
res.send(img)
|
res.send(img)
|
||||||
@ -20,7 +22,7 @@ const rateLimiter = rateLimit({
|
|||||||
skip: (_req, res) => {
|
skip: (_req, res) => {
|
||||||
res.removeHeader('X-RateLimit-Global')
|
res.removeHeader('X-RateLimit-Global')
|
||||||
return false
|
return false
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const Avatar = RequestHandler()
|
const Avatar = RequestHandler()
|
||||||
@ -31,7 +33,9 @@ const Avatar = RequestHandler()
|
|||||||
const splitted = param.split('.')
|
const splitted = param.split('.')
|
||||||
let ext = splitted[1]
|
let ext = splitted[1]
|
||||||
const id = splitted[0]
|
const id = splitted[0]
|
||||||
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false }).then(el=> el).catch(e=> {
|
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false })
|
||||||
|
.then((el) => el)
|
||||||
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -39,21 +43,31 @@ const Avatar = RequestHandler()
|
|||||||
|
|
||||||
const user = await get.discord.user.load(id)
|
const user = await get.discord.user.load(id)
|
||||||
let img: Buffer
|
let img: Buffer
|
||||||
if(!user?.avatar) img = await get.images.user.load(DiscordEnpoints.CDN.default(user?.discriminator ? Number(user.discriminator) % 5 : Math.floor(Math.random() * 6), { format: 'png', size: validated.size }))
|
if (!user?.avatar)
|
||||||
else img = await get.images.user.load(DiscordEnpoints.CDN.user(id, user.avatar, { format: validated.ext === 'gif' && !user.avatar.startsWith('a_') ? 'png' : validated.ext }))
|
img = await get.images.user.load(
|
||||||
|
DiscordEnpoints.CDN.default(
|
||||||
|
user?.discriminator ? Number(user.discriminator) % 5 : Math.floor(Math.random() * 6),
|
||||||
|
{ format: 'png', size: validated.size }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
img = await get.images.user.load(
|
||||||
|
DiscordEnpoints.CDN.user(id, user.avatar, {
|
||||||
|
format: validated.ext === 'gif' && !user.avatar.startsWith('a_') ? 'png' : validated.ext,
|
||||||
|
})
|
||||||
|
)
|
||||||
if (!img) {
|
if (!img) {
|
||||||
img = await get.images.user.load(DiscordEnpoints.CDN.default(user.discriminator, { format: 'png', size: validated.size }))
|
img = await get.images.user.load(
|
||||||
|
DiscordEnpoints.CDN.default(user.discriminator, { format: 'png', size: validated.size })
|
||||||
|
)
|
||||||
ext = 'png'
|
ext = 'png'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', `image/${ext}`)
|
res.setHeader('Content-Type', `image/${ext}`)
|
||||||
res.setHeader('Cache-Control', 'public, max-age=86400')
|
res.setHeader('Cache-Control', 'public, max-age=86400')
|
||||||
res.send(img)
|
res.send(img)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
query: {
|
query: {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@ -11,7 +11,9 @@ const rateLimiter = rateLimit({
|
|||||||
windowMs: 60 * 1000,
|
windowMs: 60 * 1000,
|
||||||
max: 150,
|
max: 150,
|
||||||
handler: async (_req, res) => {
|
handler: async (_req, res) => {
|
||||||
const img = await get.images.server.load(DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' }))
|
const img = await get.images.server.load(
|
||||||
|
DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' })
|
||||||
|
)
|
||||||
res.setHeader('Content-Type', 'image/png')
|
res.setHeader('Content-Type', 'image/png')
|
||||||
res.setHeader('Cache-Control', 'no-cache')
|
res.setHeader('Cache-Control', 'no-cache')
|
||||||
res.send(img)
|
res.send(img)
|
||||||
@ -20,7 +22,7 @@ const rateLimiter = rateLimit({
|
|||||||
skip: (_req, res) => {
|
skip: (_req, res) => {
|
||||||
res.removeHeader('X-RateLimit-Global')
|
res.removeHeader('X-RateLimit-Global')
|
||||||
return false
|
return false
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const Icon = RequestHandler()
|
const Icon = RequestHandler()
|
||||||
@ -31,7 +33,9 @@ const Icon = RequestHandler()
|
|||||||
const splitted = param.split('.')
|
const splitted = param.split('.')
|
||||||
let ext = splitted[1]
|
let ext = splitted[1]
|
||||||
const id = splitted[0]
|
const id = splitted[0]
|
||||||
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false }).then(el=> el).catch(e=> {
|
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false })
|
||||||
|
.then((el) => el)
|
||||||
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -40,20 +44,24 @@ const Icon = RequestHandler()
|
|||||||
const guild = await get.server.load(id)
|
const guild = await get.server.load(id)
|
||||||
let img: Buffer
|
let img: Buffer
|
||||||
if (!guild?.icon) img = await get.images.server.load(DiscordEnpoints.CDN.default(+id % 4))
|
if (!guild?.icon) img = await get.images.server.load(DiscordEnpoints.CDN.default(+id % 4))
|
||||||
else img = await get.images.server.load(DiscordEnpoints.CDN.guild(id, guild.icon, { format: validated.ext === 'gif' && !guild.icon.startsWith('a_') ? 'png' : validated.ext }))
|
else
|
||||||
|
img = await get.images.server.load(
|
||||||
|
DiscordEnpoints.CDN.guild(id, guild.icon, {
|
||||||
|
format: validated.ext === 'gif' && !guild.icon.startsWith('a_') ? 'png' : validated.ext,
|
||||||
|
})
|
||||||
|
)
|
||||||
if (!img) {
|
if (!img) {
|
||||||
img = await get.images.server.load(DiscordEnpoints.CDN.default(+id % 4, { format: 'png', size: validated.size }))
|
img = await get.images.server.load(
|
||||||
|
DiscordEnpoints.CDN.default(+id % 4, { format: 'png', size: validated.size })
|
||||||
|
)
|
||||||
ext = 'png'
|
ext = 'png'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', `image/${ext}`)
|
res.setHeader('Content-Type', `image/${ext}`)
|
||||||
res.setHeader('Cache-Control', 'public, max-age=86400')
|
res.setHeader('Cache-Control', 'public, max-age=86400')
|
||||||
res.send(img)
|
res.send(img)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
query: {
|
query: {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const limiter = rateLimit({
|
|||||||
res.removeHeader('X-RateLimit-Global')
|
res.removeHeader('X-RateLimit-Global')
|
||||||
if (!req.headers.authorization) return true
|
if (!req.headers.authorization) return true
|
||||||
else return false
|
else return false
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const BotStats = RequestHandler()
|
const BotStats = RequestHandler()
|
||||||
@ -28,28 +28,50 @@ const BotStats = RequestHandler()
|
|||||||
.post(async (req: PostApiRequest, res) => {
|
.post(async (req: PostApiRequest, res) => {
|
||||||
const bot = await get.BotAuthorization(req.headers.token)
|
const bot = await get.BotAuthorization(req.headers.token)
|
||||||
if (!bot) return ResponseWrapper(res, { code: 401, version: 1 })
|
if (!bot) return ResponseWrapper(res, { code: 401, version: 1 })
|
||||||
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, { abortEarly: false })
|
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, {
|
||||||
.then(el => el)
|
abortEarly: false,
|
||||||
.catch(e => {
|
})
|
||||||
|
.then((el) => el)
|
||||||
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
const botInfo = await get.bot.load(bot)
|
const botInfo = await get.bot.load(bot)
|
||||||
if(!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.', version: 1 })
|
if (!botInfo)
|
||||||
|
return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.', version: 1 })
|
||||||
if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403, version: 1 })
|
if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403, version: 1 })
|
||||||
const d = await update.updateServer(botInfo.id, validated.servers, undefined)
|
const d = await update.updateServer(botInfo.id, validated.servers, undefined)
|
||||||
if(d===1 || d===2) return ResponseWrapper(res, { code: 403, message: `서버 수를 ${[null, '1만', '100만'][d]} 이상으로 설정하실 수 없습니다. 문의해주세요.`, version: 1 })
|
if (d === 1 || d === 2)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 403,
|
||||||
|
message: `서버 수를 ${
|
||||||
|
[null, '1만', '100만'][d]
|
||||||
|
} 이상으로 설정하실 수 없습니다. 문의해주세요.`,
|
||||||
|
version: 1,
|
||||||
|
})
|
||||||
get.bot.clear(bot)
|
get.bot.clear(bot)
|
||||||
await webhookClients.internal.statsLog.send({
|
await webhookClients.internal.statsLog.send({
|
||||||
content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(`${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${validated.servers} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(validated.servers - botInfo.servers)})`, 'diff')}`,
|
content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(
|
||||||
embeds: [new EmbedBuilder().setDescription(`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(botInfo.id)}`)]
|
`${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${
|
||||||
|
validated.servers
|
||||||
|
} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(
|
||||||
|
validated.servers - botInfo.servers
|
||||||
|
)})`,
|
||||||
|
'diff'
|
||||||
|
)}`,
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder().setDescription(
|
||||||
|
`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(
|
||||||
|
botInfo.id
|
||||||
|
)}`
|
||||||
|
),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.', version: 1 })
|
return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.', version: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
interface PostApiRequest extends NextApiRequest {
|
interface PostApiRequest extends NextApiRequest {
|
||||||
headers: {
|
headers: {
|
||||||
token: string
|
token: string
|
||||||
|
|||||||
@ -5,11 +5,14 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
|||||||
import Yup from '@utils/Yup'
|
import Yup from '@utils/Yup'
|
||||||
import { VOTE_COOLDOWN } from '@utils/Constants'
|
import { VOTE_COOLDOWN } from '@utils/Constants'
|
||||||
|
|
||||||
const BotVoted = RequestHandler()
|
const BotVoted = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
.get(async (req: ApiRequest, res) => {
|
|
||||||
const bot = await get.BotAuthorization(req.headers.token)
|
const bot = await get.BotAuthorization(req.headers.token)
|
||||||
if (!bot) return ResponseWrapper(res, { code: 401, version: 1 })
|
if (!bot) return ResponseWrapper(res, { code: 401, version: 1 })
|
||||||
const userID = await Yup.string().required().validate(req.query.id).then(el => el).catch(() => null)
|
const userID = await Yup.string()
|
||||||
|
.required()
|
||||||
|
.validate(req.query.id)
|
||||||
|
.then((el) => el)
|
||||||
|
.catch(() => null)
|
||||||
if (!userID) return ResponseWrapper(res, { code: 400, version: 1 })
|
if (!userID) return ResponseWrapper(res, { code: 400, version: 1 })
|
||||||
const result = await get.botVote(userID, bot)
|
const result = await get.botVote(userID, bot)
|
||||||
return res.json({ code: 200, voted: +new Date() < result + VOTE_COOLDOWN })
|
return res.json({ code: 200, voted: +new Date() < result + VOTE_COOLDOWN })
|
||||||
|
|||||||
@ -17,8 +17,8 @@ const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
|
|||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
const validated = await DeveloperBotSchema.validate(req.body, { abortEarly: false })
|
const validated = await DeveloperBotSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -26,11 +26,18 @@ const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
|
|||||||
if (!validated) return
|
if (!validated) return
|
||||||
const bot = await get.bot.load(req.query.id)
|
const bot = await get.bot.load(req.query.id)
|
||||||
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||||
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
if (!(bot.owners as User[]).find((el) => el.id === user))
|
||||||
|
return ResponseWrapper(res, { code: 403 })
|
||||||
if (validated.webhookURL) {
|
if (validated.webhookURL) {
|
||||||
const key = await verifyWebhook(validated.webhookURL)
|
const key = await verifyWebhook(validated.webhookURL)
|
||||||
if (key === false) {
|
if (key === false) {
|
||||||
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] })
|
return ResponseWrapper(res, {
|
||||||
|
code: 400,
|
||||||
|
message: '웹후크 주소를 검증할 수 없습니다.',
|
||||||
|
errors: [
|
||||||
|
'웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.',
|
||||||
|
],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const client = webhookClients.bot.get(req.query.id)
|
const client = webhookClients.bot.get(req.query.id)
|
||||||
if (client && validated.webhookURL !== client.url) {
|
if (client && validated.webhookURL !== client.url) {
|
||||||
|
|||||||
@ -14,8 +14,8 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
|
|||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
const validated = await ResetTokenSchema.validate(req.body, { abortEarly: false })
|
const validated = await ResetTokenSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -23,7 +23,8 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
|
|||||||
if (!validated) return
|
if (!validated) return
|
||||||
const bot = await get.bot.load(req.query.id)
|
const bot = await get.bot.load(req.query.id)
|
||||||
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||||
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
if (!(bot.owners as User[]).find((el) => el.id === user))
|
||||||
|
return ResponseWrapper(res, { code: 403 })
|
||||||
const d = await update.resetBotToken(req.query.id, validated.token)
|
const d = await update.resetBotToken(req.query.id, validated.token)
|
||||||
if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
|
if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
|
||||||
return ResponseWrapper(res, { code: 200, data: { token: d } })
|
return ResponseWrapper(res, { code: 200, data: { token: d } })
|
||||||
|
|||||||
@ -17,8 +17,8 @@ const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) =
|
|||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
const validated = await DeveloperServerSchema.validate(req.body, { abortEarly: false })
|
const validated = await DeveloperServerSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -30,7 +30,13 @@ const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) =
|
|||||||
if (validated.webhookURL) {
|
if (validated.webhookURL) {
|
||||||
const key = await verifyWebhook(validated.webhookURL)
|
const key = await verifyWebhook(validated.webhookURL)
|
||||||
if (key === false) {
|
if (key === false) {
|
||||||
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] })
|
return ResponseWrapper(res, {
|
||||||
|
code: 400,
|
||||||
|
message: '웹후크 주소를 검증할 수 없습니다.',
|
||||||
|
errors: [
|
||||||
|
'웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.',
|
||||||
|
],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const client = webhookClients.server.get(req.query.id)
|
const client = webhookClients.server.get(req.query.id)
|
||||||
if (client && validated.webhookURL !== client.url) {
|
if (client && validated.webhookURL !== client.url) {
|
||||||
|
|||||||
@ -12,8 +12,8 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
|
|||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
const validated = await ResetTokenSchema.validate(req.body, { abortEarly: false })
|
const validated = await ResetTokenSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -21,8 +21,14 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
|
|||||||
if (!validated) return
|
if (!validated) return
|
||||||
const server = await get.server.load(req.query.id)
|
const server = await get.server.load(req.query.id)
|
||||||
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
||||||
if(server.state === 'unreachable') return ResponseWrapper(res, { code: 400, message: '서버 정보를 불러올 수 없습니다.', errors: ['서버에서 봇이 추방되었거나, 봇이 오프라인이여서 서버 정보를 갱신할 수 없습니다.'] })
|
if (server.state === 'unreachable')
|
||||||
if (!(await get.serverOwners(server.id)).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
return ResponseWrapper(res, {
|
||||||
|
code: 400,
|
||||||
|
message: '서버 정보를 불러올 수 없습니다.',
|
||||||
|
errors: ['서버에서 봇이 추방되었거나, 봇이 오프라인이여서 서버 정보를 갱신할 수 없습니다.'],
|
||||||
|
})
|
||||||
|
if (!(await get.serverOwners(server.id)).find((el) => el.id === user))
|
||||||
|
return ResponseWrapper(res, { code: 403 })
|
||||||
const d = await update.resetServerToken(req.query.id, validated.token)
|
const d = await update.resetServerToken(req.query.id, validated.token)
|
||||||
if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
|
if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
|
||||||
return ResponseWrapper(res, { code: 200, data: { token: d } })
|
return ResponseWrapper(res, { code: 200, data: { token: d } })
|
||||||
|
|||||||
@ -6,10 +6,23 @@ import tracer from 'dd-trace'
|
|||||||
import { CaptchaVerify, get, put, remove, update } from '@utils/Query'
|
import { CaptchaVerify, get, put, remove, update } from '@utils/Query'
|
||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
import { checkToken } from '@utils/Csrf'
|
import { checkToken } from '@utils/Csrf'
|
||||||
import { AddBotSubmit, AddBotSubmitSchema, CsrfCaptcha, ManageBot, ManageBotSchema } from '@utils/Yup'
|
import {
|
||||||
|
AddBotSubmit,
|
||||||
|
AddBotSubmitSchema,
|
||||||
|
CsrfCaptcha,
|
||||||
|
ManageBot,
|
||||||
|
ManageBotSchema,
|
||||||
|
} from '@utils/Yup'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
import { User } from '@types'
|
import { User } from '@types'
|
||||||
import { checkUserFlag, diff, inspect, makeDiscordCodeblock, objectDiff, serialize } from '@utils/Tools'
|
import {
|
||||||
|
checkUserFlag,
|
||||||
|
diff,
|
||||||
|
inspect,
|
||||||
|
makeDiscordCodeblock,
|
||||||
|
objectDiff,
|
||||||
|
serialize,
|
||||||
|
} from '@utils/Tools'
|
||||||
import { discordLog, getMainGuild, webhookClients } from '@utils/DiscordBot'
|
import { discordLog, getMainGuild, webhookClients } from '@utils/DiscordBot'
|
||||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||||
|
|
||||||
@ -21,7 +34,7 @@ const patchLimiter = rateLimit({
|
|||||||
skip: (_req, res) => {
|
skip: (_req, res) => {
|
||||||
res.removeHeader('X-RateLimit-Global')
|
res.removeHeader('X-RateLimit-Global')
|
||||||
return false
|
return false
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
const Bots = RequestHandler()
|
const Bots = RequestHandler()
|
||||||
.get(async (req: GetApiRequest, res) => {
|
.get(async (req: GetApiRequest, res) => {
|
||||||
@ -38,8 +51,8 @@ const Bots = RequestHandler()
|
|||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
|
|
||||||
const validated = await AddBotSubmitSchema.validate(req.body, { abortEarly: false })
|
const validated = await AddBotSubmitSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -82,30 +95,53 @@ const Bots = RequestHandler()
|
|||||||
return ResponseWrapper(res, {
|
return ResponseWrapper(res, {
|
||||||
code: 403,
|
code: 403,
|
||||||
message: '더 이상 해당 봇에 대한 심사 요청을 하실 수 없습니다.',
|
message: '더 이상 해당 봇에 대한 심사 요청을 하실 수 없습니다.',
|
||||||
errors: ['해당 봇은 심사에서 3회 이상 거부되었습니다. 더 이상의 심사를 요청하실 수 없습니다.', '이의 제기를 원하시는 경우 디스코드 서버를 통해 문의해주세요.'],
|
errors: [
|
||||||
|
'해당 봇은 심사에서 3회 이상 거부되었습니다. 더 이상의 심사를 요청하실 수 없습니다.',
|
||||||
|
'이의 제기를 원하시는 경우 디스코드 서버를 통해 문의해주세요.',
|
||||||
|
],
|
||||||
})
|
})
|
||||||
get.botSubmits.clear(user)
|
get.botSubmits.clear(user)
|
||||||
|
|
||||||
await discordLog('BOT/SUBMIT', user, new EmbedBuilder().setDescription(`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(result.id, result.date)})`), {
|
await discordLog(
|
||||||
|
'BOT/SUBMIT',
|
||||||
|
user,
|
||||||
|
new EmbedBuilder().setDescription(
|
||||||
|
`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(
|
||||||
|
result.id,
|
||||||
|
result.date
|
||||||
|
)})`
|
||||||
|
),
|
||||||
|
{
|
||||||
content: inspect(serialize(result)),
|
content: inspect(serialize(result)),
|
||||||
format: 'js'
|
format: 'js',
|
||||||
})
|
}
|
||||||
|
)
|
||||||
const userinfo = await get.user.load(user)
|
const userinfo = await get.user.load(user)
|
||||||
await webhookClients.internal.reviewLog.send({
|
await webhookClients.internal.reviewLog.send({
|
||||||
embeds: [
|
embeds: [
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
.setAuthor({
|
.setAuthor({
|
||||||
name: userinfo.tag === '0' ? `${userinfo.globalName} (@${userinfo.username})` : `${userinfo.username}#${userinfo.tag}`,
|
name:
|
||||||
iconURL: KoreanbotsEndPoints.URL.root + KoreanbotsEndPoints.CDN.avatar(userinfo.id, { format: 'png', size: 256 }),
|
userinfo.tag === '0'
|
||||||
url: KoreanbotsEndPoints.URL.user(userinfo.id)
|
? `${userinfo.globalName} (@${userinfo.username})`
|
||||||
|
: `${userinfo.username}#${userinfo.tag}`,
|
||||||
|
iconURL:
|
||||||
|
KoreanbotsEndPoints.URL.root +
|
||||||
|
KoreanbotsEndPoints.CDN.avatar(userinfo.id, { format: 'png', size: 256 }),
|
||||||
|
url: KoreanbotsEndPoints.URL.user(userinfo.id),
|
||||||
})
|
})
|
||||||
.setTitle('대기 중')
|
.setTitle('대기 중')
|
||||||
.setColor(Colors.Grey)
|
.setColor(Colors.Grey)
|
||||||
.setDescription(`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(result.id, result.date)})`)
|
.setDescription(
|
||||||
.setTimestamp()
|
`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(
|
||||||
]
|
result.id,
|
||||||
|
result.date
|
||||||
|
)})`
|
||||||
|
)
|
||||||
|
.setTimestamp(),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
tracer.trace('botSubmits.submitted', span => {
|
tracer.trace('botSubmits.submitted', (span) => {
|
||||||
span.setTag('id', result.id)
|
span.setTag('id', result.id)
|
||||||
span.setTag('date', result.date)
|
span.setTag('date', result.date)
|
||||||
span.setTag('user', userinfo.id)
|
span.setTag('user', userinfo.id)
|
||||||
@ -119,37 +155,64 @@ const Bots = RequestHandler()
|
|||||||
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||||
if ((bot.owners as User[])[0].id !== user) return ResponseWrapper(res, { code: 403 })
|
if ((bot.owners as User[])[0].id !== user) return ResponseWrapper(res, { code: 403 })
|
||||||
const userInfo = await get.user.load(user)
|
const userInfo = await get.user.load(user)
|
||||||
if(['reported', 'blocked', 'archived'].includes(bot.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 봇은 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] })
|
if (
|
||||||
|
['reported', 'blocked', 'archived'].includes(bot.state) &&
|
||||||
|
!checkUserFlag(userInfo?.flags, 'staff')
|
||||||
|
)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 403,
|
||||||
|
message: '해당 봇은 수정할 수 없습니다.',
|
||||||
|
errors: ['오류라고 생각되면 문의해주세요.'],
|
||||||
|
})
|
||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
const captcha = await CaptchaVerify(req.body._captcha)
|
const captcha = await CaptchaVerify(req.body._captcha)
|
||||||
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||||
if(req.body.name !== bot.name) return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' })
|
if (req.body.name !== bot.name)
|
||||||
|
return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' })
|
||||||
await remove.bot(bot.id)
|
await remove.bot(bot.id)
|
||||||
await getMainGuild().members.cache.get(bot.id)?.kick('봇 삭제됨.')
|
await getMainGuild().members.cache.get(bot.id)?.kick('봇 삭제됨.')
|
||||||
get.user.clear(user)
|
get.user.clear(user)
|
||||||
await discordLog('BOT/DELETE', user, (new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`)),
|
await discordLog(
|
||||||
|
'BOT/DELETE',
|
||||||
|
user,
|
||||||
|
new EmbedBuilder().setDescription(
|
||||||
|
`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`
|
||||||
|
),
|
||||||
{
|
{
|
||||||
content: inspect(bot),
|
content: inspect(bot),
|
||||||
format: 'js'
|
format: 'js',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 삭제했습니다.' })
|
return ResponseWrapper(res, { code: 200, message: '성공적으로 삭제했습니다.' })
|
||||||
})
|
})
|
||||||
.patch(patchLimiter).patch(async (req: PatchApiRequest, res) => {
|
.patch(patchLimiter)
|
||||||
|
.patch(async (req: PatchApiRequest, res) => {
|
||||||
const bot = await get.bot.load(req.query.id)
|
const bot = await get.bot.load(req.query.id)
|
||||||
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
const userInfo = await get.user.load(user)
|
const userInfo = await get.user.load(user)
|
||||||
if(['reported', 'blocked', 'archived'].includes(bot.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 봇은 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] })
|
if (
|
||||||
if(!(bot.owners as User[]).find(el => el.id === user) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403 })
|
['reported', 'blocked', 'archived'].includes(bot.state) &&
|
||||||
|
!checkUserFlag(userInfo?.flags, 'staff')
|
||||||
|
)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 403,
|
||||||
|
message: '해당 봇은 수정할 수 없습니다.',
|
||||||
|
errors: ['오류라고 생각되면 문의해주세요.'],
|
||||||
|
})
|
||||||
|
if (
|
||||||
|
!(bot.owners as User[]).find((el) => el.id === user) &&
|
||||||
|
!checkUserFlag(userInfo?.flags, 'staff')
|
||||||
|
)
|
||||||
|
return ResponseWrapper(res, { code: 403 })
|
||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
|
|
||||||
const validated = await ManageBotSchema.validate(req.body, { abortEarly: false })
|
const validated = await ManageBotSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -160,24 +223,43 @@ const Bots = RequestHandler()
|
|||||||
if (result === 0) return ResponseWrapper(res, { code: 400 })
|
if (result === 0) return ResponseWrapper(res, { code: 400 })
|
||||||
else {
|
else {
|
||||||
get.bot.clear(req.query.id)
|
get.bot.clear(req.query.id)
|
||||||
const embed = new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`)
|
const embed = new EmbedBuilder().setDescription(
|
||||||
const diffData = objectDiff(
|
`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`
|
||||||
{ prefix: bot.prefix, library: bot.lib, web: bot.web, git: bot.git, url: bot.url, discord: bot.discord, intro: bot.intro, category: JSON.stringify(bot.category) },
|
|
||||||
{ prefix: validated.prefix, library: validated.library, web: validated.website, git: validated.git, url: validated.url, discord: validated.discord, intro: validated.intro, category: JSON.stringify(validated.category) }
|
|
||||||
)
|
)
|
||||||
diffData.forEach(d => {
|
const diffData = objectDiff(
|
||||||
embed.addFields({name: d[0], value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
await discordLog('BOT/EDIT', user, embed,
|
|
||||||
{
|
{
|
||||||
content: `--- 설명\n${diff(bot.desc, validated.desc, true)}`,
|
prefix: bot.prefix,
|
||||||
format: 'diff'
|
library: bot.lib,
|
||||||
|
web: bot.web,
|
||||||
|
git: bot.git,
|
||||||
|
url: bot.url,
|
||||||
|
discord: bot.discord,
|
||||||
|
intro: bot.intro,
|
||||||
|
category: JSON.stringify(bot.category),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prefix: validated.prefix,
|
||||||
|
library: validated.library,
|
||||||
|
web: validated.website,
|
||||||
|
git: validated.git,
|
||||||
|
url: validated.url,
|
||||||
|
discord: validated.discord,
|
||||||
|
intro: validated.intro,
|
||||||
|
category: JSON.stringify(validated.category),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
diffData.forEach((d) => {
|
||||||
|
embed.addFields({
|
||||||
|
name: d[0],
|
||||||
|
value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await discordLog('BOT/EDIT', user, embed, {
|
||||||
|
content: `--- 설명\n${diff(bot.desc, validated.desc, true)}`,
|
||||||
|
format: 'diff',
|
||||||
|
})
|
||||||
return ResponseWrapper(res, { code: 200 })
|
return ResponseWrapper(res, { code: 200 })
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
interface GetApiRequest extends NextApiRequest {
|
interface GetApiRequest extends NextApiRequest {
|
||||||
@ -195,7 +277,7 @@ interface PatchApiRequest extends GetApiRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteApiRequest extends GetApiRequest {
|
interface DeleteApiRequest extends GetApiRequest {
|
||||||
body: CsrfCaptcha & { name: string } | null
|
body: (CsrfCaptcha & { name: string }) | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Bots
|
export default Bots
|
||||||
|
|||||||
@ -11,18 +11,26 @@ import { discordLog } from '@utils/DiscordBot'
|
|||||||
import { EmbedBuilder } from 'discord.js'
|
import { EmbedBuilder } from 'discord.js'
|
||||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||||
|
|
||||||
const BotOwners = RequestHandler()
|
const BotOwners = RequestHandler().patch(async (req: PostApiRequest, res) => {
|
||||||
.patch(async (req: PostApiRequest, res) => {
|
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
const userinfo = await get.user.load(user)
|
const userinfo = await get.user.load(user)
|
||||||
const bot = await get.bot.load(req.query.id)
|
const bot = await get.bot.load(req.query.id)
|
||||||
if (!bot) return ResponseWrapper(res, { code: 404 })
|
if (!bot) return ResponseWrapper(res, { code: 404 })
|
||||||
if((bot.owners as User[])[0].id !== user && !checkUserFlag(userinfo.flags, 'staff')) return ResponseWrapper(res, { code: 403 })
|
if ((bot.owners as User[])[0].id !== user && !checkUserFlag(userinfo.flags, 'staff'))
|
||||||
if(['reported', 'blocked', 'archived'].includes(bot.state) && !checkUserFlag(userinfo.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 봇은 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] })
|
return ResponseWrapper(res, { code: 403 })
|
||||||
|
if (
|
||||||
|
['reported', 'blocked', 'archived'].includes(bot.state) &&
|
||||||
|
!checkUserFlag(userinfo.flags, 'staff')
|
||||||
|
)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 403,
|
||||||
|
message: '해당 봇은 수정할 수 없습니다.',
|
||||||
|
errors: ['오류라고 생각되면 문의해주세요.'],
|
||||||
|
})
|
||||||
const validated = await EditBotOwnerSchema.validate(req.body, { abortEarly: false })
|
const validated = await EditBotOwnerSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -31,19 +39,40 @@ const BotOwners = RequestHandler()
|
|||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
const captcha = await CaptchaVerify(validated._captcha)
|
const captcha = await CaptchaVerify(validated._captcha)
|
||||||
if (!captcha) return
|
if (!captcha) return
|
||||||
const userFetched: User[] = await Promise.all(validated.owners.map((u: string) => get.user.load(u)))
|
const userFetched: User[] = await Promise.all(
|
||||||
if(userFetched.indexOf(null) !== -1) return ResponseWrapper(res, { code: 400, message: '올바르지 않은 유저 ID를 포함하고 있습니다.' })
|
validated.owners.map((u: string) => get.user.load(u))
|
||||||
if(userFetched.length > 1 && userFetched[0].id !== (bot.owners as User[])[0].id) return ResponseWrapper(res, { code: 400, errors: ['소유자를 이전할 때는 다른 관리자를 포함할 수 없습니다.'] })
|
)
|
||||||
|
if (userFetched.indexOf(null) !== -1)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 400,
|
||||||
|
message: '올바르지 않은 유저 ID를 포함하고 있습니다.',
|
||||||
|
})
|
||||||
|
if (userFetched.length > 1 && userFetched[0].id !== (bot.owners as User[])[0].id)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 400,
|
||||||
|
errors: ['소유자를 이전할 때는 다른 관리자를 포함할 수 없습니다.'],
|
||||||
|
})
|
||||||
await update.botOwners(bot.id, validated.owners)
|
await update.botOwners(bot.id, validated.owners)
|
||||||
get.user.clear(user)
|
get.user.clear(user)
|
||||||
await discordLog('BOT/OWNERS', userinfo.id, (new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`)), null, makeDiscordCodeblock(diff(JSON.stringify(bot.owners.map(el => el.id)), JSON.stringify(validated.owners)), 'diff'))
|
await discordLog(
|
||||||
|
'BOT/OWNERS',
|
||||||
|
userinfo.id,
|
||||||
|
new EmbedBuilder().setDescription(
|
||||||
|
`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
makeDiscordCodeblock(
|
||||||
|
diff(JSON.stringify(bot.owners.map((el) => el.id)), JSON.stringify(validated.owners)),
|
||||||
|
'diff'
|
||||||
|
)
|
||||||
|
)
|
||||||
return ResponseWrapper(res, { code: 200 })
|
return ResponseWrapper(res, { code: 200 })
|
||||||
})
|
})
|
||||||
|
|
||||||
interface PostApiRequest extends NextApiRequest {
|
interface PostApiRequest extends NextApiRequest {
|
||||||
query: {
|
query: {
|
||||||
id: string
|
id: string
|
||||||
},
|
}
|
||||||
body: EditBotOwner
|
body: EditBotOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,10 +18,11 @@ const limiter = rateLimit({
|
|||||||
skip: (_req, res) => {
|
skip: (_req, res) => {
|
||||||
res.removeHeader('X-RateLimit-Global')
|
res.removeHeader('X-RateLimit-Global')
|
||||||
return false
|
return false
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const BotReport = RequestHandler().post(limiter)
|
const BotReport = RequestHandler()
|
||||||
|
.post(limiter)
|
||||||
.post(async (req: PostApiRequest, res) => {
|
.post(async (req: PostApiRequest, res) => {
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
@ -31,18 +32,21 @@ const BotReport = RequestHandler().post(limiter)
|
|||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
||||||
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
|
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
await webhookClients.internal.reportChannel.send({ threadName: `봇-${bot.id}`, content: `Reported by <@${user}> (${user})\nReported **${bot.name}** <@${bot.id}> (${bot.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``, allowedMentions: { parse: ['users'] }})
|
await webhookClients.internal.reportChannel.send({
|
||||||
|
threadName: `봇-${bot.id}`,
|
||||||
|
content: `Reported by <@${user}> (${user})\nReported **${bot.name}** <@${bot.id}> (${bot.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``,
|
||||||
|
allowedMentions: { parse: ['users'] },
|
||||||
|
})
|
||||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
|
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
interface PostApiRequest extends NextApiRequest {
|
interface PostApiRequest extends NextApiRequest {
|
||||||
body: Report | null
|
body: Report | null
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const limiter = rateLimit({
|
|||||||
res.removeHeader('X-RateLimit-Global')
|
res.removeHeader('X-RateLimit-Global')
|
||||||
if (!req.headers.authorization) return true
|
if (!req.headers.authorization) return true
|
||||||
else return false
|
else return false
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const patchLimiter = rateLimit({
|
const patchLimiter = rateLimit({
|
||||||
@ -36,17 +36,20 @@ const patchLimiter = rateLimit({
|
|||||||
skip: (_req, res) => {
|
skip: (_req, res) => {
|
||||||
res.removeHeader('X-RateLimit-Global')
|
res.removeHeader('X-RateLimit-Global')
|
||||||
return false
|
return false
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const BotStats = RequestHandler().post(limiter)
|
const BotStats = RequestHandler()
|
||||||
|
.post(limiter)
|
||||||
.post(async (req: PostApiRequest, res) => {
|
.post(async (req: PostApiRequest, res) => {
|
||||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||||
if (!bot) return ResponseWrapper(res, { code: 401 })
|
if (!bot) return ResponseWrapper(res, { code: 401 })
|
||||||
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
||||||
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, { abortEarly: false })
|
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, {
|
||||||
.then(el => el)
|
abortEarly: false,
|
||||||
.catch(e => {
|
})
|
||||||
|
.then((el) => el)
|
||||||
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -56,7 +59,13 @@ const BotStats = RequestHandler().post(limiter)
|
|||||||
if (!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
if (!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||||
if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403 })
|
if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403 })
|
||||||
const d = await update.updateServer(botInfo.id, validated.servers, validated.shards)
|
const d = await update.updateServer(botInfo.id, validated.servers, validated.shards)
|
||||||
if(d===1 || d===2) return ResponseWrapper(res, { code: 403, message: `서버 수를 ${[null, '1만', '100만'][d]} 이상으로 설정하실 수 없습니다. 문의해주세요.` })
|
if (d === 1 || d === 2)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 403,
|
||||||
|
message: `서버 수를 ${
|
||||||
|
[null, '1만', '100만'][d]
|
||||||
|
} 이상으로 설정하실 수 없습니다. 문의해주세요.`,
|
||||||
|
})
|
||||||
get.bot.clear(req.query.id)
|
get.bot.clear(req.query.id)
|
||||||
if (validated.servers && botInfo.servers !== validated.servers) {
|
if (validated.servers && botInfo.servers !== validated.servers) {
|
||||||
sendWebhook(botInfo, {
|
sendWebhook(botInfo, {
|
||||||
@ -67,27 +76,44 @@ const BotStats = RequestHandler().post(limiter)
|
|||||||
before: botInfo.servers,
|
before: botInfo.servers,
|
||||||
after: validated.servers,
|
after: validated.servers,
|
||||||
},
|
},
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
await webhookClients.internal.statsLog.send({
|
await webhookClients.internal.statsLog.send({
|
||||||
content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(`${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${validated.servers} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(validated.servers - botInfo.servers)})`, 'diff')}`,
|
content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(
|
||||||
embeds: [new EmbedBuilder().setDescription(`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(botInfo.id)}))`)]
|
`${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${
|
||||||
|
validated.servers
|
||||||
|
} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(
|
||||||
|
validated.servers - botInfo.servers
|
||||||
|
)})`,
|
||||||
|
'diff'
|
||||||
|
)}`,
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder().setDescription(
|
||||||
|
`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(
|
||||||
|
botInfo.id
|
||||||
|
)}))`
|
||||||
|
),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.' })
|
return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.' })
|
||||||
})
|
})
|
||||||
.patch(patchLimiter).patch(async (req: ApiRequest, res) => {
|
.patch(patchLimiter)
|
||||||
|
.patch(async (req: ApiRequest, res) => {
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
const userinfo = await get.user.load(user)
|
const userinfo = await get.user.load(user)
|
||||||
const bot = await get.bot.load(req.query.id)
|
const bot = await get.bot.load(req.query.id)
|
||||||
if (!bot) return ResponseWrapper(res, { code: 404 })
|
if (!bot) return ResponseWrapper(res, { code: 404 })
|
||||||
if(!(bot.owners as User[]).find(el => el.id === user) && !checkUserFlag(userinfo.flags, 'staff')) return ResponseWrapper(res, { code: 403 })
|
if (
|
||||||
|
!(bot.owners as User[]).find((el) => el.id === user) &&
|
||||||
|
!checkUserFlag(userinfo.flags, 'staff')
|
||||||
|
)
|
||||||
|
return ResponseWrapper(res, { code: 403 })
|
||||||
get.bot.clear(req.query.id)
|
get.bot.clear(req.query.id)
|
||||||
return ResponseWrapper(res, { code: 200 })
|
return ResponseWrapper(res, { code: 200 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
query: {
|
query: {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@ -14,13 +14,21 @@ const BotVote = RequestHandler()
|
|||||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||||
if (!bot) return ResponseWrapper(res, { code: 401 })
|
if (!bot) return ResponseWrapper(res, { code: 401 })
|
||||||
if (req.query.id !== bot) return ResponseWrapper(res, { code: 403 })
|
if (req.query.id !== bot) return ResponseWrapper(res, { code: 403 })
|
||||||
const userID = await Yup.string().required().label('userID').validate(req.query.userID).then(el => el).catch(e => {
|
const userID = await Yup.string()
|
||||||
|
.required()
|
||||||
|
.label('userID')
|
||||||
|
.validate(req.query.userID)
|
||||||
|
.then((el) => el)
|
||||||
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!userID) return ResponseWrapper(res, { code: 400 })
|
if (!userID) return ResponseWrapper(res, { code: 400 })
|
||||||
const result = await get.botVote(userID, bot)
|
const result = await get.botVote(userID, bot)
|
||||||
return ResponseWrapper(res, { code: 200, data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result } })
|
return ResponseWrapper(res, {
|
||||||
|
code: 200,
|
||||||
|
data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.post(async (req: PostApiRequest, res) => {
|
.post(async (req: PostApiRequest, res) => {
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
@ -42,13 +50,12 @@ const BotVote = RequestHandler()
|
|||||||
type: WebhookType.HeartChange,
|
type: WebhookType.HeartChange,
|
||||||
before: bot.votes,
|
before: bot.votes,
|
||||||
after: bot.votes + 1,
|
after: bot.votes + 1,
|
||||||
userId: user
|
userId: user,
|
||||||
},
|
},
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
})
|
})
|
||||||
return ResponseWrapper(res, { code: 200 })
|
return ResponseWrapper(res, { code: 200 })
|
||||||
}
|
} else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
|
||||||
else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
|
|||||||
@ -6,9 +6,15 @@ import { Bot, List } from '@types'
|
|||||||
import Yup from '@utils/Yup'
|
import Yup from '@utils/Yup'
|
||||||
|
|
||||||
const VotesList = RequestHandler().get(async (req, res) => {
|
const VotesList = RequestHandler().get(async (req, res) => {
|
||||||
const page = await Yup.number().positive().integer().notRequired().default(1).label('페이지').validate(req.query.page)
|
const page = await Yup.number()
|
||||||
.then(el => el)
|
.positive()
|
||||||
.catch(e => {
|
.integer()
|
||||||
|
.notRequired()
|
||||||
|
.default(1)
|
||||||
|
.label('페이지')
|
||||||
|
.validate(req.query.page)
|
||||||
|
.then((el) => el)
|
||||||
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
})
|
})
|
||||||
if (!page) return
|
if (!page) return
|
||||||
|
|||||||
@ -8,21 +8,32 @@ import { get, update } from '@utils/Query'
|
|||||||
import { DiscordBot, webhookClients } from '@utils/DiscordBot'
|
import { DiscordBot, webhookClients } from '@utils/DiscordBot'
|
||||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||||
|
|
||||||
const ApproveBotSubmit = RequestHandler()
|
const ApproveBotSubmit = RequestHandler().post(async (req: ApiRequest, res) => {
|
||||||
.post(async (req: ApiRequest, res) => {
|
|
||||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||||
const submit = await get.botSubmit.load(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
const submit = await get.botSubmit.load(
|
||||||
|
JSON.stringify({ id: req.query.id, date: req.query.date })
|
||||||
|
)
|
||||||
if (!submit) return ResponseWrapper(res, { code: 404 })
|
if (!submit) return ResponseWrapper(res, { code: 404 })
|
||||||
if(submit.state !== 0) return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
|
if (submit.state !== 0)
|
||||||
|
return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
|
||||||
const result = await update.approveBotSubmission(submit.id, submit.date)
|
const result = await update.approveBotSubmission(submit.id, submit.date)
|
||||||
if (!result) return ResponseWrapper(res, { code: 400 })
|
if (!result) return ResponseWrapper(res, { code: 400 })
|
||||||
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
||||||
get.bot.clear(req.query.id)
|
get.bot.clear(req.query.id)
|
||||||
const embed = new EmbedBuilder().setTitle('승인').setColor(Colors.Green).setDescription(`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(submit.id, submit.date)})`).setTimestamp()
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle('승인')
|
||||||
|
.setColor(Colors.Green)
|
||||||
|
.setDescription(
|
||||||
|
`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(
|
||||||
|
submit.id,
|
||||||
|
submit.date
|
||||||
|
)})`
|
||||||
|
)
|
||||||
|
.setTimestamp()
|
||||||
if (req.body.reviewer) embed.addFields({ name: '📃 정보', value: `심사자: ${req.body.reviewer}` })
|
if (req.body.reviewer) embed.addFields({ name: '📃 정보', value: `심사자: ${req.body.reviewer}` })
|
||||||
await webhookClients.internal.reviewLog.send({ embeds: [embed] })
|
await webhookClients.internal.reviewLog.send({ embeds: [embed] })
|
||||||
tracer.trace('botSubmits.approve', span => {
|
tracer.trace('botSubmits.approve', (span) => {
|
||||||
span.setTag('id', submit.id)
|
span.setTag('id', submit.id)
|
||||||
span.setTag('date', submit.date)
|
span.setTag('date', submit.date)
|
||||||
span.setTag('reviewer', req.body.reviewer)
|
span.setTag('reviewer', req.body.reviewer)
|
||||||
|
|||||||
@ -8,22 +8,53 @@ import { get, update } from '@utils/Query'
|
|||||||
import { DiscordBot, webhookClients } from '@utils/DiscordBot'
|
import { DiscordBot, webhookClients } from '@utils/DiscordBot'
|
||||||
import { BotSubmissionDenyReasonPresetsName, KoreanbotsEndPoints } from '@utils/Constants'
|
import { BotSubmissionDenyReasonPresetsName, KoreanbotsEndPoints } from '@utils/Constants'
|
||||||
|
|
||||||
const DenyBotSubmit = RequestHandler()
|
const DenyBotSubmit = RequestHandler().post(async (req: ApiRequest, res) => {
|
||||||
.post(async (req: ApiRequest, res) => {
|
|
||||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||||
const submit = await get.botSubmit.load(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
const submit = await get.botSubmit.load(
|
||||||
|
JSON.stringify({ id: req.query.id, date: req.query.date })
|
||||||
|
)
|
||||||
if (!submit) return ResponseWrapper(res, { code: 404 })
|
if (!submit) return ResponseWrapper(res, { code: 404 })
|
||||||
if(submit.state !== 0) return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
|
if (submit.state !== 0)
|
||||||
|
return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
|
||||||
await update.denyBotSubmission(submit.id, submit.date, req.body.reason)
|
await update.denyBotSubmission(submit.id, submit.date, req.body.reason)
|
||||||
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
||||||
const embed = new EmbedBuilder().setTitle('거부').setColor(Colors.Red).setDescription(`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(submit.id, submit.date)})`).setTimestamp()
|
const embed = new EmbedBuilder()
|
||||||
if(req.body.reviewer || req.body.reason) embed.addFields({name: '📃 정보', value: `${req.body.reason ? `사유: ${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`: ''}${req.body.reviewer ? `심사자: ${req.body.reviewer}` : ''}`})
|
.setTitle('거부')
|
||||||
|
.setColor(Colors.Red)
|
||||||
|
.setDescription(
|
||||||
|
`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(
|
||||||
|
submit.id,
|
||||||
|
submit.date
|
||||||
|
)})`
|
||||||
|
)
|
||||||
|
.setTimestamp()
|
||||||
|
if (req.body.reviewer || req.body.reason)
|
||||||
|
embed.addFields({
|
||||||
|
name: '📃 정보',
|
||||||
|
value: `${
|
||||||
|
req.body.reason
|
||||||
|
? `사유: ${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`
|
||||||
|
: ''
|
||||||
|
}${req.body.reviewer ? `심사자: ${req.body.reviewer}` : ''}`,
|
||||||
|
})
|
||||||
await webhookClients.internal.reviewLog.send({ embeds: [embed] })
|
await webhookClients.internal.reviewLog.send({ embeds: [embed] })
|
||||||
const openEmbed = new EmbedBuilder().setTitle('거부').setColor(Colors.Red).setDescription(`<@${submit.id}> (${submit.id})`).setTimestamp()
|
const openEmbed = new EmbedBuilder()
|
||||||
if(req.body.reason) openEmbed.addFields({name: '📃 사유', value: `${req.body.reason ? `${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`: '없음'}`})
|
.setTitle('거부')
|
||||||
|
.setColor(Colors.Red)
|
||||||
|
.setDescription(`<@${submit.id}> (${submit.id})`)
|
||||||
|
.setTimestamp()
|
||||||
|
if (req.body.reason)
|
||||||
|
openEmbed.addFields({
|
||||||
|
name: '📃 사유',
|
||||||
|
value: `${
|
||||||
|
req.body.reason
|
||||||
|
? `${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`
|
||||||
|
: '없음'
|
||||||
|
}`,
|
||||||
|
})
|
||||||
await webhookClients.internal.openReviewLog.send({ embeds: [openEmbed] })
|
await webhookClients.internal.openReviewLog.send({ embeds: [openEmbed] })
|
||||||
tracer.trace('botSubmits.deny', span => {
|
tracer.trace('botSubmits.deny', (span) => {
|
||||||
span.setTag('id', submit.id)
|
span.setTag('id', submit.id)
|
||||||
span.setTag('date', submit.date)
|
span.setTag('date', submit.date)
|
||||||
span.setTag('reviewer', req.body.reviewer)
|
span.setTag('reviewer', req.body.reviewer)
|
||||||
|
|||||||
@ -5,11 +5,12 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
|||||||
import { get } from '@utils/Query'
|
import { get } from '@utils/Query'
|
||||||
import { DiscordBot } from '@utils/DiscordBot'
|
import { DiscordBot } from '@utils/DiscordBot'
|
||||||
|
|
||||||
const BotSubmit = RequestHandler()
|
const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
.get(async (req: ApiRequest, res) => {
|
|
||||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||||
const submit = await get.botSubmit.load(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
const submit = await get.botSubmit.load(
|
||||||
|
JSON.stringify({ id: req.query.id, date: req.query.date })
|
||||||
|
)
|
||||||
if (!submit) return ResponseWrapper(res, { code: 404 })
|
if (!submit) return ResponseWrapper(res, { code: 404 })
|
||||||
return ResponseWrapper(res, { code: 200, data: submit })
|
return ResponseWrapper(res, { code: 200, data: submit })
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,8 +5,7 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
|||||||
import { get } from '@utils/Query'
|
import { get } from '@utils/Query'
|
||||||
import { DiscordBot } from '@utils/DiscordBot'
|
import { DiscordBot } from '@utils/DiscordBot'
|
||||||
|
|
||||||
const BotSubmit = RequestHandler()
|
const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
.get(async (req: ApiRequest, res) => {
|
|
||||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||||
return ResponseWrapper(res, { code: 200, data: await get.botSubmitHistory(req.query.id) })
|
return ResponseWrapper(res, { code: 200, data: await get.botSubmitHistory(req.query.id) })
|
||||||
|
|||||||
@ -5,8 +5,7 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
|||||||
import { get } from '@utils/Query'
|
import { get } from '@utils/Query'
|
||||||
import { DiscordBot } from '@utils/DiscordBot'
|
import { DiscordBot } from '@utils/DiscordBot'
|
||||||
|
|
||||||
const BotSubmit = RequestHandler()
|
const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
.get(async (req: ApiRequest, res) => {
|
|
||||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||||
const submits = await get.botSubmitList()
|
const submits = await get.botSubmitList()
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
|||||||
import { get } from '@utils/Query'
|
import { get } from '@utils/Query'
|
||||||
import { DiscordBot } from '@utils/DiscordBot'
|
import { DiscordBot } from '@utils/DiscordBot'
|
||||||
|
|
||||||
const BotSubmits = RequestHandler()
|
const BotSubmits = RequestHandler().get(async (req, res) => {
|
||||||
.get(async (req, res) => {
|
|
||||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||||
const submits = await get.botSubmitList()
|
const submits = await get.botSubmitList()
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
|
|
||||||
const BotSubmits = RequestHandler()
|
const BotSubmits = RequestHandler().get(async (_req, res) => {
|
||||||
.get(async (_req, res) => {
|
|
||||||
return ResponseWrapper(res, { code: 403, message: 'Private API' })
|
return ResponseWrapper(res, { code: 403, message: 'Private API' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { NextApiHandler } from 'next'
|
import { NextApiHandler } from 'next'
|
||||||
import { getMainGuild } from '@utils/DiscordBot'
|
import { getMainGuild } from '@utils/DiscordBot'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import { Bot, Server, List } from '@types'
|
|||||||
|
|
||||||
const Search = RequestHandler().get(async (req: ApiRequest, res) => {
|
const Search = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||||
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: 1 })
|
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: 1 })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
})
|
})
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
@ -27,7 +27,10 @@ const Search = RequestHandler().get(async (req: ApiRequest, res) => {
|
|||||||
} catch {
|
} catch {
|
||||||
return ResponseWrapper(res, { code: 400, message: '검색 문법이 잘못되었습니다.' })
|
return ResponseWrapper(res, { code: 400, message: '검색 문법이 잘못되었습니다.' })
|
||||||
}
|
}
|
||||||
return ResponseWrapper<{ bots: Bot[], servers: Server[] }>(res, { code: 200, data: { bots: botResult?.data || [], servers: serverResult?.data || [] } })
|
return ResponseWrapper<{ bots: Bot[]; servers: Server[] }>(res, {
|
||||||
|
code: 200,
|
||||||
|
data: { bots: botResult?.data || [], servers: serverResult?.data || [] },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
|
|||||||
@ -8,9 +8,12 @@ import { SearchQuerySchema } from '@utils/Yup'
|
|||||||
import { Bot, List } from '@types'
|
import { Bot, List } from '@types'
|
||||||
|
|
||||||
const SearchBots = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
|
const SearchBots = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
|
||||||
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: req.query.page })
|
const validated = await SearchQuerySchema.validate({
|
||||||
.then(el => el)
|
q: req.query.q || req.query.query,
|
||||||
.catch(e => {
|
page: req.query.page,
|
||||||
|
})
|
||||||
|
.then((el) => el)
|
||||||
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
})
|
})
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
|
|||||||
@ -8,9 +8,12 @@ import { SearchQuerySchema } from '@utils/Yup'
|
|||||||
import { Server, List } from '@types'
|
import { Server, List } from '@types'
|
||||||
|
|
||||||
const SearchServers = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
|
const SearchServers = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
|
||||||
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: req.query.page })
|
const validated = await SearchQuerySchema.validate({
|
||||||
.then(el => el)
|
q: req.query.q || req.query.query,
|
||||||
.catch(e => {
|
page: req.query.page,
|
||||||
|
})
|
||||||
|
.then((el) => el)
|
||||||
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
})
|
})
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
|
|||||||
@ -5,9 +5,22 @@ import { EmbedBuilder, RESTJSONErrorCodes } from 'discord.js'
|
|||||||
import { CaptchaVerify, get, put, remove, update } from '@utils/Query'
|
import { CaptchaVerify, get, put, remove, update } from '@utils/Query'
|
||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
import { checkToken } from '@utils/Csrf'
|
import { checkToken } from '@utils/Csrf'
|
||||||
import { AddServerSubmitSchema, AddServerSubmit, CsrfCaptcha, ManageServerSchema, ManageServer } from '@utils/Yup'
|
import {
|
||||||
|
AddServerSubmitSchema,
|
||||||
|
AddServerSubmit,
|
||||||
|
CsrfCaptcha,
|
||||||
|
ManageServerSchema,
|
||||||
|
ManageServer,
|
||||||
|
} from '@utils/Yup'
|
||||||
import RequestHandler from '@utils/RequestHandler'
|
import RequestHandler from '@utils/RequestHandler'
|
||||||
import { checkUserFlag, diff, inspect, makeDiscordCodeblock, objectDiff, serialize } from '@utils/Tools'
|
import {
|
||||||
|
checkUserFlag,
|
||||||
|
diff,
|
||||||
|
inspect,
|
||||||
|
makeDiscordCodeblock,
|
||||||
|
objectDiff,
|
||||||
|
serialize,
|
||||||
|
} from '@utils/Tools'
|
||||||
import { DiscordBot, discordLog } from '@utils/DiscordBot'
|
import { DiscordBot, discordLog } from '@utils/DiscordBot'
|
||||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||||
|
|
||||||
@ -19,7 +32,7 @@ const patchLimiter = rateLimit({
|
|||||||
skip: (_req, res) => {
|
skip: (_req, res) => {
|
||||||
res.removeHeader('X-RateLimit-Global')
|
res.removeHeader('X-RateLimit-Global')
|
||||||
return false
|
return false
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
const Servers = RequestHandler()
|
const Servers = RequestHandler()
|
||||||
.get(async (req: GetApiRequest, res) => {
|
.get(async (req: GetApiRequest, res) => {
|
||||||
@ -36,8 +49,8 @@ const Servers = RequestHandler()
|
|||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
|
|
||||||
const validated = await AddServerSubmitSchema.validate(req.body, { abortEarly: false })
|
const validated = await AddServerSubmitSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -49,7 +62,7 @@ const Servers = RequestHandler()
|
|||||||
if (result === 1)
|
if (result === 1)
|
||||||
return ResponseWrapper(res, {
|
return ResponseWrapper(res, {
|
||||||
code: 400,
|
code: 400,
|
||||||
message: '이미 등록된 서버 입니다.'
|
message: '이미 등록된 서버 입니다.',
|
||||||
})
|
})
|
||||||
else if (result === 2)
|
else if (result === 2)
|
||||||
return ResponseWrapper(res, {
|
return ResponseWrapper(res, {
|
||||||
@ -57,7 +70,7 @@ const Servers = RequestHandler()
|
|||||||
message: '봇이 초대되지 않았습니다.',
|
message: '봇이 초대되지 않았습니다.',
|
||||||
errors: [
|
errors: [
|
||||||
'서버에 봇이 초대되지 않았습니다.',
|
'서버에 봇이 초대되지 않았습니다.',
|
||||||
'이미 봇을 초대하셨다면, 잠시 후 다시 시도해주세요.'
|
'이미 봇을 초대하셨다면, 잠시 후 다시 시도해주세요.',
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
else if (result === 3)
|
else if (result === 3)
|
||||||
@ -66,7 +79,7 @@ const Servers = RequestHandler()
|
|||||||
message: '서버의 관리자가 아닙니다.',
|
message: '서버의 관리자가 아닙니다.',
|
||||||
errors: [
|
errors: [
|
||||||
'해당 서버를 등록할 권한이 없습니다.',
|
'해당 서버를 등록할 권한이 없습니다.',
|
||||||
'서버에서 관리자 권한이 있으신지 확인해주세요.'
|
'서버에서 관리자 권한이 있으신지 확인해주세요.',
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
else if (result === 4)
|
else if (result === 4)
|
||||||
@ -75,14 +88,21 @@ const Servers = RequestHandler()
|
|||||||
message: '올바르지 않은 초대 코드 입니다.',
|
message: '올바르지 않은 초대 코드 입니다.',
|
||||||
errors: [
|
errors: [
|
||||||
'올바른 초대코드를 입력하셨는지 확인해주세요.',
|
'올바른 초대코드를 입력하셨는지 확인해주세요.',
|
||||||
'만료되지 않는 초대코드인지 확인해주세요.'
|
'만료되지 않는 초대코드인지 확인해주세요.',
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
get.user.clear(user)
|
get.user.clear(user)
|
||||||
await discordLog('SERVER/SUBMIT', user, new EmbedBuilder().setDescription(`[${req.query.id}](${KoreanbotsEndPoints.URL.server(req.query.id)})`), {
|
await discordLog(
|
||||||
|
'SERVER/SUBMIT',
|
||||||
|
user,
|
||||||
|
new EmbedBuilder().setDescription(
|
||||||
|
`[${req.query.id}](${KoreanbotsEndPoints.URL.server(req.query.id)})`
|
||||||
|
),
|
||||||
|
{
|
||||||
content: inspect(serialize(validated)),
|
content: inspect(serialize(validated)),
|
||||||
format: 'js'
|
format: 'js',
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return ResponseWrapper(res, { code: 200, data: result })
|
return ResponseWrapper(res, { code: 200, data: result })
|
||||||
})
|
})
|
||||||
@ -92,72 +112,118 @@ const Servers = RequestHandler()
|
|||||||
const server = await get.server.load(req.query.id)
|
const server = await get.server.load(req.query.id)
|
||||||
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버 입니다.' })
|
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버 입니다.' })
|
||||||
const data = await get.serverData(req.query.id)
|
const data = await get.serverData(req.query.id)
|
||||||
if((!data || server.state === 'unreachable') && (await DiscordBot.fetchInvite(server.invite).catch((e) => e.code !== RESTJSONErrorCodes.UnknownInvite))) return ResponseWrapper(res, { code: 400, message: '해당 서버의 정보를 불러올 수 없습니다.', errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'] })
|
if (
|
||||||
|
(!data || server.state === 'unreachable') &&
|
||||||
|
(await DiscordBot.fetchInvite(server.invite).catch(
|
||||||
|
(e) => e.code !== RESTJSONErrorCodes.UnknownInvite
|
||||||
|
))
|
||||||
|
)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 400,
|
||||||
|
message: '해당 서버의 정보를 불러올 수 없습니다.',
|
||||||
|
errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'],
|
||||||
|
})
|
||||||
if (![data.owner, ...data.admins].includes(user)) return ResponseWrapper(res, { code: 403 })
|
if (![data.owner, ...data.admins].includes(user)) return ResponseWrapper(res, { code: 403 })
|
||||||
const userInfo = await get.user.load(user)
|
const userInfo = await get.user.load(user)
|
||||||
if(['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 서버는 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] })
|
if (['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff'))
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 403,
|
||||||
|
message: '해당 서버는 수정할 수 없습니다.',
|
||||||
|
errors: ['오류라고 생각되면 문의해주세요.'],
|
||||||
|
})
|
||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
const captcha = await CaptchaVerify(req.body._captcha)
|
const captcha = await CaptchaVerify(req.body._captcha)
|
||||||
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||||
if(req.body.name !== server.name) return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' })
|
if (req.body.name !== server.name)
|
||||||
|
return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' })
|
||||||
await remove.server(server.id)
|
await remove.server(server.id)
|
||||||
get.user.clear(user)
|
get.user.clear(user)
|
||||||
await discordLog('SERVER/DELETE', user, (new EmbedBuilder().setDescription(`${server.name} - [${server.id}](${KoreanbotsEndPoints.URL.bot(server.id)}))`)),
|
await discordLog(
|
||||||
|
'SERVER/DELETE',
|
||||||
|
user,
|
||||||
|
new EmbedBuilder().setDescription(
|
||||||
|
`${server.name} - [${server.id}](${KoreanbotsEndPoints.URL.bot(server.id)}))`
|
||||||
|
),
|
||||||
{
|
{
|
||||||
content: inspect(server),
|
content: inspect(server),
|
||||||
format: 'js'
|
format: 'js',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 삭제했습니다.' })
|
return ResponseWrapper(res, { code: 200, message: '성공적으로 삭제했습니다.' })
|
||||||
})
|
})
|
||||||
.patch(patchLimiter).patch(async (req: PatchApiRequest, res) => {
|
.patch(patchLimiter)
|
||||||
|
.patch(async (req: PatchApiRequest, res) => {
|
||||||
const server = await get.server.load(req.query.id)
|
const server = await get.server.load(req.query.id)
|
||||||
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
const userInfo = await get.user.load(user)
|
const userInfo = await get.user.load(user)
|
||||||
const data = await get.serverData(req.query.id)
|
const data = await get.serverData(req.query.id)
|
||||||
if(!data || server.state === 'unreachable') return ResponseWrapper(res, { code: 400, message: '해당 서버의 정보를 불러올 수 없습니다.', errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'] })
|
if (!data || server.state === 'unreachable')
|
||||||
if(![data.owner, ...data.admins].includes(user) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403 })
|
return ResponseWrapper(res, {
|
||||||
if(['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 서버는 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] })
|
code: 400,
|
||||||
|
message: '해당 서버의 정보를 불러올 수 없습니다.',
|
||||||
|
errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'],
|
||||||
|
})
|
||||||
|
if (![data.owner, ...data.admins].includes(user) && !checkUserFlag(userInfo?.flags, 'staff'))
|
||||||
|
return ResponseWrapper(res, { code: 403 })
|
||||||
|
if (['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff'))
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 403,
|
||||||
|
message: '해당 서버는 수정할 수 없습니다.',
|
||||||
|
errors: ['오류라고 생각되면 문의해주세요.'],
|
||||||
|
})
|
||||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
|
|
||||||
const validated = await ManageServerSchema.validate(req.body, { abortEarly: false })
|
const validated = await ManageServerSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
const invite = await DiscordBot.fetchInvite(validated.invite).catch(() => null)
|
const invite = await DiscordBot.fetchInvite(validated.invite).catch(() => null)
|
||||||
if(invite?.guild.id !== server.id || invite.expiresAt) return ResponseWrapper(res, { code: 400, message: '올바르지 않은 초대코드입니다.', errors: ['입력하신 초대코드가 올바르지 않습니다. 올바른 초대코드를 입력했는지 다시 한 번 확인해주세요.', '만료되지 않는 초대코드인지 확인해주세요.'] })
|
if (invite?.guild.id !== server.id || invite.expiresAt)
|
||||||
|
return ResponseWrapper(res, {
|
||||||
|
code: 400,
|
||||||
|
message: '올바르지 않은 초대코드입니다.',
|
||||||
|
errors: [
|
||||||
|
'입력하신 초대코드가 올바르지 않습니다. 올바른 초대코드를 입력했는지 다시 한 번 확인해주세요.',
|
||||||
|
'만료되지 않는 초대코드인지 확인해주세요.',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
const result = await update.server(req.query.id, validated)
|
const result = await update.server(req.query.id, validated)
|
||||||
|
|
||||||
if (result === 0) return ResponseWrapper(res, { code: 400 })
|
if (result === 0) return ResponseWrapper(res, { code: 400 })
|
||||||
else {
|
else {
|
||||||
get.server.clear(req.query.id)
|
get.server.clear(req.query.id)
|
||||||
const embed = new EmbedBuilder().setDescription(`${server.name} - ([${server.id}](${KoreanbotsEndPoints.URL.server(server.id)}))`)
|
const embed = new EmbedBuilder().setDescription(
|
||||||
|
`${server.name} - ([${server.id}](${KoreanbotsEndPoints.URL.server(server.id)}))`
|
||||||
|
)
|
||||||
const diffData = objectDiff(
|
const diffData = objectDiff(
|
||||||
{ intro: server.intro, invite: server.invite, category: JSON.stringify(server.category) },
|
{ intro: server.intro, invite: server.invite, category: JSON.stringify(server.category) },
|
||||||
{ intro: validated.intro, invite: validated.invite, category: JSON.stringify(validated.category) },
|
|
||||||
)
|
|
||||||
diffData.forEach(d => {
|
|
||||||
embed.addFields({name: d[0], value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
await discordLog('SERVER/EDIT', user, embed,
|
|
||||||
{
|
{
|
||||||
content: `--- 설명\n${diff(server.desc, validated.desc, true)}`,
|
intro: validated.intro,
|
||||||
format: 'diff'
|
invite: validated.invite,
|
||||||
|
category: JSON.stringify(validated.category),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
diffData.forEach((d) => {
|
||||||
|
embed.addFields({
|
||||||
|
name: d[0],
|
||||||
|
value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await discordLog('SERVER/EDIT', user, embed, {
|
||||||
|
content: `--- 설명\n${diff(server.desc, validated.desc, true)}`,
|
||||||
|
format: 'diff',
|
||||||
|
})
|
||||||
return ResponseWrapper(res, { code: 200 })
|
return ResponseWrapper(res, { code: 200 })
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
interface GetApiRequest extends NextApiRequest {
|
interface GetApiRequest extends NextApiRequest {
|
||||||
@ -175,7 +241,7 @@ interface PatchApiRequest extends GetApiRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteApiRequest extends GetApiRequest {
|
interface DeleteApiRequest extends GetApiRequest {
|
||||||
body: CsrfCaptcha & { name: string } | null
|
body: (CsrfCaptcha & { name: string }) | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Servers
|
export default Servers
|
||||||
|
|||||||
@ -4,8 +4,7 @@ import RequestHandler from '@utils/RequestHandler'
|
|||||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||||
import { get } from '@utils/Query'
|
import { get } from '@utils/Query'
|
||||||
|
|
||||||
const ServerOwners = RequestHandler()
|
const ServerOwners = RequestHandler().get(async (req: GetApiRequest, res) => {
|
||||||
.get(async (req: GetApiRequest, res) => {
|
|
||||||
const owners = await get.serverOwners(req.query.id)
|
const owners = await get.serverOwners(req.query.id)
|
||||||
if (!owners) return ResponseWrapper(res, { code: 404 })
|
if (!owners) return ResponseWrapper(res, { code: 404 })
|
||||||
return ResponseWrapper(res, { code: 200, data: owners })
|
return ResponseWrapper(res, { code: 200, data: owners })
|
||||||
|
|||||||
@ -18,10 +18,11 @@ const limiter = rateLimit({
|
|||||||
skip: (_req, res) => {
|
skip: (_req, res) => {
|
||||||
res.removeHeader('X-RateLimit-Global')
|
res.removeHeader('X-RateLimit-Global')
|
||||||
return false
|
return false
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const ServerReport = RequestHandler().post(limiter)
|
const ServerReport = RequestHandler()
|
||||||
|
.post(limiter)
|
||||||
.post(async (req: PostApiRequest, res) => {
|
.post(async (req: PostApiRequest, res) => {
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
@ -31,18 +32,21 @@ const ServerReport = RequestHandler().post(limiter)
|
|||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
||||||
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
|
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
await webhookClients.internal.reportChannel.send({ threadName: `서버-${server.id}`, content: `Reported by <@${user}> (${user})\nReported **${server.name}** (${server.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``, allowedMentions: { parse: ['users'] }})
|
await webhookClients.internal.reportChannel.send({
|
||||||
|
threadName: `서버-${server.id}`,
|
||||||
|
content: `Reported by <@${user}> (${user})\nReported **${server.name}** (${server.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``,
|
||||||
|
allowedMentions: { parse: ['users'] },
|
||||||
|
})
|
||||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
|
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
interface PostApiRequest extends NextApiRequest {
|
interface PostApiRequest extends NextApiRequest {
|
||||||
body: Report | null
|
body: Report | null
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@ -14,13 +14,21 @@ const ServerVote = RequestHandler()
|
|||||||
const server = await get.ServerAuthorization(req.headers.authorization)
|
const server = await get.ServerAuthorization(req.headers.authorization)
|
||||||
if (!server) return ResponseWrapper(res, { code: 401 })
|
if (!server) return ResponseWrapper(res, { code: 401 })
|
||||||
if (req.query.id !== server) return ResponseWrapper(res, { code: 403 })
|
if (req.query.id !== server) return ResponseWrapper(res, { code: 403 })
|
||||||
const userID = await Yup.string().required().label('userID').validate(req.query.userID).then(el => el).catch(e => {
|
const userID = await Yup.string()
|
||||||
|
.required()
|
||||||
|
.label('userID')
|
||||||
|
.validate(req.query.userID)
|
||||||
|
.then((el) => el)
|
||||||
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!userID) return ResponseWrapper(res, { code: 400 })
|
if (!userID) return ResponseWrapper(res, { code: 400 })
|
||||||
const result = await get.vote(userID, server, 'server')
|
const result = await get.vote(userID, server, 'server')
|
||||||
return ResponseWrapper(res, { code: 200, data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result } })
|
return ResponseWrapper(res, {
|
||||||
|
code: 200,
|
||||||
|
data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.post(async (req: PostApiRequest, res) => {
|
.post(async (req: PostApiRequest, res) => {
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
@ -42,13 +50,12 @@ const ServerVote = RequestHandler()
|
|||||||
type: WebhookType.HeartChange,
|
type: WebhookType.HeartChange,
|
||||||
before: server.votes,
|
before: server.votes,
|
||||||
after: server.votes + 1,
|
after: server.votes + 1,
|
||||||
userId: user
|
userId: user,
|
||||||
},
|
},
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
})
|
})
|
||||||
return ResponseWrapper(res, { code: 200 })
|
return ResponseWrapper(res, { code: 200 })
|
||||||
}
|
} else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
|
||||||
else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
interface ApiRequest extends NextApiRequest {
|
interface ApiRequest extends NextApiRequest {
|
||||||
|
|||||||
@ -17,10 +17,11 @@ const limiter = rateLimit({
|
|||||||
skip: (_req, res) => {
|
skip: (_req, res) => {
|
||||||
res.removeHeader('X-RateLimit-Global')
|
res.removeHeader('X-RateLimit-Global')
|
||||||
return false
|
return false
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const UserReport = RequestHandler().post(limiter)
|
const UserReport = RequestHandler()
|
||||||
|
.post(limiter)
|
||||||
.post(async (req: PostApiRequest, res) => {
|
.post(async (req: PostApiRequest, res) => {
|
||||||
const user = await get.Authorization(req.cookies.token)
|
const user = await get.Authorization(req.cookies.token)
|
||||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||||
@ -30,18 +31,27 @@ const UserReport = RequestHandler().post(limiter)
|
|||||||
if (!csrfValidated) return
|
if (!csrfValidated) return
|
||||||
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
||||||
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
|
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
await webhookClients.internal.reportChannel.send({ threadName: `유저-${userInfo.id}`, content: `Reported by <@${user}> (${user})\nReported **${userInfo.tag === '0' ? userInfo.globalName + ' @' + userInfo.username : userInfo.username + '#' + userInfo.tag}** <@${userInfo.id}> (${userInfo.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``, allowedMentions: { parse: ['users'] }})
|
await webhookClients.internal.reportChannel.send({
|
||||||
|
threadName: `유저-${userInfo.id}`,
|
||||||
|
content: `Reported by <@${user}> (${user})\nReported **${
|
||||||
|
userInfo.tag === '0'
|
||||||
|
? userInfo.globalName + ' @' + userInfo.username
|
||||||
|
: userInfo.username + '#' + userInfo.tag
|
||||||
|
}** <@${userInfo.id}> (${userInfo.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${
|
||||||
|
req.body.description
|
||||||
|
}\`\`\``,
|
||||||
|
allowedMentions: { parse: ['users'] },
|
||||||
|
})
|
||||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
|
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
interface PostApiRequest extends NextApiRequest {
|
interface PostApiRequest extends NextApiRequest {
|
||||||
body: Report | null
|
body: Report | null
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@ -20,8 +20,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
|
|||||||
scale,
|
scale,
|
||||||
icon,
|
icon,
|
||||||
})
|
})
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@ -20,8 +20,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
|
|||||||
scale,
|
scale,
|
||||||
icon,
|
icon,
|
||||||
})
|
})
|
||||||
.then(el => el)
|
.then((el) => el)
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@ -45,7 +45,10 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
async function submitBot(value: ManageBot) {
|
async function submitBot(value: ManageBot) {
|
||||||
const res = await Fetch(`/bots/${bot.id}`, { method: 'PATCH', body: JSON.stringify(cleanObject<ManageBot>(value)) })
|
const res = await Fetch(`/bots/${bot.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(cleanObject<ManageBot>(value)),
|
||||||
|
})
|
||||||
setData(res)
|
setData(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,15 +59,23 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!bot) return <NotFound />
|
if (!bot) return <NotFound />
|
||||||
if(!user) return <Login>
|
if (!user)
|
||||||
|
return (
|
||||||
|
<Login>
|
||||||
<NextSeo title='봇 정보 수정하기' description='봇의 정보를 수정합니다.' />
|
<NextSeo title='봇 정보 수정하기' description='봇의 정보를 수정합니다.' />
|
||||||
</Login>
|
</Login>
|
||||||
if(!(bot.owners as User[]).find(el => el.id === user.id) && !checkUserFlag(user.flags, 'staff')) return <Forbidden />
|
)
|
||||||
|
if (
|
||||||
|
!(bot.owners as User[]).find((el) => el.id === user.id) &&
|
||||||
|
!checkUserFlag(user.flags, 'staff')
|
||||||
|
)
|
||||||
|
return <Forbidden />
|
||||||
return (
|
return (
|
||||||
<Container paddingTop className='pt-5 pb-10'>
|
<Container paddingTop className='pb-10 pt-5'>
|
||||||
<NextSeo title={`${bot.name} 수정하기`} description='봇의 정보를 수정합니다.' />
|
<NextSeo title={`${bot.name} 수정하기`} description='봇의 정보를 수정합니다.' />
|
||||||
<h1 className='text-3xl font-bold mb-8'>봇 관리하기</h1>
|
<h1 className='mb-8 text-3xl font-bold'>봇 관리하기</h1>
|
||||||
<Formik initialValues={cleanObject({
|
<Formik
|
||||||
|
initialValues={cleanObject({
|
||||||
agree: false,
|
agree: false,
|
||||||
id: bot.id,
|
id: bot.id,
|
||||||
prefix: bot.prefix,
|
prefix: bot.prefix,
|
||||||
@ -76,88 +87,191 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
url: bot.url,
|
url: bot.url,
|
||||||
git: bot.git,
|
git: bot.git,
|
||||||
discord: bot.discord,
|
discord: bot.discord,
|
||||||
_csrf: csrfToken
|
_csrf: csrfToken,
|
||||||
})}
|
})}
|
||||||
validationSchema={ManageBotSchema}
|
validationSchema={ManageBotSchema}
|
||||||
onSubmit={submitBot}>
|
onSubmit={submitBot}
|
||||||
|
>
|
||||||
{({ errors, touched, values, setFieldTouched, setFieldValue }) => (
|
{({ errors, touched, values, setFieldTouched, setFieldValue }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<div className='md:flex text-center md:text-left'>
|
<div className='text-center md:flex md:text-left'>
|
||||||
<DiscordAvatar userID={bot.id} className='md:mx-1 mx-auto rounded-full'/>
|
<DiscordAvatar userID={bot.id} className='mx-auto rounded-full md:mx-1' />
|
||||||
<div className='md:w-2/3 px-8 py-6'>
|
<div className='px-8 py-6 md:w-2/3'>
|
||||||
<h1 className='text-3xl font-bold'>{bot.name}#{bot.tag}</h1>
|
<h1 className='text-3xl font-bold'>
|
||||||
|
{bot.name}#{bot.tag}
|
||||||
|
</h1>
|
||||||
<h2>ID: {bot.id}</h2>
|
<h2>ID: {bot.id}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
{data ? (
|
||||||
data ? data.code === 200 ? <div className='mt-4'>
|
data.code === 200 ? (
|
||||||
|
<div className='mt-4'>
|
||||||
<Redirect to={makeBotURL(bot)}>
|
<Redirect to={makeBotURL(bot)}>
|
||||||
<Message type='success'>
|
<Message type='success'>
|
||||||
<h2 className='text-lg font-extrabold'>정보를 저장했습니다.</h2>
|
<h2 className='text-lg font-extrabold'>정보를 저장했습니다.</h2>
|
||||||
<p>반영까지는 시간이 조금 걸릴 수 있습니다!</p>
|
<p>반영까지는 시간이 조금 걸릴 수 있습니다!</p>
|
||||||
</Message>
|
</Message>
|
||||||
</Redirect>
|
</Redirect>
|
||||||
|
</div>
|
||||||
</div> : <div className='mt-4'>
|
) : (
|
||||||
|
<div className='mt-4'>
|
||||||
<Message type='error'>
|
<Message type='error'>
|
||||||
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
|
<h2 className='text-lg font-extrabold'>
|
||||||
<ul className='list-disc list-inside'>
|
{data.message || '오류가 발생했습니다.'}
|
||||||
|
</h2>
|
||||||
|
<ul className='list-inside list-disc'>
|
||||||
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
</Message>
|
</Message>
|
||||||
</div> : ''
|
</div>
|
||||||
}
|
)
|
||||||
<Label For='prefix' label='접두사' labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)' error={errors.prefix && touched.prefix ? errors.prefix : null} short required>
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
<Label
|
||||||
|
For='prefix'
|
||||||
|
label='접두사'
|
||||||
|
labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)'
|
||||||
|
error={errors.prefix && touched.prefix ? errors.prefix : null}
|
||||||
|
short
|
||||||
|
required
|
||||||
|
>
|
||||||
<Input name='prefix' placeholder='!' />
|
<Input name='prefix' placeholder='!' />
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='library' label='라이브러리' labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.' short required error={errors.library && touched.library ? errors.library : null}>
|
<Label
|
||||||
<Select value={{ label: bot.lib, value: bot.lib }} options={library.map(el=> ({ label: el, value: el }))} handleChange={(value) => setFieldValue('library', value.value)} handleTouch={() => setFieldTouched('library', true)} />
|
For='library'
|
||||||
|
label='라이브러리'
|
||||||
|
labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.'
|
||||||
|
short
|
||||||
|
required
|
||||||
|
error={errors.library && touched.library ? errors.library : null}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={{ label: bot.lib, value: bot.lib }}
|
||||||
|
options={library.map((el) => ({ label: el, value: el }))}
|
||||||
|
handleChange={(value) => setFieldValue('library', value.value)}
|
||||||
|
handleTouch={() => setFieldTouched('library', true)}
|
||||||
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='category' label='카테고리' labelDesc='봇에 해당되는 카테고리를 선택해주세요' required error={errors.category && touched.category ? errors.category as string : null}>
|
<Label
|
||||||
<Selects options={botCategories.map(el=> ({ label: el, value: el }))} handleChange={(value) => {
|
For='category'
|
||||||
setFieldValue('category', value.map(v=> v.value))
|
label='카테고리'
|
||||||
}} handleTouch={() => setFieldTouched('category', true)} values={values.category as string[]} setValues={(value) => setFieldValue('category', value)} />
|
labelDesc='봇에 해당되는 카테고리를 선택해주세요'
|
||||||
<span className='text-gray-400 mt-1 text-sm'>봇 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요. <strong>반드시 해당되는 카테고리만 선택해주세요.</strong></span>
|
required
|
||||||
|
error={errors.category && touched.category ? (errors.category as string) : null}
|
||||||
|
>
|
||||||
|
<Selects
|
||||||
|
options={botCategories.map((el) => ({ label: el, value: el }))}
|
||||||
|
handleChange={(value) => {
|
||||||
|
setFieldValue(
|
||||||
|
'category',
|
||||||
|
value.map((v) => v.value)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
handleTouch={() => setFieldTouched('category', true)}
|
||||||
|
values={values.category as string[]}
|
||||||
|
setValues={(value) => setFieldValue('category', value)}
|
||||||
|
/>
|
||||||
|
<span className='mt-1 text-sm text-gray-400'>
|
||||||
|
봇 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요.{' '}
|
||||||
|
<strong>반드시 해당되는 카테고리만 선택해주세요.</strong>
|
||||||
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Label For='website' label='웹사이트' labelDesc='봇의 웹사이트를 작성해주세요.' error={errors.website && touched.website ? errors.website : null}>
|
<Label
|
||||||
|
For='website'
|
||||||
|
label='웹사이트'
|
||||||
|
labelDesc='봇의 웹사이트를 작성해주세요.'
|
||||||
|
error={errors.website && touched.website ? errors.website : null}
|
||||||
|
>
|
||||||
<Input name='website' placeholder='https://koreanbots.dev' />
|
<Input name='website' placeholder='https://koreanbots.dev' />
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='git' label='Git URL' labelDesc='봇 소스코드의 Git 주소를 입력해주세요 (오픈소스인 경우)' error={errors.git && touched.git ? errors.git : null}>
|
<Label
|
||||||
|
For='git'
|
||||||
|
label='Git URL'
|
||||||
|
labelDesc='봇 소스코드의 Git 주소를 입력해주세요 (오픈소스인 경우)'
|
||||||
|
error={errors.git && touched.git ? errors.git : null}
|
||||||
|
>
|
||||||
<Input name='git' placeholder='https://github.com/koreanbots/koreanbots' />
|
<Input name='git' placeholder='https://github.com/koreanbots/koreanbots' />
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='inviteLink' label='초대링크' labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.' error={errors.url && touched.url ? errors.url : null}>
|
<Label
|
||||||
<Input name='url' placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0' />
|
For='inviteLink'
|
||||||
<span className='text-gray-400 mt-1 text-sm'>
|
label='초대링크'
|
||||||
|
labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.'
|
||||||
|
error={errors.url && touched.url ? errors.url : null}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name='url'
|
||||||
|
placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0'
|
||||||
|
/>
|
||||||
|
<span className='mt-1 text-sm text-gray-400'>
|
||||||
<Link
|
<Link
|
||||||
href='/calculator'
|
href='/calculator'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
className='text-blue-500 hover:text-blue-400'>
|
className='text-blue-500 hover:text-blue-400'
|
||||||
|
>
|
||||||
이곳
|
이곳
|
||||||
</Link>에서 초대링크를 생성하실 수 있습니다!
|
</Link>
|
||||||
|
에서 초대링크를 생성하실 수 있습니다!
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='discord' label='지원 디스코드 서버' labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)' error={errors.discord && touched.discord ? errors.discord : null} short>
|
<Label
|
||||||
|
For='discord'
|
||||||
|
label='지원 디스코드 서버'
|
||||||
|
labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)'
|
||||||
|
error={errors.discord && touched.discord ? errors.discord : null}
|
||||||
|
short
|
||||||
|
>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
discord.gg/<Input name='discord' placeholder='JEh53MQ' />
|
discord.gg/
|
||||||
|
<Input name='discord' placeholder='JEh53MQ' />
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Label For='intro' label='봇 소개' labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required>
|
<Label
|
||||||
|
For='intro'
|
||||||
|
label='봇 소개'
|
||||||
|
labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)'
|
||||||
|
error={errors.intro && touched.intro ? errors.intro : null}
|
||||||
|
required
|
||||||
|
>
|
||||||
<Input name='intro' placeholder='국내 봇을 한 곳에서.' />
|
<Input name='intro' placeholder='국내 봇을 한 곳에서.' />
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='intro' label='봇 설명' labelDesc={<>봇을 자세하게 설명해주세요! (최대 1500자)<br/>마크다운을 지원합니다!</>} error={errors.desc && touched.desc ? errors.desc : null} required>
|
<Label
|
||||||
<TextArea name='desc' placeholder='봇에 대해 최대한 자세히 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.desc} setValue={(value) => setFieldValue('desc', value)} max={1500} />
|
For='intro'
|
||||||
|
label='봇 설명'
|
||||||
|
labelDesc={
|
||||||
|
<>
|
||||||
|
봇을 자세하게 설명해주세요! (최대 1500자)
|
||||||
|
<br />
|
||||||
|
마크다운을 지원합니다!
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
error={errors.desc && touched.desc ? errors.desc : null}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
name='desc'
|
||||||
|
placeholder='봇에 대해 최대한 자세히 설명해주세요!'
|
||||||
|
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||||
|
value={values.desc}
|
||||||
|
setValue={(value) => setFieldValue('desc', value)}
|
||||||
|
max={1500}
|
||||||
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
<Label For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다'>
|
<Label
|
||||||
|
For='preview'
|
||||||
|
label='설명 미리보기'
|
||||||
|
labelDesc='다음 결과는 실제와 다를 수 있습니다'
|
||||||
|
>
|
||||||
<Segment>
|
<Segment>
|
||||||
<Markdown text={values.desc} />
|
<Markdown text={values.desc} />
|
||||||
</Segment>
|
</Segment>
|
||||||
</Label>
|
</Label>
|
||||||
<Divider />
|
<Divider />
|
||||||
<p className='text-base mt-2 mb-5'>
|
<p className='mb-5 mt-2 text-base'>
|
||||||
<span className='text-red-500 font-semibold'> *</span> = 필수 항목
|
<span className='font-semibold text-red-500'> *</span> = 필수 항목
|
||||||
</p>
|
</p>
|
||||||
<Button type='submit' onClick={() => window.scrollTo({ top: 0 })}>
|
<Button type='submit' onClick={() => window.scrollTo({ top: 0 })}>
|
||||||
<>
|
<>
|
||||||
@ -167,24 +281,41 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
{
|
{(checkUserFlag(user.flags, 'staff') || (bot.owners as User[])[0].id === user.id) && (
|
||||||
(checkUserFlag(user.flags, 'staff') || (bot.owners as User[])[0].id === user.id) && <div className='py-4'>
|
<div className='py-4'>
|
||||||
<Divider />
|
<Divider />
|
||||||
<h2 className='text-2xl font-semibold pb-2'>위험구역</h2>
|
<h2 className='pb-2 text-2xl font-semibold'>위험구역</h2>
|
||||||
<Segment>
|
<Segment>
|
||||||
<div className='lg:flex items-center'>
|
<div className='items-center lg:flex'>
|
||||||
<div className='grow py-1'>
|
<div className='grow py-1'>
|
||||||
<h3 className='text-lg font-semibold'>관리자 수정</h3>
|
<h3 className='text-lg font-semibold'>관리자 수정</h3>
|
||||||
<p className='text-gray-400'>봇의 관리자를 추가하거나 삭제합니다.</p>
|
<p className='text-gray-400'>봇의 관리자를 추가하거나 삭제합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setAdminModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-user-cog' /> 관리자 수정</Button>
|
<Button
|
||||||
<Modal full header='관리자 수정' isOpen={adminModal} dark={theme === 'dark'} onClose={() => setAdminModal(false)} closeIcon>
|
onClick={() => setAdminModal(true)}
|
||||||
<Formik initialValues={{ owners: (bot.owners as User[]), id: '', _captcha: '' }} onSubmit={async (v) => {
|
className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
|
||||||
const res = await Fetch(`/bots/${bot.id}/owners`, { method: 'PATCH', body: JSON.stringify({
|
>
|
||||||
|
<i className='fas fa-user-cog' /> 관리자 수정
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
full
|
||||||
|
header='관리자 수정'
|
||||||
|
isOpen={adminModal}
|
||||||
|
dark={theme === 'dark'}
|
||||||
|
onClose={() => setAdminModal(false)}
|
||||||
|
closeIcon
|
||||||
|
>
|
||||||
|
<Formik
|
||||||
|
initialValues={{ owners: bot.owners as User[], id: '', _captcha: '' }}
|
||||||
|
onSubmit={async (v) => {
|
||||||
|
const res = await Fetch(`/bots/${bot.id}/owners`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({
|
||||||
_captcha: v._captcha,
|
_captcha: v._captcha,
|
||||||
_csrf: csrfToken,
|
_csrf: csrfToken,
|
||||||
owners: v.owners.map(el => el.id)
|
owners: v.owners.map((el) => el.id),
|
||||||
}) })
|
}),
|
||||||
|
})
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
alert('성공적으로 수정했습니다.')
|
alert('성공적으로 수정했습니다.')
|
||||||
router.push(makeBotURL(bot))
|
router.push(makeBotURL(bot))
|
||||||
@ -192,38 +323,64 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
alert(res.message)
|
alert(res.message)
|
||||||
setAdminModal(false)
|
setAdminModal(false)
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
{
|
>
|
||||||
({ values, setFieldValue }) => <Form>
|
{({ values, setFieldValue }) => (
|
||||||
|
<Form>
|
||||||
<Message type='warning'>
|
<Message type='warning'>
|
||||||
<p>소유자는 삭제할 수 없습니다. 소유권을 이전하고 싶으시다면 소유권 이전을 사용해주세요.</p>
|
<p>
|
||||||
|
소유자는 삭제할 수 없습니다. 소유권을 이전하고 싶으시다면 소유권 이전을
|
||||||
|
사용해주세요.
|
||||||
|
</p>
|
||||||
</Message>
|
</Message>
|
||||||
<div className='py-4'>
|
<div className='py-4'>
|
||||||
<h2 className='text-md my-1'>이전하실 유저 ID를 입력해주세요.</h2>
|
<h2 className='text-md my-1'>이전하실 유저 ID를 입력해주세요.</h2>
|
||||||
<div className='flex flex-wrap'>
|
<div className='flex flex-wrap'>
|
||||||
{
|
{(values.owners as User[]).map((el, n) => (
|
||||||
(values.owners as User[]).map((el, n) => <Tag className='flex items-center' text={<>
|
<Tag
|
||||||
<DiscordAvatar userID={el.id} size={128} className='w-6 h-6 mr-1 rounded-full' /> {el.tag === '0' ? `${el.globalName} (@${el.username})` : `${el.username}#${el.tag}`}
|
className='flex items-center'
|
||||||
{
|
text={
|
||||||
n !== 0 && <button className='ml-0.5 hover:text-red-500' onClick={() => {
|
<>
|
||||||
setFieldValue('owners', (() => {
|
<DiscordAvatar
|
||||||
|
userID={el.id}
|
||||||
|
size={128}
|
||||||
|
className='mr-1 h-6 w-6 rounded-full'
|
||||||
|
/>{' '}
|
||||||
|
{el.tag === '0'
|
||||||
|
? `${el.globalName} (@${el.username})`
|
||||||
|
: `${el.username}#${el.tag}`}
|
||||||
|
{n !== 0 && (
|
||||||
|
<button
|
||||||
|
className='ml-0.5 hover:text-red-500'
|
||||||
|
onClick={() => {
|
||||||
|
setFieldValue(
|
||||||
|
'owners',
|
||||||
|
(() => {
|
||||||
const arr = [...values.owners]
|
const arr = [...values.owners]
|
||||||
arr.splice(n, 1)
|
arr.splice(n, 1)
|
||||||
return arr
|
return arr
|
||||||
})())
|
})()
|
||||||
}}>
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<i className='fas fa-times' />
|
<i className='fas fa-times' />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
</>} key={el.id} />)
|
key={el.id}
|
||||||
}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<div className='grow pr-2'>
|
<div className='grow pr-2'>
|
||||||
<Input name='id' placeholder='추가할 유저 ID' />
|
<Input name='id' placeholder='추가할 유저 ID' />
|
||||||
</div>
|
</div>
|
||||||
<Button className='w-16 bg-discord-blurple' onClick={async () => {
|
<Button
|
||||||
if(values.owners.find(el => el.id === values.id)) return alert('이미 존재하는 유저입니다.')
|
className='w-16 bg-discord-blurple'
|
||||||
|
onClick={async () => {
|
||||||
|
if (values.owners.find((el) => el.id === values.id))
|
||||||
|
return alert('이미 존재하는 유저입니다.')
|
||||||
const user = await getUser(values.id)
|
const user = await getUser(values.id)
|
||||||
const arr = [...values.owners]
|
const arr = [...values.owners]
|
||||||
if (!user) return alert('올바르지 않은 유저입니다.')
|
if (!user) return alert('올바르지 않은 유저입니다.')
|
||||||
@ -232,32 +389,63 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
setFieldValue('owners', arr)
|
setFieldValue('owners', arr)
|
||||||
setFieldValue('id', '')
|
setFieldValue('id', '')
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<i className='fas fa-user-plus text-white' />
|
<i className='fas fa-user-plus text-white' />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} />
|
<Captcha
|
||||||
<Button disabled={!values._captcha} className={`mt-2 bg-red-500 text-white ${!values._captcha ? 'opacity-80' : 'hover:opacity-80'}`} type='submit'><i className='fas fa-save text-sm' /> 저장</Button>
|
dark={theme === 'dark'}
|
||||||
|
onVerify={(k) => setFieldValue('_captcha', k)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={!values._captcha}
|
||||||
|
className={`mt-2 bg-red-500 text-white ${
|
||||||
|
!values._captcha ? 'opacity-80' : 'hover:opacity-80'
|
||||||
|
}`}
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
<i className='fas fa-save text-sm' /> 저장
|
||||||
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className='lg:flex items-center'>
|
<div className='items-center lg:flex'>
|
||||||
<div className='grow py-1'>
|
<div className='grow py-1'>
|
||||||
<h3 className='text-lg font-semibold'>소유권 이전</h3>
|
<h3 className='text-lg font-semibold'>소유권 이전</h3>
|
||||||
<p className='text-gray-400'>봇의 소유권을 이전합니다. 소유권을 이전하게 되면 소유권을 잃게 됩니다.</p>
|
<p className='text-gray-400'>
|
||||||
|
봇의 소유권을 이전합니다. 소유권을 이전하게 되면 소유권을 잃게 됩니다.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setTransferModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-exchange-alt' /> 소유권 이전</Button>
|
<Button
|
||||||
<Modal full header={`${bot.name} 소유권 이전하기`} isOpen={transferModal} dark={theme === 'dark'} onClose={() => setTransferModal(false)} closeIcon>
|
onClick={() => setTransferModal(true)}
|
||||||
<Formik initialValues={{ ownerID: '', name: '', _captcha: '' }} onSubmit={async (v) => {
|
className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
|
||||||
const res = await Fetch(`/bots/${bot.id}/owners`, { method: 'PATCH', body: JSON.stringify({
|
>
|
||||||
|
<i className='fas fa-exchange-alt' /> 소유권 이전
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
full
|
||||||
|
header={`${bot.name} 소유권 이전하기`}
|
||||||
|
isOpen={transferModal}
|
||||||
|
dark={theme === 'dark'}
|
||||||
|
onClose={() => setTransferModal(false)}
|
||||||
|
closeIcon
|
||||||
|
>
|
||||||
|
<Formik
|
||||||
|
initialValues={{ ownerID: '', name: '', _captcha: '' }}
|
||||||
|
onSubmit={async (v) => {
|
||||||
|
const res = await Fetch(`/bots/${bot.id}/owners`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({
|
||||||
_captcha: v._captcha,
|
_captcha: v._captcha,
|
||||||
_csrf: csrfToken,
|
_csrf: csrfToken,
|
||||||
owners: [ v.ownerID ]
|
owners: [v.ownerID],
|
||||||
}) })
|
}),
|
||||||
|
})
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
alert('성공적으로 소유권을 이전했습니다.')
|
alert('성공적으로 소유권을 이전했습니다.')
|
||||||
router.push('/')
|
router.push('/')
|
||||||
@ -265,62 +453,119 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
alert(res.message)
|
alert(res.message)
|
||||||
setTransferModal(false)
|
setTransferModal(false)
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
{
|
>
|
||||||
({ values, setFieldValue }) => <Form>
|
{({ values, setFieldValue }) => (
|
||||||
|
<Form>
|
||||||
<Message type='warning'>
|
<Message type='warning'>
|
||||||
<h2 className='text-2xl font-bold'>주의해주세요!</h2>
|
<h2 className='text-2xl font-bold'>주의해주세요!</h2>
|
||||||
<p>봇의 소유권을 이전하게 되면 봇의 소유자 권한을 이전하게 되며, 본인을 포함한 모든 관리자가 해당 봇에 대한 권한을 잃게됩니다.</p>
|
<p>
|
||||||
|
봇의 소유권을 이전하게 되면 봇의 소유자 권한을 이전하게 되며, 본인을
|
||||||
|
포함한 모든 관리자가 해당 봇에 대한 권한을 잃게됩니다.
|
||||||
|
</p>
|
||||||
</Message>
|
</Message>
|
||||||
<div className='py-4'>
|
<div className='py-4'>
|
||||||
<h2 className='text-md my-1'>이전하실 유저 ID를 입력해주세요.</h2>
|
<h2 className='text-md my-1'>이전하실 유저 ID를 입력해주세요.</h2>
|
||||||
<Input name='ownerID' placeholder='이전할 유저 ID' />
|
<Input name='ownerID' placeholder='이전할 유저 ID' />
|
||||||
<Divider />
|
<Divider />
|
||||||
<h2 className='text-md my-1'>계속 하시려면 <strong>{bot.name}</strong>{getJosaPicker('을')(bot.name)} 입력해주세요.</h2>
|
<h2 className='text-md my-1'>
|
||||||
|
계속 하시려면 <strong>{bot.name}</strong>
|
||||||
|
{getJosaPicker('을')(bot.name)} 입력해주세요.
|
||||||
|
</h2>
|
||||||
<Input name='name' placeholder={bot.name} />
|
<Input name='name' placeholder={bot.name} />
|
||||||
</div>
|
</div>
|
||||||
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} />
|
<Captcha
|
||||||
<Button disabled={!values.ownerID || values.name !== bot.name || !values._captcha} className={`mt-4 bg-red-500 text-white ${!values.ownerID ||values.name !== bot.name || !values._captcha ? 'opacity-80' : 'hover:opacity-80'}`} type='submit'><i className='fas fa-exchange-alt' /> 소유권 이전</Button>
|
dark={theme === 'dark'}
|
||||||
|
onVerify={(k) => setFieldValue('_captcha', k)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={!values.ownerID || values.name !== bot.name || !values._captcha}
|
||||||
|
className={`mt-4 bg-red-500 text-white ${
|
||||||
|
!values.ownerID || values.name !== bot.name || !values._captcha
|
||||||
|
? 'opacity-80'
|
||||||
|
: 'hover:opacity-80'
|
||||||
|
}`}
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
<i className='fas fa-exchange-alt' /> 소유권 이전
|
||||||
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className='lg:flex items-center'>
|
<div className='items-center lg:flex'>
|
||||||
<div className='grow py-1'>
|
<div className='grow py-1'>
|
||||||
<h3 className='text-lg font-semibold'>봇 삭제하기</h3>
|
<h3 className='text-lg font-semibold'>봇 삭제하기</h3>
|
||||||
<p className='text-gray-400'>봇을 삭제하게 되면 되돌릴 수 없습니다.</p>
|
<p className='text-gray-400'>봇을 삭제하게 되면 되돌릴 수 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setDeleteModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-trash' /> 봇 삭제하기</Button>
|
<Button
|
||||||
<Modal full header={`${bot.name} 삭제하기`} isOpen={deleteModal} dark={theme === 'dark'} onClose={() => setDeleteModal(false)} closeIcon>
|
onClick={() => setDeleteModal(true)}
|
||||||
<Formik initialValues={{ name: '', _captcha: '', _csrf: csrfToken }} onSubmit={async (v) => {
|
className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
|
||||||
const res = await Fetch(`/bots/${bot.id}`, { method: 'DELETE', body: JSON.stringify(v) })
|
>
|
||||||
|
<i className='fas fa-trash' /> 봇 삭제하기
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
full
|
||||||
|
header={`${bot.name} 삭제하기`}
|
||||||
|
isOpen={deleteModal}
|
||||||
|
dark={theme === 'dark'}
|
||||||
|
onClose={() => setDeleteModal(false)}
|
||||||
|
closeIcon
|
||||||
|
>
|
||||||
|
<Formik
|
||||||
|
initialValues={{ name: '', _captcha: '', _csrf: csrfToken }}
|
||||||
|
onSubmit={async (v) => {
|
||||||
|
const res = await Fetch(`/bots/${bot.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: JSON.stringify(v),
|
||||||
|
})
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
alert('성공적으로 삭제하였습니다.')
|
alert('성공적으로 삭제하였습니다.')
|
||||||
redirectTo(router, '/')
|
redirectTo(router, '/')
|
||||||
}
|
} else alert(res.message)
|
||||||
else alert(res.message)
|
}}
|
||||||
}}>
|
>
|
||||||
{
|
{({ values, setFieldValue }) => (
|
||||||
({ values, setFieldValue }) => <Form>
|
<Form>
|
||||||
<Message type='warning'>
|
<Message type='warning'>
|
||||||
<p>봇을 삭제하게 되면 되돌릴 수 없습니다.<br/>하트 수를 포함한 모든 봇 정보가 영구적으로 삭제됩니다.</p>
|
<p>
|
||||||
<p>계속 하시려면 봇의 이름 <strong>{bot.name}</strong>{getJosaPicker('을')(bot.name)} 입력해주세요.</p>
|
봇을 삭제하게 되면 되돌릴 수 없습니다.
|
||||||
|
<br />
|
||||||
|
하트 수를 포함한 모든 봇 정보가 영구적으로 삭제됩니다.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
계속 하시려면 봇의 이름 <strong>{bot.name}</strong>
|
||||||
|
{getJosaPicker('을')(bot.name)} 입력해주세요.
|
||||||
|
</p>
|
||||||
</Message>
|
</Message>
|
||||||
<div className='py-4'>
|
<div className='py-4'>
|
||||||
<Input name='name' placeholder={bot.name} />
|
<Input name='name' placeholder={bot.name} />
|
||||||
</div>
|
</div>
|
||||||
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} />
|
<Captcha
|
||||||
<Button disabled={values.name !== bot.name || !values._captcha} className={`mt-4 bg-red-500 text-white ${values.name !== bot.name || !values._captcha ? 'opacity-80' : 'hover:opacity-80'}`} type='submit'><i className='fas fa-trash' /> 삭제</Button>
|
dark={theme === 'dark'}
|
||||||
|
onVerify={(k) => setFieldValue('_captcha', k)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={values.name !== bot.name || !values._captcha}
|
||||||
|
className={`mt-4 bg-red-500 text-white ${
|
||||||
|
values.name !== bot.name || !values._captcha
|
||||||
|
? 'opacity-80'
|
||||||
|
: 'hover:opacity-80'
|
||||||
|
}`}
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
<i className='fas fa-trash' /> 삭제
|
||||||
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</Segment>
|
</Segment>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -328,7 +573,13 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
export const getServerSideProps = async (ctx: Context) => {
|
export const getServerSideProps = async (ctx: Context) => {
|
||||||
const parsed = parseCookie(ctx.req)
|
const parsed = parseCookie(ctx.req)
|
||||||
const user = await get.Authorization(parsed?.token)
|
const user = await get.Authorization(parsed?.token)
|
||||||
return { props: { bot: await get.bot.load(ctx.query.id), user: await get.user.load(user || ''), csrfToken: getToken(ctx.req, ctx.res) } }
|
return {
|
||||||
|
props: {
|
||||||
|
bot: await get.bot.load(ctx.query.id),
|
||||||
|
user: await get.user.load(user || ''),
|
||||||
|
csrfToken: getToken(ctx.req, ctx.res),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ManageBotProps {
|
interface ManageBotProps {
|
||||||
|
|||||||
@ -47,56 +47,84 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
|
|||||||
}, [])
|
}, [])
|
||||||
if (!data?.id) return <NotFound />
|
if (!data?.id) return <NotFound />
|
||||||
return (
|
return (
|
||||||
<div style={bg ? { background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${data.bg}") center top / cover no-repeat fixed` } : {}}>
|
<div
|
||||||
|
style={
|
||||||
|
bg
|
||||||
|
? {
|
||||||
|
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${data.bg}") center top / cover no-repeat fixed`,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
<Container paddingTop className='py-10'>
|
<Container paddingTop className='py-10'>
|
||||||
<NextSeo
|
<NextSeo
|
||||||
title={data.name}
|
title={data.name}
|
||||||
description={data.intro}
|
description={data.intro}
|
||||||
twitter={{
|
twitter={{
|
||||||
cardType: 'summary_large_image'
|
cardType: 'summary_large_image',
|
||||||
}}
|
}}
|
||||||
openGraph={{
|
openGraph={{
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: KoreanbotsEndPoints.OG.bot(data.id, data.name, data.intro, data.category, [formatNumber(data.votes), formatNumber(data.servers)]),
|
url: KoreanbotsEndPoints.OG.bot(data.id, data.name, data.intro, data.category, [
|
||||||
|
formatNumber(data.votes),
|
||||||
|
formatNumber(data.servers),
|
||||||
|
]),
|
||||||
width: 2048,
|
width: 2048,
|
||||||
height: 1170,
|
height: 1170,
|
||||||
alt: 'Bot Preview Image'
|
alt: 'Bot Preview Image',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{
|
{data.state === 'blocked' ? (
|
||||||
data.state === 'blocked' ? <div className='pb-40'>
|
<div className='pb-40'>
|
||||||
<Message type='error'>
|
<Message type='error'>
|
||||||
<h2 className='text-lg font-extrabold'>해당 봇은 관리자에 의해 삭제되었습니다.</h2>
|
<h2 className='text-lg font-extrabold'>해당 봇은 관리자에 의해 삭제되었습니다.</h2>
|
||||||
</Message>
|
</Message>
|
||||||
</div>
|
</div>
|
||||||
: data.category.includes('NSFW') && !nsfw ? <NSFW onClick={() => setNSFW(true)} onDisableClick={() => localStorage.nsfw = true} />
|
) : data.category.includes('NSFW') && !nsfw ? (
|
||||||
: <>
|
<NSFW onClick={() => setNSFW(true)} onDisableClick={() => (localStorage.nsfw = true)} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className='w-full pb-2'>
|
<div className='w-full pb-2'>
|
||||||
{
|
{data.state === 'private' ? (
|
||||||
data.state === 'private' ? <Message type='info'>
|
<Message type='info'>
|
||||||
<h2 className='text-lg font-extrabold'>해당 봇은 특수목적 봇이므로 초대하실 수 없습니다.</h2>
|
<h2 className='text-lg font-extrabold'>
|
||||||
<p>해당 봇은 공개 사용이 목적이 아닌 특수목적봇입니다. 따라서 따로 초대하실 수 없습니다.</p>
|
해당 봇은 특수목적 봇이므로 초대하실 수 없습니다.
|
||||||
</Message> :
|
</h2>
|
||||||
data.state === 'reported' ?
|
<p>
|
||||||
|
해당 봇은 공개 사용이 목적이 아닌 특수목적봇입니다. 따라서 따로 초대하실 수
|
||||||
|
없습니다.
|
||||||
|
</p>
|
||||||
|
</Message>
|
||||||
|
) : data.state === 'reported' ? (
|
||||||
<Message type='error'>
|
<Message type='error'>
|
||||||
<h2 className='text-lg font-extrabold'>해당 봇은 신고가 접수되어, 관리자에 의해 잠금 상태입니다.</h2>
|
<h2 className='text-lg font-extrabold'>
|
||||||
|
해당 봇은 신고가 접수되어, 관리자에 의해 잠금 상태입니다.
|
||||||
|
</h2>
|
||||||
<p>해당 봇 사용에 주의해주세요.</p>
|
<p>해당 봇 사용에 주의해주세요.</p>
|
||||||
<p>봇 소유자분은 <Link href='/guidelines' className='text-blue-500 hover:text-blue-400'>가이드라인</Link>에 대한 위반사항을 확인해주시고 <Link href='/discord' className='text-blue-500 hover:text-blue-400'>디스코드 서버</Link>로 문의해주세요.</p>
|
<p>
|
||||||
</Message> : ''
|
봇 소유자분은{' '}
|
||||||
}
|
<Link href='/guidelines' className='text-blue-500 hover:text-blue-400'>
|
||||||
|
가이드라인
|
||||||
|
</Link>
|
||||||
|
에 대한 위반사항을 확인해주시고{' '}
|
||||||
|
<Link href='/discord' className='text-blue-500 hover:text-blue-400'>
|
||||||
|
디스코드 서버
|
||||||
|
</Link>
|
||||||
|
로 문의해주세요.
|
||||||
|
</p>
|
||||||
|
</Message>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='lg:flex w-full'>
|
<div className='w-full lg:flex'>
|
||||||
<div className='w-full text-center lg:w-2/12'>
|
<div className='w-full text-center lg:w-2/12'>
|
||||||
<DiscordAvatar
|
<DiscordAvatar userID={data.id} size={256} className='w-full rounded-full' />
|
||||||
userID={data.id}
|
|
||||||
size={256}
|
|
||||||
className='w-full rounded-full'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='grow px-5 py-12 w-full text-center lg:w-5/12 lg:text-left'>
|
<div className='w-full grow px-5 py-12 text-center lg:w-5/12 lg:text-left'>
|
||||||
<Tag
|
<Tag
|
||||||
circular
|
circular
|
||||||
text={
|
text={
|
||||||
@ -109,54 +137,66 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
|
|||||||
<h1 className='mb-2 mt-3 text-4xl font-bold' style={bg ? { color: 'white' } : {}}>
|
<h1 className='mb-2 mt-3 text-4xl font-bold' style={bg ? { color: 'white' } : {}}>
|
||||||
{data.name}{' '}
|
{data.name}{' '}
|
||||||
{checkBotFlag(data.flags, 'trusted') ? (
|
{checkBotFlag(data.flags, 'trusted') ? (
|
||||||
<Tooltip placement='bottom' overlay='해당 봇은 한국 디스코드 리스트에서 엄격한 기준을 통과한 봇입니다!'>
|
<Tooltip
|
||||||
<span className='text-koreanbots-blue text-3xl'>
|
placement='bottom'
|
||||||
|
overlay='해당 봇은 한국 디스코드 리스트에서 엄격한 기준을 통과한 봇입니다!'
|
||||||
|
>
|
||||||
|
<span className='text-3xl text-koreanbots-blue'>
|
||||||
<i className='fas fa-award' />
|
<i className='fas fa-award' />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : ''}
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<p className={`${bg ? 'text-gray-300' : 'dark:text-gray-300 text-gray-800'} text-base`}>{data.intro}</p>
|
<p
|
||||||
|
className={`${
|
||||||
|
bg ? 'text-gray-300' : 'text-gray-800 dark:text-gray-300'
|
||||||
|
} text-base`}
|
||||||
|
>
|
||||||
|
{data.intro}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full lg:w-1/4'>
|
<div className='w-full lg:w-1/4'>
|
||||||
{
|
{data.state === 'ok' && (
|
||||||
data.state === 'ok' && <LongButton
|
<LongButton newTab href={`/bots/${router.query.id}/invite`}>
|
||||||
newTab
|
|
||||||
href={`/bots/${router.query.id}/invite`}
|
|
||||||
>
|
|
||||||
<h4 className='whitespace-nowrap'>
|
<h4 className='whitespace-nowrap'>
|
||||||
<i className='fas fa-user-plus text-discord-blurple' /> 초대하기
|
<i className='fas fa-user-plus text-discord-blurple' /> 초대하기
|
||||||
</h4>
|
</h4>
|
||||||
</LongButton>
|
</LongButton>
|
||||||
}
|
)}
|
||||||
<Link href={`/bots/${router.query.id}/vote`} legacyBehavior>
|
<Link href={`/bots/${router.query.id}/vote`} legacyBehavior>
|
||||||
<LongButton>
|
<LongButton>
|
||||||
<h4>
|
<h4>
|
||||||
<i className='fas fa-heart text-red-600' /> 하트 추가
|
<i className='fas fa-heart text-red-600' /> 하트 추가
|
||||||
</h4>
|
</h4>
|
||||||
<span className='ml-1 px-2 text-center text-black dark:text-gray-400 text-sm bg-little-white-hover dark:bg-very-black rounded-lg'>
|
<span className='ml-1 rounded-lg bg-little-white-hover px-2 text-center text-sm text-black dark:bg-very-black dark:text-gray-400'>
|
||||||
{formatNumber(data.votes)}
|
{formatNumber(data.votes)}
|
||||||
</span>
|
</span>
|
||||||
</LongButton>
|
</LongButton>
|
||||||
</Link>
|
</Link>
|
||||||
{
|
{((data.owners as User[]).find((el) => el.id === user?.id) ||
|
||||||
((data.owners as User[]).find(el => el.id === user?.id) || checkUserFlag(user?.flags, 'staff')) && <LongButton href={`/bots/${data.id}/edit`}>
|
checkUserFlag(user?.flags, 'staff')) && (
|
||||||
|
<LongButton href={`/bots/${data.id}/edit`}>
|
||||||
<h4>
|
<h4>
|
||||||
<i className='fas fa-cogs' /> 관리하기
|
<i className='fas fa-cogs' /> 관리하기
|
||||||
</h4>
|
</h4>
|
||||||
</LongButton>
|
</LongButton>
|
||||||
}
|
)}
|
||||||
{
|
{((data.owners as User[]).find((el) => el.id === user?.id) ||
|
||||||
((data.owners as User[]).find(el => el.id === user?.id) || checkUserFlag(user?.flags, 'staff')) && <LongButton onClick={async() => {
|
checkUserFlag(user?.flags, 'staff')) && (
|
||||||
|
<LongButton
|
||||||
|
onClick={async () => {
|
||||||
const res = await Fetch(`/bots/${data.id}/stats`, { method: 'PATCH' })
|
const res = await Fetch(`/bots/${data.id}/stats`, { method: 'PATCH' })
|
||||||
if (res.code !== 200) return alert(res.message)
|
if (res.code !== 200) return alert(res.message)
|
||||||
else window.location.reload()
|
else window.location.reload()
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<h4>
|
<h4>
|
||||||
<i className='fas fa-sync' /> 정보 갱신하기
|
<i className='fas fa-sync' /> 정보 갱신하기
|
||||||
</h4>
|
</h4>
|
||||||
</LongButton>
|
</LongButton>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider className='px-5' />
|
<Divider className='px-5' />
|
||||||
@ -166,7 +206,7 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
|
|||||||
<div className='lg:flex lg:flex-row-reverse' style={bg ? { color: 'white' } : {}}>
|
<div className='lg:flex lg:flex-row-reverse' style={bg ? { color: 'white' } : {}}>
|
||||||
<div className='mb-1 w-full lg:w-1/4'>
|
<div className='mb-1 w-full lg:w-1/4'>
|
||||||
<h2 className='3xl mb-2 font-bold'>정보</h2>
|
<h2 className='3xl mb-2 font-bold'>정보</h2>
|
||||||
<div className='grid gap-4 grid-cols-2 px-4 py-4 text-black dark:text-gray-400 dark:bg-discord-black bg-little-white rounded-sm'>
|
<div className='grid grid-cols-2 gap-4 rounded-sm bg-little-white px-4 py-4 text-black dark:bg-discord-black dark:text-gray-400'>
|
||||||
<div>
|
<div>
|
||||||
<i className='far fa-flag' /> 접두사
|
<i className='far fa-flag' /> 접두사
|
||||||
</div>
|
</div>
|
||||||
@ -177,36 +217,36 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
|
|||||||
<i className='fas fa-users' /> 서버수
|
<i className='fas fa-users' /> 서버수
|
||||||
</div>
|
</div>
|
||||||
<div>{data.servers || 'N/A'}</div>
|
<div>{data.servers || 'N/A'}</div>
|
||||||
{
|
{data.shards && data.servers > 1500 && (
|
||||||
data.shards && data.servers > 1500 && <>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<i className='fas fa-sitemap' /> 샤드수
|
<i className='fas fa-sitemap' /> 샤드수
|
||||||
</div>
|
</div>
|
||||||
<div>{data.shards}</div>
|
<div>{data.shards}</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<i className='fas fa-calendar-day' /> 봇 생성일
|
<i className='fas fa-calendar-day' /> 봇 생성일
|
||||||
</div>
|
</div>
|
||||||
<div>{Day(date).fromNow(false)}</div>
|
<div>{Day(date).fromNow(false)}</div>
|
||||||
{
|
{checkBotFlag(data.flags, 'verified') ? (
|
||||||
checkBotFlag(data.flags, 'verified') ?
|
|
||||||
<Tooltip overlay='해당 봇은 디스코드측에서 인증된 봇입니다.'>
|
<Tooltip overlay='해당 봇은 디스코드측에서 인증된 봇입니다.'>
|
||||||
<div className='col-span-2'>
|
<div className='col-span-2'>
|
||||||
<i className='fas fa-check text-discord-blurple' /> 디스코드 인증됨
|
<i className='fas fa-check text-discord-blurple' /> 디스코드 인증됨
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
: ''
|
) : (
|
||||||
}
|
''
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h2 className='3xl mb-2 mt-2 font-bold'>카테고리</h2>
|
<h2 className='3xl mb-2 mt-2 font-bold'>카테고리</h2>
|
||||||
<div className='flex flex-wrap'>
|
<div className='flex flex-wrap'>
|
||||||
{data.category.map(el => (
|
{data.category.map((el) => (
|
||||||
<Tag key={el} text={el} href={`/bots/categories/${el}`} />
|
<Tag key={el} text={el} href={`/bots/categories/${el}`} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<h2 className='3xl mb-2 mt-2 font-bold'>제작자</h2>
|
<h2 className='3xl mb-2 mt-2 font-bold'>제작자</h2>
|
||||||
{(data.owners as User[]).map(el => (
|
{(data.owners as User[]).map((el) => (
|
||||||
<Owner
|
<Owner
|
||||||
key={el.id}
|
key={el.id}
|
||||||
id={el.id}
|
id={el.id}
|
||||||
@ -218,33 +258,55 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
|
|||||||
<div className='list grid'>
|
<div className='list grid'>
|
||||||
<Link
|
<Link
|
||||||
href={`/bots/${router.query.id}/report`}
|
href={`/bots/${router.query.id}/report`}
|
||||||
className='text-red-600 hover:underline cursor-pointer'
|
className='cursor-pointer text-red-600 hover:underline'
|
||||||
aria-hidden='true'>
|
aria-hidden='true'
|
||||||
|
>
|
||||||
<i className='far fa-flag' />신고하기
|
<i className='far fa-flag' />
|
||||||
|
신고하기
|
||||||
</Link>
|
</Link>
|
||||||
<Modal header={`${data.name}#${data.tag} 신고하기`} closeIcon isOpen={reportModal} onClose={() => {
|
<Modal
|
||||||
|
header={`${data.name}#${data.tag} 신고하기`}
|
||||||
|
closeIcon
|
||||||
|
isOpen={reportModal}
|
||||||
|
onClose={() => {
|
||||||
setReportModal(false)
|
setReportModal(false)
|
||||||
setReportRes(null)
|
setReportRes(null)
|
||||||
}} full dark={theme === 'dark'}>
|
}}
|
||||||
{
|
full
|
||||||
reportRes?.code === 200 ? <Message type='success'>
|
dark={theme === 'dark'}
|
||||||
|
>
|
||||||
|
{reportRes?.code === 200 ? (
|
||||||
|
<Message type='success'>
|
||||||
<h2 className='text-lg font-semibold'>성공적으로 신고하였습니다!</h2>
|
<h2 className='text-lg font-semibold'>성공적으로 신고하였습니다!</h2>
|
||||||
<p>더 자세한 설명이 필요할 수 있습니다! <a className='text-blue-600 hover:text-blue-500' href='/discord'>공식 디스코드</a>에 참여해주세요</p>
|
<p>
|
||||||
</Message> : <Formik onSubmit={async (body) => {
|
더 자세한 설명이 필요할 수 있습니다!{' '}
|
||||||
const res = await Fetch(`/bots/${data.id}/report`, { method: 'POST', body: JSON.stringify(body) })
|
<a className='text-blue-600 hover:text-blue-500' href='/discord'>
|
||||||
|
공식 디스코드
|
||||||
|
</a>
|
||||||
|
에 참여해주세요
|
||||||
|
</p>
|
||||||
|
</Message>
|
||||||
|
) : (
|
||||||
|
<Formik
|
||||||
|
onSubmit={async (body) => {
|
||||||
|
const res = await Fetch(`/bots/${data.id}/report`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
setReportRes(res)
|
setReportRes(res)
|
||||||
}} validationSchema={ReportSchema} initialValues={{
|
}}
|
||||||
|
validationSchema={ReportSchema}
|
||||||
|
initialValues={{
|
||||||
category: null,
|
category: null,
|
||||||
description: '',
|
description: '',
|
||||||
_csrf: csrfToken
|
_csrf: csrfToken,
|
||||||
}}>
|
}}
|
||||||
{
|
>
|
||||||
({ errors, touched, values, setFieldValue }) => (
|
{({ errors, touched, values, setFieldValue }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<div className='mb-5'>
|
<div className='mb-5'>
|
||||||
{
|
{reportRes && (
|
||||||
reportRes && <div className='my-5'>
|
<div className='my-5'>
|
||||||
<Message type='error'>
|
<Message type='error'>
|
||||||
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
|
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
|
||||||
<ul className='list-disc'>
|
<ul className='list-disc'>
|
||||||
@ -252,34 +314,64 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
|
|||||||
</ul>
|
</ul>
|
||||||
</Message>
|
</Message>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
<h3 className='font-bold'>신고 구분</h3>
|
<h3 className='font-bold'>신고 구분</h3>
|
||||||
<p className='text-gray-400 text-sm mb-1'>해당되는 항목을 선택해주세요.</p>
|
<p className='mb-1 text-sm text-gray-400'>
|
||||||
{
|
해당되는 항목을 선택해주세요.
|
||||||
reportCats.map(el =>
|
</p>
|
||||||
|
{reportCats.map((el) => (
|
||||||
<div key={el}>
|
<div key={el}>
|
||||||
<label>
|
<label>
|
||||||
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
|
<Field
|
||||||
|
type='radio'
|
||||||
|
name='category'
|
||||||
|
value={el}
|
||||||
|
className='mr-1.5 py-2'
|
||||||
|
/>
|
||||||
{el}
|
{el}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
}
|
<div className='mt-1 text-xs font-light text-red-500'>
|
||||||
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category as string: null}</div>
|
{errors.category && touched.category
|
||||||
<h3 className='font-bold mt-2'>설명</h3>
|
? (errors.category as string)
|
||||||
<p className='text-gray-400 text-sm mb-1'>신고하시는 내용을 자세하게 설명해주세요.</p>
|
: null}
|
||||||
<TextArea name='description' placeholder='최대한 자세하게 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.description} setValue={(value) => setFieldValue('description', value)} />
|
</div>
|
||||||
<div className='mt-1 text-red-500 text-xs font-light'>{errors.description && touched.description ? errors.description : null}</div>
|
<h3 className='mt-2 font-bold'>설명</h3>
|
||||||
|
<p className='mb-1 text-sm text-gray-400'>
|
||||||
|
신고하시는 내용을 자세하게 설명해주세요.
|
||||||
|
</p>
|
||||||
|
<TextArea
|
||||||
|
name='description'
|
||||||
|
placeholder='최대한 자세하게 설명해주세요!'
|
||||||
|
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||||
|
value={values.description}
|
||||||
|
setValue={(value) => setFieldValue('description', value)}
|
||||||
|
/>
|
||||||
|
<div className='mt-1 text-xs font-light text-red-500'>
|
||||||
|
{errors.description && touched.description
|
||||||
|
? errors.description
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-right'>
|
<div className='text-right'>
|
||||||
<Button className='bg-gray-500 hover:opacity-90 text-white' onClick={()=> setReportModal(false)}>취소</Button>
|
<Button
|
||||||
<Button type='submit' className='bg-red-500 hover:opacity-90 text-white'>제출</Button>
|
className='bg-gray-500 text-white hover:opacity-90'
|
||||||
|
onClick={() => setReportModal(false)}
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='bg-red-500 text-white hover:opacity-90'
|
||||||
|
>
|
||||||
|
제출
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Formik>
|
</Formik>
|
||||||
}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
{data.discord && (
|
{data.discord && (
|
||||||
<a
|
<a
|
||||||
@ -310,23 +402,40 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
|
|||||||
className='hover:underline'
|
className='hover:underline'
|
||||||
href={data.git}
|
href={data.git}
|
||||||
>
|
>
|
||||||
<i className={`fab fa-${git[new URL(data.git).hostname]?.icon ?? 'git-alt'}`} />
|
<i
|
||||||
|
className={`fab fa-${git[new URL(data.git).hostname]?.icon ?? 'git-alt'}`}
|
||||||
|
/>
|
||||||
{git[new URL(data.git).hostname]?.text ?? 'Git'}
|
{git[new URL(data.git).hostname]?.text ?? 'Git'}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Advertisement size='tall' />
|
<Advertisement size='tall' />
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full lg:pr-5 lg:w-3/4'>
|
<div className='w-full lg:w-3/4 lg:pr-5'>
|
||||||
{
|
{checkBotFlag(data.flags, 'hackerthon') ? (
|
||||||
checkBotFlag(data.flags, 'hackerthon') ? <Segment className='mt-10'>
|
<Segment className='mt-10'>
|
||||||
<h1 className='text-3xl font-semibold'>
|
<h1 className='text-3xl font-semibold'>
|
||||||
<i className='fas fa-trophy mr-4 my-2 text-amber-300' /> 해당 봇은 한국 디스코드 리스트 해커톤 수상작품입니다!
|
<i className='fas fa-trophy my-2 mr-4 text-amber-300' /> 해당 봇은 한국
|
||||||
|
디스코드 리스트 해커톤 수상작품입니다!
|
||||||
</h1>
|
</h1>
|
||||||
<p>해당 봇은 한국 디스코드 리스트 주최로 진행되었던 "한국 디스코드 리스트 제1회 해커톤"에서 우수한 성적을 거둔 봇입니다.</p>
|
<p>
|
||||||
<p>자세한 내용은 <a className='text-blue-500 hover:text-blue-400' href='https://blog.koreanbots.dev/first-hackathon-results/'>해당 글</a>을 확인해주세요.</p>
|
해당 봇은 한국 디스코드 리스트 주최로 진행되었던 "한국 디스코드 리스트 제1회
|
||||||
</Segment> : ''
|
해커톤"에서 우수한 성적을 거둔 봇입니다.
|
||||||
}
|
</p>
|
||||||
|
<p>
|
||||||
|
자세한 내용은{' '}
|
||||||
|
<a
|
||||||
|
className='text-blue-500 hover:text-blue-400'
|
||||||
|
href='https://blog.koreanbots.dev/first-hackathon-results/'
|
||||||
|
>
|
||||||
|
해당 글
|
||||||
|
</a>
|
||||||
|
을 확인해주세요.
|
||||||
|
</p>
|
||||||
|
</Segment>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
<Segment className='my-4'>
|
<Segment className='my-4'>
|
||||||
<Markdown text={desc} />
|
<Markdown text={desc} />
|
||||||
</Segment>
|
</Segment>
|
||||||
@ -334,7 +443,7 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -343,20 +452,25 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
|
|||||||
export const getServerSideProps = async (ctx: Context) => {
|
export const getServerSideProps = async (ctx: Context) => {
|
||||||
const parsed = parseCookie(ctx.req)
|
const parsed = parseCookie(ctx.req)
|
||||||
const data = await get.bot.load(ctx.query.id)
|
const data = await get.bot.load(ctx.query.id)
|
||||||
if(!data) return {
|
if (!data)
|
||||||
|
return {
|
||||||
props: {
|
props: {
|
||||||
data
|
data,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
const desc = await get.botDescSafe(data.id)
|
const desc = await get.botDescSafe(data.id)
|
||||||
const user = await get.Authorization(parsed?.token)
|
const user = await get.Authorization(parsed?.token)
|
||||||
if((checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) && data.vanity && data.vanity !== ctx.query.id) {
|
if (
|
||||||
|
(checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) &&
|
||||||
|
data.vanity &&
|
||||||
|
data.vanity !== ctx.query.id
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: `/bots/${data.vanity}`,
|
destination: `/bots/${data.vanity}`,
|
||||||
permanent: true
|
permanent: true,
|
||||||
},
|
},
|
||||||
props: {}
|
props: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -365,7 +479,7 @@ export const getServerSideProps = async (ctx: Context) => {
|
|||||||
desc,
|
desc,
|
||||||
date: Number(SnowflakeUtil.deconstruct(data.id ?? '0').timestamp),
|
date: Number(SnowflakeUtil.deconstruct(data.id ?? '0').timestamp),
|
||||||
user: await get.user.load(user || ''),
|
user: await get.user.load(user || ''),
|
||||||
csrfToken: getToken(ctx.req, ctx.res)
|
csrfToken: getToken(ctx.req, ctx.res),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,12 +8,24 @@ const Invite: NextPage = () => <NotFound />
|
|||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
const data = await get.bot.load(ctx.query.id as string)
|
const data = await get.bot.load(ctx.query.id as string)
|
||||||
if (!data) return { props: {} }
|
if (!data) return { props: {} }
|
||||||
const record = await Bots.updateOne({ _id: data.id, 'inviteMetrix.day': getYYMMDD() }, { $inc: { 'inviteMetrix.$.count': 1 } })
|
const record = await Bots.updateOne(
|
||||||
if(record.matchedCount === 0) await Bots.findByIdAndUpdate(data.id, { $push: { inviteMetrix: { count: 1 } } }, { upsert: true })
|
{ _id: data.id, 'inviteMetrix.day': getYYMMDD() },
|
||||||
|
{ $inc: { 'inviteMetrix.$.count': 1 } }
|
||||||
|
)
|
||||||
|
if (record.matchedCount === 0)
|
||||||
|
await Bots.findByIdAndUpdate(
|
||||||
|
data.id,
|
||||||
|
{ $push: { inviteMetrix: { count: 1 } } },
|
||||||
|
{ upsert: true }
|
||||||
|
)
|
||||||
ctx.res.statusCode = 307
|
ctx.res.statusCode = 307
|
||||||
ctx.res.setHeader('Location', data.url || `https://discordapp.com/oauth2/authorize?client_id=${data.id}&scope=bot&permissions=0`)
|
ctx.res.setHeader(
|
||||||
|
'Location',
|
||||||
|
data.url ||
|
||||||
|
`https://discordapp.com/oauth2/authorize?client_id=${data.id}&scope=bot&permissions=0`
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
props: {}
|
props: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import { getJosaPicker } from 'josa'
|
|||||||
import { reportCats } from '@utils/Constants'
|
import { reportCats } from '@utils/Constants'
|
||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
|
|
||||||
|
|
||||||
const Container = dynamic(() => import('@components/Container'))
|
const Container = dynamic(() => import('@components/Container'))
|
||||||
const Message = dynamic(() => import('@components/Message'))
|
const Message = dynamic(() => import('@components/Message'))
|
||||||
const Login = dynamic(() => import('@components/Login'))
|
const Login = dynamic(() => import('@components/Login'))
|
||||||
@ -27,33 +26,54 @@ const Login = dynamic(() => import('@components/Login'))
|
|||||||
const ReportBot: NextPage<ReportBotProps> = ({ data, user, csrfToken }) => {
|
const ReportBot: NextPage<ReportBotProps> = ({ data, user, csrfToken }) => {
|
||||||
const [reportRes, setReportRes] = useState<ResponseProps<unknown>>(null)
|
const [reportRes, setReportRes] = useState<ResponseProps<unknown>>(null)
|
||||||
if (!data?.id) return <NotFound />
|
if (!data?.id) return <NotFound />
|
||||||
if(!user) return <Login>
|
if (!user)
|
||||||
|
return (
|
||||||
|
<Login>
|
||||||
<NextSeo title='신고하기' />
|
<NextSeo title='신고하기' />
|
||||||
</Login>
|
</Login>
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<Container paddingTop className='py-10'>
|
<Container paddingTop className='py-10'>
|
||||||
<NextSeo title={`${data.name} 신고하기`} />
|
<NextSeo title={`${data.name} 신고하기`} />
|
||||||
<Link href={makeBotURL(data)} className='text-blue-500 hover:opacity-80'>
|
<Link href={makeBotURL(data)} className='text-blue-500 hover:opacity-80'>
|
||||||
<i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.name}</strong>{getJosaPicker('로')(data.name)}돌아가기
|
<i className='fas fa-arrow-left mb-3 mt-3' /> <strong>{data.name}</strong>
|
||||||
|
{getJosaPicker('로')(data.name)}돌아가기
|
||||||
</Link>
|
</Link>
|
||||||
{
|
{reportRes?.code === 200 ? (
|
||||||
reportRes?.code === 200 ? <Message type='success'>
|
<Message type='success'>
|
||||||
<h2 className='text-lg font-semibold'>성공적으로 제출하였습니다!</h2>
|
<h2 className='text-lg font-semibold'>성공적으로 제출하였습니다!</h2>
|
||||||
<p>더 자세한 설명이 필요할 수 있습니다. <strong>반드시 <a className='text-blue-600 hover:text-blue-500' href='/discord'>공식 디스코드</a>에 참여해주세요!!</strong></p>
|
<p>
|
||||||
</Message> : <Formik onSubmit={async (body) => {
|
더 자세한 설명이 필요할 수 있습니다.{' '}
|
||||||
const res = await Fetch(`/bots/${data.id}/report`, { method: 'POST', body: JSON.stringify(body) })
|
<strong>
|
||||||
|
반드시{' '}
|
||||||
|
<a className='text-blue-600 hover:text-blue-500' href='/discord'>
|
||||||
|
공식 디스코드
|
||||||
|
</a>
|
||||||
|
에 참여해주세요!!
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
</Message>
|
||||||
|
) : (
|
||||||
|
<Formik
|
||||||
|
onSubmit={async (body) => {
|
||||||
|
const res = await Fetch(`/bots/${data.id}/report`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
setReportRes(res)
|
setReportRes(res)
|
||||||
}} validationSchema={ReportSchema} initialValues={{
|
}}
|
||||||
|
validationSchema={ReportSchema}
|
||||||
|
initialValues={{
|
||||||
category: null,
|
category: null,
|
||||||
description: '',
|
description: '',
|
||||||
_csrf: csrfToken
|
_csrf: csrfToken,
|
||||||
}}>
|
}}
|
||||||
{
|
>
|
||||||
({ errors, touched, values, setFieldValue }) => (
|
{({ errors, touched, values, setFieldValue }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<div className='mb-5'>
|
<div className='mb-5'>
|
||||||
{
|
{reportRes && (
|
||||||
reportRes && <div className='my-5'>
|
<div className='my-5'>
|
||||||
<Message type='error'>
|
<Message type='error'>
|
||||||
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
|
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
|
||||||
<ul className='list-disc'>
|
<ul className='list-disc'>
|
||||||
@ -61,52 +81,82 @@ const ReportBot: NextPage<ReportBotProps> = ({ data, user, csrfToken }) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</Message>
|
</Message>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
<h3 className='font-bold'>신고 구분</h3>
|
<h3 className='font-bold'>신고 구분</h3>
|
||||||
<p className='text-gray-400 text-sm mb-1'>해당되는 항목을 선택해주세요.</p>
|
<p className='mb-1 text-sm text-gray-400'>해당되는 항목을 선택해주세요.</p>
|
||||||
{
|
{reportCats.map((el) => (
|
||||||
reportCats.map(el =>
|
|
||||||
<div key={el}>
|
<div key={el}>
|
||||||
<label>
|
<label>
|
||||||
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
|
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
|
||||||
{el}
|
{el}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
}
|
<div className='mt-1 text-xs font-light text-red-500'>
|
||||||
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category as string: null}</div>
|
{errors.category && touched.category ? (errors.category as string) : null}
|
||||||
{
|
</div>
|
||||||
values.category && <>
|
{values.category && (
|
||||||
|
<>
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
[reportCats[2]]: <Message type='info'>
|
[reportCats[2]]: (
|
||||||
<h3 className='font-bold text-xl'>본인 혹은 다른 사람이 위험에 처해 있나요?</h3>
|
<Message type='info'>
|
||||||
|
<h3 className='text-xl font-bold'>
|
||||||
|
본인 혹은 다른 사람이 위험에 처해 있나요?
|
||||||
|
</h3>
|
||||||
<p>당신은 소중한 사람입니다.</p>
|
<p>당신은 소중한 사람입니다.</p>
|
||||||
<p className='list-disc list-item list-inside'>자살예방상담전화 1393 | 청소년전화 1388</p>
|
<p className='list-item list-inside list-disc'>
|
||||||
</Message>,
|
자살예방상담전화 1393 | 청소년전화 1388
|
||||||
[reportCats[5]]: <DMCA values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />,
|
</p>
|
||||||
[reportCats[6]]: <Message type='warning'>
|
|
||||||
<h3 className='font-bold text-xl'>디스코드 약관을 위반사항을 신고하시려고요?</h3>
|
|
||||||
<p><a className='text-blue-400' target='_blank' rel='noreferrer' href='http://dis.gd/report'>디스코드 문의</a>를 통해 직접 디스코드에 신고하실 수도 있습니다.</p>
|
|
||||||
</Message>
|
</Message>
|
||||||
|
),
|
||||||
|
[reportCats[5]]: (
|
||||||
|
<DMCA
|
||||||
|
values={values}
|
||||||
|
errors={errors}
|
||||||
|
touched={touched}
|
||||||
|
setFieldValue={setFieldValue}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[reportCats[6]]: (
|
||||||
|
<Message type='warning'>
|
||||||
|
<h3 className='text-xl font-bold'>
|
||||||
|
디스코드 약관을 위반사항을 신고하시려고요?
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
className='text-blue-400'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
href='http://dis.gd/report'
|
||||||
|
>
|
||||||
|
디스코드 문의
|
||||||
|
</a>
|
||||||
|
를 통해 직접 디스코드에 신고하실 수도 있습니다.
|
||||||
|
</p>
|
||||||
|
</Message>
|
||||||
|
),
|
||||||
}[values.category]
|
}[values.category]
|
||||||
}
|
}
|
||||||
{
|
{!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].includes(values.category) && (
|
||||||
!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].includes(values.category) && <>
|
<>
|
||||||
<h3 className='font-bold mt-2'>설명</h3>
|
<h3 className='mt-2 font-bold'>설명</h3>
|
||||||
<p className='text-gray-400 text-sm mb-1'>최대한 자세하게 기재해주세요.</p>
|
<p className='mb-1 text-sm text-gray-400'>최대한 자세하게 기재해주세요.</p>
|
||||||
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />
|
<TextField
|
||||||
|
values={values}
|
||||||
|
errors={errors}
|
||||||
|
touched={touched}
|
||||||
|
setFieldValue={setFieldValue}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Formik>
|
</Formik>
|
||||||
}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -120,7 +170,7 @@ export const getServerSideProps = async (ctx: Context) => {
|
|||||||
props: {
|
props: {
|
||||||
csrfToken: getToken(ctx.req, ctx.res),
|
csrfToken: getToken(ctx.req, ctx.res),
|
||||||
data,
|
data,
|
||||||
user: await get.user.load(user || '')
|
user: await get.user.load(user || ''),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import { getJosaPicker } from 'josa'
|
|||||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
|
|
||||||
|
|
||||||
const Container = dynamic(() => import('@components/Container'))
|
const Container = dynamic(() => import('@components/Container'))
|
||||||
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
|
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
|
||||||
const Button = dynamic(() => import('@components/Button'))
|
const Button = dynamic(() => import('@components/Button'))
|
||||||
@ -34,72 +33,118 @@ const VoteBot: NextPage<VoteBotProps> = ({ data, user, theme, csrfToken }) => {
|
|||||||
const [result, setResult] = useState<ResponseProps<{ retryAfter?: number }>>(null)
|
const [result, setResult] = useState<ResponseProps<{ retryAfter?: number }>>(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
if (!data?.id) return <NotFound />
|
if (!data?.id) return <NotFound />
|
||||||
if(!user) return <Login>
|
if (!user)
|
||||||
<NextSeo title={data.name} description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`} openGraph={{
|
return (
|
||||||
|
<Login>
|
||||||
|
<NextSeo
|
||||||
|
title={data.name}
|
||||||
|
description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`}
|
||||||
|
openGraph={{
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
|
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
|
||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
alt: 'Bot Avatar'
|
alt: 'Bot Avatar',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</Login>
|
</Login>
|
||||||
|
)
|
||||||
|
|
||||||
if((checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) && data.vanity && data.vanity !== router.query.id) router.push(`/bots/${data.vanity}/vote?csrfToken=${csrfToken}`)
|
if (
|
||||||
|
(checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) &&
|
||||||
|
data.vanity &&
|
||||||
|
data.vanity !== router.query.id
|
||||||
|
)
|
||||||
|
router.push(`/bots/${data.vanity}/vote?csrfToken=${csrfToken}`)
|
||||||
return (
|
return (
|
||||||
<Container paddingTop className='py-10'>
|
<Container paddingTop className='py-10'>
|
||||||
<NextSeo title={data.name} description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`} openGraph={{
|
<NextSeo
|
||||||
|
title={data.name}
|
||||||
|
description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`}
|
||||||
|
openGraph={{
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
|
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
|
||||||
width: 256,
|
width: 256,
|
||||||
height: 256,
|
height: 256,
|
||||||
alt: 'Bot Avatar'
|
alt: 'Bot Avatar',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}} />
|
}}
|
||||||
{
|
/>
|
||||||
data.state === 'blocked' ? <div className='pb-40'>
|
{data.state === 'blocked' ? (
|
||||||
|
<div className='pb-40'>
|
||||||
<Message type='error'>
|
<Message type='error'>
|
||||||
<h2 className='text-lg font-extrabold'>해당 봇은 관리자에 의해 삭제되었습니다.</h2>
|
<h2 className='text-lg font-extrabold'>해당 봇은 관리자에 의해 삭제되었습니다.</h2>
|
||||||
</Message>
|
</Message>
|
||||||
</div> : <>
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
<Link href={makeBotURL(data)} className='text-blue-500 hover:opacity-80'>
|
<Link href={makeBotURL(data)} className='text-blue-500 hover:opacity-80'>
|
||||||
<i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.name}</strong>{getJosaPicker('로')(data.name)}돌아가기
|
<i className='fas fa-arrow-left mb-3 mt-3' /> <strong>{data.name}</strong>
|
||||||
|
{getJosaPicker('로')(data.name)}돌아가기
|
||||||
</Link>
|
</Link>
|
||||||
<Segment className='mb-16 py-8'>
|
<Segment className='mb-16 py-8'>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<DiscordAvatar userID={data.id} className='mx-auto w-52 h-52 bg-white mb-4 rounded-full' />
|
<DiscordAvatar
|
||||||
<Tag text={<span><i className='fas fa-heart text-red-600' /> {data.votes}</span>} dark />
|
userID={data.id}
|
||||||
<h1 className='text-3xl font-bold mt-3'>{data.name}</h1>
|
className='mx-auto mb-4 h-52 w-52 rounded-full bg-white'
|
||||||
|
/>
|
||||||
|
<Tag
|
||||||
|
text={
|
||||||
|
<span>
|
||||||
|
<i className='fas fa-heart text-red-600' /> {data.votes}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
dark
|
||||||
|
/>
|
||||||
|
<h1 className='mt-3 text-3xl font-bold'>{data.name}</h1>
|
||||||
<h4 className='text-md mt-1'>12시간마다 다시 투표하실 수 있습니다.</h4>
|
<h4 className='text-md mt-1'>12시간마다 다시 투표하실 수 있습니다.</h4>
|
||||||
<div className='inline-block mt-2'>
|
<div className='mt-2 inline-block'>
|
||||||
{
|
{votingStatus === 0 ? (
|
||||||
votingStatus === 0 ? <Button onClick={()=> setVotingStatus(1)}>
|
<Button onClick={() => setVotingStatus(1)}>
|
||||||
<><i className='far fa-heart text-red-600'/> 하트 추가</>
|
<>
|
||||||
|
<i className='far fa-heart text-red-600' /> 하트 추가
|
||||||
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
: votingStatus === 1 ? <Captcha dark={theme === 'dark'} onVerify={async (key) => {
|
) : votingStatus === 1 ? (
|
||||||
const res = await Fetch<{ retryAfter: number }|unknown>(`/bots/${data.id}/vote`, { method: 'POST', body: JSON.stringify({ _csrf: csrfToken, _captcha: key }) })
|
<Captcha
|
||||||
|
dark={theme === 'dark'}
|
||||||
|
onVerify={async (key) => {
|
||||||
|
const res = await Fetch<{ retryAfter: number } | unknown>(
|
||||||
|
`/bots/${data.id}/vote`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ _csrf: csrfToken, _captcha: key }),
|
||||||
|
}
|
||||||
|
)
|
||||||
setResult(res)
|
setResult(res)
|
||||||
setVotingStatus(2)
|
setVotingStatus(2)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
: result.code === 200 ? <h2 className='text-2xl font-bold'>해당 봇에 투표했습니다!</h2>
|
) : result.code === 200 ? (
|
||||||
: result.code === 429 ? <>
|
<h2 className='text-2xl font-bold'>해당 봇에 투표했습니다!</h2>
|
||||||
|
) : result.code === 429 ? (
|
||||||
|
<>
|
||||||
<h2 className='text-2xl font-bold'>이미 해당 봇에 투표하였습니다.</h2>
|
<h2 className='text-2xl font-bold'>이미 해당 봇에 투표하였습니다.</h2>
|
||||||
<h4 className='text-md mt-1'>{Day(+new Date() + result.data?.retryAfter).fromNow()} 다시 투표하실 수 있습니다.</h4>
|
<h4 className='text-md mt-1'>
|
||||||
|
{Day(+new Date() + result.data?.retryAfter).fromNow()} 다시 투표하실 수
|
||||||
|
있습니다.
|
||||||
|
</h4>
|
||||||
</>
|
</>
|
||||||
: <p>{result.message}</p>
|
) : (
|
||||||
}
|
<p>{result.message}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Segment>
|
</Segment>
|
||||||
<Advertisement /></>
|
<Advertisement />
|
||||||
}
|
</>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -113,7 +158,7 @@ export const getServerSideProps = async (ctx: Context) => {
|
|||||||
props: {
|
props: {
|
||||||
csrfToken: getToken(ctx.req, ctx.res),
|
csrfToken: getToken(ctx.req, ctx.res),
|
||||||
data,
|
data,
|
||||||
user: await get.user.load(user || '')
|
user: await get.user.load(user || ''),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,42 +25,68 @@ const Category: NextPage<CategoryProps> = ({ data, query }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNSFW(localStorage.nsfw)
|
setNSFW(localStorage.nsfw)
|
||||||
}, [])
|
}, [])
|
||||||
if(!data || data.data.length === 0 || data.totalPage < Number(query.page)) return <NotFound message={data?.data.length === 0 ? '해당 카테고리에 해당되는 봇이 존재하지 않습니다.' : null} />
|
if (!data || data.data.length === 0 || data.totalPage < Number(query.page))
|
||||||
return <>
|
return (
|
||||||
<Hero type='bots' header={`${query.category} 카테고리 봇들`} description={`다양한 "${query.category}" 카테고리의 봇들을 만나보세요.`} />
|
<NotFound
|
||||||
{
|
message={
|
||||||
query.category === 'NSFW' && !nsfw ? <NSFW onClick={() => setNSFW(true)} onDisableClick={() => localStorage.nsfw = true} />
|
data?.data.length === 0 ? '해당 카테고리에 해당되는 봇이 존재하지 않습니다.' : null
|
||||||
: <Container>
|
|
||||||
{
|
|
||||||
router.query.category === '빗금 명령어' && <Segment className='mb-4'>
|
|
||||||
<h1 className='text-2xl font-bold pt-3.5 pb-1'>빗금 명령어</h1>
|
|
||||||
<Markdown text={'빗금 명렁어는 디스코드 채팅창에 `/` 를 입력하여 사용할 수 있습니다.'} />
|
|
||||||
</Segment>
|
|
||||||
}
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Hero
|
||||||
|
type='bots'
|
||||||
|
header={`${query.category} 카테고리 봇들`}
|
||||||
|
description={`다양한 "${query.category}" 카테고리의 봇들을 만나보세요.`}
|
||||||
|
/>
|
||||||
|
{query.category === 'NSFW' && !nsfw ? (
|
||||||
|
<NSFW onClick={() => setNSFW(true)} onDisableClick={() => (localStorage.nsfw = true)} />
|
||||||
|
) : (
|
||||||
|
<Container>
|
||||||
|
{router.query.category === '빗금 명령어' && (
|
||||||
|
<Segment className='mb-4'>
|
||||||
|
<h1 className='pb-1 pt-3.5 text-2xl font-bold'>빗금 명령어</h1>
|
||||||
|
<Markdown
|
||||||
|
text={'빗금 명렁어는 디스코드 채팅창에 `/` 를 입력하여 사용할 수 있습니다.'}
|
||||||
|
/>
|
||||||
|
</Segment>
|
||||||
|
)}
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
<ResponsiveGrid>
|
<ResponsiveGrid>
|
||||||
{
|
{data.data.map((bot) => (
|
||||||
data.data.map(bot => <BotCard key={bot.id} bot={bot} /> )
|
<BotCard key={bot.id} bot={bot} />
|
||||||
}
|
))}
|
||||||
</ResponsiveGrid>
|
</ResponsiveGrid>
|
||||||
<Paginator totalPage={data.totalPage} currentPage={data.currentPage} pathname={`/bots/categories/${query.category}`} />
|
<Paginator
|
||||||
|
totalPage={data.totalPage}
|
||||||
|
currentPage={data.currentPage}
|
||||||
|
pathname={`/bots/categories/${query.category}`}
|
||||||
|
/>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
</Container>
|
</Container>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (ctx: Context) => {
|
export const getServerSideProps = async (ctx: Context) => {
|
||||||
let data: List<Bot>
|
let data: List<Bot>
|
||||||
if (!ctx.query.page) ctx.query.page = '1'
|
if (!ctx.query.page) ctx.query.page = '1'
|
||||||
const validate = await botCategoryListArgumentSchema.validate(ctx.query).then(el => el).catch(() => null)
|
const validate = await botCategoryListArgumentSchema
|
||||||
|
.validate(ctx.query)
|
||||||
|
.then((el) => el)
|
||||||
|
.catch(() => null)
|
||||||
if (!validate || isNaN(Number(ctx.query.page))) data = null
|
if (!validate || isNaN(Number(ctx.query.page))) data = null
|
||||||
else data = await get.list.category.load(JSON.stringify({ page: Number(ctx.query.page), category: ctx.query.category }))
|
else
|
||||||
|
data = await get.list.category.load(
|
||||||
|
JSON.stringify({ page: Number(ctx.query.page), category: ctx.query.category })
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
data,
|
data,
|
||||||
query: ctx.query
|
query: ctx.query,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,32 +4,56 @@ import { NextSeo } from 'next-seo'
|
|||||||
|
|
||||||
import { botCategories, botCategoryIcon } from '@utils/Constants'
|
import { botCategories, botCategoryIcon } from '@utils/Constants'
|
||||||
|
|
||||||
|
|
||||||
const Container = dynamic(() => import('@components/Container'))
|
const Container = dynamic(() => import('@components/Container'))
|
||||||
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
||||||
const Tag = dynamic(() => import('@components/Tag'))
|
const Tag = dynamic(() => import('@components/Tag'))
|
||||||
const Segment = dynamic(() => import('@components/Segment'))
|
const Segment = dynamic(() => import('@components/Segment'))
|
||||||
|
|
||||||
const Categories: NextPage = () => {
|
const Categories: NextPage = () => {
|
||||||
return <Container paddingTop>
|
return (
|
||||||
|
<Container paddingTop>
|
||||||
<NextSeo title='전체 카테고리' description='한국 디스코드 리스트의 전체 카테고리입니다.' />
|
<NextSeo title='전체 카테고리' description='한국 디스코드 리스트의 전체 카테고리입니다.' />
|
||||||
<h1 className='text-2xl font-bold mt-2 mb-5'>전체 카테고리</h1>
|
<h1 className='mb-5 mt-2 text-2xl font-bold'>전체 카테고리</h1>
|
||||||
<Segment className='mb-10'>
|
<Segment className='mb-10'>
|
||||||
<div className='text-center flex flex-wrap mt-1.5'>
|
<div className='mt-1.5 flex flex-wrap text-center'>
|
||||||
{
|
{botCategories.map((t) => (
|
||||||
botCategories.map(t => <Tag key={t} text={<>
|
<Tag
|
||||||
{
|
key={t}
|
||||||
{ '빗금 명령어': <span className='fa-stack' style={{ fontSize: '1em', height: '1.2em', lineHeight: '1em', width: '20px', verticalAlign: 'middle' }}>
|
text={
|
||||||
|
<>
|
||||||
|
{{
|
||||||
|
'빗금 명령어': (
|
||||||
|
<span
|
||||||
|
className='fa-stack'
|
||||||
|
style={{
|
||||||
|
fontSize: '1em',
|
||||||
|
height: '1.2em',
|
||||||
|
lineHeight: '1em',
|
||||||
|
width: '20px',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<i className='fas fa-square fa-stack-1x fa-md' />
|
<i className='fas fa-square fa-stack-1x fa-md' />
|
||||||
<i className='fas fa-slash fa-rotate-90 fa-xs fa-stack-1x fa-inverse' style={{ fontSize: '0.3rem' }} />
|
<i
|
||||||
</span> }[t] ?? <i className={botCategoryIcon[t]} />
|
className='fas fa-slash fa-rotate-90 fa-xs fa-stack-1x fa-inverse'
|
||||||
} {t}
|
style={{ fontSize: '0.3rem' }}
|
||||||
</>} href={`/bots/categories/${t}`} dark bigger /> )
|
/>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
}[t] ?? <i className={botCategoryIcon[t]} />}{' '}
|
||||||
|
{t}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
href={`/bots/categories/${t}`}
|
||||||
|
dark
|
||||||
|
bigger
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Segment>
|
</Segment>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
</Container>
|
</Container>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Categories
|
export default Categories
|
||||||
@ -18,36 +18,42 @@ const Index: NextPage<IndexProps> = ({ votes, newBots, trusted }) => {
|
|||||||
<Hero type='bots' />
|
<Hero type='bots' />
|
||||||
<Container className='pb-10'>
|
<Container className='pb-10'>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
<h1 className='text-3xl font-bold mt-10 mb-2'>
|
<h1 className='mb-2 mt-10 text-3xl font-bold'>
|
||||||
<i className='far fa-heart mr-3 text-pink-600' /> 하트 랭킹
|
<i className='far fa-heart mr-3 text-pink-600' /> 하트 랭킹
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-base'>하트를 많이 받은 봇들의 순위입니다!</p>
|
<p className='text-base'>하트를 많이 받은 봇들의 순위입니다!</p>
|
||||||
<ResponsiveGrid>
|
<ResponsiveGrid>
|
||||||
{
|
{votes.data.map((bot) => (
|
||||||
votes.data.map(bot=> <BotCard key={bot.id} bot={bot} />)
|
<BotCard key={bot.id} bot={bot} />
|
||||||
}
|
))}
|
||||||
</ResponsiveGrid>
|
</ResponsiveGrid>
|
||||||
<Paginator totalPage={votes.totalPage} currentPage={votes.currentPage} pathname='/bots/list/votes' />
|
<Paginator
|
||||||
|
totalPage={votes.totalPage}
|
||||||
|
currentPage={votes.currentPage}
|
||||||
|
pathname='/bots/list/votes'
|
||||||
|
/>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
<h1 className='text-3xl font-bold mb-2'>
|
<h1 className='mb-2 text-3xl font-bold'>
|
||||||
<i className='fa fa-check mr-3 mt-10 text-emerald-500' /> 신뢰된 봇
|
<i className='fa fa-check mr-3 mt-10 text-emerald-500' /> 신뢰된 봇
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-base'>한국 디스코드 리스트에서 인증받은 신뢰할 수 있는 봇들입니다!!</p>
|
<p className='text-base'>한국 디스코드 리스트에서 인증받은 신뢰할 수 있는 봇들입니다!!</p>
|
||||||
<ResponsiveGrid>
|
<ResponsiveGrid>
|
||||||
{
|
{trusted.data.slice(0, 4).map((bot) => (
|
||||||
trusted.data.slice(0, 4).map(bot=> <BotCard key={bot.id} bot={bot} />)
|
<BotCard key={bot.id} bot={bot} />
|
||||||
}
|
))}
|
||||||
</ResponsiveGrid>
|
</ResponsiveGrid>
|
||||||
<h1 className='text-3xl font-bold mt-20 mb-2'>
|
<h1 className='mb-2 mt-20 text-3xl font-bold'>
|
||||||
<i className='far fa-star mr-3 text-amber-500' /> 새로운 봇
|
<i className='far fa-star mr-3 text-amber-500' /> 새로운 봇
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-base'>최근에 한국 디스코드 리스트에 추가된 따끈따끈한 봇입니다.</p>
|
<p className='text-base'>최근에 한국 디스코드 리스트에 추가된 따끈따끈한 봇입니다.</p>
|
||||||
<ResponsiveGrid>
|
<ResponsiveGrid>
|
||||||
{
|
{newBots.data.slice(0, 4).map((bot) => (
|
||||||
newBots.data.slice(0, 4).map(bot=> <BotCard key={bot.id} bot={bot} />)
|
<BotCard key={bot.id} bot={bot} />
|
||||||
}
|
))}
|
||||||
</ResponsiveGrid>
|
</ResponsiveGrid>
|
||||||
<LongButton href='/bots/list/new' center>더보기</LongButton>
|
<LongButton href='/bots/list/new' center>
|
||||||
|
더보기
|
||||||
|
</LongButton>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
@ -60,7 +66,6 @@ export const getServerSideProps = async() => {
|
|||||||
const trusted = await Query.get.list.trusted.load(1)
|
const trusted = await Query.get.list.trusted.load(1)
|
||||||
|
|
||||||
return { props: { votes, newBots, trusted } }
|
return { props: { votes, newBots, trusted } }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IndexProps {
|
interface IndexProps {
|
||||||
|
|||||||
@ -11,26 +11,32 @@ const Container = dynamic(() => import('@components/Container'))
|
|||||||
const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid'))
|
const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid'))
|
||||||
|
|
||||||
const New: NextPage<NewProps> = ({ data }) => {
|
const New: NextPage<NewProps> = ({ data }) => {
|
||||||
return <>
|
return (
|
||||||
<Hero type='bots' header='새로운 봇' description='최근에 한국 디스코드 리스트에 추가된 봇들입니다!' />
|
<>
|
||||||
|
<Hero
|
||||||
|
type='bots'
|
||||||
|
header='새로운 봇'
|
||||||
|
description='최근에 한국 디스코드 리스트에 추가된 봇들입니다!'
|
||||||
|
/>
|
||||||
<Container className='pb-10'>
|
<Container className='pb-10'>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
<ResponsiveGrid>
|
<ResponsiveGrid>
|
||||||
{
|
{data.data.map((bot) => (
|
||||||
data.data.map(bot => <BotCard key={bot.id} bot={bot} /> )
|
<BotCard key={bot.id} bot={bot} />
|
||||||
}
|
))}
|
||||||
</ResponsiveGrid>
|
</ResponsiveGrid>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async () => {
|
export const getServerSideProps = async () => {
|
||||||
const data = await get.list.new.load(1)
|
const data = await get.list.new.load(1)
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
data
|
data,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,33 +18,42 @@ const Paginator = dynamic(() => import('@components/Paginator'))
|
|||||||
|
|
||||||
const Votes: NextPage<VotesProps> = ({ data }) => {
|
const Votes: NextPage<VotesProps> = ({ data }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
if(!data || data.data.length === 0 || data.totalPage < Number(router.query.page)) return <NotFound />
|
if (!data || data.data.length === 0 || data.totalPage < Number(router.query.page))
|
||||||
return <>
|
return <NotFound />
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<Hero type='bots' header='하트 랭킹' description='하트를 많이 받은 봇들의 순위입니다!' />
|
<Hero type='bots' header='하트 랭킹' description='하트를 많이 받은 봇들의 순위입니다!' />
|
||||||
<section id='list'>
|
<section id='list'>
|
||||||
<Container className='pb-10'>
|
<Container className='pb-10'>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
<ResponsiveGrid>
|
<ResponsiveGrid>
|
||||||
{
|
{data.data.map((bot) => (
|
||||||
data.data.map(bot => <BotCard key={bot.id} bot={bot} /> )
|
<BotCard key={bot.id} bot={bot} />
|
||||||
}
|
))}
|
||||||
</ResponsiveGrid>
|
</ResponsiveGrid>
|
||||||
<Paginator totalPage={data.totalPage} currentPage={data.currentPage} pathname='/bots/list/votes' />
|
<Paginator
|
||||||
|
totalPage={data.totalPage}
|
||||||
|
currentPage={data.currentPage}
|
||||||
|
pathname='/bots/list/votes'
|
||||||
|
/>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
export const getServerSideProps = async (ctx: Context) => {
|
export const getServerSideProps = async (ctx: Context) => {
|
||||||
let data: List<Bot>
|
let data: List<Bot>
|
||||||
if (!ctx.query.page) ctx.query.page = '1'
|
if (!ctx.query.page) ctx.query.page = '1'
|
||||||
const validate = await PageCount.validate(ctx.query.page).then(el => el).catch(() => null)
|
const validate = await PageCount.validate(ctx.query.page)
|
||||||
|
.then((el) => el)
|
||||||
|
.catch(() => null)
|
||||||
if (!validate || isNaN(Number(ctx.query.page))) data = null
|
if (!validate || isNaN(Number(ctx.query.page))) data = null
|
||||||
else data = await get.list.votes.load(Number(ctx.query.page))
|
else data = await get.list.votes.load(Number(ctx.query.page))
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
data
|
data,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { get } from '@utils/Query'
|
|||||||
import { SearchQuerySchema } from '@utils/Yup'
|
import { SearchQuerySchema } from '@utils/Yup'
|
||||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||||
|
|
||||||
|
|
||||||
const Hero = dynamic(() => import('@components/Hero'))
|
const Hero = dynamic(() => import('@components/Hero'))
|
||||||
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
||||||
const BotCard = dynamic(() => import('@components/BotCard'))
|
const BotCard = dynamic(() => import('@components/BotCard'))
|
||||||
@ -18,35 +17,52 @@ const Paginator = dynamic(() => import('@components/Paginator'))
|
|||||||
const LongButton = dynamic(() => import('@components/LongButton'))
|
const LongButton = dynamic(() => import('@components/LongButton'))
|
||||||
const Redirect = dynamic(() => import('@components/Redirect'))
|
const Redirect = dynamic(() => import('@components/Redirect'))
|
||||||
|
|
||||||
const SearchComponent: FC<{data: List<Bot>, query: URLQuery }> = ({ data, query }) => {
|
const SearchComponent: FC<{ data: List<Bot>; query: URLQuery }> = ({ data, query }) => {
|
||||||
return <div className='py-10'>
|
return (
|
||||||
{ !data || data.data.length === 0 ? <h1 className='text-3xl font-bold text-center py-20'>검색 결과가 없습니다.</h1> :
|
<div className='py-10'>
|
||||||
|
{!data || data.data.length === 0 ? (
|
||||||
|
<h1 className='py-20 text-center text-3xl font-bold'>검색 결과가 없습니다.</h1>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<ResponsiveGrid>
|
<ResponsiveGrid>
|
||||||
{
|
{data.data.map((el) => (
|
||||||
data.data.map(el => <BotCard key={el.id} bot={el as Bot} /> )
|
<BotCard key={el.id} bot={el as Bot} />
|
||||||
}
|
))}
|
||||||
</ResponsiveGrid>
|
</ResponsiveGrid>
|
||||||
<Paginator totalPage={data.totalPage} currentPage={data.currentPage} pathname='/search' searchParams={query} />
|
<Paginator
|
||||||
|
totalPage={data.totalPage}
|
||||||
|
currentPage={data.currentPage}
|
||||||
|
pathname='/search'
|
||||||
|
searchParams={query}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const Search: NextPage<SearchProps> = ({ botData, query }) => {
|
const Search: NextPage<SearchProps> = ({ botData, query }) => {
|
||||||
if (!query?.q) return <Redirect text={false} to='/' />
|
if (!query?.q) return <Redirect text={false} to='/' />
|
||||||
return <>
|
return (
|
||||||
<Hero type='bots' header={`"${query.q}" 검색 결과`} description={`'${query.q}' 에 대한 검색 결과입니다.`} />
|
<>
|
||||||
|
<Hero
|
||||||
|
type='bots'
|
||||||
|
header={`"${query.q}" 검색 결과`}
|
||||||
|
description={`'${query.q}' 에 대한 검색 결과입니다.`}
|
||||||
|
/>
|
||||||
<Container>
|
<Container>
|
||||||
<section id='list'>
|
<section id='list'>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
<h1 className='text-4xl font-bold'>봇</h1>
|
<h1 className='text-4xl font-bold'>봇</h1>
|
||||||
<SearchComponent data={botData} query={query} />
|
<SearchComponent data={botData} query={query} />
|
||||||
<h1 className='text-2xl font-bold py-10'>서버를 찾으시나요?</h1>
|
<h1 className='py-10 text-2xl font-bold'>서버를 찾으시나요?</h1>
|
||||||
<LongButton center href={KoreanbotsEndPoints.URL.searchServer(query.q)}>서버 검색 결과 보기</LongButton>
|
<LongButton center href={KoreanbotsEndPoints.URL.searchServer(query.q)}>
|
||||||
|
서버 검색 결과 보기
|
||||||
|
</LongButton>
|
||||||
<Advertisement />
|
<Advertisement />
|
||||||
</section>
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (ctx: Context) => {
|
export const getServerSideProps = async (ctx: Context) => {
|
||||||
@ -55,24 +71,28 @@ export const getServerSideProps = async(ctx: Context) => {
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: '/',
|
destination: '/',
|
||||||
permanent: true
|
permanent: true,
|
||||||
},
|
},
|
||||||
props: {}
|
props: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!ctx.query.page) ctx.query.page = '1'
|
if (!ctx.query.page) ctx.query.page = '1'
|
||||||
const validate = await SearchQuerySchema.validate(ctx.query).then(el => el).catch(() => null)
|
const validate = await SearchQuerySchema.validate(ctx.query)
|
||||||
|
.then((el) => el)
|
||||||
|
.catch(() => null)
|
||||||
if (!validate || isNaN(Number(ctx.query.page))) return { props: { query: ctx.query } }
|
if (!validate || isNaN(Number(ctx.query.page))) return { props: { query: ctx.query } }
|
||||||
else {
|
else {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
botData: await get.list.search.load(JSON.stringify({ query: ctx.query.q || '', page: ctx.query.page })).then(el => el).catch(() => null),
|
botData: await get.list.search
|
||||||
query: ctx.query
|
.load(JSON.stringify({ query: ctx.query.q || '', page: ctx.query.page }))
|
||||||
|
.then((el) => el)
|
||||||
|
.catch(() => null),
|
||||||
|
query: ctx.query,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface SearchProps {
|
interface SearchProps {
|
||||||
botData?: List<Bot>
|
botData?: List<Bot>
|
||||||
|
|||||||
@ -12,79 +12,110 @@ const Input = dynamic(() => import('@components/Form/Input'))
|
|||||||
|
|
||||||
const Calculator: NextPage<CalculatorProps> = ({ query }) => {
|
const Calculator: NextPage<CalculatorProps> = ({ query }) => {
|
||||||
const [value, setValue] = useState<{ [perm: string]: boolean }>({})
|
const [value, setValue] = useState<{ [perm: string]: boolean }>({})
|
||||||
const Perm = ({ name, perm, yellow }:{ name: string, perm: number, yellow?: boolean }) => {
|
const Perm = ({ name, perm, yellow }: { name: string; perm: number; yellow?: boolean }) => {
|
||||||
return <li>
|
return (
|
||||||
|
<li>
|
||||||
<label className='inline-flex items-center py-1'>
|
<label className='inline-flex items-center py-1'>
|
||||||
<input className='form-checkbox text-discord-blurple bg-gray-300 h-5 w-5 rounded' type='checkbox' checked={value[perm]} onChange={() => {
|
<input
|
||||||
|
className='form-checkbox h-5 w-5 rounded bg-gray-300 text-discord-blurple'
|
||||||
|
type='checkbox'
|
||||||
|
checked={value[perm]}
|
||||||
|
onChange={() => {
|
||||||
setValue({ ...value, [perm]: !value[perm] })
|
setValue({ ...value, [perm]: !value[perm] })
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
<span className={`ml-2.5 text-lg ${yellow ? 'text-amber-500' : ''}`}>{name}</span>
|
<span className={`ml-2.5 text-lg ${yellow ? 'text-amber-500' : ''}`}>{name}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return <Container paddingTop className='pb-10'>
|
return (
|
||||||
<NextSeo title='봇 초대링크 생성기' description='디스코드 봇 초대링크를 간편하게 생성하세요' openGraph={{
|
<Container paddingTop className='pb-10'>
|
||||||
|
<NextSeo
|
||||||
|
title='봇 초대링크 생성기'
|
||||||
|
description='디스코드 봇 초대링크를 간편하게 생성하세요'
|
||||||
|
openGraph={{
|
||||||
title: '봇 초대링크 생성기',
|
title: '봇 초대링크 생성기',
|
||||||
description: '디스코드 봇 초대링크를 간편하게 생성하세요'
|
description: '디스코드 봇 초대링크를 간편하게 생성하세요',
|
||||||
}} />
|
}}
|
||||||
<h1 className='text-4xl font-bold mt-2 mb-4'>봇 초대링크 생성기</h1>
|
/>
|
||||||
<div className='text-2xl font-bold inline-flex items-center'>권한: {String(Object.keys(value).filter(el => value[el]).map(el => Number(el)).reduce((prev, curr) => BigInt(prev) | BigInt(curr), BigInt(0)))}
|
<h1 className='mb-4 mt-2 text-4xl font-bold'>봇 초대링크 생성기</h1>
|
||||||
<span className='ml-2 text-lg font-semibold'>= { Object.keys(value).filter(el => value[el]).map(el => `0x${Number(el).toString(16)}`).join(' | ') }</span>
|
<div className='inline-flex items-center text-2xl font-bold'>
|
||||||
|
권한:{' '}
|
||||||
|
{String(
|
||||||
|
Object.keys(value)
|
||||||
|
.filter((el) => value[el])
|
||||||
|
.map((el) => Number(el))
|
||||||
|
.reduce((prev, curr) => BigInt(prev) | BigInt(curr), BigInt(0))
|
||||||
|
)}
|
||||||
|
<span className='ml-2 text-lg font-semibold'>
|
||||||
|
={' '}
|
||||||
|
{Object.keys(value)
|
||||||
|
.filter((el) => value[el])
|
||||||
|
.map((el) => `0x${Number(el).toString(16)}`)
|
||||||
|
.join(' | ')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid gap-2 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 mt-2'>
|
<div className='mt-2 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3'>
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-2xl font-bold'>일반 권한</h2>
|
<h2 className='text-2xl font-bold'>일반 권한</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{GuildPermissions.general.map((el) => (
|
||||||
GuildPermissions.general.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
|
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
|
||||||
}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-2xl font-bold'>멤버쉽 권한</h2>
|
<h2 className='text-2xl font-bold'>멤버쉽 권한</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{GuildPermissions.membership.map((el) => (
|
||||||
GuildPermissions.membership.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
|
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
|
||||||
}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-2xl font-bold'>채팅 채널 권한</h2>
|
<h2 className='text-2xl font-bold'>채팅 채널 권한</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{GuildPermissions.channel.map((el) => (
|
||||||
GuildPermissions.channel.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
|
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
|
||||||
}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-2xl font-bold'>음성 채널 권한</h2>
|
<h2 className='text-2xl font-bold'>음성 채널 권한</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{GuildPermissions.voice.map((el) => (
|
||||||
GuildPermissions.voice.map(el => <Perm key={el.name} name={el.name} perm={el.flag} />)
|
<Perm key={el.name} name={el.name} perm={el.flag} />
|
||||||
}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-2xl font-bold'>고급 권한</h2>
|
<h2 className='text-2xl font-bold'>고급 권한</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{GuildPermissions.advanced.map((el) => (
|
||||||
GuildPermissions.advanced.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
|
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
|
||||||
}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='py-10'>
|
<div className='py-10'>
|
||||||
<span className='text-amber-500'>노란색 = 서버에 2단계 인증 필수가 활성화되어있다면, 봇 소유자는 <a href='https://support.discord.com/hc/ko/articles/219576828-2단계-인증-설정하기'>2단계 인증</a>이 완료되어있어야합니다.</span>
|
<span className='text-amber-500'>
|
||||||
|
노란색 = 서버에 2단계 인증 필수가 활성화되어있다면, 봇 소유자는{' '}
|
||||||
|
<a href='https://support.discord.com/hc/ko/articles/219576828-2단계-인증-설정하기'>
|
||||||
|
2단계 인증
|
||||||
|
</a>
|
||||||
|
이 완료되어있어야합니다.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Formik onSubmit={()=> console.log('Pong?')} initialValues={{
|
<Formik
|
||||||
|
onSubmit={() => console.log('Pong?')}
|
||||||
|
initialValues={{
|
||||||
id: query.id?.toString() || '',
|
id: query.id?.toString() || '',
|
||||||
scope: 'bot',
|
scope: 'bot',
|
||||||
redirect: ''
|
redirect: '',
|
||||||
}}>
|
}}
|
||||||
{
|
>
|
||||||
({ values, setFieldValue }) => (
|
{({ values, setFieldValue }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<div className='grid gap-3 lg:grid-cols-4'>
|
<div className='grid gap-3 lg:grid-cols-4'>
|
||||||
<div>
|
<div>
|
||||||
@ -94,7 +125,12 @@ const Calculator:NextPage<CalculatorProps> = ({ query }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6>스코프 (Scope)</h6>
|
<h6>스코프 (Scope)</h6>
|
||||||
<button onClick={() => setFieldValue('scope', 'bot applications.commands')} className='text-blue-500 hover:text-blue-400'>빗금 명령어(Slash Command) 봇인가요?</button>
|
<button
|
||||||
|
onClick={() => setFieldValue('scope', 'bot applications.commands')}
|
||||||
|
className='text-blue-500 hover:text-blue-400'
|
||||||
|
>
|
||||||
|
빗금 명령어(Slash Command) 봇인가요?
|
||||||
|
</button>
|
||||||
<Input name='scope' placeholder='bot' />
|
<Input name='scope' placeholder='bot' />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -104,20 +140,37 @@ const Calculator:NextPage<CalculatorProps> = ({ query }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-2'>
|
<div className='mt-2'>
|
||||||
초대링크: <a rel='noreferrer' target='_blank' href={values.id ? DiscordEnpoints.InviteApplication(values.id, value, values.scope, values.redirect) : null} className='cursor-pointer text-blue-500 hover:text-blue-400'>{DiscordEnpoints.InviteApplication(values.id, value, values.scope, values.redirect)}</a>
|
초대링크:{' '}
|
||||||
|
<a
|
||||||
|
rel='noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
href={
|
||||||
|
values.id
|
||||||
|
? DiscordEnpoints.InviteApplication(
|
||||||
|
values.id,
|
||||||
|
value,
|
||||||
|
values.scope,
|
||||||
|
values.redirect
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
className='cursor-pointer text-blue-500 hover:text-blue-400'
|
||||||
|
>
|
||||||
|
{DiscordEnpoints.InviteApplication(values.id, value, values.scope, values.redirect)}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Formik>
|
</Formik>
|
||||||
</Container>
|
</Container>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
query: ctx.query
|
query: ctx.query,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,9 +20,13 @@ const DiscordCallback:NextPage = () => {
|
|||||||
}
|
}
|
||||||
}, [router])
|
}, [router])
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
<Loader text={notRedirecting ? '해당 창을 닫고 원래 앱으로 돌아가주세요.' : '리다이렉트 중 입니다.'} />
|
<>
|
||||||
|
<Loader
|
||||||
|
text={notRedirecting ? '해당 창을 닫고 원래 앱으로 돌아가주세요.' : '리다이렉트 중 입니다.'}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DiscordCallback
|
export default DiscordCallback
|
||||||
@ -3,17 +3,12 @@ import dynamic from 'next/dynamic'
|
|||||||
|
|
||||||
import { SpecialEndPoints } from '@utils/Constants'
|
import { SpecialEndPoints } from '@utils/Constants'
|
||||||
|
|
||||||
|
|
||||||
const Docs = dynamic(() => import('@components/Docs'))
|
const Docs = dynamic(() => import('@components/Docs'))
|
||||||
const Markdown = dynamic(() => import('@components/Markdown'))
|
const Markdown = dynamic(() => import('@components/Markdown'))
|
||||||
|
|
||||||
|
|
||||||
const CommunityRule: NextPage<CommunityRuleProps> = ({ content }) => {
|
const CommunityRule: NextPage<CommunityRuleProps> = ({ content }) => {
|
||||||
return (
|
return (
|
||||||
<Docs
|
<Docs header='커뮤니티 규칙' description='한국 디스코드 리스트 커뮤니티 규칙입니다.'>
|
||||||
header='커뮤니티 규칙'
|
|
||||||
description='한국 디스코드 리스트 커뮤니티 규칙입니다.'
|
|
||||||
>
|
|
||||||
<Markdown text={content} />
|
<Markdown text={content} />
|
||||||
</Docs>
|
</Docs>
|
||||||
)
|
)
|
||||||
@ -24,14 +19,15 @@ interface CommunityRuleProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<CommunityRuleProps> = async () => {
|
export const getStaticProps: GetStaticProps<CommunityRuleProps> = async () => {
|
||||||
const res = await fetch(SpecialEndPoints.Github.Content('koreanbots', 'terms', 'community-rule.md'))
|
const res = await fetch(
|
||||||
|
SpecialEndPoints.Github.Content('koreanbots', 'terms', 'community-rule.md')
|
||||||
|
)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
content: Buffer.from(json.content, 'base64').toString('utf-8')
|
content: Buffer.from(json.content, 'base64').toString('utf-8'),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CommunityRule
|
export default CommunityRule
|
||||||
|
|
||||||
|
|||||||
@ -24,38 +24,52 @@ const ClientInfo = ():JSX.Element => {
|
|||||||
|
|
||||||
[트위터](https://twitter.com/koreanbots)
|
[트위터](https://twitter.com/koreanbots)
|
||||||
https://github.com/koreanbots
|
https://github.com/koreanbots
|
||||||
`
|
`,
|
||||||
|
},
|
||||||
|
onSubmit: () => {
|
||||||
|
alert('Pong')
|
||||||
},
|
},
|
||||||
onSubmit: ()=>{ alert('Pong') }
|
|
||||||
})
|
})
|
||||||
return <Container paddingTop className='pb-10'>
|
return (
|
||||||
<h1 className='text-4xl font-bold mb-3 mt-3'>개발자모드</h1>
|
<Container paddingTop className='pb-10'>
|
||||||
<h2 className='text-3xl font-semibold mb-4'>정보들</h2>
|
<h1 className='mb-3 mt-3 text-4xl font-bold'>개발자모드</h1>
|
||||||
|
<h2 className='mb-4 text-3xl font-semibold'>정보들</h2>
|
||||||
<Segment>
|
<Segment>
|
||||||
<div className='markdown-body text-black dark:text-white'>
|
<div className='markdown-body text-black dark:text-white'>
|
||||||
<h1>빌드정보</h1>
|
<h1>빌드정보</h1>
|
||||||
<ul className='list-disc'>
|
<ul className='list-disc'>
|
||||||
<li>Tag: <code>{parseDockerhubTag(process.env.NEXT_PUBLIC_TAG)}</code></li>
|
<li>
|
||||||
<li>Version: <code>v{Package.version}</code></li>
|
Tag: <code>{parseDockerhubTag(process.env.NEXT_PUBLIC_TAG)}</code>
|
||||||
<li>Hash: <code>{process.env.NEXT_PUBLIC_SOURCE_COMMIT}</code></li>
|
</li>
|
||||||
|
<li>
|
||||||
|
Version: <code>v{Package.version}</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Hash: <code>{process.env.NEXT_PUBLIC_SOURCE_COMMIT}</code>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Segment>
|
</Segment>
|
||||||
<Divider />
|
<Divider />
|
||||||
<h2 className='text-3xl font-semibold mb-2'>테스트</h2>
|
<h2 className='mb-2 text-3xl font-semibold'>테스트</h2>
|
||||||
<h3 className='text-2xl font-semibold mb-2'>마크다운</h3>
|
<h3 className='mb-2 text-2xl font-semibold'>마크다운</h3>
|
||||||
<Segment>
|
<Segment>
|
||||||
<div className='lg:flex'>
|
<div className='lg:flex'>
|
||||||
<div className='w-full lg:w-1/2 min-h-48'>
|
<div className='min-h-48 w-full lg:w-1/2'>
|
||||||
<textarea className='resize-none w-full h-full dark:bg-discord-dark outline-none p-5' name='markdown' value={formik.values.markdown} onChange={formik.handleChange}/>
|
<textarea
|
||||||
|
className='h-full w-full resize-none p-5 outline-none dark:bg-discord-dark'
|
||||||
|
name='markdown'
|
||||||
|
value={formik.values.markdown}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full lg:w-1/2 p-10'>
|
<div className='w-full p-10 lg:w-1/2'>
|
||||||
<Markdown text={formik.values.markdown} />
|
<Markdown text={formik.values.markdown} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Segment>
|
</Segment>
|
||||||
</Container>
|
</Container>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ClientInfo
|
export default ClientInfo
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user