mirror of
https://github.com/koreanbots/core.git
synced 2025-12-15 06:10:22 +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,25 +1,31 @@
|
||||
import AdSense from 'react-adsense'
|
||||
|
||||
const Advertisement: React.FC<AdvertisementProps> = ({ size = 'short' }) => {
|
||||
return <div className='py-5'>
|
||||
<div
|
||||
className={`z-0 mx-auto w-full text-center text-white ${
|
||||
process.env.NODE_ENV === 'production' ? '' : 'py-12 bg-gray-700'
|
||||
}`}
|
||||
style={size === 'short' ? { height: '90px' } : { height: '330px' }}
|
||||
>
|
||||
{process.env.NODE_ENV === 'production' ? (
|
||||
<AdSense.Google
|
||||
style={{ display: 'inline-block', width: '100%', height: size === 'short' ? '90px' : '330px'}}
|
||||
client='ca-pub-4856582423981759'
|
||||
slot='3250141451'
|
||||
format=''
|
||||
/>
|
||||
) : (
|
||||
'Advertisement'
|
||||
)}
|
||||
return (
|
||||
<div className='py-5'>
|
||||
<div
|
||||
className={`z-0 mx-auto w-full text-center text-white ${
|
||||
process.env.NODE_ENV === 'production' ? '' : 'bg-gray-700 py-12'
|
||||
}`}
|
||||
style={size === 'short' ? { height: '90px' } : { height: '330px' }}
|
||||
>
|
||||
{process.env.NODE_ENV === 'production' ? (
|
||||
<AdSense.Google
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
height: size === 'short' ? '90px' : '330px',
|
||||
}}
|
||||
client='ca-pub-4856582423981759'
|
||||
slot='3250141451'
|
||||
format=''
|
||||
/>
|
||||
) : (
|
||||
'Advertisement'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@ -7,17 +7,16 @@ const ServerIcon = dynamic(() => import('@components/ServerIcon'))
|
||||
const Application: React.FC<ApplicationProps> = ({ type, id, name }) => {
|
||||
return (
|
||||
<Link href={`/developers/applications/${type + 's'}/${id}`} legacyBehavior>
|
||||
<div className='relative px-2 py-4 text-center dark:bg-discord-black bg-little-white rounded-lg cursor-pointer transform hover:-translate-y-1 transition duration-100 ease-in'>
|
||||
{
|
||||
type === 'bot' ?
|
||||
<DiscordAvatar userID={id} className='px-2 w-full rounded-xl' /> :
|
||||
<ServerIcon id={id} className='px-2 w-full rounded-xl' />
|
||||
}
|
||||
<h2 className='pt-2 whitespace-nowrap text-xl font-medium truncate'>{name}</h2>
|
||||
<div className='relative transform cursor-pointer rounded-lg bg-little-white px-2 py-4 text-center transition duration-100 ease-in hover:-translate-y-1 dark:bg-discord-black'>
|
||||
{type === 'bot' ? (
|
||||
<DiscordAvatar userID={id} className='w-full rounded-xl px-2' />
|
||||
) : (
|
||||
<ServerIcon id={id} className='w-full rounded-xl px-2' />
|
||||
)}
|
||||
<h2 className='truncate whitespace-nowrap pt-2 text-xl font-medium'>{name}</h2>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
interface ApplicationProps {
|
||||
|
||||
@ -11,18 +11,18 @@ const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
|
||||
|
||||
const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
||||
return (
|
||||
<div className='min-w-0 container mb-16 transform hover:-translate-y-1 transition duration-100 ease-in cursor-pointer'>
|
||||
<div className='container mb-16 min-w-0 transform cursor-pointer transition duration-100 ease-in hover:-translate-y-1'>
|
||||
<div className='relative'>
|
||||
<div className='container mx-auto'>
|
||||
<div className='h-full'>
|
||||
<div
|
||||
className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl'
|
||||
className='relative mx-auto h-full rounded-2xl bg-little-white text-black shadow-xl dark:bg-discord-black dark:text-white'
|
||||
style={
|
||||
checkBotFlag(bot.flags, 'trusted') && bot.banner
|
||||
? {
|
||||
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${bot.banner}") center top / cover no-repeat`,
|
||||
color: 'white',
|
||||
}
|
||||
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${bot.banner}") center top / cover no-repeat`,
|
||||
color: 'white',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
@ -30,15 +30,15 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
||||
<div>
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex'>
|
||||
<div className='w-3/5 flex justify-start'>
|
||||
<div className='flex w-3/5 justify-start'>
|
||||
<DiscordAvatar
|
||||
size={128}
|
||||
userID={bot.id}
|
||||
alt='Avatar'
|
||||
className='absolute -left-2 -top-8 mx-auto w-32 h-32 bg-white rounded-full'
|
||||
className='absolute -left-2 -top-8 mx-auto h-32 w-32 rounded-full bg-white'
|
||||
/>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 pr-5 pt-5 w-2/5'>
|
||||
<div className='grid w-2/5 grid-cols-1 pr-5 pt-5'>
|
||||
<Tag
|
||||
text={
|
||||
<>
|
||||
@ -55,19 +55,19 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3 px-4 h-16'>
|
||||
<div className='mt-3 h-16 px-4'>
|
||||
<h2 className='px-1 text-sm'>
|
||||
<i className={`fas fa-circle text-${Status[bot.status]?.color}`} />
|
||||
{Status[bot.status]?.text}
|
||||
</h2>
|
||||
<h1 className='mb-3 text-left text-xl sm:text-2xl font-bold truncate'>{bot.name}</h1>
|
||||
<h1 className='mb-3 truncate text-left text-xl font-bold sm:text-2xl'>
|
||||
{bot.name}
|
||||
</h1>
|
||||
</div>
|
||||
<p className='mb-10 px-4 h-6 text-left text-gray-400 text-sm'>
|
||||
{bot.intro}
|
||||
</p>
|
||||
<p className='mb-10 h-6 px-4 text-left text-sm text-gray-400'>{bot.intro}</p>
|
||||
<div>
|
||||
<div className='category flex flex-wrap px-2'>
|
||||
{bot.category.slice(0, 3).map(el => (
|
||||
{bot.category.slice(0, 3).map((el) => (
|
||||
<Tag key={el} text={el} href={`/bots/categories/${el}`} dark />
|
||||
))}{' '}
|
||||
{bot.category.length > 3 && <Tag text={`+${bot.category.length - 3}`} dark />}
|
||||
@ -80,35 +80,31 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
||||
<div className='flex justify-evenly'>
|
||||
<Link
|
||||
href={makeBotURL(bot)}
|
||||
className='py-3 w-full text-center text-koreanbots-blue hover:text-white text-sm font-bold hover:bg-koreanbots-blue rounded-bl-2xl hover:shadow-lg transition duration-100 ease-in'>
|
||||
|
||||
보기
|
||||
|
||||
className='w-full rounded-bl-2xl py-3 text-center text-sm font-bold text-koreanbots-blue transition duration-100 ease-in hover:bg-koreanbots-blue hover:text-white hover:shadow-lg'
|
||||
>
|
||||
보기
|
||||
</Link>
|
||||
{manage ? (
|
||||
<Link
|
||||
href={`/bots/${bot.id}/edit`}
|
||||
className='py-3 w-full text-center text-emerald-500 hover:text-white text-sm font-bold hover:bg-emerald-500 rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'>
|
||||
|
||||
관리하기
|
||||
|
||||
className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-emerald-500 transition duration-100 ease-in hover:bg-emerald-500 hover:text-white hover:shadow-lg'
|
||||
>
|
||||
관리하기
|
||||
</Link>
|
||||
) : bot.state !== 'ok' ? <a
|
||||
className='py-3 w-full text-center text-discord-blurple text-sm font-bold rounded-br-2xl hover:shadow-lg transition duration-100 ease-in opacity-50 cursor-default select-none'
|
||||
>
|
||||
초대하기
|
||||
</a> :
|
||||
) : bot.state !== 'ok' ? (
|
||||
<a className='w-full cursor-default select-none rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple opacity-50 transition duration-100 ease-in hover:shadow-lg'>
|
||||
초대하기
|
||||
</a>
|
||||
) : (
|
||||
<a
|
||||
href={
|
||||
makeBotURL(bot) + '/invite'
|
||||
}
|
||||
href={makeBotURL(bot) + '/invite'}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
className='py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'
|
||||
className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple transition duration-100 ease-in hover:bg-discord-blurple hover:text-white hover:shadow-lg'
|
||||
>
|
||||
초대하기
|
||||
초대하기
|
||||
</a>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -117,7 +113,6 @@ const BotCard: React.FC<BotCardProps> = ({ manage = false, bot }) => {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
interface BotCardProps {
|
||||
@ -125,4 +120,4 @@ interface BotCardProps {
|
||||
bot: Bot
|
||||
}
|
||||
|
||||
export default BotCard
|
||||
export default BotCard
|
||||
|
||||
@ -6,33 +6,41 @@ const Button: React.FC<ButtonProps> = ({
|
||||
className,
|
||||
children,
|
||||
href,
|
||||
disabled=false,
|
||||
disabled = false,
|
||||
onClick,
|
||||
}) => {
|
||||
return href ? <Link
|
||||
href={!disabled && href}
|
||||
className={`cursor-pointer rounded-md px-4 py-2 transition duration-300 ease select-none outline-none foucs:outline-none mr-1.5 ${className ??
|
||||
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}>
|
||||
|
||||
{children}
|
||||
|
||||
</Link>
|
||||
: onClick ? <button
|
||||
return href ? (
|
||||
<Link
|
||||
href={!disabled && href}
|
||||
className={`ease foucs:outline-none mr-1.5 cursor-pointer select-none rounded-md px-4 py-2 outline-none transition duration-300 ${
|
||||
className ??
|
||||
'bg-discord-blurple text-white hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
) : onClick ? (
|
||||
<button
|
||||
type={disabled ? 'button' : type}
|
||||
onClick={disabled ? null : onClick}
|
||||
className={`cursor-pointer rounded-md px-4 py-2 transition duration-300 ease select-none outline-none foucs:outline-none mr-1.5 ${className ??
|
||||
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}
|
||||
className={`ease foucs:outline-none mr-1.5 cursor-pointer select-none rounded-md px-4 py-2 outline-none transition duration-300 ${
|
||||
className ??
|
||||
'bg-discord-blurple text-white hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
:
|
||||
<button
|
||||
type={disabled ? 'button' : type}
|
||||
className={`cursor-pointer rounded-md px-4 py-2 transition duration-300 ease select-none outline-none foucs:outline-none mr-1.5 ${className ??
|
||||
'bg-discord-blurple hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover text-white'}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type={disabled ? 'button' : type}
|
||||
className={`ease foucs:outline-none mr-1.5 cursor-pointer select-none rounded-md px-4 py-2 outline-none transition duration-300 ${
|
||||
className ??
|
||||
'bg-discord-blurple text-white hover:opacity-80 dark:bg-very-black dark:hover:bg-discord-dark-hover'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
interface ButtonProps {
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
import { Ref } from 'react'
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
|
||||
|
||||
const Captcha: React.FC<CaptchaProps> = ({ dark, onVerify }) => {
|
||||
return <HCaptcha sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITEKEY} theme={dark ? 'dark' : 'light'} onVerify={onVerify}/>
|
||||
return (
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITEKEY}
|
||||
theme={dark ? 'dark' : 'light'}
|
||||
onVerify={onVerify}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface CaptchaProps {
|
||||
dark: boolean
|
||||
onVerify(token: string, eKey?: string): void
|
||||
ref?: Ref<HCaptcha>
|
||||
dark: boolean
|
||||
onVerify(token: string, eKey?: string): void
|
||||
ref?: Ref<HCaptcha>
|
||||
}
|
||||
|
||||
export default Captcha
|
||||
export default Captcha
|
||||
|
||||
@ -3,7 +3,7 @@ const ColorCard: React.FC<ColorCardProps> = ({ header, first, second, className
|
||||
<div className={`rounded-lg p-10 ${className} shadow-lg`}>
|
||||
<h2 className='text-2xl font-bold'>{header}</h2>
|
||||
<p className='opacity-80'>
|
||||
{first}
|
||||
{first}
|
||||
<br />
|
||||
{second}
|
||||
</p>
|
||||
|
||||
@ -9,106 +9,169 @@ import { NextSeo } from 'next-seo'
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
const Divider = dynamic(() => import('@components/Divider'))
|
||||
|
||||
const DeveloperLayout: React.FC<DeveloperLayout> = ({ children, enabled, docs, currentDoc }:DeveloperLayout) => {
|
||||
const [ navbarEnabled, setNavbarOpen ] = useState(false)
|
||||
const DeveloperLayout: React.FC<DeveloperLayout> = ({
|
||||
children,
|
||||
enabled,
|
||||
docs,
|
||||
currentDoc,
|
||||
}: DeveloperLayout) => {
|
||||
const [navbarEnabled, setNavbarOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className='flex min-h-screen'>
|
||||
<NextSeo title='한디리 개발자' description='한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.' openGraph={{
|
||||
title:'한디리 개발자',
|
||||
description:'한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.'
|
||||
}} />
|
||||
<div className='block lg:hidden h-screen relative'>
|
||||
<div className='w-18 pt-20 px-2 h-full text-center bg-little-white dark:bg-discord-black fixed'>
|
||||
<div className='min-h-screen flex'>
|
||||
<NextSeo
|
||||
title='한디리 개발자'
|
||||
description='한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.'
|
||||
openGraph={{
|
||||
title: '한디리 개발자',
|
||||
description: '한국 디스코드 리스트 API를 활용하여 봇에 다양한 기능을 추가해보세요.',
|
||||
}}
|
||||
/>
|
||||
<div className='relative block h-screen lg:hidden'>
|
||||
<div className='w-18 fixed h-full bg-little-white px-2 pt-20 text-center dark:bg-discord-black'>
|
||||
<ul className='text-gray-600 dark:text-gray-300'>
|
||||
<li className={`cursor-pointer py-2 px-4 mb-2 rounded-md ${enabled === 'applications' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
||||
<Link href='/developers/applications' legacyBehavior><i className='fas fa-robot'/></Link>
|
||||
<li
|
||||
className={`mb-2 cursor-pointer rounded-md px-4 py-2 ${
|
||||
enabled === 'applications'
|
||||
? 'bg-discord-blurple text-white'
|
||||
: 'hover:text-gray-500 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Link href='/developers/applications' legacyBehavior>
|
||||
<i className='fas fa-robot' />
|
||||
</Link>
|
||||
</li>
|
||||
<li className={`cursor-pointer py-2 px-4 my-2 rounded-md ${enabled === 'docs' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
||||
<Link href='/developers/docs' legacyBehavior><i className='fas fa-book'/></Link>
|
||||
<li
|
||||
className={`my-2 cursor-pointer rounded-md px-4 py-2 ${
|
||||
enabled === 'docs'
|
||||
? 'bg-discord-blurple text-white'
|
||||
: 'hover:text-gray-500 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Link href='/developers/docs' legacyBehavior>
|
||||
<i className='fas fa-book' />
|
||||
</Link>
|
||||
</li>
|
||||
{
|
||||
enabled === 'docs' && <>
|
||||
{enabled === 'docs' && (
|
||||
<>
|
||||
<Divider />
|
||||
<li className='cursor-pointer py-2 px-4 my-2 rounded-md hover:text-gray-500 dark:hover:text-white' onKeyDown={() => setNavbarOpen(true)} onClick={() => setNavbarOpen(true)}>
|
||||
<i className='fas fa-bars'/>
|
||||
</li></>
|
||||
}
|
||||
<li
|
||||
className='my-2 cursor-pointer rounded-md px-4 py-2 hover:text-gray-500 dark:hover:text-white'
|
||||
onKeyDown={() => setNavbarOpen(true)}
|
||||
onClick={() => setNavbarOpen(true)}
|
||||
>
|
||||
<i className='fas fa-bars' />
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${navbarEnabled ? 'block' : 'hidden'} lg:block relative`}>
|
||||
<div className='bg-little-white dark:bg-discord-black pt-20 px-6 fixed h-screen w-screen lg:w-60 overflow-y-auto'>
|
||||
<ul className='text-base text-gray-600 dark:text-gray-300 mb-6 hidden lg:block'>
|
||||
<li className='cursor-pointer py-2 px-4 rounded-md hover:text-gray-500 dark:hover:text-white lg:hidden' onKeyDown={() => setNavbarOpen(false)} onClick={() => setNavbarOpen(false)}>닫기</li>
|
||||
<div className={`${navbarEnabled ? 'block' : 'hidden'} relative lg:block`}>
|
||||
<div className='fixed h-screen w-screen overflow-y-auto bg-little-white px-6 pt-20 dark:bg-discord-black lg:w-60'>
|
||||
<ul className='mb-6 hidden text-base text-gray-600 dark:text-gray-300 lg:block'>
|
||||
<li
|
||||
className='cursor-pointer rounded-md px-4 py-2 hover:text-gray-500 dark:hover:text-white lg:hidden'
|
||||
onKeyDown={() => setNavbarOpen(false)}
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
>
|
||||
닫기
|
||||
</li>
|
||||
<Divider className='lg:hidden' />
|
||||
<Link href='/developers/applications' legacyBehavior>
|
||||
<li className={`cursor-pointer py-2 px-4 rounded-md ${enabled === 'applications' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
||||
나의 리스트
|
||||
<li
|
||||
className={`cursor-pointer rounded-md px-4 py-2 ${
|
||||
enabled === 'applications'
|
||||
? 'bg-discord-blurple text-white'
|
||||
: 'hover:text-gray-500 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
나의 리스트
|
||||
</li>
|
||||
</Link>
|
||||
<Link href='/developers/docs' legacyBehavior>
|
||||
<li className={`cursor-pointer py-2 px-4 rounded-md ${enabled === 'docs' ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
||||
문서
|
||||
<li
|
||||
className={`cursor-pointer rounded-md px-4 py-2 ${
|
||||
enabled === 'docs'
|
||||
? 'bg-discord-blurple text-white'
|
||||
: 'hover:text-gray-500 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
문서
|
||||
</li>
|
||||
</Link>
|
||||
</ul>
|
||||
{
|
||||
enabled === 'docs' && <>
|
||||
{enabled === 'docs' && (
|
||||
<>
|
||||
<Divider className='hidden lg:block' />
|
||||
<ul className='text-sm text-gray-600 dark:text-gray-300 px-0.5 lg:mt-6'>
|
||||
<li onClick={() => setNavbarOpen(false)} className='lg:hidden cursor-pointer py-1 px-4 rounded-md mb-2'>
|
||||
<ul className='px-0.5 text-sm text-gray-600 dark:text-gray-300 lg:mt-6'>
|
||||
<li
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
className='mb-2 cursor-pointer rounded-md px-4 py-1 lg:hidden'
|
||||
>
|
||||
<i className='fas fa-times' /> 닫기
|
||||
</li>
|
||||
<Divider className='lg:hidden' />
|
||||
{
|
||||
docs?.map(el => {
|
||||
if(el.list) return (
|
||||
{docs?.map((el) => {
|
||||
if (el.list)
|
||||
return (
|
||||
<div key={el.name} className='mt-2'>
|
||||
<span className='text-gray-600 dark:text-gray-100 font-bold mb-1'>{el.name}</span>
|
||||
<ul className='text-sm py-3'>
|
||||
{
|
||||
el.list.map(e =>
|
||||
<Link
|
||||
key={e.name}
|
||||
href={`/developers/docs/${el.name}/${e.name}`}
|
||||
legacyBehavior>
|
||||
<li onClick={() => setNavbarOpen(false)} className={`cursor-pointer px-4 py-2 rounded-md ${currentDoc === e.name ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
||||
{e.name}
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<span className='mb-1 font-bold text-gray-600 dark:text-gray-100'>
|
||||
{el.name}
|
||||
</span>
|
||||
<ul className='py-3 text-sm'>
|
||||
{el.list.map((e) => (
|
||||
<Link
|
||||
key={e.name}
|
||||
href={`/developers/docs/${el.name}/${e.name}`}
|
||||
legacyBehavior
|
||||
>
|
||||
<li
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
className={`cursor-pointer rounded-md px-4 py-2 ${
|
||||
currentDoc === e.name
|
||||
? 'bg-discord-blurple text-white'
|
||||
: 'hover:text-gray-500 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{e.name}
|
||||
</li>
|
||||
</Link>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Link key={el.name} href={`/developers/docs/${el.name}`} legacyBehavior>
|
||||
<li onClick={() => setNavbarOpen(false)} className={`cursor-pointer py-2 px-4 rounded-md ${currentDoc === el.name ? 'bg-discord-blurple text-white' : 'hover:text-gray-500 dark:hover:text-white'}`}>
|
||||
{el.name}
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Link key={el.name} href={`/developers/docs/${el.name}`} legacyBehavior>
|
||||
<li
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
className={`cursor-pointer rounded-md px-4 py-2 ${
|
||||
currentDoc === el.name
|
||||
? 'bg-discord-blurple text-white'
|
||||
: 'hover:text-gray-500 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{el.name}
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full py-28 lg:pl-60 pl-16'>
|
||||
<Container>
|
||||
{children}
|
||||
</Container>
|
||||
<div className='w-full py-28 pl-16 lg:pl-60'>
|
||||
<Container>{children}</Container>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface DeveloperLayout {
|
||||
children: ReactNode
|
||||
enabled: 'applications' | 'docs'
|
||||
children: ReactNode
|
||||
enabled: 'applications' | 'docs'
|
||||
docs?: DocsData[]
|
||||
currentDoc?: string
|
||||
}
|
||||
|
||||
@ -4,20 +4,27 @@ import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||
|
||||
const Image = dynamic(() => import('@components/Image'))
|
||||
|
||||
const DiscordAvatar: React.FC<DiscordAvatarProps> = props => {
|
||||
return <Image
|
||||
{...props}
|
||||
src={KoreanbotsEndPoints.CDN.avatar(props.userID, { format: 'webp', size: props.size ?? 256})}
|
||||
fallbackSrc={KoreanbotsEndPoints.CDN.avatar(props.userID, { format: 'png', size: props.size ?? 256})}
|
||||
/>
|
||||
|
||||
const DiscordAvatar: React.FC<DiscordAvatarProps> = (props) => {
|
||||
return (
|
||||
<Image
|
||||
{...props}
|
||||
src={KoreanbotsEndPoints.CDN.avatar(props.userID, {
|
||||
format: 'webp',
|
||||
size: props.size ?? 256,
|
||||
})}
|
||||
fallbackSrc={KoreanbotsEndPoints.CDN.avatar(props.userID, {
|
||||
format: 'png',
|
||||
size: props.size ?? 256,
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface DiscordAvatarProps {
|
||||
alt?: string
|
||||
userID: string
|
||||
className?: string
|
||||
size? : 128 | 256 | 512
|
||||
size?: 128 | 256 | 512
|
||||
}
|
||||
|
||||
interface ImageEvent extends Event {
|
||||
|
||||
@ -8,26 +8,28 @@ const Docs: React.FC<DocsProps> = ({ title, header, description, subheader, chil
|
||||
const d = description || subheader
|
||||
return (
|
||||
<>
|
||||
<NextSeo title={t} description={d}
|
||||
<NextSeo
|
||||
title={t}
|
||||
description={d}
|
||||
openGraph={{
|
||||
title: t,
|
||||
description: d
|
||||
description: d,
|
||||
}}
|
||||
/>
|
||||
<div className='dark:bg-discord-black bg-discord-blurple z-20'>
|
||||
<div className='z-20 bg-discord-blurple dark:bg-discord-black'>
|
||||
<Container className='py-20' ignoreColor>
|
||||
<h1 className='mt-10 text-center text-gray-100 text-4xl font-bold sm:text-left'>
|
||||
<h1 className='mt-10 text-center text-4xl font-bold text-gray-100 sm:text-left'>
|
||||
{header}
|
||||
</h1>
|
||||
<h2 className='mt-5 text-center text-gray-200 text-xl font-medium sm:text-left'>
|
||||
<h2 className='mt-5 text-center text-xl font-medium text-gray-200 sm:text-left'>
|
||||
{description}
|
||||
</h2>
|
||||
<h2 className='mt-5 text-center text-gray-200 text-xl font-medium sm:text-left'>
|
||||
<h2 className='mt-5 text-center text-xl font-medium text-gray-200 sm:text-left'>
|
||||
{subheader}
|
||||
</h2>
|
||||
</Container>
|
||||
</div>
|
||||
<Container className='pt-10 pb-20'>
|
||||
<Container className='pb-20 pt-10'>
|
||||
<div>{children}</div>
|
||||
</Container>
|
||||
</>
|
||||
|
||||
@ -9,16 +9,16 @@ const Toggle = dynamic(() => import('@components/Toggle'))
|
||||
const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
||||
return (
|
||||
<div className='releative z-30'>
|
||||
<div className='bottom-0 text-white bg-discord-black py-24'>
|
||||
<Container className='w-11/12 lg:flex lg:pt-0 lg:w-4/5' ignoreColor>
|
||||
<div className='bottom-0 bg-discord-black py-24 text-white'>
|
||||
<Container className='w-11/12 lg:flex lg:w-4/5 lg:pt-0' ignoreColor>
|
||||
<div className='w-full lg:w-2/5'>
|
||||
<h1 className='text-koreanbots-blue text-2xl font-bold'>국내 디스코드의 모든 것을 한 곳에서.</h1>
|
||||
<h1 className='text-2xl font-bold text-koreanbots-blue'>
|
||||
국내 디스코드의 모든 것을 한 곳에서.
|
||||
</h1>
|
||||
<span className='text-base'>2020-2023 한국 디스코드 리스트, All rights reserved.</span>
|
||||
<div className='text-2xl flex space-x-1'>
|
||||
<div className='flex space-x-1 text-2xl'>
|
||||
<Link href='/discord'>
|
||||
|
||||
<i className='fab fa-discord inline-block w-full' />
|
||||
|
||||
</Link>
|
||||
<a href='https://github.com/koreanbots'>
|
||||
<i className='fab fa-github inline-block w-full' />
|
||||
@ -28,9 +28,9 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grow gap-2 grid-cols-2 md:grid-cols-7'>
|
||||
<div className='grid grow grid-cols-2 gap-2 md:grid-cols-7'>
|
||||
<div className='col-span-2 mb-2'>
|
||||
<h2 className='text-koreanbots-blue text-base font-bold'>한국 디스코드 리스트</h2>
|
||||
<h2 className='text-base font-bold text-koreanbots-blue'>한국 디스코드 리스트</h2>
|
||||
<ul className='text-sm'>
|
||||
<li>
|
||||
<Link href='/about' className='hover:text-gray-300'>
|
||||
@ -50,7 +50,7 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
||||
</ul>
|
||||
</div>
|
||||
<div className='col-span-2 mb-2'>
|
||||
<h2 className='text-koreanbots-blue text-base font-bold'>정책</h2>
|
||||
<h2 className='text-base font-bold text-koreanbots-blue'>정책</h2>
|
||||
<ul className='text-sm'>
|
||||
<li>
|
||||
<Link href='/tos' className='hover:text-gray-300'>
|
||||
@ -75,7 +75,7 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
||||
</ul>
|
||||
</div>
|
||||
<div className='col-span-1 mb-2'>
|
||||
<h2 className='text-koreanbots-blue text-base font-bold'>커뮤니티</h2>
|
||||
<h2 className='text-base font-bold text-koreanbots-blue'>커뮤니티</h2>
|
||||
<ul className='text-sm'>
|
||||
{/* <li>
|
||||
<Link href='/partners'>
|
||||
@ -90,7 +90,7 @@ const Footer: React.FC<FooterProps> = ({ theme, setTheme }) => {
|
||||
</ul>
|
||||
</div>
|
||||
<div className='col-span-2 mb-2'>
|
||||
<h2 className='text-koreanbots-blue text-base font-bold'>기타</h2>
|
||||
<h2 className='text-base font-bold text-koreanbots-blue'>기타</h2>
|
||||
<div className='flex'>
|
||||
<a className='mr-2 hover:text-gray-300'>다크모드</a>
|
||||
<Toggle
|
||||
|
||||
@ -6,21 +6,23 @@ import { ErrorText } from '@utils/Constants'
|
||||
|
||||
const Button = dynamic(() => import('@components/Button'))
|
||||
|
||||
const Forbidden:React.FC = () => {
|
||||
const Forbidden: React.FC = () => {
|
||||
const router = useRouter()
|
||||
return <>
|
||||
<NextSeo title='권한이 없습니다' />
|
||||
<div className='flex items-center justify-center h-screen select-none'>
|
||||
<div className='container mx-auto px-20 md:text-left text-center'>
|
||||
<h1 className='text-8xl font-semibold'>403</h1>
|
||||
<h2 className='text-2xl font-semibold py-2'>
|
||||
{ErrorText[403]}
|
||||
</h2>
|
||||
<Button onClick={router.back}>뒤로 가기</Button>
|
||||
<p className='text-gray-400 text-sm mt-2'>해당 작업을 수행할 수 있는 권한이 없습니다. 무언가 잘못된 것 같다면 문의해주세요.</p>
|
||||
return (
|
||||
<>
|
||||
<NextSeo title='권한이 없습니다' />
|
||||
<div className='flex h-screen select-none items-center justify-center'>
|
||||
<div className='container mx-auto px-20 text-center md:text-left'>
|
||||
<h1 className='text-8xl font-semibold'>403</h1>
|
||||
<h2 className='py-2 text-2xl font-semibold'>{ErrorText[403]}</h2>
|
||||
<Button onClick={router.back}>뒤로 가기</Button>
|
||||
<p className='mt-2 text-sm text-gray-400'>
|
||||
해당 작업을 수행할 수 있는 권한이 없습니다. 무언가 잘못된 것 같다면 문의해주세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Forbidden
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { Field } from 'formik'
|
||||
|
||||
const CheckBox: React.FC<CheckBoxProps> = ({ name, ...props }) => {
|
||||
return <Field type='checkbox' name={name} className='form-checkbox text-koreanbots-blue bg-gray-300 h-4 w-4 rounded' {...props} />
|
||||
return (
|
||||
<Field
|
||||
type='checkbox'
|
||||
name={name}
|
||||
className='form-checkbox h-4 w-4 rounded bg-gray-300 text-koreanbots-blue'
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface CheckBoxProps {
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import { Field } from 'formik'
|
||||
|
||||
const Input: React.FC<InputProps> = ({ name, placeholder, ...props }) => {
|
||||
return <Field
|
||||
{...props}
|
||||
name={name}
|
||||
className={'border-grey-light relative px-3 w-full h-10 text-black dark:text-white dark:bg-very-black border dark:border-transparent rounded outline-none'}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
return (
|
||||
<Field
|
||||
{...props}
|
||||
name={name}
|
||||
className={
|
||||
'border-grey-light relative h-10 w-full rounded border px-3 text-black outline-none dark:border-transparent dark:bg-very-black dark:text-white'
|
||||
}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface InputProps {
|
||||
|
||||
@ -6,28 +6,30 @@ const Label: React.FC<LabelProps> = ({
|
||||
error = null,
|
||||
grid = true,
|
||||
short = false,
|
||||
required = false
|
||||
required = false,
|
||||
}) => {
|
||||
return <label
|
||||
className={grid ? 'grid grid-cols-1 xl:grid-cols-4 gap-2 my-4' : 'inline-flex items-center'}
|
||||
htmlFor={For}
|
||||
>
|
||||
{label && (
|
||||
<div className='col-span-1 text-sm'>
|
||||
<h3 className='text-koreanbots-blue text-lg font-bold'>
|
||||
{label}
|
||||
{required && (
|
||||
<span className='align-text-top text-red-500 text-base font-semibold'> *</span>
|
||||
)}
|
||||
</h3>
|
||||
{labelDesc}
|
||||
return (
|
||||
<label
|
||||
className={grid ? 'my-4 grid grid-cols-1 gap-2 xl:grid-cols-4' : 'inline-flex items-center'}
|
||||
htmlFor={For}
|
||||
>
|
||||
{label && (
|
||||
<div className='col-span-1 text-sm'>
|
||||
<h3 className='text-lg font-bold text-koreanbots-blue'>
|
||||
{label}
|
||||
{required && (
|
||||
<span className='align-text-top text-base font-semibold text-red-500'> *</span>
|
||||
)}
|
||||
</h3>
|
||||
{labelDesc}
|
||||
</div>
|
||||
)}
|
||||
<div className={short ? 'col-span-1' : 'col-span-3'}>
|
||||
{children}
|
||||
<div className='mt-1 text-xs font-light text-red-500'>{error}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={short ? 'col-span-1' : 'col-span-3'}>
|
||||
{children}
|
||||
<div className='mt-1 text-red-500 text-xs font-light'>{error}</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
interface LabelProps {
|
||||
|
||||
@ -1,42 +1,50 @@
|
||||
import ReactSelect from 'react-select'
|
||||
|
||||
const Select: React.FC<SelectProps> = ({ placeholder, options, handleChange, handleTouch, value }) => {
|
||||
return <ReactSelect
|
||||
styles={{
|
||||
control: provided => {
|
||||
return { ...provided, border: 'none' }
|
||||
},
|
||||
option: provided => {
|
||||
return {
|
||||
...provided,
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
opacity: '0.7',
|
||||
},
|
||||
}
|
||||
},
|
||||
placeholder: provided => {
|
||||
return {
|
||||
...provided,
|
||||
position: 'absolute'
|
||||
}
|
||||
},
|
||||
singleValue: provided => {
|
||||
return {
|
||||
...provided,
|
||||
position: 'absolute'
|
||||
}
|
||||
}
|
||||
}}
|
||||
className='border-grey-light border dark:border-transparent rounded'
|
||||
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white '
|
||||
placeholder={placeholder || '선택해주세요.'}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
onBlur={handleTouch}
|
||||
noOptionsMessage={() => '검색 결과가 없습니다.'}
|
||||
defaultValue={value}
|
||||
/>
|
||||
const Select: React.FC<SelectProps> = ({
|
||||
placeholder,
|
||||
options,
|
||||
handleChange,
|
||||
handleTouch,
|
||||
value,
|
||||
}) => {
|
||||
return (
|
||||
<ReactSelect
|
||||
styles={{
|
||||
control: (provided) => {
|
||||
return { ...provided, border: 'none' }
|
||||
},
|
||||
option: (provided) => {
|
||||
return {
|
||||
...provided,
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
opacity: '0.7',
|
||||
},
|
||||
}
|
||||
},
|
||||
placeholder: (provided) => {
|
||||
return {
|
||||
...provided,
|
||||
position: 'absolute',
|
||||
}
|
||||
},
|
||||
singleValue: (provided) => {
|
||||
return {
|
||||
...provided,
|
||||
position: 'absolute',
|
||||
}
|
||||
},
|
||||
}}
|
||||
className='border-grey-light rounded border dark:border-transparent'
|
||||
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white '
|
||||
placeholder={placeholder || '선택해주세요.'}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
onBlur={handleTouch}
|
||||
noOptionsMessage={() => '검색 결과가 없습니다.'}
|
||||
defaultValue={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
import React, { MouseEventHandler } from 'react'
|
||||
import ReactSelect, {
|
||||
components,
|
||||
MultiValueProps,
|
||||
MultiValueRemoveProps,
|
||||
} from 'react-select'
|
||||
import ReactSelect, { components, MultiValueProps, MultiValueRemoveProps } from 'react-select'
|
||||
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core'
|
||||
import { restrictToParentElement } from '@dnd-kit/modifiers'
|
||||
import {
|
||||
@ -20,8 +16,9 @@ const MultiValue = (props: MultiValueProps<Option>) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
const innerProps = { ...props.innerProps, onMouseDown }
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: props.data.value })
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||
id: props.data.value,
|
||||
})
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
@ -46,61 +43,80 @@ const MultiValueRemove = (props: MultiValueRemoveProps<Option>) => {
|
||||
)
|
||||
}
|
||||
|
||||
const Select: React.FC<SelectProps> = ({ placeholder, options, values, setValues, handleChange, handleTouch }) => {
|
||||
const Select: React.FC<SelectProps> = ({
|
||||
placeholder,
|
||||
options,
|
||||
values,
|
||||
setValues,
|
||||
handleChange,
|
||||
handleTouch,
|
||||
}) => {
|
||||
const onSortEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
const newValue = arrayMove(values, values.findIndex(i => i === active.id), values.findIndex(i => i === over.id))
|
||||
const newValue = arrayMove(
|
||||
values,
|
||||
values.findIndex((i) => i === active.id),
|
||||
values.findIndex((i) => i === over.id)
|
||||
)
|
||||
setValues(newValue)
|
||||
}
|
||||
return <DndContext modifiers={[restrictToParentElement]} onDragEnd={onSortEnd} collisionDetection={closestCenter}>
|
||||
<SortableContext
|
||||
items={values}
|
||||
strategy={horizontalListSortingStrategy}>
|
||||
<ReactSelect
|
||||
styles={{
|
||||
placeholder: (provided) => {
|
||||
return { ...provided, position: 'absolute' }
|
||||
},
|
||||
control: (provided) => {
|
||||
return { ...provided, border: 'none' }
|
||||
},
|
||||
option: (provided) => {
|
||||
return { ...provided, cursor: 'pointer', ':hover': {
|
||||
opacity: '0.7'
|
||||
} }
|
||||
}
|
||||
}}
|
||||
isMulti
|
||||
className='border border-grey-light dark:border-transparent rounded'
|
||||
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white cursor-pointer '
|
||||
placeholder={placeholder || '선택해주세요.'}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
onBlur={handleTouch}
|
||||
noOptionsMessage={() => '검색 결과가 없습니다.'}
|
||||
value={values.map(el => ({ label: el, value: el}))}
|
||||
components={{
|
||||
MultiValue,
|
||||
MultiValueRemove,
|
||||
}}
|
||||
closeMenuOnSelect={false}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
return (
|
||||
<DndContext
|
||||
modifiers={[restrictToParentElement]}
|
||||
onDragEnd={onSortEnd}
|
||||
collisionDetection={closestCenter}
|
||||
>
|
||||
<SortableContext items={values} strategy={horizontalListSortingStrategy}>
|
||||
<ReactSelect
|
||||
styles={{
|
||||
placeholder: (provided) => {
|
||||
return { ...provided, position: 'absolute' }
|
||||
},
|
||||
control: (provided) => {
|
||||
return { ...provided, border: 'none' }
|
||||
},
|
||||
option: (provided) => {
|
||||
return {
|
||||
...provided,
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
opacity: '0.7',
|
||||
},
|
||||
}
|
||||
},
|
||||
}}
|
||||
isMulti
|
||||
className='border-grey-light rounded border dark:border-transparent'
|
||||
classNamePrefix='outline-none text-black dark:bg-very-black dark:text-white cursor-pointer '
|
||||
placeholder={placeholder || '선택해주세요.'}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
onBlur={handleTouch}
|
||||
noOptionsMessage={() => '검색 결과가 없습니다.'}
|
||||
value={values.map((el) => ({ label: el, value: el }))}
|
||||
components={{
|
||||
MultiValue,
|
||||
MultiValueRemove,
|
||||
}}
|
||||
closeMenuOnSelect={false}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
placeholder?: string
|
||||
options: Option[]
|
||||
placeholder?: string
|
||||
options: Option[]
|
||||
values: string[]
|
||||
setValues: (value: string[]) => void
|
||||
handleChange: (value: Option[]) => void
|
||||
handleTouch: () => void
|
||||
handleTouch: () => void
|
||||
}
|
||||
|
||||
interface Option {
|
||||
value: string
|
||||
label: string
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export default Select
|
||||
export default Select
|
||||
|
||||
@ -8,64 +8,93 @@ import useOutsideClick from '@utils/useOutsideClick'
|
||||
|
||||
import 'emoji-mart/css/emoji-mart.css'
|
||||
|
||||
|
||||
|
||||
const TextArea: React.FC<TextAreaProps> = ({ name, placeholder, theme='auto', max, setValue, value }) => {
|
||||
const TextArea: React.FC<TextAreaProps> = ({
|
||||
name,
|
||||
placeholder,
|
||||
theme = 'auto',
|
||||
max,
|
||||
setValue,
|
||||
value,
|
||||
}) => {
|
||||
const ref = useRef()
|
||||
const [ emojiPickerHidden, setEmojiPickerHidden ] = useState(true)
|
||||
const [emojiPickerHidden, setEmojiPickerHidden] = useState(true)
|
||||
useOutsideClick(ref, () => {
|
||||
setEmojiPickerHidden(true)
|
||||
})
|
||||
|
||||
return <div className='border border-grey-light dark:border-transparent h-96 text-black dark:bg-very-black dark:text-white rounded px-4 py-3 inline-block relative w-full'>
|
||||
<Field as='textarea' name={name} className='dark:border-transparent text-black dark:bg-very-black dark:text-white w-full relative h-full resize-none outline-none' placeholder={placeholder} />
|
||||
<div ref={ref}>
|
||||
<div className='absolute bottom-12 left-10 z-30'>
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
!emojiPickerHidden && <Picker title='선택해주세요' emoji='sunglasses' set='twitter' enableFrequentEmojiSort theme={theme} showSkinTones={false} onSelect={(e) => {
|
||||
setEmojiPickerHidden(true)
|
||||
setValue(value + ' ' + ((e as { native: string }).native || e.colons))
|
||||
}} i18n={{
|
||||
search: '검색',
|
||||
notfound: '검색 결과가 없습니다.',
|
||||
categories: {
|
||||
search: '검색 결과',
|
||||
recent: '최근 사용',
|
||||
people: '사람',
|
||||
nature: '자연',
|
||||
foods: '음식',
|
||||
activity: '활동',
|
||||
places: '장소',
|
||||
objects: '사물',
|
||||
symbols: '기호',
|
||||
flags: '국기',
|
||||
custom: '커스텀'
|
||||
}
|
||||
}} custom={KoreanbotsEmoji}/>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='border-grey-light relative inline-block h-96 w-full rounded border px-4 py-3 text-black dark:border-transparent dark:bg-very-black dark:text-white'>
|
||||
<Field
|
||||
as='textarea'
|
||||
name={name}
|
||||
className='relative h-full w-full resize-none text-black outline-none dark:border-transparent dark:bg-very-black dark:text-white'
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<div ref={ref}>
|
||||
<div className='absolute bottom-12 left-10 z-30'>
|
||||
{!emojiPickerHidden && (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<Picker
|
||||
title='선택해주세요'
|
||||
emoji='sunglasses'
|
||||
set='twitter'
|
||||
enableFrequentEmojiSort
|
||||
theme={theme}
|
||||
showSkinTones={false}
|
||||
onSelect={(e) => {
|
||||
setEmojiPickerHidden(true)
|
||||
setValue(value + ' ' + ((e as { native: string }).native || e.colons))
|
||||
}}
|
||||
i18n={{
|
||||
search: '검색',
|
||||
notfound: '검색 결과가 없습니다.',
|
||||
categories: {
|
||||
search: '검색 결과',
|
||||
recent: '최근 사용',
|
||||
people: '사람',
|
||||
nature: '자연',
|
||||
foods: '음식',
|
||||
activity: '활동',
|
||||
places: '장소',
|
||||
objects: '사물',
|
||||
symbols: '기호',
|
||||
flags: '국기',
|
||||
custom: '커스텀',
|
||||
},
|
||||
}}
|
||||
custom={KoreanbotsEmoji}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='absolute bottom-2 left-4 hidden sm:block'>
|
||||
<div
|
||||
className='emoji-selector-button outline-none'
|
||||
onClick={() => setEmojiPickerHidden(false)}
|
||||
onKeyPress={() => setEmojiPickerHidden(false)}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
{max && (
|
||||
<span
|
||||
className={`absolute bottom-2 right-4 ${max < value.length ? ' text-red-400' : ''}`}
|
||||
>
|
||||
{max - value.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='absolute bottom-2 left-4 hidden sm:block'>
|
||||
<div className='emoji-selector-button outline-none' onClick={() => setEmojiPickerHidden(false)} onKeyPress={() => setEmojiPickerHidden(false)} role='button' tabIndex={0} />
|
||||
</div>
|
||||
{
|
||||
max && <span className={`absolute bottom-2 right-4 ${max < value.length ? ' text-red-400' : ''}`}>
|
||||
{max-value.length}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TextAreaProps {
|
||||
name: string
|
||||
placeholder?: string
|
||||
theme?: 'auto' | 'dark' | 'light'
|
||||
name: string
|
||||
placeholder?: string
|
||||
theme?: 'auto' | 'dark' | 'light'
|
||||
max?: number
|
||||
value: string
|
||||
setValue(value: string): void
|
||||
value: string
|
||||
setValue(value: string): void
|
||||
}
|
||||
|
||||
export default TextArea
|
||||
|
||||
|
||||
@ -1,71 +1,154 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
import { NextSeo } from 'next-seo'
|
||||
|
||||
import { botCategories, botCategoryIcon, serverCategories, serverCategoryIcon } from '@utils/Constants'
|
||||
import {
|
||||
botCategories,
|
||||
botCategoryIcon,
|
||||
serverCategories,
|
||||
serverCategoryIcon,
|
||||
} from '@utils/Constants'
|
||||
|
||||
const Container = dynamic(()=> import('@components/Container'))
|
||||
const Tag = dynamic(()=> import('@components/Tag'))
|
||||
const Search = dynamic(()=> import('@components/Search'))
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
const Tag = dynamic(() => import('@components/Tag'))
|
||||
const Search = dynamic(() => import('@components/Search'))
|
||||
|
||||
const Hero:React.FC<HeroProps> = ({ type='all', header, description }) => {
|
||||
const Hero: React.FC<HeroProps> = ({ type = 'all', header, description }) => {
|
||||
const link = `/${type}/categories`
|
||||
return <>
|
||||
<NextSeo title={header} description={description} openGraph={{
|
||||
title: header,
|
||||
description
|
||||
}} />
|
||||
<div className='dark:bg-discord-black bg-discord-blurple text-gray-100 md:p-0 mb-8'>
|
||||
<Container className='pt-24 pb-16 md:pb-20' ignoreColor>
|
||||
<h1 className='hidden md:block text-left text-3xl font-bold'>
|
||||
{ header && `${header} - `}한국 디스코드 리스트
|
||||
</h1>
|
||||
<h1 className='md:hidden text-center text-3xl font-semibold'>
|
||||
{ header && <span className='text-4xl'>{header}<br/></span>}한국 디스코드 리스트
|
||||
</h1>
|
||||
<p className='text-center sm:text-left text-xl font-base mt-2'>{description || `${type !== 'all' ? '다양한 ' : ''}국내 디스코드${{ all: '의 모든 것을', bots: ' 봇들을', servers: ' 서버들을' }[type]} 한 곳에서 확인하세요!`}</p>
|
||||
<Search />
|
||||
<div className='flex flex-wrap mt-5'>
|
||||
{
|
||||
type === 'all' ? <>
|
||||
<Tag text={
|
||||
<>
|
||||
<i className='fas fa-robot text-koreanbots-blue'/> 봇 리스트
|
||||
</>
|
||||
} dark bigger href='/bots' />
|
||||
<Tag text={
|
||||
<>
|
||||
<i className='fas fa-users text-koreanbots-blue'/> 서버 리스트
|
||||
</>
|
||||
} dark bigger href='/servers' />
|
||||
{
|
||||
botCategories.slice(0, 2).map(t => <Tag key={t} text={<><i className={botCategoryIcon[t]} /> {t} 봇</>} dark bigger href={`/bots/categories/${t}`} />)
|
||||
}
|
||||
|
||||
{
|
||||
serverCategories.slice(0, 2).map(t => <Tag key={t} text={<><i className={serverCategoryIcon[t]} /> {t} 서버</>} dark bigger href={`/servers/categories/${t}`} />)
|
||||
}
|
||||
</>: <>
|
||||
<Tag key='list' text={<>
|
||||
<i className='fas fa-heart text-red-600'/> 하트 랭킹
|
||||
</>} dark bigger href={type === 'bots' ? '/bots/list/votes' : '/servers/list/votes'} />
|
||||
{ (type === 'bots' ? botCategories : serverCategories).slice(0, 4).map(t=> <Tag key={t} text={<>
|
||||
<i className={(type === 'bots' ? botCategoryIcon : serverCategoryIcon)[t]} /> {t}
|
||||
</>} dark bigger href={`${link}/${t}`} />) }
|
||||
<Tag key='tag' text={<>
|
||||
<i className='fas fa-tag'/> 카테고리 더보기
|
||||
</>} dark bigger href={link} />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={header}
|
||||
description={description}
|
||||
openGraph={{
|
||||
title: header,
|
||||
description,
|
||||
}}
|
||||
/>
|
||||
<div className='mb-8 bg-discord-blurple text-gray-100 dark:bg-discord-black md:p-0'>
|
||||
<Container className='pb-16 pt-24 md:pb-20' ignoreColor>
|
||||
<h1 className='hidden text-left text-3xl font-bold md:block'>
|
||||
{header && `${header} - `}한국 디스코드 리스트
|
||||
</h1>
|
||||
<h1 className='text-center text-3xl font-semibold md:hidden'>
|
||||
{header && (
|
||||
<span className='text-4xl'>
|
||||
{header}
|
||||
<br />
|
||||
</span>
|
||||
)}
|
||||
한국 디스코드 리스트
|
||||
</h1>
|
||||
<p className='font-base mt-2 text-center text-xl sm:text-left'>
|
||||
{description ||
|
||||
`${type !== 'all' ? '다양한 ' : ''}국내 디스코드${
|
||||
{ all: '의 모든 것을', bots: ' 봇들을', servers: ' 서버들을' }[type]
|
||||
} 한 곳에서 확인하세요!`}
|
||||
</p>
|
||||
<Search />
|
||||
<div className='mt-5 flex flex-wrap'>
|
||||
{type === 'all' ? (
|
||||
<>
|
||||
<Tag
|
||||
text={
|
||||
<>
|
||||
<i className='fas fa-robot text-koreanbots-blue' /> 봇 리스트
|
||||
</>
|
||||
}
|
||||
dark
|
||||
bigger
|
||||
href='/bots'
|
||||
/>
|
||||
<Tag
|
||||
text={
|
||||
<>
|
||||
<i className='fas fa-users text-koreanbots-blue' /> 서버 리스트
|
||||
</>
|
||||
}
|
||||
dark
|
||||
bigger
|
||||
href='/servers'
|
||||
/>
|
||||
{botCategories.slice(0, 2).map((t) => (
|
||||
<Tag
|
||||
key={t}
|
||||
text={
|
||||
<>
|
||||
<i className={botCategoryIcon[t]} /> {t} 봇
|
||||
</>
|
||||
}
|
||||
dark
|
||||
bigger
|
||||
href={`/bots/categories/${t}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
{serverCategories.slice(0, 2).map((t) => (
|
||||
<Tag
|
||||
key={t}
|
||||
text={
|
||||
<>
|
||||
<i className={serverCategoryIcon[t]} /> {t} 서버
|
||||
</>
|
||||
}
|
||||
dark
|
||||
bigger
|
||||
href={`/servers/categories/${t}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Tag
|
||||
key='list'
|
||||
text={
|
||||
<>
|
||||
<i className='fas fa-heart text-red-600' /> 하트 랭킹
|
||||
</>
|
||||
}
|
||||
dark
|
||||
bigger
|
||||
href={type === 'bots' ? '/bots/list/votes' : '/servers/list/votes'}
|
||||
/>
|
||||
{(type === 'bots' ? botCategories : serverCategories).slice(0, 4).map((t) => (
|
||||
<Tag
|
||||
key={t}
|
||||
text={
|
||||
<>
|
||||
<i
|
||||
className={(type === 'bots' ? botCategoryIcon : serverCategoryIcon)[t]}
|
||||
/>{' '}
|
||||
{t}
|
||||
</>
|
||||
}
|
||||
dark
|
||||
bigger
|
||||
href={`${link}/${t}`}
|
||||
/>
|
||||
))}
|
||||
<Tag
|
||||
key='tag'
|
||||
text={
|
||||
<>
|
||||
<i className='fas fa-tag' /> 카테고리 더보기
|
||||
</>
|
||||
}
|
||||
dark
|
||||
bigger
|
||||
href={link}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface HeroProps {
|
||||
type?: 'all' | 'bots' | 'servers'
|
||||
header?: string
|
||||
description?: string
|
||||
header?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export default Hero
|
||||
export default Hero
|
||||
|
||||
@ -2,45 +2,46 @@ import { SyntheticEvent, useEffect, useState } from 'react'
|
||||
import { supportsWebP } from '@utils/Tools'
|
||||
import Logger from '@utils/Logger'
|
||||
|
||||
const BaseImage: React.FC<ImageProps> = props => {
|
||||
const BaseImage: React.FC<ImageProps> = (props) => {
|
||||
const fallback = '/img/default.png'
|
||||
const [ webpUnavailable, setWebpUnavailable ] = useState<boolean>()
|
||||
|
||||
useEffect(()=> {
|
||||
const [webpUnavailable, setWebpUnavailable] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
setWebpUnavailable(localStorage.webp === 'false')
|
||||
}, [])
|
||||
|
||||
return <img
|
||||
alt={props.alt ?? 'Image'}
|
||||
loading='lazy'
|
||||
className={props.className}
|
||||
src={
|
||||
webpUnavailable && props.fallbackSrc || props.src
|
||||
}
|
||||
onError={(e: SyntheticEvent<HTMLImageElement, ImageEvent>)=> {
|
||||
if(webpUnavailable) {
|
||||
(e.target as ImageTarget).onerror = (event) => {
|
||||
// All Fails
|
||||
(event.target as ImageTarget).onerror = () => { Logger.warn('FALLBACK IMAGE LOAD FAIL') }
|
||||
(event.target as ImageTarget).src = fallback
|
||||
|
||||
|
||||
return (
|
||||
<img
|
||||
alt={props.alt ?? 'Image'}
|
||||
loading='lazy'
|
||||
className={props.className}
|
||||
src={(webpUnavailable && props.fallbackSrc) || props.src}
|
||||
onError={(e: SyntheticEvent<HTMLImageElement, ImageEvent>) => {
|
||||
if (webpUnavailable) {
|
||||
;(e.target as ImageTarget).onerror = (event) => {
|
||||
// All Fails
|
||||
;(event.target as ImageTarget).onerror = () => {
|
||||
Logger.warn('FALLBACK IMAGE LOAD FAIL')
|
||||
}
|
||||
;(event.target as ImageTarget).src = fallback
|
||||
}
|
||||
} else if (props.fallbackSrc) {
|
||||
;(e.target as ImageTarget).onerror = (event) => {
|
||||
// All Fails
|
||||
;(event.target as ImageTarget).onerror = () => {
|
||||
Logger.warn('FALLBACK IMAGE LOAD FAIL')
|
||||
}
|
||||
;(event.target as ImageTarget).src = fallback
|
||||
}
|
||||
// Webp Load Fail
|
||||
;(e.target as ImageTarget).src = props.fallbackSrc
|
||||
if (!supportsWebP()) localStorage.setItem('webp', 'false')
|
||||
} else {
|
||||
;(e.target as ImageTarget).src = fallback
|
||||
}
|
||||
}
|
||||
else if (props.fallbackSrc) {
|
||||
(e.target as ImageTarget).onerror = (event) => {
|
||||
// All Fails
|
||||
(event.target as ImageTarget).onerror = () => { Logger.warn('FALLBACK IMAGE LOAD FAIL') }
|
||||
(event.target as ImageTarget).src = fallback
|
||||
}
|
||||
// Webp Load Fail
|
||||
(e.target as ImageTarget).src = props.fallbackSrc
|
||||
if(!supportsWebP()) localStorage.setItem('webp', 'false')
|
||||
}
|
||||
else {
|
||||
(e.target as ImageTarget).src = fallback
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface ImageProps {
|
||||
|
||||
@ -3,9 +3,9 @@ const Loader: React.FC<LoaderProps> = ({ text, visible = true }) => {
|
||||
<div
|
||||
className={`${
|
||||
visible ? '' : 'hidden '
|
||||
} w-full h-full fixed block top-0 left-0 bg-gray-500 bg-opacity-75 z-50 dark:text-black`}
|
||||
} fixed left-0 top-0 z-50 block h-full w-full bg-gray-500 bg-opacity-75 dark:text-black`}
|
||||
>
|
||||
<h1 className='relative top-1/2 block mx-auto my-0 text-center text-2xl font-semibold opacity-100'>
|
||||
<h1 className='relative top-1/2 mx-auto my-0 block text-center text-2xl font-semibold opacity-100'>
|
||||
{text}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@ -9,9 +9,7 @@ const Login: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||
localStorage.redirectTo = window.location.href
|
||||
redirectTo(router, 'login')
|
||||
})
|
||||
return <>
|
||||
{children}
|
||||
</>
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
export default Login
|
||||
export default Login
|
||||
|
||||
@ -1,42 +1,68 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import Link from 'next/link'
|
||||
|
||||
const LongButton: React.FC<LongButtonProps> = ({ children, newTab=false, href, onClick, center=false }) => {
|
||||
if(href) {
|
||||
if(newTab) return <a href={href} rel='noopener noreferrer'
|
||||
target='_blank'>
|
||||
<div className={`${center ? 'justify-center ': '' }text-base bg-little-white dark:bg-discord-black text-black dark:text-gray-400 rounded flex hover:bg-little-white-hover dark:hover:bg-discord-dark-hover cursor-pointer px-4 py-4 mb-1`}>
|
||||
const LongButton: React.FC<LongButtonProps> = ({
|
||||
children,
|
||||
newTab = false,
|
||||
href,
|
||||
onClick,
|
||||
center = false,
|
||||
}) => {
|
||||
if (href) {
|
||||
if (newTab)
|
||||
return (
|
||||
<a href={href} rel='noopener noreferrer' target='_blank'>
|
||||
<div
|
||||
className={`${
|
||||
center ? 'justify-center ' : ''
|
||||
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={`${
|
||||
center ? 'justify-center ' : ''
|
||||
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
if (onClick)
|
||||
return (
|
||||
<div
|
||||
onKeyPress={onClick}
|
||||
onClick={onClick}
|
||||
className={`${
|
||||
center ? 'justify-center ' : ''
|
||||
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</a>
|
||||
else return (
|
||||
<Link
|
||||
href={href}
|
||||
className={`${center ? 'justify-center ': '' }text-base bg-little-white dark:bg-discord-black text-black dark:text-gray-400 rounded flex hover:bg-little-white-hover dark:hover:bg-discord-dark-hover cursor-pointer px-4 py-4 mb-1`}>
|
||||
|
||||
{children}
|
||||
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
if(onClick) return <div onKeyPress={onClick} onClick={onClick} className={`${center ? 'justify-center ': '' }text-base bg-little-white dark:bg-discord-black text-black dark:text-gray-400 rounded flex hover:bg-little-white-hover dark:hover:bg-discord-dark-hover cursor-pointer px-4 py-4 mb-1`}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
return <a className={`${center ? 'justify-center ': '' }text-base bg-little-white dark:bg-discord-black text-black dark:text-gray-400 rounded flex hover:bg-little-white-hover dark:hover:bg-discord-dark-hover cursor-pointer px-4 py-4 mb-1`}>
|
||||
{children}
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<a
|
||||
className={`${
|
||||
center ? 'justify-center ' : ''
|
||||
}text-base mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover`}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default LongButton
|
||||
|
||||
interface LongButtonProps {
|
||||
newTab?: boolean
|
||||
onClick?: (event: React.KeyboardEvent<HTMLDivElement>|React.MouseEvent<HTMLDivElement>) => void
|
||||
children: string | JSX.Element | JSX.Element[]
|
||||
href?: string
|
||||
center?: boolean
|
||||
}
|
||||
newTab?: boolean
|
||||
onClick?: (event: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement>) => void
|
||||
children: string | JSX.Element | JSX.Element[]
|
||||
href?: string
|
||||
center?: boolean
|
||||
}
|
||||
|
||||
@ -5,7 +5,12 @@ import * as Emoji from 'node-emoji'
|
||||
|
||||
import { anchorHeader, customEmoji, twemoji } from '@utils/Tools'
|
||||
|
||||
const Markdown: React.FC<MarkdownProps> = ({ text, options={}, allowedTag=[], components={} }) => {
|
||||
const Markdown: React.FC<MarkdownProps> = ({
|
||||
text,
|
||||
options = {},
|
||||
allowedTag = [],
|
||||
components = {},
|
||||
}) => {
|
||||
return (
|
||||
<div className='markdown-body w-full'>
|
||||
<MarkdownView
|
||||
@ -23,10 +28,10 @@ const Markdown: React.FC<MarkdownProps> = ({ text, options={}, allowedTag=[], co
|
||||
tasklists: true,
|
||||
ghCompatibleHeaderId: true,
|
||||
encodeEmails: true,
|
||||
...options
|
||||
...options,
|
||||
}}
|
||||
components={components}
|
||||
sanitizeHtml={html =>
|
||||
sanitizeHtml={(html) =>
|
||||
sanitizeHtml(html, {
|
||||
allowedTags: [
|
||||
'addr',
|
||||
@ -98,16 +103,16 @@ const Markdown: React.FC<MarkdownProps> = ({ text, options={}, allowedTag=[], co
|
||||
'svg',
|
||||
'path',
|
||||
'input',
|
||||
...allowedTag
|
||||
...allowedTag,
|
||||
],
|
||||
allowedAttributes: false,
|
||||
allowedClasses: {
|
||||
'*': ['align-middle'],
|
||||
a: ['anchor', 'mr-1'],
|
||||
svg: ['octicon-link'],
|
||||
img: ['emoji', 'special']
|
||||
img: ['emoji', 'special'],
|
||||
},
|
||||
allowedStyles: {}
|
||||
allowedStyles: {},
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
@ -4,9 +4,9 @@ import Markdown from './Markdown'
|
||||
const Message: React.FC<MessageProps> = ({ type, children }) => {
|
||||
return (
|
||||
<div
|
||||
className={`${MessageColor[type]} px-6 py-4 rounded-md text-base mx-auto w-full text-left`}
|
||||
className={`${MessageColor[type]} mx-auto w-full rounded-md px-6 py-4 text-left text-base`}
|
||||
>
|
||||
{ typeof children === 'string' ? <Markdown text={children} /> : children }
|
||||
{typeof children === 'string' ? <Markdown text={children} /> : children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,7 +2,15 @@ import { ReactNode } from 'react'
|
||||
import { Modal as ReactModal } from 'react-responsive-modal'
|
||||
import 'react-responsive-modal/styles.css'
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({ children, isOpen, onClose, closeIcon=false, dark, header, full=false }) => {
|
||||
const Modal: React.FC<ModalProps> = ({
|
||||
children,
|
||||
isOpen,
|
||||
onClose,
|
||||
closeIcon = false,
|
||||
dark,
|
||||
header,
|
||||
full = false,
|
||||
}) => {
|
||||
return (
|
||||
<ReactModal
|
||||
open={isOpen}
|
||||
@ -12,13 +20,13 @@ const Modal: React.FC<ModalProps> = ({ children, isOpen, onClose, closeIcon=fals
|
||||
showCloseIcon={closeIcon}
|
||||
styles={{
|
||||
closeButton: {
|
||||
color: dark ? 'white' : 'black'
|
||||
color: dark ? 'white' : 'black',
|
||||
},
|
||||
modal: {
|
||||
borderRadius: '10px',
|
||||
background: dark ? '#2C2F33' : '#fbfbfb',
|
||||
color: dark ? 'white' : 'black',
|
||||
width: full ? '90%' : 'inherit'
|
||||
width: full ? '90%' : 'inherit',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@ -4,30 +4,43 @@ const Button = dynamic(() => import('@components/Button'))
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
|
||||
const NSFW: React.FC<NSFWProps> = ({ onClick, onDisableClick }) => {
|
||||
return <Container>
|
||||
<div className='flex items-center h-screen select-none'>
|
||||
<div className='px-10'>
|
||||
<h1 className='text-2xl font-bold flex'>
|
||||
<img draggable='false' alt='⚠' src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/26a0.svg' className='emoji mr-2 w-8' />
|
||||
해당 컨텐츠는 만19세 이상의 성인만 열람할 수 있습니다.</h1>
|
||||
<p className='text-lg mb-3'>계속하시겠습니까?</p>
|
||||
<Button onClick={onClick}>
|
||||
<i className='fas fa-arrow-right' /> 계속하기
|
||||
</Button>
|
||||
<div className='mt-1'>
|
||||
<button className='text-blue-500 hover:text-blue-600' onClick={() => {
|
||||
onClick()
|
||||
onDisableClick()
|
||||
}}>다시 표시하지 않기.</button>
|
||||
return (
|
||||
<Container>
|
||||
<div className='flex h-screen select-none items-center'>
|
||||
<div className='px-10'>
|
||||
<h1 className='flex text-2xl font-bold'>
|
||||
<img
|
||||
draggable='false'
|
||||
alt='⚠'
|
||||
src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/26a0.svg'
|
||||
className='emoji mr-2 w-8'
|
||||
/>
|
||||
해당 컨텐츠는 만19세 이상의 성인만 열람할 수 있습니다.
|
||||
</h1>
|
||||
<p className='mb-3 text-lg'>계속하시겠습니까?</p>
|
||||
<Button onClick={onClick}>
|
||||
<i className='fas fa-arrow-right' /> 계속하기
|
||||
</Button>
|
||||
<div className='mt-1'>
|
||||
<button
|
||||
className='text-blue-500 hover:text-blue-600'
|
||||
onClick={() => {
|
||||
onClick()
|
||||
onDisableClick()
|
||||
}}
|
||||
>
|
||||
다시 표시하지 않기.
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
interface NSFWProps {
|
||||
onClick(): void
|
||||
onDisableClick(): void
|
||||
onClick(): void
|
||||
onDisableClick(): void
|
||||
}
|
||||
|
||||
export default NSFW
|
||||
export default NSFW
|
||||
|
||||
@ -20,309 +20,355 @@ const Navbar: React.FC<NavbarProps> = ({ token }) => {
|
||||
const [mobileAddDropdownOpen, setMobileAddDropdownOpen] = useState<boolean>(false)
|
||||
const router = useRouter()
|
||||
const logged = userCache?.id && userCache.version === 2
|
||||
const type: Nullable<'bot'|'server'> = router.pathname.startsWith('/bots') ? 'bot' : router.pathname.startsWith('/servers') ? 'server' : null
|
||||
const type: Nullable<'bot' | 'server'> = router.pathname.startsWith('/bots')
|
||||
? 'bot'
|
||||
: router.pathname.startsWith('/servers')
|
||||
? 'server'
|
||||
: null
|
||||
const dev = router.pathname.startsWith('/developers')
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if(localStorage.userCache) {
|
||||
if (localStorage.userCache) {
|
||||
setUserCache(token ? JSON.parse(localStorage.userCache) : null)
|
||||
}
|
||||
Fetch<User>('/users/@me').then(data => {
|
||||
if(data.code !== 200) return
|
||||
setUserCache(JSON.parse(localStorage.userCache = JSON.stringify({
|
||||
id: data.data.id,
|
||||
username: data.data.globalName,
|
||||
tag: data.data.tag,
|
||||
version: 2
|
||||
})))
|
||||
Fetch<User>('/users/@me').then((data) => {
|
||||
if (data.code !== 200) return
|
||||
setUserCache(
|
||||
JSON.parse(
|
||||
(localStorage.userCache = JSON.stringify({
|
||||
id: data.data.id,
|
||||
username: data.data.globalName,
|
||||
tag: data.data.tag,
|
||||
version: 2,
|
||||
}))
|
||||
)
|
||||
)
|
||||
})
|
||||
} catch {
|
||||
setUserCache(null)
|
||||
}
|
||||
}, [ token ])
|
||||
return <>
|
||||
<nav className='fixed z-40 top-0 flex flex-wrap items-center justify-between px-2 py-3 w-full text-gray-100 dark:bg-discord-black bg-discord-blurple lg:absolute'>
|
||||
<div className='container flex flex-wrap items-center justify-between mx-auto px-4'>
|
||||
<div className='relative flex justify-between w-full lg:justify-start lg:w-auto'>
|
||||
<Link
|
||||
href={dev ? '/developers' : '/'}
|
||||
className={`${dev ? 'dark:text-koreanbots-blue ' : ''}logofont text-large whitespace-no-wrap inline-block mr-4 py-2 hover:text-gray-300 font-semibold leading-relaxed uppercase sm:text-2xl`}>
|
||||
|
||||
{ dev ? <><i className='fas fa-tools mr-1'/> DEVELOPERS</> : 'KOREANLIST'}
|
||||
|
||||
</Link>
|
||||
<button
|
||||
className='block px-3 py-1 dark:text-gray-200 text-xl leading-none bg-transparent border border-solid border-transparent rounded outline-none focus:outline-none cursor-pointer lg:hidden'
|
||||
type='button'
|
||||
onClick={() => setNavbarOpen(!navbarOpen)}
|
||||
>
|
||||
<i className={`fas ${!navbarOpen ? 'fa-bars' : 'fa-times'}`}></i>
|
||||
</button>
|
||||
<ul className='hidden lg:flex flex-col list-none lg:flex-row lg:ml-auto'>
|
||||
<li className='flex items-center'>
|
||||
<Link
|
||||
href={dev ? '/' : '/developers'}
|
||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'>
|
||||
|
||||
{dev ? '홈' : '개발자'}
|
||||
|
||||
</Link>
|
||||
</li>
|
||||
{
|
||||
type !== 'bot' && <li className='flex items-center'>
|
||||
}, [token])
|
||||
return (
|
||||
<>
|
||||
<nav className='fixed top-0 z-40 flex w-full flex-wrap items-center justify-between bg-discord-blurple px-2 py-3 text-gray-100 dark:bg-discord-black lg:absolute'>
|
||||
<div className='container mx-auto flex flex-wrap items-center justify-between px-4'>
|
||||
<div className='relative flex w-full justify-between lg:w-auto lg:justify-start'>
|
||||
<Link
|
||||
href={dev ? '/developers' : '/'}
|
||||
className={`${
|
||||
dev ? 'dark:text-koreanbots-blue ' : ''
|
||||
}logofont text-large whitespace-no-wrap mr-4 inline-block py-2 font-semibold uppercase leading-relaxed hover:text-gray-300 sm:text-2xl`}
|
||||
>
|
||||
{dev ? (
|
||||
<>
|
||||
<i className='fas fa-tools mr-1' /> DEVELOPERS
|
||||
</>
|
||||
) : (
|
||||
'KOREANLIST'
|
||||
)}
|
||||
</Link>
|
||||
<button
|
||||
className='block cursor-pointer rounded border border-solid border-transparent bg-transparent px-3 py-1 text-xl leading-none outline-none focus:outline-none dark:text-gray-200 lg:hidden'
|
||||
type='button'
|
||||
onClick={() => setNavbarOpen(!navbarOpen)}
|
||||
>
|
||||
<i className={`fas ${!navbarOpen ? 'fa-bars' : 'fa-times'}`}></i>
|
||||
</button>
|
||||
<ul className='hidden list-none flex-col lg:ml-auto lg:flex lg:flex-row'>
|
||||
<li className='flex items-center'>
|
||||
<Link
|
||||
href='/bots'
|
||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'>
|
||||
|
||||
봇 리스트
|
||||
|
||||
href={dev ? '/' : '/developers'}
|
||||
className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||
>
|
||||
{dev ? '홈' : '개발자'}
|
||||
</Link>
|
||||
</li>
|
||||
}
|
||||
{
|
||||
type !== 'server' && <li className='flex items-center'>
|
||||
{type !== 'bot' && (
|
||||
<li className='flex items-center'>
|
||||
<Link
|
||||
href='/bots'
|
||||
className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||
>
|
||||
봇 리스트
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{type !== 'server' && (
|
||||
<li className='flex items-center'>
|
||||
<Link
|
||||
href='/servers'
|
||||
className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||
>
|
||||
서버 리스트
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
<li className='flex items-center'>
|
||||
<Link
|
||||
href='/servers'
|
||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'>
|
||||
|
||||
서버 리스트
|
||||
|
||||
href='/discord'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||
>
|
||||
디스코드
|
||||
</Link>
|
||||
</li>
|
||||
}
|
||||
<li className='flex items-center'>
|
||||
<Link
|
||||
href='/discord'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'>
|
||||
|
||||
디스코드
|
||||
|
||||
</Link>
|
||||
</li>
|
||||
<li className='flex items-center'>
|
||||
<Link
|
||||
href='/about'
|
||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100'>
|
||||
|
||||
소개
|
||||
|
||||
</Link>
|
||||
</li>
|
||||
<li className='flex items-center' onFocus={() => setAddDropdownOpen(true)} onMouseOver={() => setAddDropdownOpen(true)} onMouseOut={() => setAddDropdownOpen(false)} onBlur={() => setAddDropdownOpen(false)}>
|
||||
<span className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100 cursor-pointer'>
|
||||
추가하기
|
||||
</span>
|
||||
<div className={`rounded shadow-md absolute mt-11 top-0 w-40 bg-white text-black dark:bg-very-black dark:text-gray-300 text-sm ${addDropdownOpen ? 'block' : 'hidden'}`}>
|
||||
<ul className='relative'>
|
||||
<li>
|
||||
<Link
|
||||
href='/addbot'
|
||||
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-t'>
|
||||
<i className='fas fa-robot' />봇 추가하기
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href='/addserver'
|
||||
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-b'>
|
||||
<i className='fas fa-users' />서버 추가하기
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='hidden grow items-center bg-white lg:flex lg:bg-transparent lg:shadow-none'>
|
||||
<ul className='flex flex-col list-none lg:flex-row lg:ml-auto'>
|
||||
<li className='flex items-center outline-none' onFocus={() => setDropdownOpen(true)} onMouseOver={() => setDropdownOpen(true)} onMouseOut={() => setDropdownOpen(false)} onBlur={() => setDropdownOpen(false)}>
|
||||
{
|
||||
logged ?
|
||||
<li className='flex items-center'>
|
||||
<Link
|
||||
href='/about'
|
||||
className='flex w-full items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||
>
|
||||
소개
|
||||
</Link>
|
||||
</li>
|
||||
<li
|
||||
className='flex items-center'
|
||||
onFocus={() => setAddDropdownOpen(true)}
|
||||
onMouseOver={() => setAddDropdownOpen(true)}
|
||||
onMouseOut={() => setAddDropdownOpen(false)}
|
||||
onBlur={() => setAddDropdownOpen(false)}
|
||||
>
|
||||
<span className='flex w-full cursor-pointer items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'>
|
||||
추가하기
|
||||
</span>
|
||||
<div
|
||||
className={`absolute top-0 mt-11 w-40 rounded bg-white text-sm text-black shadow-md dark:bg-very-black dark:text-gray-300 ${
|
||||
addDropdownOpen ? 'block' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<ul className='relative'>
|
||||
<li>
|
||||
<Link
|
||||
href='/addbot'
|
||||
className='block rounded-t px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
|
||||
>
|
||||
<i className='fas fa-robot' />봇 추가하기
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href='/addserver'
|
||||
className='block rounded-b px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
|
||||
>
|
||||
<i className='fas fa-users' />
|
||||
서버 추가하기
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className='hidden grow items-center bg-white lg:flex lg:bg-transparent lg:shadow-none'>
|
||||
<ul className='flex list-none flex-col lg:ml-auto lg:flex-row'>
|
||||
<li
|
||||
className='flex items-center outline-none'
|
||||
onFocus={() => setDropdownOpen(true)}
|
||||
onMouseOver={() => setDropdownOpen(true)}
|
||||
onMouseOut={() => setDropdownOpen(false)}
|
||||
onBlur={() => setDropdownOpen(false)}
|
||||
>
|
||||
{logged ? (
|
||||
<>
|
||||
<a
|
||||
className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100 cursor-pointer'>
|
||||
<DiscordAvatar userID={userCache.id} className='w-8 h-8 rounded-full mr-1.5' size={128}/>
|
||||
{userCache.username} <i className='ml-2 fas fa-sort-down' />
|
||||
<a className='flex w-full cursor-pointer items-center px-3 py-4 text-sm font-semibold text-gray-700 hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'>
|
||||
<DiscordAvatar
|
||||
userID={userCache.id}
|
||||
className='mr-1.5 h-8 w-8 rounded-full'
|
||||
size={128}
|
||||
/>
|
||||
{userCache.username} <i className='fas fa-sort-down ml-2' />
|
||||
</a>
|
||||
<div className={`rounded shadow-md absolute mt-14 top-0 w-48 bg-white text-black dark:bg-very-black dark:text-gray-300 text-sm ${dropdownOpen ? 'block' : 'hidden'}`}>
|
||||
<div
|
||||
className={`absolute top-0 mt-14 w-48 rounded bg-white text-sm text-black shadow-md dark:bg-very-black dark:text-gray-300 ${
|
||||
dropdownOpen ? 'block' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<ul className='relative'>
|
||||
<li>
|
||||
<Link
|
||||
href={`/users/${userCache.id}`}
|
||||
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-t'>
|
||||
<i className='fas fa-user' />프로필
|
||||
className='block rounded-t px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
|
||||
>
|
||||
<i className='fas fa-user' />
|
||||
프로필
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href='/panel'
|
||||
className='px-4 py-2 block hover:bg-gray-100 dark:hover:bg-discord-dark-hover'>
|
||||
<i className='fas fa-cogs' />관리패널
|
||||
className='block px-4 py-2 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
|
||||
>
|
||||
<i className='fas fa-cogs' />
|
||||
관리패널
|
||||
</Link>
|
||||
</li>
|
||||
{/* <li><hr className='border-t mx-2'/></li> */}
|
||||
<li>
|
||||
<a onKeyPress={() => {
|
||||
localStorage.removeItem('userCache')
|
||||
redirectTo(router, 'logout')
|
||||
}
|
||||
} onClick={() => {
|
||||
localStorage.removeItem('userCache')
|
||||
redirectTo(router, 'logout')
|
||||
}} className='px-4 py-2 block text-red-500 hover:bg-gray-100 dark:hover:bg-discord-dark-hover rounded-b cursor-pointer'><i className='fas fa-sign-out-alt' /> 로그아웃</a>
|
||||
<a
|
||||
onKeyPress={() => {
|
||||
localStorage.removeItem('userCache')
|
||||
redirectTo(router, 'logout')
|
||||
}}
|
||||
onClick={() => {
|
||||
localStorage.removeItem('userCache')
|
||||
redirectTo(router, 'logout')
|
||||
}}
|
||||
className='block cursor-pointer rounded-b px-4 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-discord-dark-hover'
|
||||
>
|
||||
<i className='fas fa-sign-out-alt' /> 로그아웃
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</> :
|
||||
<a tabIndex={0} onClick={()=> {
|
||||
localStorage.redirectTo = window.location.href
|
||||
setNavbarOpen(false)
|
||||
redirectTo(router, 'login')
|
||||
}} className='lg:hover:text-gray-300 flex items-center px-3 py-4 w-full hover:text-gray-500 text-gray-700 text-sm font-semibold sm:w-auto lg:py-2 lg:text-gray-100 cursor-pointer outline-none'>
|
||||
로그인
|
||||
</>
|
||||
) : (
|
||||
<a
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
localStorage.redirectTo = window.location.href
|
||||
setNavbarOpen(false)
|
||||
redirectTo(router, 'login')
|
||||
}}
|
||||
className='flex w-full cursor-pointer items-center px-3 py-4 text-sm font-semibold text-gray-700 outline-none hover:text-gray-500 sm:w-auto lg:py-2 lg:text-gray-100 lg:hover:text-gray-300'
|
||||
>
|
||||
로그인
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div
|
||||
className={`z-30 w-full h-full fixed bg-discord-blurple dark:bg-discord-black mt-8 sm:mt-0 lg:hidden overflow-y-scroll lg:scroll-none ${
|
||||
navbarOpen ? 'block' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<nav className='mt-20'>
|
||||
<Link
|
||||
href={dev ? '/' : '/developers'}
|
||||
onClick={()=> setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
||||
|
||||
{
|
||||
dev ? <i className='fas fa-home' /> : <i className='fas fa-tools' />
|
||||
}
|
||||
<span className='px-2 font-medium'>
|
||||
{dev ? '홈' : '개발자'}
|
||||
</span>
|
||||
|
||||
</Link>
|
||||
{
|
||||
type !== 'bot' && <Link
|
||||
href='/bots'
|
||||
onClick={()=> setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
||||
|
||||
<i className='fas fa-robot' />
|
||||
<span className='px-2 font-medium'>봇 리스트</span>
|
||||
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
type !== 'server' && <Link
|
||||
href='/servers'
|
||||
onClick={()=> setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
||||
|
||||
<i className='fas fa-users' />
|
||||
<span className='px-2 font-medium'>서버 리스트</span>
|
||||
|
||||
</Link>
|
||||
}
|
||||
<Link
|
||||
href='/discord'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
onClick={()=> setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
||||
|
||||
<i className='fab fa-discord' />
|
||||
<span className='px-2 font-medium'>디스코드 서버</span>
|
||||
|
||||
</Link>
|
||||
<Link
|
||||
href='/about'
|
||||
onClick={()=> setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
||||
|
||||
<i className='fas fa-layer-group' />
|
||||
<span className='px-2 font-medium'>소개</span>
|
||||
|
||||
</Link>
|
||||
<a
|
||||
onClick={()=> {
|
||||
setMobileAddDropdownOpen(!mobileAddDropdownOpen)
|
||||
}}
|
||||
className='flex items-center px-8 py-2 text-gray-100'
|
||||
>
|
||||
<i className='fas fa-plus' />
|
||||
<span className='px-2 font-medium'>추가하기</span>
|
||||
</a>
|
||||
<div className={mobileAddDropdownOpen ? 'px-4 flex flex-col' : 'px-4 hidden'}>
|
||||
<Link
|
||||
href='/addbot'
|
||||
onClick={()=> setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
||||
|
||||
<i className='fas fa-robot' />
|
||||
<span className='px-2 font-medium'>봇 추가하기</span>
|
||||
|
||||
</Link>
|
||||
<Link
|
||||
href='/addserver'
|
||||
onClick={()=> setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
||||
|
||||
<i className='fas fa-users' />
|
||||
<span className='px-2 font-medium'>서버 추가하기</span>
|
||||
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className='my-10'>
|
||||
{
|
||||
logged ? <>
|
||||
<div
|
||||
className={`lg:scroll-none fixed z-30 mt-8 h-full w-full overflow-y-scroll bg-discord-blurple dark:bg-discord-black sm:mt-0 lg:hidden ${
|
||||
navbarOpen ? 'block' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<nav className='mt-20'>
|
||||
<Link
|
||||
href={dev ? '/' : '/developers'}
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||
>
|
||||
{dev ? <i className='fas fa-home' /> : <i className='fas fa-tools' />}
|
||||
<span className='px-2 font-medium'>{dev ? '홈' : '개발자'}</span>
|
||||
</Link>
|
||||
{type !== 'bot' && (
|
||||
<Link
|
||||
href={`/users/${userCache.id}`}
|
||||
href='/bots'
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||
onClick={() => setNavbarOpen(!navbarOpen)}>
|
||||
|
||||
<i className='far fa-user' />
|
||||
<span className='px-2 font-medium'>{userCache.username}</span>
|
||||
|
||||
>
|
||||
<i className='fas fa-robot' />
|
||||
<span className='px-2 font-medium'>봇 리스트</span>
|
||||
</Link>
|
||||
)}
|
||||
{type !== 'server' && (
|
||||
<Link
|
||||
href='/panel'
|
||||
href='/servers'
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||
onClick={() => setNavbarOpen(!navbarOpen)}>
|
||||
|
||||
<i className='fas fa-cogs' />
|
||||
<span className='px-2 font-medium'>관리패널</span>
|
||||
|
||||
>
|
||||
<i className='fas fa-users' />
|
||||
<span className='px-2 font-medium'>서버 리스트</span>
|
||||
</Link>
|
||||
<a onClick={()=> {
|
||||
setNavbarOpen(!navbarOpen)
|
||||
localStorage.removeItem('userCache')
|
||||
redirectTo(router, 'logout')
|
||||
}} className='flex items-center px-8 py-2 text-red-500 hover:text-red-400'>
|
||||
<i className='fas fa-sign-out-alt' />
|
||||
<span className='px-2 font-medium'>로그아웃</span>
|
||||
</a>
|
||||
</> : <a onClick={() => {
|
||||
localStorage.redirectTo = window.location.href
|
||||
setNavbarOpen(false)
|
||||
redirectTo(router, 'login')
|
||||
}} className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'>
|
||||
<i className='far fa-user' />
|
||||
<span className='px-2 font-medium'>로그인</span>
|
||||
)}
|
||||
<Link
|
||||
href='/discord'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||
>
|
||||
<i className='fab fa-discord' />
|
||||
<span className='px-2 font-medium'>디스코드 서버</span>
|
||||
</Link>
|
||||
<Link
|
||||
href='/about'
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||
>
|
||||
<i className='fas fa-layer-group' />
|
||||
<span className='px-2 font-medium'>소개</span>
|
||||
</Link>
|
||||
<a
|
||||
onClick={() => {
|
||||
setMobileAddDropdownOpen(!mobileAddDropdownOpen)
|
||||
}}
|
||||
className='flex items-center px-8 py-2 text-gray-100'
|
||||
>
|
||||
<i className='fas fa-plus' />
|
||||
<span className='px-2 font-medium'>추가하기</span>
|
||||
</a>
|
||||
}
|
||||
<div className={mobileAddDropdownOpen ? 'flex flex-col px-4' : 'hidden px-4'}>
|
||||
<Link
|
||||
href='/addbot'
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||
>
|
||||
<i className='fas fa-robot' />
|
||||
<span className='px-2 font-medium'>봇 추가하기</span>
|
||||
</Link>
|
||||
<Link
|
||||
href='/addserver'
|
||||
onClick={() => setNavbarOpen(false)}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||
>
|
||||
<i className='fas fa-users' />
|
||||
<span className='px-2 font-medium'>서버 추가하기</span>
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className='my-10'>
|
||||
{logged ? (
|
||||
<>
|
||||
<Link
|
||||
href={`/users/${userCache.id}`}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||
onClick={() => setNavbarOpen(!navbarOpen)}
|
||||
>
|
||||
<i className='far fa-user' />
|
||||
<span className='px-2 font-medium'>{userCache.username}</span>
|
||||
</Link>
|
||||
<Link
|
||||
href='/panel'
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||
onClick={() => setNavbarOpen(!navbarOpen)}
|
||||
>
|
||||
<i className='fas fa-cogs' />
|
||||
<span className='px-2 font-medium'>관리패널</span>
|
||||
</Link>
|
||||
<a
|
||||
onClick={() => {
|
||||
setNavbarOpen(!navbarOpen)
|
||||
localStorage.removeItem('userCache')
|
||||
redirectTo(router, 'logout')
|
||||
}}
|
||||
className='flex items-center px-8 py-2 text-red-500 hover:text-red-400'
|
||||
>
|
||||
<i className='fas fa-sign-out-alt' />
|
||||
<span className='px-2 font-medium'>로그아웃</span>
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<a
|
||||
onClick={() => {
|
||||
localStorage.redirectTo = window.location.href
|
||||
setNavbarOpen(false)
|
||||
redirectTo(router, 'login')
|
||||
}}
|
||||
className='flex items-center px-8 py-2 text-gray-100 hover:text-gray-300'
|
||||
>
|
||||
<i className='far fa-user' />
|
||||
<span className='px-2 font-medium'>로그인</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface NavbarProps {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const Notice: React.FC<NoticeProps> = ({ header, desc }) => {
|
||||
return (
|
||||
<div className='mx-auto my-auto px-10 py-48 h-screen text-center'>
|
||||
<div className='mx-auto my-auto h-screen px-10 py-48 text-center'>
|
||||
<h1 className='text-4xl font-bold'>KOREANBOTS</h1>
|
||||
<br />
|
||||
<div>
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
import Link from 'next/link'
|
||||
import DiscordAvatar from '@components/DiscordAvatar'
|
||||
|
||||
const Owner: React.FC<OwnerProps> = ({ id, globalName, username, tag, crown=false }) => {
|
||||
const Owner: React.FC<OwnerProps> = ({ id, globalName, username, tag, crown = false }) => {
|
||||
return (
|
||||
(<Link
|
||||
<Link
|
||||
href={`/users/${id}`}
|
||||
className='dark:hover:bg-discord-dark-hover flex mb-1 px-4 py-4 text-black dark:text-gray-400 text-base dark:bg-discord-black bg-little-white hover:bg-little-white-hover rounded cursor-pointer'>
|
||||
|
||||
<div className='relative shrink-0 mr-3 mt-1 w-8 h-8 rounded-full shadow-inner overflow-hidden'>
|
||||
<DiscordAvatar userID={id} className='z-negative absolute inset-0 w-full h-full' />
|
||||
className='mb-1 flex cursor-pointer rounded bg-little-white px-4 py-4 text-base text-black hover:bg-little-white-hover dark:bg-discord-black dark:text-gray-400 dark:hover:bg-discord-dark-hover'
|
||||
>
|
||||
<div className='relative mr-3 mt-1 h-8 w-8 shrink-0 overflow-hidden rounded-full shadow-inner'>
|
||||
<DiscordAvatar userID={id} className='z-negative absolute inset-0 h-full w-full' />
|
||||
</div>
|
||||
<div className='flex-1 w-0 leading-snug'>
|
||||
<h4 className='whitespace-nowrap truncate'>{ crown && <i className='fas fa-crown text-amber-300 text-xs' /> }{tag === '0' ? globalName : username}</h4>
|
||||
<span className='text-gray-600 text-sm'>{tag === '0' ? '@' + username : '#' + tag}</span>
|
||||
<div className='w-0 flex-1 leading-snug'>
|
||||
<h4 className='truncate whitespace-nowrap'>
|
||||
{crown && <i className='fas fa-crown text-xs text-amber-300' />}
|
||||
{tag === '0' ? globalName : username}
|
||||
</h4>
|
||||
<span className='text-sm text-gray-600'>{tag === '0' ? '@' + username : '#' + tag}</span>
|
||||
</div>
|
||||
|
||||
</Link>)
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import Link from 'next/link'
|
||||
const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname, searchParams }) => {
|
||||
const Paginator: React.FC<PaginatorProps> = ({
|
||||
currentPage,
|
||||
totalPage,
|
||||
pathname,
|
||||
searchParams,
|
||||
}) => {
|
||||
let pages = []
|
||||
if (currentPage < 4)
|
||||
pages = [
|
||||
@ -25,7 +30,7 @@ const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname,
|
||||
currentPage + 1 > totalPage ? null : currentPage + 1,
|
||||
currentPage + 2 > totalPage ? null : currentPage + 2,
|
||||
]
|
||||
pages = pages.filter(el => el)
|
||||
pages = pages.filter((el) => el)
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center py-4 text-center'>
|
||||
<div className='flex'>
|
||||
@ -33,41 +38,38 @@ const Paginator: React.FC<PaginatorProps> = ({ currentPage, totalPage, pathname,
|
||||
href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage - 1 } }}
|
||||
className={`${
|
||||
currentPage === 1 ? 'invisible' : ''
|
||||
} h-12 w-12 mr-1 flex justify-center items-center rounded-full transition duration-150 ease-in bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover cursor-pointer text-center`}>
|
||||
|
||||
} mr-1 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-gray-200 text-center transition duration-150 ease-in hover:bg-gray-300 dark:bg-discord-black dark:hover:bg-discord-dark-hover`}
|
||||
>
|
||||
<i className='fas fa-chevron-left'></i>
|
||||
|
||||
</Link>
|
||||
{pages.map((el, i) => (
|
||||
(<Link
|
||||
<Link
|
||||
key={i}
|
||||
href={{ pathname, hash: 'list', query: { ...searchParams, page: el } }}
|
||||
className={`w-12 flex justify-center items-center cursor-pointer leading-5 transition duration-150 ease-in ${
|
||||
className={`flex w-12 cursor-pointer items-center justify-center leading-5 transition duration-150 ease-in ${
|
||||
i === 0 && i === pages.length - 1
|
||||
? 'rounded-full'
|
||||
: i === 0
|
||||
? 'rounded-l-full'
|
||||
: i === pages.length - 1
|
||||
? 'rounded-r-full'
|
||||
: ''
|
||||
? 'rounded-l-full'
|
||||
: i === pages.length - 1
|
||||
? 'rounded-r-full'
|
||||
: ''
|
||||
} ${
|
||||
currentPage === el
|
||||
? 'bg-gray-300 dark:bg-discord-dark-hover'
|
||||
: 'bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover'
|
||||
}`}>
|
||||
|
||||
: 'bg-gray-200 hover:bg-gray-300 dark:bg-discord-black dark:hover:bg-discord-dark-hover'
|
||||
}`}
|
||||
>
|
||||
{el}
|
||||
|
||||
</Link>)
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
href={{ pathname, hash: 'list', query: { ...searchParams, page: currentPage + 1 } }}
|
||||
className={`${
|
||||
currentPage === totalPage ? 'invisible' : ''
|
||||
} h-12 w-12 ml-1 flex justify-center items-center rounded-full transition duration-150 ease-in bg-gray-200 dark:bg-discord-black hover:bg-gray-300 dark:hover:bg-discord-dark-hover cursor-pointer text-center`}>
|
||||
|
||||
} ml-1 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-gray-200 text-center transition duration-150 ease-in hover:bg-gray-300 dark:bg-discord-black dark:hover:bg-discord-dark-hover`}
|
||||
>
|
||||
<i className='fas fa-chevron-right'></i>
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@ -78,7 +80,7 @@ interface PaginatorProps {
|
||||
pathname: string
|
||||
currentPage: number
|
||||
totalPage: number
|
||||
searchParams?: Record<string, string|string[]>
|
||||
searchParams?: Record<string, string | string[]>
|
||||
}
|
||||
|
||||
export default Paginator
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
const PlatformDisplay: React.FC<PlatformDisplayProps> = ({ osx, children }:PlatformDisplayProps) => {
|
||||
const PlatformDisplay: React.FC<PlatformDisplayProps> = ({
|
||||
osx,
|
||||
children,
|
||||
}: PlatformDisplayProps) => {
|
||||
const isOSX = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)
|
||||
return <>{isOSX ? osx ?? children : children}</>
|
||||
}
|
||||
|
||||
interface PlatformDisplayProps {
|
||||
osx?: ReactNode
|
||||
children: ReactNode
|
||||
osx?: ReactNode
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default PlatformDisplay
|
||||
export default PlatformDisplay
|
||||
|
||||
@ -6,26 +6,28 @@ import { redirectTo } from '@utils/Tools'
|
||||
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
|
||||
const Redirect: React.FC<RedirectProps> = ({ to, text=true, children }) => {
|
||||
const Redirect: React.FC<RedirectProps> = ({ to, text = true, children }) => {
|
||||
const router = useRouter()
|
||||
if(!to) throw new Error('No Link')
|
||||
if (!to) throw new Error('No Link')
|
||||
useEffect(() => {
|
||||
redirectTo(router, to)
|
||||
})
|
||||
if(children) return <>
|
||||
{children}
|
||||
</>
|
||||
return <Container paddingTop>
|
||||
<div>
|
||||
<a href={to} className='text-blue-400'>{text && '자동으로 리다이렉트되지 않는다면 클릭하세요.'}</a>
|
||||
</div>
|
||||
</Container>
|
||||
if (children) return <>{children}</>
|
||||
return (
|
||||
<Container paddingTop>
|
||||
<div>
|
||||
<a href={to} className='text-blue-400'>
|
||||
{text && '자동으로 리다이렉트되지 않는다면 클릭하세요.'}
|
||||
</a>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
interface RedirectProps {
|
||||
to: string
|
||||
to: string
|
||||
text?: boolean
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export default Redirect
|
||||
export default Redirect
|
||||
|
||||
@ -5,74 +5,123 @@ import { FormikErrors, FormikTouched } from 'formik'
|
||||
const Button = dynamic(() => import('@components/Button'))
|
||||
const TextArea = dynamic(() => import('@components/Form/TextArea'))
|
||||
|
||||
export const Check: FC<{ checked: boolean, text: string }> = ({ checked, text }) => <>
|
||||
{checked && <i className='text-emerald-400 fas fa-check-circle mr-1' />}
|
||||
{text}
|
||||
</>
|
||||
export const Check: FC<{ checked: boolean; text: string }> = ({ checked, text }) => (
|
||||
<>
|
||||
{checked && <i className='fas fa-check-circle mr-1 text-emerald-400' />}
|
||||
{text}
|
||||
</>
|
||||
)
|
||||
|
||||
export const SubmitButton: FC = () => <div className='text-right'>
|
||||
<Button type='submit'>제출</Button>
|
||||
</div>
|
||||
export const SubmitButton: FC = () => (
|
||||
<div className='text-right'>
|
||||
<Button type='submit'>제출</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const TextField: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => <>
|
||||
<TextArea name='description' placeholder='최대한 자세하게 설명해주세요!' value={values.description} setValue={(value) => setFieldValue('description', value)} />
|
||||
<div className='mt-1 text-red-500 text-xs font-light'>{errors.description && touched.description ? errors.description : null}</div>
|
||||
<SubmitButton />
|
||||
</>
|
||||
export const TextField: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => (
|
||||
<>
|
||||
<TextArea
|
||||
name='description'
|
||||
placeholder='최대한 자세하게 설명해주세요!'
|
||||
value={values.description}
|
||||
setValue={(value) => setFieldValue('description', value)}
|
||||
/>
|
||||
<div className='mt-1 text-xs font-light text-red-500'>
|
||||
{errors.description && touched.description ? errors.description : null}
|
||||
</div>
|
||||
<SubmitButton />
|
||||
</>
|
||||
)
|
||||
|
||||
export const DMCA: FC<ReportTemplateProps> = ({ values, errors, touched, setFieldValue }) => {
|
||||
const [ isOwner, setOwner ] = useState(null)
|
||||
const [ contacted, setContacted ] = useState(null)
|
||||
return <div>
|
||||
<h3 className='font-bold my-2'>권리자와는 어떤 관계인가요?</h3>
|
||||
<Button onClick={() => setOwner(true)}>
|
||||
<Check checked={isOwner} text='권리자 본인 혹은 대리인입니다.' />
|
||||
</Button>
|
||||
<Button onClick={() => setOwner(false)}>
|
||||
<Check checked={isOwner === false} text='권리자가 아닙니다.' />
|
||||
</Button>
|
||||
{
|
||||
isOwner === true ? <>
|
||||
<h3 className='font-bold my-2'>권리 침해자에게 연락하여 라이선스 위반사항을 고지하셨나요?</h3>
|
||||
<Button onClick={() => setContacted(true)}>
|
||||
<Check checked={contacted} text='최대한 연락을 시도하였지만 개선되지 않았습니다.' />
|
||||
</Button>
|
||||
<Button onClick={() => setContacted(false)}>
|
||||
<Check checked={contacted === false} text='아니요, 아직 연락하지 않았습니다.' />
|
||||
</Button>
|
||||
{
|
||||
contacted ? <div>
|
||||
<h3 className='font-bold mt-2'>설명</h3>
|
||||
<p className='text-gray-400 text-sm mb-1'>반드시 아래 항목들을 포함해야합니다.</p>
|
||||
<ul className='text-gray-400 text-sm mb-1 list-disc list-inside'>
|
||||
<li>권리자 본인임을 증명 (단체 소속인 경우 어떤 자격으로 단체를 대표하여 신고하는지 설명)</li>
|
||||
<li>본인의 권리를 입증 (원본 컨텐츠의 주소, 라이선스 등을 포함)</li>
|
||||
</ul>
|
||||
<p className='text-gray-400 text-sm mb-1'>컨텐츠를 추가로 첨부해야하는 경우 <a className='text-blue-400' target='_blank' rel='noreferrer' href={`mailto:dmca@koreanbots.dev?subject=${encodeURI('[DMCA] 추가 컨텐츠')}&body=${encodeURI('디스코드 태그:')}`}>dmca@koreanbots.dev</a>의 이메일로 첨부해주시고, 해당 이메일로 첨부했음을 아래 설명에 기재해주세요.</p>
|
||||
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />
|
||||
</div>
|
||||
: contacted === false ? <>
|
||||
<h2 className='font-bold mt-4 text-xl'>먼저 권리 침해자에게 연락을 시도해주세요.</h2>
|
||||
<p>본인의 권리를 침해하신 분께 먼저 연락을 시도하셔서 위반사항을 고지하시고, 연락이 불가하다면 신고 기능을 이용해주세요.</p>
|
||||
</> : ''
|
||||
}
|
||||
</>
|
||||
: isOwner === false ? <>
|
||||
<h2 className='font-bold mt-4 text-xl'>아쉽지만, 권리자 본인 혹은 대리인만 신고하실 수 있습니다.</h2>
|
||||
const [isOwner, setOwner] = useState(null)
|
||||
const [contacted, setContacted] = useState(null)
|
||||
return (
|
||||
<div>
|
||||
<h3 className='my-2 font-bold'>권리자와는 어떤 관계인가요?</h3>
|
||||
<Button onClick={() => setOwner(true)}>
|
||||
<Check checked={isOwner} text='권리자 본인 혹은 대리인입니다.' />
|
||||
</Button>
|
||||
<Button onClick={() => setOwner(false)}>
|
||||
<Check checked={isOwner === false} text='권리자가 아닙니다.' />
|
||||
</Button>
|
||||
{isOwner === true ? (
|
||||
<>
|
||||
<h3 className='my-2 font-bold'>
|
||||
권리 침해자에게 연락하여 라이선스 위반사항을 고지하셨나요?
|
||||
</h3>
|
||||
<Button onClick={() => setContacted(true)}>
|
||||
<Check checked={contacted} text='최대한 연락을 시도하였지만 개선되지 않았습니다.' />
|
||||
</Button>
|
||||
<Button onClick={() => setContacted(false)}>
|
||||
<Check checked={contacted === false} text='아니요, 아직 연락하지 않았습니다.' />
|
||||
</Button>
|
||||
{contacted ? (
|
||||
<div>
|
||||
<h3 className='mt-2 font-bold'>설명</h3>
|
||||
<p className='mb-1 text-sm text-gray-400'>반드시 아래 항목들을 포함해야합니다.</p>
|
||||
<ul className='mb-1 list-inside list-disc text-sm text-gray-400'>
|
||||
<li>
|
||||
권리자 본인임을 증명 (단체 소속인 경우 어떤 자격으로 단체를 대표하여 신고하는지
|
||||
설명)
|
||||
</li>
|
||||
<li>본인의 권리를 입증 (원본 컨텐츠의 주소, 라이선스 등을 포함)</li>
|
||||
</ul>
|
||||
<p className='mb-1 text-sm text-gray-400'>
|
||||
컨텐츠를 추가로 첨부해야하는 경우{' '}
|
||||
<a
|
||||
className='text-blue-400'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
href={`mailto:dmca@koreanbots.dev?subject=${encodeURI(
|
||||
'[DMCA] 추가 컨텐츠'
|
||||
)}&body=${encodeURI('디스코드 태그:')}`}
|
||||
>
|
||||
dmca@koreanbots.dev
|
||||
</a>
|
||||
의 이메일로 첨부해주시고, 해당 이메일로 첨부했음을 아래 설명에 기재해주세요.
|
||||
</p>
|
||||
<TextField
|
||||
values={values}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
setFieldValue={setFieldValue}
|
||||
/>
|
||||
</div>
|
||||
) : contacted === false ? (
|
||||
<>
|
||||
<h2 className='mt-4 text-xl font-bold'>먼저 권리 침해자에게 연락을 시도해주세요.</h2>
|
||||
<p>
|
||||
본인의 권리를 침해하신 분께 먼저 연락을 시도하셔서 위반사항을 고지하시고, 연락이
|
||||
불가하다면 신고 기능을 이용해주세요.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
) : isOwner === false ? (
|
||||
<>
|
||||
<h2 className='mt-4 text-xl font-bold'>
|
||||
아쉽지만, 권리자 본인 혹은 대리인만 신고하실 수 있습니다.
|
||||
</h2>
|
||||
<p>권리자 분께 말씀드려, 권리자 본인이 직접 신고하시도록 해주세요!</p>
|
||||
</> : ''
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ReportValues {
|
||||
category: string | null
|
||||
description: string
|
||||
_csrf: string
|
||||
category: string | null
|
||||
description: string
|
||||
_csrf: string
|
||||
}
|
||||
interface ReportTemplateProps {
|
||||
values?: ReportValues
|
||||
errors?: FormikErrors<ReportValues>
|
||||
touched?: FormikTouched<ReportValues>
|
||||
setFieldValue?(field: string, value: unknown): void
|
||||
}
|
||||
values?: ReportValues
|
||||
errors?: FormikErrors<ReportValues>
|
||||
touched?: FormikTouched<ReportValues>
|
||||
setFieldValue?(field: string, value: unknown): void
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
const ResponsiveGrid: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||
return <div className='grid gap-x-4 grid-rows-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 mt-10 -mb-10'>
|
||||
{children}
|
||||
</div>
|
||||
return (
|
||||
<div className='-mb-10 mt-10 grid grid-rows-1 gap-x-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResponsiveGrid
|
||||
export default ResponsiveGrid
|
||||
|
||||
@ -46,7 +46,7 @@ const Search: React.FC = () => {
|
||||
const res = await Fetch<ListAll>(`/search/all?q=${encodeURIComponent(value)}`, {
|
||||
signal: controller.signal,
|
||||
}).catch((e) => {
|
||||
if(e.name !== 'AbortError') throw e
|
||||
if (e.name !== 'AbortError') throw e
|
||||
else return
|
||||
})
|
||||
setData(res || {})
|
||||
@ -54,23 +54,29 @@ const Search: React.FC = () => {
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
if(query.length < 2) return
|
||||
if(!localStorage.recentSearch) localStorage.recentSearch = '[]'
|
||||
if (query.length < 2) return
|
||||
if (!localStorage.recentSearch) localStorage.recentSearch = '[]'
|
||||
try {
|
||||
const d = JSON.parse(localStorage.recentSearch).reverse()
|
||||
if(d.findIndex(n => n.value === query) !== -1) d.splice(d.findIndex(n => n.value === query), 1)
|
||||
if (d.findIndex((n) => n.value === query) !== -1)
|
||||
d.splice(
|
||||
d.findIndex((n) => n.value === query),
|
||||
1
|
||||
)
|
||||
d.push({
|
||||
value: query,
|
||||
date: Date.now()
|
||||
date: Date.now(),
|
||||
})
|
||||
d.reverse()
|
||||
setRecentSearch(d.slice(0, 10))
|
||||
localStorage.recentSearch = JSON.stringify(d.slice(0, 10))
|
||||
} catch {
|
||||
setRecentSearch([{
|
||||
value: query,
|
||||
date: Date.now()
|
||||
}])
|
||||
setRecentSearch([
|
||||
{
|
||||
value: query,
|
||||
date: Date.now(),
|
||||
},
|
||||
])
|
||||
localStorage.recentSearch = JSON.stringify(recentSearch)
|
||||
} finally {
|
||||
redirectTo(router, `/search/?q=${encodeURIComponent(query)}`)
|
||||
@ -79,19 +85,17 @@ const Search: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div onFocus={() => setHidden(false)} ref={ref}>
|
||||
<div
|
||||
className='relative z-10 flex mt-5 w-full text-black dark:text-gray-100 dark:bg-very-black bg-white rounded-lg'
|
||||
>
|
||||
<div className='relative z-10 mt-5 flex w-full rounded-lg bg-white text-black dark:bg-very-black dark:text-gray-100'>
|
||||
<input
|
||||
type='search'
|
||||
maxLength={50}
|
||||
className='grow pr-20 px-7 py-3 h-16 text-xl bg-transparent border-0 border-none outline-none shadow'
|
||||
className='h-16 grow border-0 border-none bg-transparent px-7 py-3 pr-20 text-xl shadow outline-none'
|
||||
placeholder='검색...'
|
||||
value={query}
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
SearchResults(e.target.value)
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
onSubmit()
|
||||
}
|
||||
@ -101,103 +105,116 @@ const Search: React.FC = () => {
|
||||
className='cusor-pointer absolute right-0 top-0 mr-5 mt-5 outline-none'
|
||||
onClick={onSubmit}
|
||||
>
|
||||
<i className='fas fa-search text-gray-600 hover:text-gray-700 text-2xl' />
|
||||
<i className='fas fa-search text-2xl text-gray-600 hover:text-gray-700' />
|
||||
</button>
|
||||
</div>
|
||||
<div className={`relative ${hidden ? 'hidden' : 'block'} z-50`}>
|
||||
<div className='pin-t pin-l absolute my-2 w-full h-60 text-black dark:text-gray-100 dark:bg-very-black bg-white rounded shadow-md overflow-y-scroll md:h-80'>
|
||||
<div className='pin-t pin-l absolute my-2 h-60 w-full overflow-y-scroll rounded bg-white text-black shadow-md dark:bg-very-black dark:text-gray-100 md:h-80'>
|
||||
<ul>
|
||||
{(data && data.code === 200) ? (
|
||||
{data && data.code === 200 ? (
|
||||
<div className='grid lg:grid-cols-2'>
|
||||
<ul>
|
||||
<li className='px-3 py-3.5 font-bold'>봇</li>
|
||||
{
|
||||
data.data.bots.length === 0 ?
|
||||
<li className='px-3 py-3.5'>검색 결과가 없습니다.</li> :
|
||||
data.data.bots.map(el => (
|
||||
<Link key={el.id} href={makeBotURL(el)} legacyBehavior>
|
||||
<li className='h-15 flex px-3 py-2 cursor-pointer'>
|
||||
<DiscordAvatar className='mt-1 w-12 h-12' size={128} userID={el.id} />
|
||||
<div className='ml-2'>
|
||||
<h1 className='text-black dark:text-gray-100 text-lg'>{el.name}</h1>
|
||||
<p className='text-gray-400 text-sm'>{el.intro}</p>
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<ul>
|
||||
<li className='px-3 py-3.5 font-bold'>서버</li>
|
||||
{
|
||||
data.data.servers.length === 0 ?
|
||||
<li className='px-3 py-3.5'>검색 결과가 없습니다.</li> :
|
||||
data.data.servers.map(el => (
|
||||
<Link key={el.id} href={makeServerURL(el)} legacyBehavior>
|
||||
<li className='h-15 flex px-3 py-2 cursor-pointer'>
|
||||
<ServerIcon className='mt-1 w-12 h-12' size={128} id={el.id} />
|
||||
<div className='ml-2'>
|
||||
<h1 className='text-black dark:text-gray-100 text-lg'>{el.name}</h1>
|
||||
<p className='text-gray-400 text-sm'>{el.intro}</p>
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
) : loading ? <ul>
|
||||
<li className='px-3 py-3.5'>검색중입니다...</li>
|
||||
</ul> : <ul>
|
||||
{query && data ? (
|
||||
data.message?.includes('문법') ? (
|
||||
<li className='px-3 py-3.5'>
|
||||
검색 문법이 잘못되었습니다.
|
||||
<br />
|
||||
<a
|
||||
className='hover:text-blue-400 text-blue-500'
|
||||
href='https://docs.koreanbots.dev/bots/usage/search'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
더 알아보기
|
||||
</a>
|
||||
</li>
|
||||
) : <li className='px-3 py-3.5'>{(data.errors && data.errors[0]) || data.message || '검색중입니다...'}</li>
|
||||
) : query.length === 0 ? !recentSearch || !Array.isArray(recentSearch) || recentSearch.length === 0? <li className='px-3 py-3.5'>최근 검색 기록이 없습니다.</li>
|
||||
: <>
|
||||
<li className='h-15 px-3 py-2 cursor-pointer font-semibold'>
|
||||
최근 검색어
|
||||
<button className='absolute right-0 pr-10 text-sm text-red-500 hover:opacity-90' onClick={() => {
|
||||
setRecentSearch([])
|
||||
localStorage.recentSearch = '[]'
|
||||
}}>
|
||||
전체 삭제
|
||||
</button>
|
||||
</li>
|
||||
{
|
||||
recentSearch.slice(0, 10).map((el, n) => (
|
||||
<Link
|
||||
key={n}
|
||||
href={`/search?q=${encodeURIComponent(el?.value)}`}
|
||||
legacyBehavior>
|
||||
<li className='h-15 px-3 py-2 cursor-pointer'>
|
||||
<i className='fas fa-history' /> {el?.value}
|
||||
<span className='absolute right-0 pr-10 text-gray-400 text-sm'>
|
||||
{Day(el?.date).format('MM.DD.')}
|
||||
</span>
|
||||
{data.data.bots.length === 0 ? (
|
||||
<li className='px-3 py-3.5'>검색 결과가 없습니다.</li>
|
||||
) : (
|
||||
data.data.bots.map((el) => (
|
||||
<Link key={el.id} href={makeBotURL(el)} legacyBehavior>
|
||||
<li className='h-15 flex cursor-pointer px-3 py-2'>
|
||||
<DiscordAvatar className='mt-1 h-12 w-12' size={128} userID={el.id} />
|
||||
<div className='ml-2'>
|
||||
<h1 className='text-lg text-black dark:text-gray-100'>{el.name}</h1>
|
||||
<p className='text-sm text-gray-400'>{el.intro}</p>
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
</> :
|
||||
query.length < 3 ? (
|
||||
)}
|
||||
</ul>
|
||||
<ul>
|
||||
<li className='px-3 py-3.5 font-bold'>서버</li>
|
||||
{data.data.servers.length === 0 ? (
|
||||
<li className='px-3 py-3.5'>검색 결과가 없습니다.</li>
|
||||
) : (
|
||||
data.data.servers.map((el) => (
|
||||
<Link key={el.id} href={makeServerURL(el)} legacyBehavior>
|
||||
<li className='h-15 flex cursor-pointer px-3 py-2'>
|
||||
<ServerIcon className='mt-1 h-12 w-12' size={128} id={el.id} />
|
||||
<div className='ml-2'>
|
||||
<h1 className='text-lg text-black dark:text-gray-100'>{el.name}</h1>
|
||||
<p className='text-sm text-gray-400'>{el.intro}</p>
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<ul>
|
||||
<li className='px-3 py-3.5'>검색중입니다...</li>
|
||||
</ul>
|
||||
) : (
|
||||
<ul>
|
||||
{query && data ? (
|
||||
data.message?.includes('문법') ? (
|
||||
<li className='px-3 py-3.5'>
|
||||
검색 문법이 잘못되었습니다.
|
||||
<br />
|
||||
<a
|
||||
className='text-blue-500 hover:text-blue-400'
|
||||
href='https://docs.koreanbots.dev/bots/usage/search'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
더 알아보기
|
||||
</a>
|
||||
</li>
|
||||
) : (
|
||||
<li className='px-3 py-3.5'>
|
||||
{(data.errors && data.errors[0]) || data.message || '검색중입니다...'}
|
||||
</li>
|
||||
)
|
||||
) : query.length === 0 ? (
|
||||
!recentSearch || !Array.isArray(recentSearch) || recentSearch.length === 0 ? (
|
||||
<li className='px-3 py-3.5'>최근 검색 기록이 없습니다.</li>
|
||||
) : (
|
||||
<>
|
||||
<li className='h-15 cursor-pointer px-3 py-2 font-semibold'>
|
||||
최근 검색어
|
||||
<button
|
||||
className='absolute right-0 pr-10 text-sm text-red-500 hover:opacity-90'
|
||||
onClick={() => {
|
||||
setRecentSearch([])
|
||||
localStorage.recentSearch = '[]'
|
||||
}}
|
||||
>
|
||||
전체 삭제
|
||||
</button>
|
||||
</li>
|
||||
{recentSearch.slice(0, 10).map((el, n) => (
|
||||
<Link
|
||||
key={n}
|
||||
href={`/search?q=${encodeURIComponent(el?.value)}`}
|
||||
legacyBehavior
|
||||
>
|
||||
<li className='h-15 cursor-pointer px-3 py-2'>
|
||||
<i className='fas fa-history' /> {el?.value}
|
||||
<span className='absolute right-0 pr-10 text-sm text-gray-400'>
|
||||
{Day(el?.date).format('MM.DD.')}
|
||||
</span>
|
||||
</li>
|
||||
</Link>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
) : query.length < 3 ? (
|
||||
'최소 2글자 이상 입력해주세요.'
|
||||
) : (
|
||||
'검색어를 입력해주세요.'
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
</ul>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -208,6 +225,6 @@ const Search: React.FC = () => {
|
||||
export default Search
|
||||
|
||||
interface ListAll {
|
||||
bots: Bot[],
|
||||
bots: Bot[]
|
||||
servers: Server[]
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { ReactNode } from 'react'
|
||||
const Segment: React.FC<SegmentProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<div
|
||||
className={`py-3 px-7 text-black dark:text-white dark:bg-discord-black bg-little-white rounded ${className}`}
|
||||
className={`rounded bg-little-white px-7 py-3 text-black dark:bg-discord-black dark:text-white ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -10,43 +10,50 @@ const Tag = dynamic(() => import('@components/Tag'))
|
||||
const ServerIcon = dynamic(() => import('@components/ServerIcon'))
|
||||
|
||||
const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
|
||||
const newServerLink = server.data ? `/addserver/${server.id}` : `${DiscordEnpoints.InviteApplication(DSKR_BOT_ID, {}, 'bot', null, server.id)}&disable_guild_select=true`
|
||||
const newServerLink = server.data
|
||||
? `/addserver/${server.id}`
|
||||
: `${DiscordEnpoints.InviteApplication(
|
||||
DSKR_BOT_ID,
|
||||
{},
|
||||
'bot',
|
||||
null,
|
||||
server.id
|
||||
)}&disable_guild_select=true`
|
||||
return (
|
||||
<div className='min-w-0 container mb-16 transform hover:-translate-y-1 transition duration-100 ease-in cursor-pointer'>
|
||||
<div className='container mb-16 min-w-0 transform cursor-pointer transition duration-100 ease-in hover:-translate-y-1'>
|
||||
<div className='relative'>
|
||||
<div className='container mx-auto'>
|
||||
<div className='h-full'>
|
||||
<div
|
||||
className='relative mx-auto h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl'
|
||||
className='relative mx-auto h-full rounded-2xl bg-little-white text-black shadow-xl dark:bg-discord-black dark:text-white'
|
||||
style={
|
||||
checkServerFlag(server.flags, 'trusted') && server.banner
|
||||
? {
|
||||
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${server.banner}") center top / cover no-repeat`,
|
||||
color: 'white',
|
||||
}
|
||||
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${server.banner}") center top / cover no-repeat`,
|
||||
color: 'white',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Link
|
||||
href={type !== 'add' ? makeServerURL(server) : newServerLink}
|
||||
legacyBehavior>
|
||||
<Link href={type !== 'add' ? makeServerURL(server) : newServerLink} legacyBehavior>
|
||||
<div>
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex'>
|
||||
<div className='w-3/5 flex justify-start'>
|
||||
<div className='flex w-3/5 justify-start'>
|
||||
<ServerIcon
|
||||
size={128}
|
||||
id={server.id}
|
||||
hash={type === 'add' && server.icon}
|
||||
alt='Icon'
|
||||
className='absolute -left-2 -top-8 mx-auto w-32 h-32 bg-white rounded-full'
|
||||
className='absolute -left-2 -top-8 mx-auto h-32 w-32 rounded-full bg-white'
|
||||
/>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 pr-5 pt-5 w-2/5'>
|
||||
<div className='grid w-2/5 grid-cols-1 pr-5 pt-5'>
|
||||
<Tag
|
||||
text={
|
||||
<>
|
||||
<i className='fas fa-heart text-red-600' /> {formatNumber(server.votes)}
|
||||
<i className='fas fa-heart text-red-600' />{' '}
|
||||
{formatNumber(server.votes)}
|
||||
</>
|
||||
}
|
||||
dark
|
||||
@ -58,26 +65,38 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3 px-4 h-16'>
|
||||
<h2 className={`px-1 text-sm ${server.state !== 'unreachable' ? ' invisible' : ''}`}>
|
||||
<i className='fas fa-ban text-red-600' />정보 갱신 불가
|
||||
<div className='mt-3 h-16 px-4'>
|
||||
<h2
|
||||
className={`px-1 text-sm ${
|
||||
server.state !== 'unreachable' ? ' invisible' : ''
|
||||
}`}
|
||||
>
|
||||
<i className='fas fa-ban text-red-600' />
|
||||
정보 갱신 불가
|
||||
</h2>
|
||||
<h1 className='mb-3 text-left text-xl sm:text-2xl font-bold truncate'>{server.name}</h1>
|
||||
<h1 className='mb-3 truncate text-left text-xl font-bold sm:text-2xl'>
|
||||
{server.name}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className='mb-10 px-4 h-6 text-left text-gray-400 text-sm font'>
|
||||
{type === 'add' ?
|
||||
server.data ? '지금 바로 서버를 등록할 수 있습니다.' : '봇을 초대해야 서버를 등록할 수 있습니다.'
|
||||
: server.intro
|
||||
}
|
||||
|
||||
<p className='font mb-10 h-6 px-4 text-left text-sm text-gray-400'>
|
||||
{type === 'add'
|
||||
? server.data
|
||||
? '지금 바로 서버를 등록할 수 있습니다.'
|
||||
: '봇을 초대해야 서버를 등록할 수 있습니다.'
|
||||
: server.intro}
|
||||
</p>
|
||||
<div>
|
||||
<div className='category flex flex-wrap px-2'>
|
||||
{server.category?.slice(0, 3).map(el => (
|
||||
<Tag key={el} text={el} href={`/servers/categories/${el}`} dark />
|
||||
))}{' '}
|
||||
{server.category?.length > 3 && <Tag text={`+${server.category.length - 3}`} dark />}
|
||||
{server.category
|
||||
?.slice(0, 3)
|
||||
.map((el) => (
|
||||
<Tag key={el} text={el} href={`/servers/categories/${el}`} dark />
|
||||
))}{' '}
|
||||
{server.category?.length > 3 && (
|
||||
<Tag text={`+${server.category.length - 3}`} dark />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -85,59 +104,55 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
|
||||
<Divider />
|
||||
<div className='w-full'>
|
||||
<div className='flex justify-evenly'>
|
||||
{
|
||||
type === 'add' ?
|
||||
server.data ? <Link
|
||||
{type === 'add' ? (
|
||||
server.data ? (
|
||||
<Link
|
||||
href={newServerLink}
|
||||
className='py-3 w-full text-center text-emerald-500 hover:text-white text-sm font-bold hover:bg-emerald-500 rounded-b-2xl hover:shadow-lg transition duration-100 ease-in'>
|
||||
|
||||
등록하기
|
||||
|
||||
</Link> : <Link
|
||||
href={newServerLink}
|
||||
className='py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple rounded-b-2xl hover:shadow-lg transition duration-100 ease-in'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'>
|
||||
|
||||
봇 초대하기
|
||||
|
||||
className='w-full rounded-b-2xl py-3 text-center text-sm font-bold text-emerald-500 transition duration-100 ease-in hover:bg-emerald-500 hover:text-white hover:shadow-lg'
|
||||
>
|
||||
등록하기
|
||||
</Link>
|
||||
:
|
||||
<>
|
||||
) : (
|
||||
<Link
|
||||
href={newServerLink}
|
||||
className='w-full rounded-b-2xl py-3 text-center text-sm font-bold text-discord-blurple transition duration-100 ease-in hover:bg-discord-blurple hover:text-white hover:shadow-lg'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
봇 초대하기
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={makeServerURL(server)}
|
||||
className='w-full rounded-bl-2xl py-3 text-center text-sm font-bold text-koreanbots-blue transition duration-100 ease-in hover:bg-koreanbots-blue hover:text-white hover:shadow-lg'
|
||||
>
|
||||
보기
|
||||
</Link>
|
||||
{type === 'manage' ? (
|
||||
<Link
|
||||
href={makeServerURL(server)}
|
||||
className='py-3 w-full text-center text-koreanbots-blue hover:text-white text-sm font-bold hover:bg-koreanbots-blue rounded-bl-2xl hover:shadow-lg transition duration-100 ease-in'>
|
||||
|
||||
보기
|
||||
|
||||
</Link>
|
||||
{type === 'manage' ? (
|
||||
<Link
|
||||
href={`/servers/${server.id}/edit`}
|
||||
className='py-3 w-full text-center text-emerald-500 hover:text-white text-sm font-bold hover:bg-emerald-500 rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'>
|
||||
|
||||
관리하기
|
||||
|
||||
</Link>
|
||||
) : !['ok', 'unreachable'].includes(server.state) ? <a
|
||||
className='py-3 w-full text-center text-discord-blurple text-sm font-bold rounded-br-2xl hover:shadow-lg transition duration-100 ease-in opacity-50 cursor-default select-none'
|
||||
href={`/servers/${server.id}/edit`}
|
||||
className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-emerald-500 transition duration-100 ease-in hover:bg-emerald-500 hover:text-white hover:shadow-lg'
|
||||
>
|
||||
참가하기
|
||||
</a> :
|
||||
<a
|
||||
href={
|
||||
makeServerURL(server) + '/join'
|
||||
}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
className='py-3 w-full text-center text-discord-blurple hover:text-white text-sm font-bold hover:bg-discord-blurple rounded-br-2xl hover:shadow-lg transition duration-100 ease-in'
|
||||
>
|
||||
참가하기
|
||||
</a>
|
||||
}
|
||||
</>
|
||||
|
||||
}
|
||||
관리하기
|
||||
</Link>
|
||||
) : !['ok', 'unreachable'].includes(server.state) ? (
|
||||
<a className='w-full cursor-default select-none rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple opacity-50 transition duration-100 ease-in hover:shadow-lg'>
|
||||
참가하기
|
||||
</a>
|
||||
) : (
|
||||
<a
|
||||
href={makeServerURL(server) + '/join'}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
className='w-full rounded-br-2xl py-3 text-center text-sm font-bold text-discord-blurple transition duration-100 ease-in hover:bg-discord-blurple hover:text-white hover:shadow-lg'
|
||||
>
|
||||
참가하기
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -146,28 +161,26 @@ const ServerCard: React.FC<BotCardProps> = ({ type, server }) => {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
interface BotCardProps {
|
||||
type: 'list' | 'manage' | 'add'
|
||||
server: {
|
||||
id: string,
|
||||
name: string,
|
||||
id: string
|
||||
name: string
|
||||
intro?: string
|
||||
desc?: string,
|
||||
desc?: string
|
||||
flags?: number
|
||||
state?: ServerState
|
||||
icon: string | null,
|
||||
banner?: string | null,
|
||||
bg?: string | null,
|
||||
icon: string | null
|
||||
banner?: string | null
|
||||
bg?: string | null
|
||||
vanity?: string | null
|
||||
category?: string[]
|
||||
votes?: number | null
|
||||
members?: number | null,
|
||||
members?: number | null
|
||||
data?: ServerData
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default ServerCard
|
||||
export default ServerCard
|
||||
|
||||
@ -5,13 +5,22 @@ import { DiscordEnpoints, KoreanbotsEndPoints } from '@utils/Constants'
|
||||
const Image = dynamic(() => import('@components/Image'))
|
||||
|
||||
const ServerIcon: React.FC<ServerIconProps> = ({ id, size, className, alt, hash }) => {
|
||||
return <Image
|
||||
className={className}
|
||||
alt={alt}
|
||||
src={hash ? DiscordEnpoints.CDN.guild(id, hash, { format: 'webp', size: size ?? 256 }) : KoreanbotsEndPoints.CDN.icon(id, { format: 'webp', size: size ?? 256})}
|
||||
fallbackSrc={hash ? DiscordEnpoints.CDN.guild(id, hash, { format: 'png', size: size ?? 256 }) : KoreanbotsEndPoints.CDN.icon(id, { format: 'png', size: size ?? 256})}
|
||||
/>
|
||||
|
||||
return (
|
||||
<Image
|
||||
className={className}
|
||||
alt={alt}
|
||||
src={
|
||||
hash
|
||||
? DiscordEnpoints.CDN.guild(id, hash, { format: 'webp', size: size ?? 256 })
|
||||
: KoreanbotsEndPoints.CDN.icon(id, { format: 'webp', size: size ?? 256 })
|
||||
}
|
||||
fallbackSrc={
|
||||
hash
|
||||
? DiscordEnpoints.CDN.guild(id, hash, { format: 'png', size: size ?? 256 })
|
||||
: KoreanbotsEndPoints.CDN.icon(id, { format: 'png', size: size ?? 256 })
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface ServerIconProps {
|
||||
@ -20,7 +29,7 @@ interface ServerIconProps {
|
||||
hash?: string
|
||||
fromDiscord?: boolean
|
||||
className?: string
|
||||
size? : 128 | 256 | 512
|
||||
size?: 128 | 256 | 512
|
||||
}
|
||||
|
||||
interface ImageEvent extends Event {
|
||||
|
||||
@ -7,23 +7,23 @@ import Link from 'next/link'
|
||||
|
||||
const SubmittedBotCard: React.FC<SubmittedBotProps> = ({ href, submit }) => {
|
||||
return (
|
||||
(<Link
|
||||
<Link
|
||||
href={href}
|
||||
className='relative mx-auto px-4 py-5 w-full h-full text-black dark:text-white dark:bg-discord-black bg-little-white rounded-2xl shadow-xl transform hover:-translate-y-1 transition duration-100 ease-in'>
|
||||
|
||||
className='relative mx-auto h-full w-full transform rounded-2xl bg-little-white px-4 py-5 text-black shadow-xl transition duration-100 ease-in hover:-translate-y-1 dark:bg-discord-black dark:text-white'
|
||||
>
|
||||
<div className='h-18'>
|
||||
<div className='flex'>
|
||||
<div className='grow w-full'>
|
||||
<div className='w-full grow'>
|
||||
<h2 className='text-lg'>{submit.id}</h2>
|
||||
</div>
|
||||
<div className='absolute right-0 grid grid-cols-1 px-4 w-2/5 h-0'>
|
||||
<div className='absolute right-0 grid h-0 w-2/5 grid-cols-1 px-4'>
|
||||
<Tag
|
||||
text={
|
||||
<>
|
||||
<i
|
||||
className={`fas fa-circle text-${
|
||||
[Status.offline, Status.online, Status.dnd][submit.state]?.color
|
||||
}`}
|
||||
className={`fas fa-circle text-${[Status.offline, Status.online, Status.dnd][
|
||||
submit.state
|
||||
]?.color}`}
|
||||
/>{' '}
|
||||
{['대기중', '승인됨', '거부됨'][submit.state]}
|
||||
</>
|
||||
@ -32,13 +32,12 @@ const SubmittedBotCard: React.FC<SubmittedBotProps> = ({ href, submit }) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className='mt-1.5 w-full h-6 text-left text-gray-400 text-sm font-medium truncate'>
|
||||
<p className='mt-1.5 h-6 w-full truncate text-left text-sm font-medium text-gray-400'>
|
||||
{submit.intro.slice(0, 25)}
|
||||
{submit.intro.length > 25 && '...'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</Link>)
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -26,18 +26,18 @@ const Tag: React.FC<LabelProps> = ({
|
||||
? 'bg-discord-blurple text-white'
|
||||
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
|
||||
: github
|
||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||
: 'bg-little-white hover:bg-little-white-hover dark:bg-discord-black'
|
||||
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
||||
circular
|
||||
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
||||
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
||||
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}
|
||||
} mr-1 mb-${marginBottom} transition duration-100 ease-in dark:hover:bg-discord-dark-hover`}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
) : (
|
||||
(<Link
|
||||
<Link
|
||||
href={href}
|
||||
className={`${className ?? ''} text-center text-base ${
|
||||
dark
|
||||
@ -45,19 +45,18 @@ const Tag: React.FC<LabelProps> = ({
|
||||
? 'bg-discord-blurple text-white'
|
||||
: 'bg-little-white-hover hover:bg-little-white dark:bg-very-black'
|
||||
: github
|
||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||
: 'bg-little-white dark:bg-discord-black hover:bg-little-white-hover'
|
||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||
: 'bg-little-white hover:bg-little-white-hover dark:bg-discord-black'
|
||||
} ${
|
||||
!blurple && !github ? 'text-black dark:text-gray-400' : 'hover:bg-little-white-hover'
|
||||
} ${
|
||||
circular
|
||||
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
||||
: `rounded ${bigger ? 'px-3 py-2' : 'px-2 py-1'}`
|
||||
} mr-1 mb-${marginBottom} dark:hover:bg-discord-dark-hover transition duration-100 ease-in`}>
|
||||
|
||||
} mr-1 mb-${marginBottom} transition duration-100 ease-in dark:hover:bg-discord-dark-hover`}
|
||||
>
|
||||
{text}
|
||||
|
||||
</Link>)
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<a
|
||||
@ -67,17 +66,17 @@ const Tag: React.FC<LabelProps> = ({
|
||||
? blurple
|
||||
? 'font-bg bg-discord-blurple text-white'
|
||||
: github
|
||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||
: `bg-little-white-hover dark:bg-very-black ${
|
||||
props.onClick
|
||||
? 'hover:bg-little-white dark:hover:bg-discord-dark-hover transition duration-100 ease-in'
|
||||
: ''
|
||||
}`
|
||||
? 'bg-gray-900 text-white hover:bg-gray-700'
|
||||
: `bg-little-white-hover dark:bg-very-black ${
|
||||
props.onClick
|
||||
? 'transition duration-100 ease-in hover:bg-little-white dark:hover:bg-discord-dark-hover'
|
||||
: ''
|
||||
}`
|
||||
: `bg-little-white dark:bg-discord-black ${
|
||||
props.onClick
|
||||
? 'hover:bg-little-white-hover dark:hover:bg-discord-dark-hover transition duration-100 ease-in'
|
||||
: ''
|
||||
}`
|
||||
props.onClick
|
||||
? 'transition duration-100 ease-in hover:bg-little-white-hover dark:hover:bg-discord-dark-hover'
|
||||
: ''
|
||||
}`
|
||||
} ${!blurple && !github ? 'text-black dark:text-gray-400' : ''} ${
|
||||
circular
|
||||
? `rounded-3xl ${bigger ? 'px-3.5 py-2.5' : 'px-2.5 py-1.5'}`
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
const Toggle: React.FC<ToggleProps> = ({ checked, onChange }: ToggleProps) => {
|
||||
return (
|
||||
<button
|
||||
className='relative inline-block align-middle mr-2 w-10 outline-none select-none'
|
||||
className='relative mr-2 inline-block w-10 select-none align-middle outline-none'
|
||||
onClick={onChange}
|
||||
onKeyPress={onChange}
|
||||
>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={checked}
|
||||
className='absolute checked:right-0 block w-6 h-6 bg-white border-4 border-transparent rounded-full outline-none appearance-none cursor-pointer'
|
||||
className='absolute block h-6 w-6 cursor-pointer appearance-none rounded-full border-4 border-transparent bg-white outline-none checked:right-0'
|
||||
readOnly
|
||||
/>
|
||||
<span
|
||||
className={`block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer ${
|
||||
className={`block h-6 cursor-pointer overflow-hidden rounded-full bg-gray-300 ${
|
||||
checked ? 'bg-koreanbots-blue' : ''
|
||||
}`}
|
||||
></span>
|
||||
|
||||
@ -8,15 +8,14 @@ const Tooltip: React.FC<TooltipProps> = ({
|
||||
text,
|
||||
}) => {
|
||||
return href ? (
|
||||
(<Link href={href} className='inline'>
|
||||
|
||||
<Link href={href} className='inline'>
|
||||
<div className='relative inline py-3'>
|
||||
<div className='group relative inline-block text-center cursor-pointer'>
|
||||
<div className='group relative inline-block cursor-pointer text-center'>
|
||||
{children}
|
||||
<div
|
||||
className={`opacity-0 ${
|
||||
size === 'small' ? 'w-44' : 'w-60'
|
||||
} bg-black text-white text-center text-xs rounded-lg py-2 px-3 absolute z-10 group-hover:opacity-100 bottom-full -left-4 pointer-events-none`}
|
||||
} pointer-events-none absolute -left-4 bottom-full z-10 rounded-lg bg-black px-3 py-2 text-center text-xs text-white group-hover:opacity-100`}
|
||||
>
|
||||
{text}
|
||||
{direction === 'left' ? (
|
||||
@ -31,7 +30,7 @@ const Tooltip: React.FC<TooltipProps> = ({
|
||||
</svg>
|
||||
) : direction === 'center' ? (
|
||||
<svg
|
||||
className='absolute left-0 top-full w-full h-2 text-black'
|
||||
className='absolute left-0 top-full h-2 w-full text-black'
|
||||
x='0px'
|
||||
y='0px'
|
||||
viewBox='0 0 255 255'
|
||||
@ -53,17 +52,16 @@ const Tooltip: React.FC<TooltipProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Link>)
|
||||
</Link>
|
||||
) : (
|
||||
<a className='inline'>
|
||||
<div className='relative inline py-3'>
|
||||
<div className='group relative inline-block text-center cursor-pointer'>
|
||||
<div className='group relative inline-block cursor-pointer text-center'>
|
||||
{children}
|
||||
<div
|
||||
className={`opacity-0 ${
|
||||
size === 'small' ? 'w-44' : 'w-60'
|
||||
} bg-black text-white text-center text-xs rounded-lg py-2 px-3 absolute z-10 group-hover:opacity-100 bottom-full -left-4 pointer-events-none`}
|
||||
} pointer-events-none absolute -left-4 bottom-full z-10 rounded-lg bg-black px-3 py-2 text-center text-xs text-white group-hover:opacity-100`}
|
||||
>
|
||||
{text}
|
||||
{direction === 'left' ? (
|
||||
@ -78,7 +76,7 @@ const Tooltip: React.FC<TooltipProps> = ({
|
||||
</svg>
|
||||
) : direction === 'center' ? (
|
||||
<svg
|
||||
className='absolute left-0 top-full w-full h-2 text-black'
|
||||
className='absolute left-0 top-full h-2 w-full text-black'
|
||||
x='0px'
|
||||
y='0px'
|
||||
viewBox='0 0 255 255'
|
||||
|
||||
@ -3,20 +3,19 @@ import { ErrorText } from '@utils/Constants'
|
||||
|
||||
const NotFound: NextPage<{ message?: string }> = ({ message }) => {
|
||||
return (
|
||||
<div
|
||||
className='flex items-center justify-center h-screen select-none text-center'
|
||||
>
|
||||
<div className='flex h-screen select-none items-center justify-center text-center'>
|
||||
<div>
|
||||
<div className='flex flex-row justify-center text-9xl'>
|
||||
4
|
||||
<img alt='robot' src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f916.svg' className='w-24 mx-6 md:mx-12 rounded-full' />
|
||||
<img
|
||||
alt='robot'
|
||||
src='https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f916.svg'
|
||||
className='mx-6 w-24 rounded-full md:mx-12'
|
||||
/>
|
||||
4
|
||||
</div>
|
||||
<h2 className='text-2xl font-semibold'>
|
||||
{message || ErrorText[404]}
|
||||
</h2>
|
||||
<h2 className='text-2xl font-semibold'>{message || ErrorText[404]}</h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
252
pages/_app.tsx
252
pages/_app.tsx
@ -34,9 +34,9 @@ Router.events.on('routeChangeError', NProgress.done)
|
||||
ReactGA.initialize('UA-165454387-1')
|
||||
|
||||
const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps): JSX.Element => {
|
||||
const [ shortcutModal, setShortcutModal ] = useState(false)
|
||||
const [ theme, setTheme ] = useState<Theme>('system')
|
||||
const [ standalone, setStandalone ] = useState(false)
|
||||
const [shortcutModal, setShortcutModal] = useState(false)
|
||||
const [theme, setTheme] = useState<Theme>('system')
|
||||
const [standalone, setStandalone] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
@ -44,7 +44,11 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
|
||||
'%c' + 'KOREANBOTS',
|
||||
'color: #3366FF; -webkit-text-stroke: 2px black; font-size: 72px; font-weight: bold;'
|
||||
)
|
||||
Logger.debug(`[BUILD INFO] Tag: ${parseDockerhubTag(process.env.NEXT_PUBLIC_TAG)}, Version: v${Package.version}, Hash: ${process.env.NEXT_PUBLIC_SOURCE_COMMIT}`)
|
||||
Logger.debug(
|
||||
`[BUILD INFO] Tag: ${parseDockerhubTag(process.env.NEXT_PUBLIC_TAG)}, Version: v${
|
||||
Package.version
|
||||
}, Hash: ${process.env.NEXT_PUBLIC_SOURCE_COMMIT}`
|
||||
)
|
||||
console.log(
|
||||
'%c' + '이곳에 코드를 붙여넣으면 공격자에게 엑세스 토큰을 넘겨줄 수 있습니다!!',
|
||||
'color: #ff0000; font-size: 20px; font-weight: bold;'
|
||||
@ -52,127 +56,147 @@ const KoreanbotsApp = ({ Component, pageProps, err, cookie }: KoreanbotsProps):
|
||||
if (!localStorage.theme) {
|
||||
Logger.debug(`[THEME] ${systemTheme().toUpperCase()} THEME DETECTED`)
|
||||
setTheme(systemTheme())
|
||||
}
|
||||
else setTheme(localStorage.theme)
|
||||
} else setTheme(localStorage.theme)
|
||||
setStandalone(handlePWA())
|
||||
|
||||
const script = document.querySelector('script[src*=googlesyndication]')
|
||||
|
||||
if (script) script.addEventListener('error', () => {ReactGA.ga('send', 'event', 'adblock', 'adblock_' + (navigator.userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i) ? 'mobile' : 'pc'))})
|
||||
if (script)
|
||||
script.addEventListener('error', () => {
|
||||
ReactGA.ga(
|
||||
'send',
|
||||
'event',
|
||||
'adblock',
|
||||
'adblock_' +
|
||||
(navigator.userAgent.match(
|
||||
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
|
||||
)
|
||||
? 'mobile'
|
||||
: 'pc')
|
||||
)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return <div className={theme}>
|
||||
<DefaultSeo
|
||||
titleTemplate='%s - 한국 디스코드 리스트'
|
||||
defaultTitle={TITLE}
|
||||
description={DESCRIPTION}
|
||||
openGraph={{
|
||||
type: 'website',
|
||||
title: TITLE,
|
||||
url: 'https://koreanbots.dev',
|
||||
site_name: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [
|
||||
{
|
||||
url: '/logo.png',
|
||||
width: 300,
|
||||
height: 300,
|
||||
alt: 'Logo'
|
||||
}
|
||||
]
|
||||
}}
|
||||
twitter={{
|
||||
site: '@koreanbots',
|
||||
handle: '@koreanbots',
|
||||
cardType: 'summary'
|
||||
}}
|
||||
/>
|
||||
<Head>
|
||||
{/* META */}
|
||||
<meta charSet='utf-8' />
|
||||
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
|
||||
<meta name='keywords' content='Korea, Korean, Discord, Bot, 디스코드봇, 한디리' />
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' />
|
||||
return (
|
||||
<div className={theme}>
|
||||
<DefaultSeo
|
||||
titleTemplate='%s - 한국 디스코드 리스트'
|
||||
defaultTitle={TITLE}
|
||||
description={DESCRIPTION}
|
||||
openGraph={{
|
||||
type: 'website',
|
||||
title: TITLE,
|
||||
url: 'https://koreanbots.dev',
|
||||
site_name: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [
|
||||
{
|
||||
url: '/logo.png',
|
||||
width: 300,
|
||||
height: 300,
|
||||
alt: 'Logo',
|
||||
},
|
||||
],
|
||||
}}
|
||||
twitter={{
|
||||
site: '@koreanbots',
|
||||
handle: '@koreanbots',
|
||||
cardType: 'summary',
|
||||
}}
|
||||
/>
|
||||
<Head>
|
||||
{/* META */}
|
||||
<meta charSet='utf-8' />
|
||||
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
|
||||
<meta name='keywords' content='Korea, Korean, Discord, Bot, 디스코드봇, 한디리' />
|
||||
<meta
|
||||
name='viewport'
|
||||
content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no'
|
||||
/>
|
||||
|
||||
{/* Android */}
|
||||
<meta name='theme-color' content={THEME_COLOR} />
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
|
||||
{/* iOS */}
|
||||
<meta name='apple-mobile-web-app-title' content='Application Title' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
<meta name='apple-mobile-web-app-status-bar-style' content='default' />
|
||||
|
||||
{/* Windows */}
|
||||
<meta name='msapplication-navbutton-color' content={THEME_COLOR} />
|
||||
<meta name='msapplication-TileColor' content={THEME_COLOR} />
|
||||
<meta name='msapplication-TileImage' content='/static/ms-icon-144x144.png' />
|
||||
<meta name='msapplication-config' content='browserconfig.xml' />
|
||||
{/* Android */}
|
||||
<meta name='theme-color' content={THEME_COLOR} />
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
|
||||
{/* Pinned Sites */}
|
||||
<meta name='application-name' content={TITLE} />
|
||||
<meta name='msapplication-tooltip' content={DESCRIPTION} />
|
||||
<meta name='msapplication-starturl' content='/' />
|
||||
{/* iOS */}
|
||||
<meta name='apple-mobile-web-app-title' content='Application Title' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
<meta name='apple-mobile-web-app-status-bar-style' content='default' />
|
||||
|
||||
{/* Tap highlighting */}
|
||||
<meta name='msapplication-tap-highlight' content='no' />
|
||||
{/* Windows */}
|
||||
<meta name='msapplication-navbutton-color' content={THEME_COLOR} />
|
||||
<meta name='msapplication-TileColor' content={THEME_COLOR} />
|
||||
<meta name='msapplication-TileImage' content='/static/ms-icon-144x144.png' />
|
||||
<meta name='msapplication-config' content='browserconfig.xml' />
|
||||
|
||||
{/* UC Mobile Browser */}
|
||||
<meta name='full-screen' content='yes' />
|
||||
<meta name='browsermode' content='application' />
|
||||
{/* Pinned Sites */}
|
||||
<meta name='application-name' content={TITLE} />
|
||||
<meta name='msapplication-tooltip' content={DESCRIPTION} />
|
||||
<meta name='msapplication-starturl' content='/' />
|
||||
|
||||
<meta name='nightmode' content='disable' />
|
||||
<meta name='layoutmode' content='fitscreen' />
|
||||
<meta name='imagemode' content='force' />
|
||||
<meta name='screen-orientation' content='portrait' />
|
||||
|
||||
</Head>
|
||||
<Navbar token={cookie.token} />
|
||||
<div className='iu-is-the-best min-h-screen text-black dark:text-gray-100 dark:bg-discord-dark bg-white'>
|
||||
<Component {...pageProps} err={err} theme={theme} setTheme={setTheme} pwa={standalone} />
|
||||
</div>
|
||||
{
|
||||
!(router.pathname.startsWith('/developers')) && <Footer theme={theme} setTheme={setTheme} />
|
||||
}
|
||||
<Modal full isOpen={shortcutModal} onClose={() => setShortcutModal(false)} dark={theme === 'dark'} header='단축키 안내'>
|
||||
<div className='px-3 h-80'>
|
||||
<h3 className='text-md font-semibold'>일반</h3>
|
||||
<ul>
|
||||
<li className='pt-2'>
|
||||
<h4 className='text-gray-500 dark:text-gray-400 text-xs'>단축키 도움말 표시</h4>
|
||||
<kbd>
|
||||
<PlatformDisplay osx='CMD'>
|
||||
Ctrl
|
||||
</PlatformDisplay>
|
||||
</kbd> <kbd>/</kbd>
|
||||
</li>
|
||||
<li className='pt-2'>
|
||||
<h4 className='text-gray-500 dark:text-gray-400 text-xs'>다크모드 전환</h4>
|
||||
<kbd>
|
||||
<PlatformDisplay osx='CMD'>
|
||||
Ctrl
|
||||
</PlatformDisplay>
|
||||
</kbd>
|
||||
<kbd>Shift</kbd> <kbd>D</kbd>
|
||||
</li>
|
||||
</ul>
|
||||
{/* Tap highlighting */}
|
||||
<meta name='msapplication-tap-highlight' content='no' />
|
||||
|
||||
{/* UC Mobile Browser */}
|
||||
<meta name='full-screen' content='yes' />
|
||||
<meta name='browsermode' content='application' />
|
||||
|
||||
<meta name='nightmode' content='disable' />
|
||||
<meta name='layoutmode' content='fitscreen' />
|
||||
<meta name='imagemode' content='force' />
|
||||
<meta name='screen-orientation' content='portrait' />
|
||||
</Head>
|
||||
<Navbar token={cookie.token} />
|
||||
<div className='iu-is-the-best min-h-screen bg-white text-black dark:bg-discord-dark dark:text-gray-100'>
|
||||
<Component {...pageProps} err={err} theme={theme} setTheme={setTheme} pwa={standalone} />
|
||||
</div>
|
||||
</Modal>
|
||||
<GlobalHotKeys keyMap={shortcutKeyMap} handlers={{
|
||||
SHORTCUT_HELP: (event) => {
|
||||
event.preventDefault()
|
||||
setShortcutModal(value => !value)
|
||||
return
|
||||
},
|
||||
CHANGE_THEME: (event) => {
|
||||
event.preventDefault()
|
||||
const overwrite = (localStorage.theme || systemTheme()) === 'dark' ? 'light' : 'dark'
|
||||
setTheme(overwrite)
|
||||
localStorage.setItem('theme', overwrite)
|
||||
return false
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
{!router.pathname.startsWith('/developers') && <Footer theme={theme} setTheme={setTheme} />}
|
||||
<Modal
|
||||
full
|
||||
isOpen={shortcutModal}
|
||||
onClose={() => setShortcutModal(false)}
|
||||
dark={theme === 'dark'}
|
||||
header='단축키 안내'
|
||||
>
|
||||
<div className='h-80 px-3'>
|
||||
<h3 className='text-md font-semibold'>일반</h3>
|
||||
<ul>
|
||||
<li className='pt-2'>
|
||||
<h4 className='text-xs text-gray-500 dark:text-gray-400'>단축키 도움말 표시</h4>
|
||||
<kbd>
|
||||
<PlatformDisplay osx='CMD'>Ctrl</PlatformDisplay>
|
||||
</kbd>{' '}
|
||||
<kbd>/</kbd>
|
||||
</li>
|
||||
<li className='pt-2'>
|
||||
<h4 className='text-xs text-gray-500 dark:text-gray-400'>다크모드 전환</h4>
|
||||
<kbd>
|
||||
<PlatformDisplay osx='CMD'>Ctrl</PlatformDisplay>
|
||||
</kbd>
|
||||
<kbd>Shift</kbd> <kbd>D</kbd>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
<GlobalHotKeys
|
||||
keyMap={shortcutKeyMap}
|
||||
handlers={{
|
||||
SHORTCUT_HELP: (event) => {
|
||||
event.preventDefault()
|
||||
setShortcutModal((value) => !value)
|
||||
return
|
||||
},
|
||||
CHANGE_THEME: (event) => {
|
||||
event.preventDefault()
|
||||
const overwrite = (localStorage.theme || systemTheme()) === 'dark' ? 'light' : 'dark'
|
||||
setTheme(overwrite)
|
||||
localStorage.setItem('theme', overwrite)
|
||||
return false
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
KoreanbotsApp.getInitialProps = async (appCtx: AppContext) => {
|
||||
@ -180,7 +204,7 @@ KoreanbotsApp.getInitialProps = async (appCtx: AppContext) => {
|
||||
const parsed = parseCookie(appCtx.ctx.req)
|
||||
return {
|
||||
...appProps,
|
||||
cookie: parsed
|
||||
cookie: parsed,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,12 @@ class MyDocument extends Document {
|
||||
<Head>
|
||||
{/* LINK */}
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
<link rel='search' type='application/opensearchdescription+xml' title={TITLE} href='/opensearch.xml' />
|
||||
<link
|
||||
rel='search'
|
||||
type='application/opensearchdescription+xml'
|
||||
title={TITLE}
|
||||
href='/opensearch.xml'
|
||||
/>
|
||||
<link
|
||||
rel='stylesheet'
|
||||
href='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.6.0/styles/solarized-dark.min.css'
|
||||
@ -21,8 +26,13 @@ class MyDocument extends Document {
|
||||
<link rel='icon' type='image/png' sizes='32x32' href='/favicon-32x32.png' />
|
||||
<link rel='icon' type='image/png' sizes='96x96' href='/favicon-96x96.png' />
|
||||
<link rel='icon' type='image/png' sizes='16x16' href='/favicon-16x16.png' />
|
||||
<link rel='stylesheet' as='style' crossOrigin='anonymous' href='https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.6/dist/web/variable/pretendardvariable-dynamic-subset.css' />
|
||||
|
||||
<link
|
||||
rel='stylesheet'
|
||||
as='style'
|
||||
crossOrigin='anonymous'
|
||||
href='https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.6/dist/web/variable/pretendardvariable-dynamic-subset.css'
|
||||
/>
|
||||
|
||||
{/* iOS */}
|
||||
<link rel='apple-touch-icon' sizes='57x57' href='/static/apple-icon-57x57.png' />
|
||||
<link rel='apple-touch-icon' sizes='60x60' href='/static/apple-icon-60x60.png' />
|
||||
@ -35,43 +45,160 @@ class MyDocument extends Document {
|
||||
<link rel='apple-touch-icon' sizes='180x180' href='/static/apple-icon-180x180.png' />
|
||||
<link rel='apple-touch-icon' sizes='256x256' href='/static/apple-icon-256x256.png' />
|
||||
<link rel='apple-touch-icon' sizes='512x512' href='/static/apple-icon-512x512.png' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2048-2732.jpg' media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2732-2048.jpg' media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1668-2388.jpg' media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2388-1668.jpg' media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1536-2048.jpg' media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2048-1536.jpg' media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1668-2224.jpg' media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2224-1668.jpg' media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1620-2160.jpg' media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2160-1620.jpg' media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1284-2778.jpg' media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2778-1284.jpg' media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1170-2532.jpg' media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2532-1170.jpg' media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1125-2436.jpg' media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2436-1125.jpg' media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1242-2688.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2688-1242.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-828-1792.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1792-828.jpg' media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1242-2208.jpg' media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-2208-1242.jpg' media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-750-1334.jpg' media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1334-750.jpg' media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-640-1136.jpg' media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)' />
|
||||
<link rel='apple-touch-startup-image' href='/static/apple-splash-1136-640.jpg' media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)' />
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2048-2732.jpg'
|
||||
media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2732-2048.jpg'
|
||||
media='(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1668-2388.jpg'
|
||||
media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2388-1668.jpg'
|
||||
media='(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1536-2048.jpg'
|
||||
media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2048-1536.jpg'
|
||||
media='(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1668-2224.jpg'
|
||||
media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2224-1668.jpg'
|
||||
media='(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1620-2160.jpg'
|
||||
media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2160-1620.jpg'
|
||||
media='(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1284-2778.jpg'
|
||||
media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2778-1284.jpg'
|
||||
media='(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1170-2532.jpg'
|
||||
media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2532-1170.jpg'
|
||||
media='(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1125-2436.jpg'
|
||||
media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2436-1125.jpg'
|
||||
media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1242-2688.jpg'
|
||||
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2688-1242.jpg'
|
||||
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-828-1792.jpg'
|
||||
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1792-828.jpg'
|
||||
media='(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1242-2208.jpg'
|
||||
media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-2208-1242.jpg'
|
||||
media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-750-1334.jpg'
|
||||
media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1334-750.jpg'
|
||||
media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-640-1136.jpg'
|
||||
media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'
|
||||
/>
|
||||
<link
|
||||
rel='apple-touch-startup-image'
|
||||
href='/static/apple-splash-1136-640.jpg'
|
||||
media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)'
|
||||
/>
|
||||
|
||||
{/* Android */}
|
||||
<link rel='icon' type='image/png' sizes='192x192' href='/static/android-icon-192x192.png' />
|
||||
<link
|
||||
rel='icon'
|
||||
type='image/png'
|
||||
sizes='192x192'
|
||||
href='/static/android-icon-192x192.png'
|
||||
/>
|
||||
|
||||
{/* Others */}
|
||||
<link rel='shortcut icon' href='/favicon.ico' />
|
||||
|
||||
{/* SCRIPT */}
|
||||
<script src='//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js'></script>
|
||||
<script data-ad-client='ca-pub-4856582423981759' async src='//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'></script>
|
||||
<script data-cfasync='false' async src='//www.googletagmanager.com/gtag/js?id=UA-165454387-1'></script>
|
||||
<script
|
||||
data-ad-client='ca-pub-4856582423981759'
|
||||
async
|
||||
src='//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'
|
||||
></script>
|
||||
<script
|
||||
data-cfasync='false'
|
||||
async
|
||||
src='//www.googletagmanager.com/gtag/js?id=UA-165454387-1'
|
||||
></script>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@ -91,7 +218,7 @@ class MyDocument extends Document {
|
||||
}}
|
||||
/>
|
||||
</Head>
|
||||
<body className='h-full text-black dark:text-gray-100 dark:bg-discord-dark bg-white overflow-x-hidden'>
|
||||
<body className='h-full overflow-x-hidden bg-white text-black dark:bg-discord-dark dark:text-gray-100'>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
|
||||
@ -8,24 +8,35 @@ const Container = dynamic(() => import('@components/Container'))
|
||||
|
||||
const MyError: NextPage = () => {
|
||||
return (
|
||||
<div
|
||||
className='flex items-center h-screen select-none px-20'
|
||||
>
|
||||
<div className='flex h-screen select-none items-center px-20'>
|
||||
<Container>
|
||||
<h2 className='text-4xl font-semibold'>{getRandom(ErrorMessage)}</h2>
|
||||
<p className='text-md mt-1'>예상치 못한 오류가 발생하였습니다. 문제가 지속적으로 발생한다면 문의해주세요!</p>
|
||||
<a className='text-sm text-blue-500 hover:text-blue-400' href='https://status.koreanbots.dev' target='_blank' rel='noreferrer'>상태 페이지</a>
|
||||
<p className='text-md mt-1'>
|
||||
예상치 못한 오류가 발생하였습니다. 문제가 지속적으로 발생한다면 문의해주세요!
|
||||
</p>
|
||||
<a
|
||||
className='text-sm text-blue-500 hover:text-blue-400'
|
||||
href='https://status.koreanbots.dev'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
상태 페이지
|
||||
</a>
|
||||
<div>
|
||||
<Link
|
||||
href='/discord'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className='text-lg hover:opacity-80 cursor-pointer'>
|
||||
|
||||
className='cursor-pointer text-lg hover:opacity-80'
|
||||
>
|
||||
<i className='fab fa-discord' />
|
||||
|
||||
</Link>
|
||||
<a href='https://twitter.com/koreanbots' target='_blank' rel='noreferrer' className='text-lg ml-2 hover:opacity-80 cursor-pointer'>
|
||||
<a
|
||||
href='https://twitter.com/koreanbots'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className='ml-2 cursor-pointer text-lg hover:opacity-80'
|
||||
>
|
||||
<i className='fab fa-twitter' />
|
||||
</a>
|
||||
</div>
|
||||
@ -34,4 +45,4 @@ const MyError: NextPage = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default MyError
|
||||
export default MyError
|
||||
|
||||
@ -6,24 +6,33 @@ const Container = dynamic(() => import('@components/Container'))
|
||||
|
||||
const MyError: NextPage = () => {
|
||||
return (
|
||||
<div
|
||||
className='flex items-center h-screen select-none px-20'
|
||||
>
|
||||
<div className='flex h-screen select-none items-center px-20'>
|
||||
<Container>
|
||||
<h2 className='text-4xl font-semibold'>인터넷이 끊어졌나봐요...</h2>
|
||||
<p className='text-md mt-1'>인터넷 연결을 확인하시고 다시 시도 해주세요!</p>
|
||||
<a className='text-sm text-blue-500 hover:text-blue-400' href='https://status.koreanbots.dev' target='_blank' rel='noreferrer'>상태 페이지</a>
|
||||
<a
|
||||
className='text-sm text-blue-500 hover:text-blue-400'
|
||||
href='https://status.koreanbots.dev'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
상태 페이지
|
||||
</a>
|
||||
<div>
|
||||
<Link
|
||||
href='/discord'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className='text-lg hover:opacity-80 cursor-pointer'>
|
||||
|
||||
className='cursor-pointer text-lg hover:opacity-80'
|
||||
>
|
||||
<i className='fab fa-discord' />
|
||||
|
||||
</Link>
|
||||
<a href='https://twitter.com/koreanbots' target='_blank' rel='noreferrer' className='text-lg ml-2 hover:opacity-80 cursor-pointer'>
|
||||
<a
|
||||
href='https://twitter.com/koreanbots'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
className='ml-2 cursor-pointer text-lg hover:opacity-80'
|
||||
>
|
||||
<i className='fab fa-twitter' />
|
||||
</a>
|
||||
</div>
|
||||
@ -32,4 +41,4 @@ const MyError: NextPage = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default MyError
|
||||
export default MyError
|
||||
|
||||
154
pages/about.tsx
154
pages/about.tsx
@ -9,70 +9,102 @@ import { ThemeColors } from '@utils/Constants'
|
||||
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
|
||||
const About:NextPage = () => {
|
||||
return <div className='pb-10'>
|
||||
<Docs title='소개' header={<h1 className='font-black text-4xl dark:text-koreanbots-blue'>“국내 디스코드의 모든 것을 한 곳에서.”</h1>} subheader='한국 디스코드 리스트에서 자신에게 필요한 디스코드의 모든 것을 찾아보세요!'>
|
||||
<Container>
|
||||
<div className='py-1'>
|
||||
<h1 className='font-bold text-5xl my-5'>소개</h1>
|
||||
<p className='text-lg'><span className='text-koreanbots-blue font-bold'>한국 디스코드 리스트</span>는 본인의 봇과 서버를 직접 등록하고, 유저 분은 봇 또는 서버를 카테고리별로 확인할 수 있는 플랫폼입니다.</p>
|
||||
<p className='text-lg'>자신에게 필요한 디스코드의 모든 것을 찾아보세요!</p>
|
||||
<Divider />
|
||||
<h1 className='font-bold text-5xl my-5'>특징</h1>
|
||||
<div className='grid md:grid-cols-3 gap-12 px-4 pb-5'>
|
||||
<div className='mx-auto font-normal'>
|
||||
<h2 className='text-3xl mb-1 font-bold text-koreanbots-blue'>하트 시스템</h2>
|
||||
<p className='text-base'>마음에 드는 봇이나 서버에 투표하는 하트 시스템으로 유용한 봇 또는 서버가 상단에 노출될 수 있는 기회를 제공합니다.</p>
|
||||
const About: NextPage = () => {
|
||||
return (
|
||||
<div className='pb-10'>
|
||||
<Docs
|
||||
title='소개'
|
||||
header={
|
||||
<h1 className='text-4xl font-black dark:text-koreanbots-blue'>
|
||||
“국내 디스코드의 모든 것을 한 곳에서.”
|
||||
</h1>
|
||||
}
|
||||
subheader='한국 디스코드 리스트에서 자신에게 필요한 디스코드의 모든 것을 찾아보세요!'
|
||||
>
|
||||
<Container>
|
||||
<div className='py-1'>
|
||||
<h1 className='my-5 text-5xl font-bold'>소개</h1>
|
||||
<p className='text-lg'>
|
||||
<span className='font-bold text-koreanbots-blue'>한국 디스코드 리스트</span>는 본인의
|
||||
봇과 서버를 직접 등록하고, 유저 분은 봇 또는 서버를 카테고리별로 확인할 수 있는
|
||||
플랫폼입니다.
|
||||
</p>
|
||||
<p className='text-lg'>자신에게 필요한 디스코드의 모든 것을 찾아보세요!</p>
|
||||
<Divider />
|
||||
<h1 className='my-5 text-5xl font-bold'>특징</h1>
|
||||
<div className='grid gap-12 px-4 pb-5 md:grid-cols-3'>
|
||||
<div className='mx-auto font-normal'>
|
||||
<h2 className='mb-1 text-3xl font-bold text-koreanbots-blue'>하트 시스템</h2>
|
||||
<p className='text-base'>
|
||||
마음에 드는 봇이나 서버에 투표하는 하트 시스템으로 유용한 봇 또는 서버가 상단에
|
||||
노출될 수 있는 기회를 제공합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className='mx-auto font-normal'>
|
||||
<h2 className='mb-1 text-3xl font-bold text-koreanbots-blue'>인증 시스템</h2>
|
||||
<p className='text-base'>
|
||||
봇은 디스코드 봇 인증보다 한 단계 까다로운 기준을 적용하며 서버는 신뢰할 수 있는
|
||||
서버를 정해, 이용자분들에게 신뢰감을 줍니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className='mx-auto font-normal'>
|
||||
<h2 className='mb-1 text-3xl font-bold text-koreanbots-blue'>API 제공</h2>
|
||||
<p className='text-base'>
|
||||
정보부터, 유저 투표 여부 확인, 위젯까지.
|
||||
<br />
|
||||
다양한 API를 제공하여 커스텀할 수 있습니다!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-auto font-normal'>
|
||||
<h2 className='text-3xl mb-1 font-bold text-koreanbots-blue'>인증 시스템</h2>
|
||||
<p className='text-base'>봇은 디스코드 봇 인증보다 한 단계 까다로운 기준을 적용하며 서버는 신뢰할 수 있는 서버를 정해, 이용자분들에게 신뢰감을 줍니다.</p>
|
||||
</div>
|
||||
<div className='mx-auto font-normal'>
|
||||
<h2 className='text-3xl mb-1 font-bold text-koreanbots-blue'>API 제공</h2>
|
||||
<p className='text-base'>정보부터, 유저 투표 여부 확인, 위젯까지.<br />다양한 API를 제공하여 커스텀할 수 있습니다!</p>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<h1 className='font-bold text-5xl my-5'>브랜드</h1>
|
||||
<h2 className='font-semibold text-3xl mb-7'>슬로건</h2>
|
||||
<Segment>
|
||||
<h2 className='font-semibold text-xl py-10 text-center'>
|
||||
<i className='fas fa-quote-left text-xs align-top' />
|
||||
국내 디스코드의 모든 것을 한 곳에서.
|
||||
<i className='fas fa-quote-right text-xs align-bottom' />
|
||||
</h2>
|
||||
</Segment>
|
||||
<Divider className='mt-7' />
|
||||
<h2 className='font-semibold text-3xl my-7'>로고</h2>
|
||||
<Segment>
|
||||
<>
|
||||
로고를 수정하거나, 변경, 왜곡 등 기타 다른 방법으로 로고를 수정하지 말아주세요.
|
||||
<div className='grid md:grid-cols-2 lg:grid-cols-4'>
|
||||
<div>
|
||||
<img src='/logo.png' alt='Logo' />
|
||||
<div className='text-right text-blue-400'>
|
||||
<a href='/logo.png' download='koreanbots.png'>.png</a>
|
||||
<Divider />
|
||||
<h1 className='my-5 text-5xl font-bold'>브랜드</h1>
|
||||
<h2 className='mb-7 text-3xl font-semibold'>슬로건</h2>
|
||||
<Segment>
|
||||
<h2 className='py-10 text-center text-xl font-semibold'>
|
||||
<i className='fas fa-quote-left align-top text-xs' />
|
||||
국내 디스코드의 모든 것을 한 곳에서.
|
||||
<i className='fas fa-quote-right align-bottom text-xs' />
|
||||
</h2>
|
||||
</Segment>
|
||||
<Divider className='mt-7' />
|
||||
<h2 className='my-7 text-3xl font-semibold'>로고</h2>
|
||||
<Segment>
|
||||
<>
|
||||
로고를 수정하거나, 변경, 왜곡 등 기타 다른 방법으로 로고를 수정하지 말아주세요.
|
||||
<div className='grid md:grid-cols-2 lg:grid-cols-4'>
|
||||
<div>
|
||||
<img src='/logo.png' alt='Logo' />
|
||||
<div className='text-right text-blue-400'>
|
||||
<a href='/logo.png' download='koreanbots.png'>
|
||||
.png
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className='font-bold text-xl my-1'>폰트</h3>
|
||||
<p className='font-bold text-md my-1'>영문: Uni Sans Heavy | 한글: Gugi</p>
|
||||
</>
|
||||
</Segment>
|
||||
<Divider className='mt-7' />
|
||||
<h2 className='font-semibold text-3xl my-5'>색상</h2>
|
||||
<div className='grid md:grid-cols-2 lg:grid-cols-4 gap-4'>
|
||||
{
|
||||
ThemeColors.map(el => (
|
||||
<ColorCard key={el.color} header={el.name} first={el.rgb} second={el.hex} className={`bg-${el.color} ${el.color.includes('white') ? 'text-black' : 'text-white'}`} />
|
||||
))
|
||||
}
|
||||
<h3 className='my-1 text-xl font-bold'>폰트</h3>
|
||||
<p className='text-md my-1 font-bold'>영문: Uni Sans Heavy | 한글: Gugi</p>
|
||||
</>
|
||||
</Segment>
|
||||
<Divider className='mt-7' />
|
||||
<h2 className='my-5 text-3xl font-semibold'>색상</h2>
|
||||
<div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4'>
|
||||
{ThemeColors.map((el) => (
|
||||
<ColorCard
|
||||
key={el.color}
|
||||
header={el.name}
|
||||
first={el.rgb}
|
||||
second={el.hex}
|
||||
className={`bg-${el.color} ${
|
||||
el.color.includes('white') ? 'text-black' : 'text-white'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</Docs>
|
||||
</div>
|
||||
</Container>
|
||||
</Docs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default About
|
||||
export default About
|
||||
|
||||
369
pages/addbot.tsx
369
pages/addbot.tsx
@ -31,10 +31,10 @@ const Container = dynamic(() => import('@components/Container'))
|
||||
const Message = dynamic(() => import('@components/Message'))
|
||||
const Captcha = dynamic(() => import('@components/Captcha'))
|
||||
|
||||
const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
|
||||
const [ data, setData ] = useState<ResponseProps<SubmittedBot>>(null)
|
||||
const [ captcha, setCaptcha ] = useState(false)
|
||||
const [ touchedSumbit, setTouched ] = useState(false)
|
||||
const AddBot: NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
|
||||
const [data, setData] = useState<ResponseProps<SubmittedBot>>(null)
|
||||
const [captcha, setCaptcha] = useState(false)
|
||||
const [touchedSumbit, setTouched] = useState(false)
|
||||
const captchaRef = useRef<HCaptcha>()
|
||||
const router = useRouter()
|
||||
const initialValues: AddBotSubmit = {
|
||||
@ -58,159 +58,334 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
|
||||
- 기능
|
||||
- 있나요?`,
|
||||
_csrf: csrfToken,
|
||||
_captcha: 'captcha'
|
||||
_captcha: 'captcha',
|
||||
}
|
||||
|
||||
function toLogin() {
|
||||
localStorage.redirectTo = window.location.href
|
||||
redirectTo(router, 'login')
|
||||
}
|
||||
|
||||
|
||||
async function submitBot(value: AddBotSubmit, token: string) {
|
||||
const res = await Fetch<SubmittedBot>(`/bots/${value.id}`, { method: 'POST', body: JSON.stringify(cleanObject<AddBotSubmit>({ ...value, _captcha: token})) })
|
||||
const res = await Fetch<SubmittedBot>(`/bots/${value.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(cleanObject<AddBotSubmit>({ ...value, _captcha: token })),
|
||||
})
|
||||
setData(res)
|
||||
}
|
||||
|
||||
if(!logged) return <Login>
|
||||
<NextSeo title='새로운 봇 추가하기' description='자신의 봇을 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
||||
title:'새로운 봇 추가하기', description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.'
|
||||
}} />
|
||||
</Login>
|
||||
if(data?.data && data.code === 200) {
|
||||
setTimeout(
|
||||
() => redirectTo(router, `/pendingBots/${data.data.id}/${data.data.date}`),
|
||||
1_000
|
||||
if (!logged)
|
||||
return (
|
||||
<Login>
|
||||
<NextSeo
|
||||
title='새로운 봇 추가하기'
|
||||
description='자신의 봇을 한국 디스코드 리스트에 등록하세요.'
|
||||
openGraph={{
|
||||
title: '새로운 봇 추가하기',
|
||||
description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.',
|
||||
}}
|
||||
/>
|
||||
</Login>
|
||||
)
|
||||
if (data?.data && data.code === 200) {
|
||||
setTimeout(() => redirectTo(router, `/pendingBots/${data.data.id}/${data.data.date}`), 1_000)
|
||||
}
|
||||
return (
|
||||
<Container paddingTop className='py-5'>
|
||||
<NextSeo title='새로운 봇 추가하기' description='자신의 봇을 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
||||
title:'새로운 봇 추가하기', description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.'
|
||||
}} />
|
||||
<NextSeo
|
||||
title='새로운 봇 추가하기'
|
||||
description='자신의 봇을 한국 디스코드 리스트에 등록하세요.'
|
||||
openGraph={{
|
||||
title: '새로운 봇 추가하기',
|
||||
description: '자신의 봇을 한국 디스코드 리스트에 등록하세요.',
|
||||
}}
|
||||
/>
|
||||
<h1 className='text-3xl font-bold'>새로운 봇 추가하기</h1>
|
||||
<div className='mt-1 mb-5'>
|
||||
안녕하세요, <span className='font-semibold'>{user.tag === '0' ? `@${user.username}` : `${user.username}#${user.tag}`}</span>님! <a role='button' tabIndex={0} onKeyDown={toLogin} onClick={toLogin} className='text-discord-blurple cursor-pointer outline-none'>본인이 아니신가요?</a>
|
||||
<div className='mb-5 mt-1'>
|
||||
안녕하세요,{' '}
|
||||
<span className='font-semibold'>
|
||||
{user.tag === '0' ? `@${user.username}` : `${user.username}#${user.tag}`}
|
||||
</span>
|
||||
님!{' '}
|
||||
<a
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={toLogin}
|
||||
onClick={toLogin}
|
||||
className='cursor-pointer text-discord-blurple outline-none'
|
||||
>
|
||||
본인이 아니신가요?
|
||||
</a>
|
||||
</div>
|
||||
{
|
||||
data ? data.code == 200 && data.data ? <Message type='success'>
|
||||
<h2 className='text-lg font-extrabold'>봇 신청 성공!</h2>
|
||||
<p>봇을 성공적으로 신청했습니다! 심사 페이지로 리다이렉트됩니다.</p>
|
||||
</Message> : <Message type='error'>
|
||||
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
|
||||
<ul className='list-disc list-inside'>
|
||||
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||
</ul>
|
||||
|
||||
</Message> : <></>
|
||||
}
|
||||
<Formik initialValues={initialValues}
|
||||
{data ? (
|
||||
data.code == 200 && data.data ? (
|
||||
<Message type='success'>
|
||||
<h2 className='text-lg font-extrabold'>봇 신청 성공!</h2>
|
||||
<p>봇을 성공적으로 신청했습니다! 심사 페이지로 리다이렉트됩니다.</p>
|
||||
</Message>
|
||||
) : (
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
|
||||
<ul className='list-inside list-disc'>
|
||||
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||
</ul>
|
||||
</Message>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={AddBotSubmitSchema}
|
||||
onSubmit={() => setCaptcha(true)}>
|
||||
onSubmit={() => setCaptcha(true)}
|
||||
>
|
||||
{({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className='py-3'>
|
||||
<Message type='warning'>
|
||||
<h2 className='text-lg font-extrabold'>신청하시기 전에 다음 사항을 확인해 주세요!</h2>
|
||||
<ul className='list-disc list-inside'>
|
||||
<li><Link
|
||||
href='/discord'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:text-blue-600'>디스코드 서버</Link>에 참가하셨나요?</li>
|
||||
<li>봇이 <Link
|
||||
href='/guidelines'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:text-blue-600'>가이드라인</Link>을 지키고 있나요?</li>
|
||||
<li>봇 소유자가 두 명 이상인가요? 봇 소유자는 봇이 승인된 뒤, 더 추가하실 수 있습니다.</li>
|
||||
<h2 className='text-lg font-extrabold'>
|
||||
신청하시기 전에 다음 사항을 확인해 주세요!
|
||||
</h2>
|
||||
<ul className='list-inside list-disc'>
|
||||
<li>
|
||||
<Link
|
||||
href='/discord'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:text-blue-600'
|
||||
>
|
||||
디스코드 서버
|
||||
</Link>
|
||||
에 참가하셨나요?
|
||||
</li>
|
||||
<li>
|
||||
봇이{' '}
|
||||
<Link
|
||||
href='/guidelines'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:text-blue-600'
|
||||
>
|
||||
가이드라인
|
||||
</Link>
|
||||
을 지키고 있나요?
|
||||
</li>
|
||||
<li>
|
||||
봇 소유자가 두 명 이상인가요? 봇 소유자는 봇이 승인된 뒤, 더 추가하실 수
|
||||
있습니다.
|
||||
</li>
|
||||
<li>또한, 봇을 등록하게 되면 작성하신 모든 정보는 웹과 API에 공개됩니다.</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
<Label For='agree' error={errors.agree && touched.agree ? errors.agree : null} grid={false}>
|
||||
<Label
|
||||
For='agree'
|
||||
error={errors.agree && touched.agree ? errors.agree : null}
|
||||
grid={false}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<CheckBox name='agree' />
|
||||
<strong className='text-sm ml-2'>해당 내용을 숙지하였으며, 모두 이행하였고 위 내용에 해당하는 거부 사유는 답변받지 않는다는 점을 이해합니다.</strong>
|
||||
<strong className='ml-2 text-sm'>
|
||||
해당 내용을 숙지하였으며, 모두 이행하였고 위 내용에 해당하는 거부 사유는 답변받지
|
||||
않는다는 점을 이해합니다.
|
||||
</strong>
|
||||
</div>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Advertisement />
|
||||
<Label For='id' label='봇 ID' labelDesc='봇의 클라이언트 ID를 의미합니다.' error={errors.id && touched.id ? errors.id : null} short required>
|
||||
<Label
|
||||
For='id'
|
||||
label='봇 ID'
|
||||
labelDesc='봇의 클라이언트 ID를 의미합니다.'
|
||||
error={errors.id && touched.id ? errors.id : null}
|
||||
short
|
||||
required
|
||||
>
|
||||
<Input name='id' placeholder='653534001742741552' />
|
||||
</Label>
|
||||
<Label For='prefix' label='접두사' labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)' error={errors.prefix && touched.prefix ? errors.prefix : null} short required>
|
||||
<Label
|
||||
For='prefix'
|
||||
label='접두사'
|
||||
labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)'
|
||||
error={errors.prefix && touched.prefix ? errors.prefix : null}
|
||||
short
|
||||
required
|
||||
>
|
||||
<Input name='prefix' placeholder='!' />
|
||||
</Label>
|
||||
<Label For='library' label='라이브러리' labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.' short required error={errors.library && touched.library ? errors.library : null}>
|
||||
<Select options={library.map(el=> ({ label: el, value: el }))} handleChange={(value) => setFieldValue('library', value.value)} handleTouch={() => setFieldTouched('library', true)} />
|
||||
<Label
|
||||
For='library'
|
||||
label='라이브러리'
|
||||
labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.'
|
||||
short
|
||||
required
|
||||
error={errors.library && touched.library ? errors.library : null}
|
||||
>
|
||||
<Select
|
||||
options={library.map((el) => ({ label: el, value: el }))}
|
||||
handleChange={(value) => setFieldValue('library', value.value)}
|
||||
handleTouch={() => setFieldTouched('library', true)}
|
||||
/>
|
||||
</Label>
|
||||
<Label For='category' label='카테고리' labelDesc='봇에 해당되는 카테고리를 선택해주세요' required error={errors.category && touched.category ? errors.category as string : null}>
|
||||
<Selects options={botCategories.map(el=> ({ label: el, value: el }))} handleChange={(value) => {
|
||||
setFieldValue('category', value.map(v=> v.value))
|
||||
}} handleTouch={() => setFieldTouched('category', true)} values={values.category as string[]} setValues={(value) => setFieldValue('category', value)} />
|
||||
<p className='text-gray-400 mt-1 text-sm'>봇 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요. <strong>반드시 해당되는 카테고리만 선택해주세요.</strong><br/>
|
||||
<a className='text-blue-500 hover:text-blue-400' href='https://contents.koreanbots.dev/categories'>이곳</a>에서 카테고리에 관한 자세한 설명을 확인하실 수 있습니다!</p>
|
||||
<Label
|
||||
For='category'
|
||||
label='카테고리'
|
||||
labelDesc='봇에 해당되는 카테고리를 선택해주세요'
|
||||
required
|
||||
error={errors.category && touched.category ? (errors.category as string) : null}
|
||||
>
|
||||
<Selects
|
||||
options={botCategories.map((el) => ({ label: el, value: el }))}
|
||||
handleChange={(value) => {
|
||||
setFieldValue(
|
||||
'category',
|
||||
value.map((v) => v.value)
|
||||
)
|
||||
}}
|
||||
handleTouch={() => setFieldTouched('category', true)}
|
||||
values={values.category as string[]}
|
||||
setValues={(value) => setFieldValue('category', value)}
|
||||
/>
|
||||
<p className='mt-1 text-sm text-gray-400'>
|
||||
봇 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요.{' '}
|
||||
<strong>반드시 해당되는 카테고리만 선택해주세요.</strong>
|
||||
<br />
|
||||
<a
|
||||
className='text-blue-500 hover:text-blue-400'
|
||||
href='https://contents.koreanbots.dev/categories'
|
||||
>
|
||||
이곳
|
||||
</a>
|
||||
에서 카테고리에 관한 자세한 설명을 확인하실 수 있습니다!
|
||||
</p>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Label For='website' label='웹사이트' labelDesc='봇의 웹사이트를 작성해주세요.' error={errors.website && touched.website ? errors.website : null}>
|
||||
<Label
|
||||
For='website'
|
||||
label='웹사이트'
|
||||
labelDesc='봇의 웹사이트를 작성해주세요.'
|
||||
error={errors.website && touched.website ? errors.website : null}
|
||||
>
|
||||
<Input name='website' placeholder='https://koreanbots.dev' />
|
||||
</Label>
|
||||
<Label For='git' label='Git URL' labelDesc='봇 소스코드의 Git 주소를 입력해주세요. (오픈소스인 경우)' error={errors.git && touched.git ? errors.git : null}>
|
||||
<Input name='git' placeholder='https://github.com/koreanbots/koreanbots'/>
|
||||
<Label
|
||||
For='git'
|
||||
label='Git URL'
|
||||
labelDesc='봇 소스코드의 Git 주소를 입력해주세요. (오픈소스인 경우)'
|
||||
error={errors.git && touched.git ? errors.git : null}
|
||||
>
|
||||
<Input name='git' placeholder='https://github.com/koreanbots/koreanbots' />
|
||||
</Label>
|
||||
<Label For='inviteLink' label='초대링크' labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.' error={errors.url && touched.url ? errors.url : null}>
|
||||
<Input name='url' placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0' />
|
||||
<span className='text-gray-400 mt-1 text-sm'>
|
||||
<Label
|
||||
For='inviteLink'
|
||||
label='초대링크'
|
||||
labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.'
|
||||
error={errors.url && touched.url ? errors.url : null}
|
||||
>
|
||||
<Input
|
||||
name='url'
|
||||
placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0'
|
||||
/>
|
||||
<span className='mt-1 text-sm text-gray-400'>
|
||||
<Link
|
||||
href='/calculator'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:text-blue-400'>
|
||||
이곳
|
||||
</Link>에서 초대링크를 생성하실 수 있습니다!
|
||||
className='text-blue-500 hover:text-blue-400'
|
||||
>
|
||||
이곳
|
||||
</Link>
|
||||
에서 초대링크를 생성하실 수 있습니다!
|
||||
</span>
|
||||
</Label>
|
||||
<Label For='discord' label='지원 디스코드 서버' labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)' error={errors.discord && touched.discord ? errors.discord : null} short>
|
||||
<Label
|
||||
For='discord'
|
||||
label='지원 디스코드 서버'
|
||||
labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)'
|
||||
error={errors.discord && touched.discord ? errors.discord : null}
|
||||
short
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
discord.gg/<Input name='discord' placeholder='JEh53MQ' />
|
||||
discord.gg/
|
||||
<Input name='discord' placeholder='JEh53MQ' />
|
||||
</div>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Label For='intro' label='봇 소개' labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required>
|
||||
<Label
|
||||
For='intro'
|
||||
label='봇 소개'
|
||||
labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)'
|
||||
error={errors.intro && touched.intro ? errors.intro : null}
|
||||
required
|
||||
>
|
||||
<Input name='intro' placeholder='국내 봇을 한 곳에서.' />
|
||||
</Label>
|
||||
<Label For='intro' label='봇 설명' labelDesc={<>봇을 자세하게 설명해주세요! (최대 1500자)<br/>마크다운을 지원합니다!</>} error={errors.desc && touched.desc ? errors.desc : null} required>
|
||||
<TextArea max={1500} name='desc' placeholder='봇에 대해 최대한 자세히 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.desc} setValue={(value) => setFieldValue('desc', value)} />
|
||||
<Label
|
||||
For='intro'
|
||||
label='봇 설명'
|
||||
labelDesc={
|
||||
<>
|
||||
봇을 자세하게 설명해주세요! (최대 1500자)
|
||||
<br />
|
||||
마크다운을 지원합니다!
|
||||
</>
|
||||
}
|
||||
error={errors.desc && touched.desc ? errors.desc : null}
|
||||
required
|
||||
>
|
||||
<TextArea
|
||||
max={1500}
|
||||
name='desc'
|
||||
placeholder='봇에 대해 최대한 자세히 설명해주세요!'
|
||||
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||
value={values.desc}
|
||||
setValue={(value) => setFieldValue('desc', value)}
|
||||
/>
|
||||
</Label>
|
||||
<Label For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다.'>
|
||||
<Label
|
||||
For='preview'
|
||||
label='설명 미리보기'
|
||||
labelDesc='다음 결과는 실제와 다를 수 있습니다.'
|
||||
>
|
||||
<Segment>
|
||||
<Markdown text={values.desc} />
|
||||
</Segment>
|
||||
</Label>
|
||||
<Divider />
|
||||
<p className='text-base mt-2 mb-5'>
|
||||
<span className='text-red-500 font-semibold'> *</span> = 필수 항목
|
||||
<p className='mb-5 mt-2 text-base'>
|
||||
<span className='font-semibold text-red-500'> *</span> = 필수 항목
|
||||
</p>
|
||||
{
|
||||
captcha ? <Captcha ref={captchaRef} dark={theme === 'dark'} onVerify={(token) => {
|
||||
submitBot(values, token)
|
||||
window.scrollTo({ top: 0 })
|
||||
setCaptcha(false)
|
||||
captchaRef?.current?.resetCaptcha()
|
||||
}} /> : <>
|
||||
{
|
||||
touchedSumbit && !isValid && <div className='my-1 text-red-500 text-xs font-light'>누락되거나 잘못된 항목이 있습니다. 다시 확인해주세요.</div>
|
||||
}
|
||||
<Button type='submit' onClick={() => {
|
||||
setTouched(true)
|
||||
if(!isValid) window.scrollTo({ top: 0 })
|
||||
} }>
|
||||
{captcha ? (
|
||||
<Captcha
|
||||
ref={captchaRef}
|
||||
dark={theme === 'dark'}
|
||||
onVerify={(token) => {
|
||||
submitBot(values, token)
|
||||
window.scrollTo({ top: 0 })
|
||||
setCaptcha(false)
|
||||
captchaRef?.current?.resetCaptcha()
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{touchedSumbit && !isValid && (
|
||||
<div className='my-1 text-xs font-light text-red-500'>
|
||||
누락되거나 잘못된 항목이 있습니다. 다시 확인해주세요.
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type='submit'
|
||||
onClick={() => {
|
||||
setTouched(true)
|
||||
if (!isValid) window.scrollTo({ top: 0 })
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<i className='far fa-paper-plane'/> 제출
|
||||
<i className='far fa-paper-plane' /> 제출
|
||||
</>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
@ -222,7 +397,13 @@ const AddBot:NextPage<AddBotProps> = ({ logged, user, csrfToken, theme }) => {
|
||||
export const getServerSideProps = async (ctx: NextPageContext) => {
|
||||
const parsed = parseCookie(ctx.req)
|
||||
const user = await get.Authorization(parsed?.token)
|
||||
return { props: { logged: !!user, user: await get.user.load(user || ''), csrfToken: getToken(ctx.req, ctx.res) } }
|
||||
return {
|
||||
props: {
|
||||
logged: !!user,
|
||||
user: await get.user.load(user || ''),
|
||||
csrfToken: getToken(ctx.req, ctx.res),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface AddBotProps {
|
||||
|
||||
@ -30,10 +30,17 @@ const Container = dynamic(() => import('@components/Container'))
|
||||
const Message = dynamic(() => import('@components/Message'))
|
||||
const Captcha = dynamic(() => import('@components/Captcha'))
|
||||
|
||||
const AddServer:NextPage<AddServerProps> = ({ logged, user, csrfToken, server, serverData, theme }) => {
|
||||
const [ data, setData ] = useState<ResponseProps<AddServerSubmit>>(null)
|
||||
const [ captcha, setCaptcha ] = useState(false)
|
||||
const [ touchedSumbit, setTouched ] = useState(false)
|
||||
const AddServer: NextPage<AddServerProps> = ({
|
||||
logged,
|
||||
user,
|
||||
csrfToken,
|
||||
server,
|
||||
serverData,
|
||||
theme,
|
||||
}) => {
|
||||
const [data, setData] = useState<ResponseProps<AddServerSubmit>>(null)
|
||||
const [captcha, setCaptcha] = useState(false)
|
||||
const [touchedSumbit, setTouched] = useState(false)
|
||||
const captchaRef = useRef<HCaptcha>()
|
||||
const router = useRouter()
|
||||
const initialValues: AddServerSubmit = {
|
||||
@ -58,151 +65,284 @@ const AddServer:NextPage<AddServerProps> = ({ logged, user, csrfToken, server, s
|
||||
- 있나요?`,
|
||||
category: [],
|
||||
_csrf: csrfToken,
|
||||
_captcha: 'captcha'
|
||||
_captcha: 'captcha',
|
||||
}
|
||||
|
||||
function toLogin() {
|
||||
localStorage.redirectTo = window.location.href
|
||||
redirectTo(router, 'login')
|
||||
}
|
||||
|
||||
|
||||
async function submitServer(id: string, value: AddServerSubmit, token: string) {
|
||||
const res = await Fetch<AddServerSubmit>(`/servers/${id}`, { method: 'POST', body: JSON.stringify(cleanObject<AddServerSubmit>({ ...value, _captcha: token })) })
|
||||
const res = await Fetch<AddServerSubmit>(`/servers/${id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(cleanObject<AddServerSubmit>({ ...value, _captcha: token })),
|
||||
})
|
||||
setData(res)
|
||||
}
|
||||
|
||||
if(!logged) return <Login>
|
||||
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
||||
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||
}} />
|
||||
</Login>
|
||||
if(data?.data && data.code == 200) {
|
||||
setTimeout(
|
||||
() => redirectTo(router, `/servers/${router.query.id}`),
|
||||
1_000
|
||||
if (!logged)
|
||||
return (
|
||||
<Login>
|
||||
<NextSeo
|
||||
title='새로운 서버 추가하기'
|
||||
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||
openGraph={{
|
||||
title: '새로운 서버 추가하기',
|
||||
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
|
||||
}}
|
||||
/>
|
||||
</Login>
|
||||
)
|
||||
if (data?.data && data.code == 200) {
|
||||
setTimeout(() => redirectTo(router, `/servers/${router.query.id}`), 1_000)
|
||||
}
|
||||
return (
|
||||
<Container paddingTop className='py-5'>
|
||||
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
||||
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||
}} />
|
||||
<NextSeo
|
||||
title='새로운 서버 추가하기'
|
||||
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||
openGraph={{
|
||||
title: '새로운 서버 추가하기',
|
||||
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
|
||||
}}
|
||||
/>
|
||||
<h1 className='text-3xl font-bold'>새로운 서버 추가하기</h1>
|
||||
<div className='mt-1 mb-5'>
|
||||
안녕하세요, <span className='font-semibold'>{user.username}#{user.tag}</span>님! <a role='button' tabIndex={0} onKeyDown={toLogin} onClick={toLogin} className='text-discord-blurple cursor-pointer outline-none'>본인이 아니신가요?</a>
|
||||
<div className='mb-5 mt-1'>
|
||||
안녕하세요,{' '}
|
||||
<span className='font-semibold'>
|
||||
{user.username}#{user.tag}
|
||||
</span>
|
||||
님!{' '}
|
||||
<a
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={toLogin}
|
||||
onClick={toLogin}
|
||||
className='cursor-pointer text-discord-blurple outline-none'
|
||||
>
|
||||
본인이 아니신가요?
|
||||
</a>
|
||||
</div>
|
||||
{
|
||||
data ? data.code == 200 && data.data ? <Message type='success'>
|
||||
<h2 className='text-lg font-extrabold'>서버 등록 성공!</h2>
|
||||
<p>서버를 성공적으로 등록했습니다! 서버 페이지로 리다이렉트 됩니다!</p>
|
||||
</Message> : <Message type='error'>
|
||||
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
|
||||
<ul className='list-disc list-inside'>
|
||||
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||
</ul>
|
||||
|
||||
</Message> : <></>
|
||||
}
|
||||
{
|
||||
server ? <Message type='warning'>
|
||||
<h2 className='text-lg font-extrabold'>이미 등록된 서버입니다.</h2>
|
||||
</Message> :
|
||||
!serverData ? <Message type='info'>
|
||||
<h2 className='text-lg font-extrabold'>서버에 봇이 초대되지 않았습니다.</h2>
|
||||
<p>서버를 등록하시려면 먼저 봇을 초대해야합니다.</p>
|
||||
<p>서버에 이미 봇이 초대되었다면 반영까지 최대 1분이 소요될 수 있습니다.</p>
|
||||
{data ? (
|
||||
data.code == 200 && data.data ? (
|
||||
<Message type='success'>
|
||||
<h2 className='text-lg font-extrabold'>서버 등록 성공!</h2>
|
||||
<p>서버를 성공적으로 등록했습니다! 서버 페이지로 리다이렉트 됩니다!</p>
|
||||
</Message>
|
||||
: serverData.admins.includes(user.id) || serverData.owner.includes(user.id) ? <Formik initialValues={initialValues}
|
||||
validationSchema={AddServerSubmitSchema}
|
||||
onSubmit={() => setCaptcha(true)}>
|
||||
{({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className='py-3'>
|
||||
<Message type='warning'>
|
||||
<h2 className='text-lg font-extrabold'>등록하시기 전에 다음 사항을 확인해 주세요!</h2>
|
||||
<ul className='list-disc list-inside'>
|
||||
<li><Link
|
||||
href='/discord'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:text-blue-600'>디스코드 서버</Link>에 참여를 권장드립니다.</li>
|
||||
<li>서버가 <Link
|
||||
href='/guidelines'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:text-blue-600'>가이드라인</Link>을 지키고 있나요?</li>
|
||||
<li>서버에서 <strong>관리자</strong> 권한을 갖고 있는 모든 분은 삭제를 제외한 모든 행동을 할 수 있습니다.</li>
|
||||
<li>서버를 등록한 이후 봇을 추방하시게 되면 서버 정보가 웹에 업데이트 되지 않습니다.</li>
|
||||
<li>또한, 서버를 등록하게 되면 작성하신 모든 정보와 서버에서 수집된 정보는 웹과 API에 공개됩니다.</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
<Label For='agree' error={errors.agree && touched.agree ? errors.agree : null} grid={false}>
|
||||
<div className='flex items-center'>
|
||||
<CheckBox name='agree' />
|
||||
<strong className='text-sm ml-2'>해당 내용을 숙지하였으며, 등록 이후에 가이드라인을 위반할시 서버가 웹에서 삭제될 수 있다는 점을 확인했습니다.</strong>
|
||||
) : (
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
|
||||
<ul className='list-inside list-disc'>
|
||||
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||
</ul>
|
||||
</Message>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{server ? (
|
||||
<Message type='warning'>
|
||||
<h2 className='text-lg font-extrabold'>이미 등록된 서버입니다.</h2>
|
||||
</Message>
|
||||
) : !serverData ? (
|
||||
<Message type='info'>
|
||||
<h2 className='text-lg font-extrabold'>서버에 봇이 초대되지 않았습니다.</h2>
|
||||
<p>서버를 등록하시려면 먼저 봇을 초대해야합니다.</p>
|
||||
<p>서버에 이미 봇이 초대되었다면 반영까지 최대 1분이 소요될 수 있습니다.</p>
|
||||
</Message>
|
||||
) : serverData.admins.includes(user.id) || serverData.owner.includes(user.id) ? (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={AddServerSubmitSchema}
|
||||
onSubmit={() => setCaptcha(true)}
|
||||
>
|
||||
{({ errors, touched, values, isValid, setFieldTouched, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className='py-3'>
|
||||
<Message type='warning'>
|
||||
<h2 className='text-lg font-extrabold'>
|
||||
등록하시기 전에 다음 사항을 확인해 주세요!
|
||||
</h2>
|
||||
<ul className='list-inside list-disc'>
|
||||
<li>
|
||||
<Link
|
||||
href='/discord'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:text-blue-600'
|
||||
>
|
||||
디스코드 서버
|
||||
</Link>
|
||||
에 참여를 권장드립니다.
|
||||
</li>
|
||||
<li>
|
||||
서버가{' '}
|
||||
<Link
|
||||
href='/guidelines'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:text-blue-600'
|
||||
>
|
||||
가이드라인
|
||||
</Link>
|
||||
을 지키고 있나요?
|
||||
</li>
|
||||
<li>
|
||||
서버에서 <strong>관리자</strong> 권한을 갖고 있는 모든 분은 삭제를 제외한 모든
|
||||
행동을 할 수 있습니다.
|
||||
</li>
|
||||
<li>
|
||||
서버를 등록한 이후 봇을 추방하시게 되면 서버 정보가 웹에 업데이트 되지
|
||||
않습니다.
|
||||
</li>
|
||||
<li>
|
||||
또한, 서버를 등록하게 되면 작성하신 모든 정보와 서버에서 수집된 정보는 웹과
|
||||
API에 공개됩니다.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
<Label
|
||||
For='agree'
|
||||
error={errors.agree && touched.agree ? errors.agree : null}
|
||||
grid={false}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<CheckBox name='agree' />
|
||||
<strong className='ml-2 text-sm'>
|
||||
해당 내용을 숙지하였으며, 등록 이후에 가이드라인을 위반할시 서버가 웹에서 삭제될
|
||||
수 있다는 점을 확인했습니다.
|
||||
</strong>
|
||||
</div>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Label For='id' label='서버' labelDesc='등록하시는 대상 서버 입니다.'>
|
||||
<p>
|
||||
<strong>{serverData.name}</strong>
|
||||
<br /> ID: {router.query.id}
|
||||
</p>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Label
|
||||
For='category'
|
||||
label='카테고리'
|
||||
labelDesc='서버에 해당되는 카테고리를 선택해주세요'
|
||||
required
|
||||
error={errors.category && touched.category ? (errors.category as string) : null}
|
||||
>
|
||||
<Selects
|
||||
options={serverCategories.map((el) => ({ label: el, value: el }))}
|
||||
handleChange={(value) => {
|
||||
setFieldValue(
|
||||
'category',
|
||||
value.map((v) => v.value)
|
||||
)
|
||||
}}
|
||||
handleTouch={() => setFieldTouched('category', true)}
|
||||
values={values.category as string[]}
|
||||
setValues={(value) => setFieldValue('category', value)}
|
||||
/>
|
||||
<span className='mt-1 text-sm text-gray-400'>
|
||||
서버 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요.{' '}
|
||||
<strong>반드시 해당되는 카테고리만 선택해주세요.</strong>
|
||||
</span>
|
||||
</Label>
|
||||
<Label
|
||||
For='invite'
|
||||
label='서버 초대코드'
|
||||
labelDesc='서버의 초대코드를 입력해주세요. (만료되지 않는 코드로 입력해주세요!)'
|
||||
error={errors.invite && touched.invite ? errors.invite : null}
|
||||
short
|
||||
required
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
discord.gg/
|
||||
<Input name='invite' placeholder='JEh53MQ' />
|
||||
</div>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Label
|
||||
For='intro'
|
||||
label='서버 소개'
|
||||
labelDesc='서버를 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)'
|
||||
error={errors.intro && touched.intro ? errors.intro : null}
|
||||
required
|
||||
>
|
||||
<Input name='intro' placeholder={getRandom(ServerIntroList)} />
|
||||
</Label>
|
||||
<Label
|
||||
For='desc'
|
||||
label='서버 설명'
|
||||
labelDesc={
|
||||
<>
|
||||
서버를 자세하게 설명해주세요! (최대 1500자)
|
||||
<br />
|
||||
마크다운을 지원합니다!
|
||||
</>
|
||||
}
|
||||
error={errors.desc && touched.desc ? errors.desc : null}
|
||||
required
|
||||
>
|
||||
<TextArea
|
||||
max={1500}
|
||||
name='desc'
|
||||
placeholder='서버에 대해 최대한 자세히 설명해주세요!'
|
||||
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||
value={values.desc}
|
||||
setValue={(value) => setFieldValue('desc', value)}
|
||||
/>
|
||||
</Label>
|
||||
<Label
|
||||
For='preview'
|
||||
label='설명 미리보기'
|
||||
labelDesc='다음 결과는 실제와 다를 수 있습니다.'
|
||||
>
|
||||
<Segment>
|
||||
<Markdown text={values.desc} />
|
||||
</Segment>
|
||||
</Label>
|
||||
<Divider />
|
||||
<p className='mb-5 mt-2 text-base'>
|
||||
<span className='font-semibold text-red-500'> *</span> = 필수 항목
|
||||
</p>
|
||||
{captcha ? (
|
||||
<Captcha
|
||||
ref={captchaRef}
|
||||
dark={theme === 'dark'}
|
||||
onVerify={(token) => {
|
||||
submitServer(router.query.id as string, values, token)
|
||||
window.scrollTo({ top: 0 })
|
||||
setCaptcha(false)
|
||||
captchaRef?.current?.resetCaptcha()
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{touchedSumbit && !isValid && (
|
||||
<div className='my-1 text-xs font-light text-red-500'>
|
||||
누락되거나 잘못된 항목이 있습니다. 다시 확인해주세요.
|
||||
</div>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Label For='id' label='서버' labelDesc='등록하시는 대상 서버 입니다.'>
|
||||
<p>
|
||||
<strong>{serverData.name}</strong>
|
||||
<br/> ID: {router.query.id}
|
||||
</p>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Label For='category' label='카테고리' labelDesc='서버에 해당되는 카테고리를 선택해주세요' required error={errors.category && touched.category ? errors.category as string : null}>
|
||||
<Selects options={serverCategories.map(el=> ({ label: el, value: el }))} handleChange={(value) => {
|
||||
setFieldValue('category', value.map(v=> v.value))
|
||||
}} handleTouch={() => setFieldTouched('category', true)} values={values.category as string[]} setValues={(value) => setFieldValue('category', value)} />
|
||||
<span className='text-gray-400 mt-1 text-sm'>서버 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요. <strong>반드시 해당되는 카테고리만 선택해주세요.</strong></span>
|
||||
</Label>
|
||||
<Label For='invite' label='서버 초대코드' labelDesc='서버의 초대코드를 입력해주세요. (만료되지 않는 코드로 입력해주세요!)' error={errors.invite && touched.invite ? errors.invite : null} short required>
|
||||
<div className='flex items-center'>
|
||||
discord.gg/<Input name='invite' placeholder='JEh53MQ' />
|
||||
</div>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Label For='intro' label='서버 소개' labelDesc='서버를 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required>
|
||||
<Input name='intro' placeholder={getRandom(ServerIntroList)} />
|
||||
</Label>
|
||||
<Label For='desc' label='서버 설명' labelDesc={<>서버를 자세하게 설명해주세요! (최대 1500자)<br/>마크다운을 지원합니다!</>} error={errors.desc && touched.desc ? errors.desc : null} required>
|
||||
<TextArea max={1500} name='desc' placeholder='서버에 대해 최대한 자세히 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.desc} setValue={(value) => setFieldValue('desc', value)} />
|
||||
</Label>
|
||||
<Label For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다.'>
|
||||
<Segment>
|
||||
<Markdown text={values.desc} />
|
||||
</Segment>
|
||||
</Label>
|
||||
<Divider />
|
||||
<p className='text-base mt-2 mb-5'>
|
||||
<span className='text-red-500 font-semibold'> *</span> = 필수 항목
|
||||
</p>
|
||||
{
|
||||
captcha ? <Captcha ref={captchaRef} dark={theme === 'dark'} onVerify={(token) => {
|
||||
submitServer(router.query.id as string, values, token)
|
||||
window.scrollTo({ top: 0 })
|
||||
setCaptcha(false)
|
||||
captchaRef?.current?.resetCaptcha()
|
||||
}} /> : <>
|
||||
{
|
||||
touchedSumbit && !isValid && <div className='my-1 text-red-500 text-xs font-light'>누락되거나 잘못된 항목이 있습니다. 다시 확인해주세요.</div>
|
||||
}
|
||||
<Button type='submit' onClick={() => {
|
||||
setTouched(true)
|
||||
if(!isValid) window.scrollTo({ top: 0 })
|
||||
} }>
|
||||
<>
|
||||
<i className='far fa-paper-plane'/> 등록
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type='submit'
|
||||
onClick={() => {
|
||||
setTouched(true)
|
||||
if (!isValid) window.scrollTo({ top: 0 })
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<i className='far fa-paper-plane' /> 등록
|
||||
</>
|
||||
}
|
||||
</Form>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
: <Forbidden />
|
||||
}
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
) : (
|
||||
<Forbidden />
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -212,12 +352,16 @@ export const getServerSideProps = async (ctx: NextPageContext) => {
|
||||
const user = await get.Authorization(parsed?.token)
|
||||
const server = (await get.server.load(ctx.query.id as string)) || null
|
||||
const serverData = (await get.serverData(ctx.query.id as string)) || null
|
||||
return { props: {
|
||||
logged: !!user, user: await get.user.load(user || ''),
|
||||
csrfToken: getToken(ctx.req, ctx.res),
|
||||
server,
|
||||
serverData: (+new Date() - +new Date(serverData?.updatedAt)) < 2 * 60 * 1000 ? serverData : null
|
||||
} }
|
||||
return {
|
||||
props: {
|
||||
logged: !!user,
|
||||
user: await get.user.load(user || ''),
|
||||
csrfToken: getToken(ctx.req, ctx.res),
|
||||
server,
|
||||
serverData:
|
||||
+new Date() - +new Date(serverData?.updatedAt) < 2 * 60 * 1000 ? serverData : null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface AddServerProps {
|
||||
@ -229,4 +373,4 @@ interface AddServerProps {
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
export default AddServer
|
||||
export default AddServer
|
||||
|
||||
@ -3,7 +3,7 @@ import dynamic from 'next/dynamic'
|
||||
import { NextSeo } from 'next-seo'
|
||||
|
||||
import { get } from '@utils/Query'
|
||||
import { parseCookie} from '@utils/Tools'
|
||||
import { parseCookie } from '@utils/Tools'
|
||||
import { RawGuild, ServerData, Theme, User } from '@types'
|
||||
|
||||
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
||||
@ -12,40 +12,71 @@ const ServerCard = dynamic(() => import('@components/ServerCard'))
|
||||
const Login = dynamic(() => import('@components/Login'))
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
|
||||
const AddBot:NextPage<AddBotProps> = ({ logged, guilds }) => {
|
||||
if(!logged) return <Login>
|
||||
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
||||
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||
}} />
|
||||
</Login>
|
||||
return <Container paddingTop className='py-5'>
|
||||
<NextSeo title='새로운 서버 추가하기' description='자신의 서버를 한국 디스코드 리스트에 등록하세요.' openGraph={{
|
||||
title:'새로운 서버 추가하기', description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||
}} />
|
||||
<h1 className='text-3xl font-bold'>새로운 서버 추가하기</h1>
|
||||
<p className='text-gray-400'>관리자이신 서버 목록입니다.</p>
|
||||
<p className='text-gray-400 pb-5'>봇을 초대한 뒤 새로고침 해주세요. 또한, 반영까지 최대 1분이 소요될 수 있습니다.</p>
|
||||
<Advertisement />
|
||||
<ResponsiveGrid>
|
||||
{
|
||||
guilds.sort((a ,b) => (+!!b.data || 0) - (+!!a.data || 0)).map(g => (
|
||||
<ServerCard type={g.exists ? 'manage' : 'add'} server={g} key={g.id} />
|
||||
))
|
||||
}
|
||||
</ResponsiveGrid>
|
||||
<Advertisement />
|
||||
</Container>
|
||||
const AddBot: NextPage<AddBotProps> = ({ logged, guilds }) => {
|
||||
if (!logged)
|
||||
return (
|
||||
<Login>
|
||||
<NextSeo
|
||||
title='새로운 서버 추가하기'
|
||||
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||
openGraph={{
|
||||
title: '새로운 서버 추가하기',
|
||||
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
|
||||
}}
|
||||
/>
|
||||
</Login>
|
||||
)
|
||||
return (
|
||||
<Container paddingTop className='py-5'>
|
||||
<NextSeo
|
||||
title='새로운 서버 추가하기'
|
||||
description='자신의 서버를 한국 디스코드 리스트에 등록하세요.'
|
||||
openGraph={{
|
||||
title: '새로운 서버 추가하기',
|
||||
description: '자신의 서버를 한국 디스코드 리스트에 등록하세요.',
|
||||
}}
|
||||
/>
|
||||
<h1 className='text-3xl font-bold'>새로운 서버 추가하기</h1>
|
||||
<p className='text-gray-400'>관리자이신 서버 목록입니다.</p>
|
||||
<p className='pb-5 text-gray-400'>
|
||||
봇을 초대한 뒤 새로고침 해주세요. 또한, 반영까지 최대 1분이 소요될 수 있습니다.
|
||||
</p>
|
||||
<Advertisement />
|
||||
<ResponsiveGrid>
|
||||
{guilds
|
||||
.sort((a, b) => (+!!b.data || 0) - (+!!a.data || 0))
|
||||
.map((g) => (
|
||||
<ServerCard type={g.exists ? 'manage' : 'add'} server={g} key={g.id} />
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
<Advertisement />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (ctx: NextPageContext) => {
|
||||
const parsed = parseCookie(ctx.req)
|
||||
const user = await get.Authorization(parsed?.token)
|
||||
const guilds = (await get.userGuilds.load(user || ''))?.filter(g=> (g.permissions & 8) || g.owner).map(async g => {
|
||||
const server = (await get.server.load(g.id))
|
||||
const data = await get.serverData(g.id)
|
||||
return { ...g, ...(server || {}), ...((+new Date() - +new Date(data?.updatedAt)) < 2 * 60 * 1000 ? { data } : {}), members: data?.memberCount || null, exists: !!server }
|
||||
})
|
||||
return { props: { logged: !!user || !!guilds, user: await get.user.load(user || ''), guilds: guilds ? (await Promise.all(guilds)).filter(g => !g?.exists) : null } }
|
||||
const guilds = (await get.userGuilds.load(user || ''))
|
||||
?.filter((g) => g.permissions & 8 || g.owner)
|
||||
.map(async (g) => {
|
||||
const server = await get.server.load(g.id)
|
||||
const data = await get.serverData(g.id)
|
||||
return {
|
||||
...g,
|
||||
...(server || {}),
|
||||
...(+new Date() - +new Date(data?.updatedAt) < 2 * 60 * 1000 ? { data } : {}),
|
||||
members: data?.memberCount || null,
|
||||
exists: !!server,
|
||||
}
|
||||
})
|
||||
return {
|
||||
props: {
|
||||
logged: !!user || !!guilds,
|
||||
user: await get.user.load(user || ''),
|
||||
guilds: guilds ? (await Promise.all(guilds)).filter((g) => !g?.exists) : null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface AddBotProps {
|
||||
@ -53,7 +84,7 @@ interface AddBotProps {
|
||||
user: User
|
||||
csrfToken: string
|
||||
theme: Theme
|
||||
guilds: (RawGuild & { data: ServerData, exists?: boolean })[]
|
||||
guilds: (RawGuild & { data: ServerData; exists?: boolean })[]
|
||||
}
|
||||
|
||||
export default AddBot
|
||||
export default AddBot
|
||||
|
||||
@ -13,8 +13,8 @@ import RequestHandler from '@utils/RequestHandler'
|
||||
|
||||
const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
const validate = await OauthCallbackSchema.validate(req.query)
|
||||
.then(r => r)
|
||||
.catch(e => {
|
||||
.then((r) => r)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
@ -35,7 +35,7 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}).then(r => r.json())
|
||||
}).then((r) => r.json())
|
||||
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
|
||||
|
||||
const user: DiscordUserInfo = await fetch(DiscordEnpoints.Me, {
|
||||
@ -43,7 +43,7 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
headers: {
|
||||
Authorization: `${token.token_type} ${token.access_token}`,
|
||||
},
|
||||
}).then(r => r.json())
|
||||
}).then((r) => r.json())
|
||||
|
||||
const userToken = await update.assignToken({
|
||||
id: user.id,
|
||||
@ -53,11 +53,13 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
discriminator: user.discriminator,
|
||||
verified: user.verified
|
||||
verified: user.verified,
|
||||
})
|
||||
|
||||
if(userToken === 1) return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/unverified')
|
||||
else if(userToken === 2) return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/blocked')
|
||||
if (userToken === 1)
|
||||
return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/unverified')
|
||||
else if (userToken === 2)
|
||||
return res.redirect(301, 'https://docs.koreanbots.dev/bots/account/blocked')
|
||||
const info = verify(userToken)
|
||||
res.setHeader(
|
||||
'set-cookie',
|
||||
|
||||
@ -10,8 +10,8 @@ import RequestHandler from '@utils/RequestHandler'
|
||||
|
||||
const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
const validate = await OauthCallbackSchema.validate(req.query)
|
||||
.then(r => r)
|
||||
.catch(e => {
|
||||
.then((r) => r)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
@ -20,21 +20,29 @@ const Callback = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const token: GithubTokenInfo = await fetch(SpecialEndPoints.Github.Token(process.env.GITHUB_CLIENT_ID, process.env.GITHUB_CLIENT_SECRET,req.query.code), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
}).then(r => r.json())
|
||||
const token: GithubTokenInfo = await fetch(
|
||||
SpecialEndPoints.Github.Token(
|
||||
process.env.GITHUB_CLIENT_ID,
|
||||
process.env.GITHUB_CLIENT_SECRET,
|
||||
req.query.code
|
||||
),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
).then((r) => r.json())
|
||||
if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] })
|
||||
|
||||
|
||||
const github: { login: string } = await fetch(SpecialEndPoints.Github.Me, {
|
||||
headers: {
|
||||
Authorization: `token ${token.access_token}`
|
||||
}
|
||||
}).then(r => r.json())
|
||||
Authorization: `token ${token.access_token}`,
|
||||
},
|
||||
}).then((r) => r.json())
|
||||
const result = await update.Github(user, github.login)
|
||||
if(result === 0) return ResponseWrapper(res, { code: 400, message: '이미 등록되어있는 깃허브 계정입니다.' })
|
||||
if (result === 0)
|
||||
return ResponseWrapper(res, { code: 400, message: '이미 등록되어있는 깃허브 계정입니다.' })
|
||||
get.user.clear(user)
|
||||
res.redirect(301, '/panel')
|
||||
})
|
||||
|
||||
@ -5,17 +5,15 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { get, update } from '@utils/Query'
|
||||
import { checkToken } from '@utils/Csrf'
|
||||
|
||||
const Github = RequestHandler().get(async (_req: NextApiRequest, res: NextApiResponse) => {
|
||||
res.redirect(
|
||||
301,
|
||||
generateOauthURL('github', process.env.GITHUB_CLIENT_ID)
|
||||
)
|
||||
})
|
||||
const Github = RequestHandler()
|
||||
.get(async (_req: NextApiRequest, res: NextApiResponse) => {
|
||||
res.redirect(301, generateOauthURL('github', process.env.GITHUB_CLIENT_ID))
|
||||
})
|
||||
.delete(async (req: DeleteApiRequest, res) => {
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if(!csrfValidated) return
|
||||
if (!csrfValidated) return
|
||||
await update.Github(user, null)
|
||||
get.user.clear(user)
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
|
||||
@ -11,7 +11,9 @@ const rateLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 150,
|
||||
handler: async (_req, res) => {
|
||||
const img = await get.images.user.load(DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' }))
|
||||
const img = await get.images.user.load(
|
||||
DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' })
|
||||
)
|
||||
res.setHeader('Content-Type', 'image/png')
|
||||
res.setHeader('Cache-Control', 'no-cache')
|
||||
res.send(img)
|
||||
@ -20,45 +22,57 @@ const rateLimiter = rateLimit({
|
||||
skip: (_req, res) => {
|
||||
res.removeHeader('X-RateLimit-Global')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const Avatar = RequestHandler()
|
||||
.get(rateLimiter)
|
||||
.get(async(req: ApiRequest, res) => {
|
||||
.get(async (req: ApiRequest, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', process.env.KOREANBOTS_URL)
|
||||
const { id: param, size='256' } = req.query
|
||||
const { id: param, size = '256' } = req.query
|
||||
const splitted = param.split('.')
|
||||
let ext = splitted[1]
|
||||
const id = splitted[0]
|
||||
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false }).then(el=> el).catch(e=> {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
if(!validated) return
|
||||
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false })
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
if (!validated) return
|
||||
|
||||
const user = await get.discord.user.load(id)
|
||||
let img: Buffer
|
||||
if(!user?.avatar) img = await get.images.user.load(DiscordEnpoints.CDN.default(user?.discriminator ? Number(user.discriminator) % 5 : Math.floor(Math.random() * 6), { format: 'png', size: validated.size }))
|
||||
else img = await get.images.user.load(DiscordEnpoints.CDN.user(id, user.avatar, { format: validated.ext === 'gif' && !user.avatar.startsWith('a_') ? 'png' : validated.ext }))
|
||||
if(!img) {
|
||||
img = await get.images.user.load(DiscordEnpoints.CDN.default(user.discriminator, { format: 'png', size: validated.size }))
|
||||
if (!user?.avatar)
|
||||
img = await get.images.user.load(
|
||||
DiscordEnpoints.CDN.default(
|
||||
user?.discriminator ? Number(user.discriminator) % 5 : Math.floor(Math.random() * 6),
|
||||
{ format: 'png', size: validated.size }
|
||||
)
|
||||
)
|
||||
else
|
||||
img = await get.images.user.load(
|
||||
DiscordEnpoints.CDN.user(id, user.avatar, {
|
||||
format: validated.ext === 'gif' && !user.avatar.startsWith('a_') ? 'png' : validated.ext,
|
||||
})
|
||||
)
|
||||
if (!img) {
|
||||
img = await get.images.user.load(
|
||||
DiscordEnpoints.CDN.default(user.discriminator, { format: 'png', size: validated.size })
|
||||
)
|
||||
ext = 'png'
|
||||
}
|
||||
|
||||
|
||||
res.setHeader('Content-Type', `image/${ext}`)
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400')
|
||||
res.send(img)
|
||||
})
|
||||
|
||||
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
size?: '128' | '256' | '512'
|
||||
size?: '128' | '256' | '512'
|
||||
}
|
||||
}
|
||||
|
||||
export default Avatar
|
||||
export default Avatar
|
||||
|
||||
@ -11,7 +11,9 @@ const rateLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 150,
|
||||
handler: async (_req, res) => {
|
||||
const img = await get.images.server.load(DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' }))
|
||||
const img = await get.images.server.load(
|
||||
DiscordEnpoints.CDN.default(Math.floor(Math.random() * 6), { format: 'png' })
|
||||
)
|
||||
res.setHeader('Content-Type', 'image/png')
|
||||
res.setHeader('Cache-Control', 'no-cache')
|
||||
res.send(img)
|
||||
@ -20,45 +22,51 @@ const rateLimiter = rateLimit({
|
||||
skip: (_req, res) => {
|
||||
res.removeHeader('X-RateLimit-Global')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const Icon = RequestHandler()
|
||||
.get(rateLimiter)
|
||||
.get(async(req: ApiRequest, res) => {
|
||||
.get(async (req: ApiRequest, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', process.env.KOREANBOTS_URL)
|
||||
const { id: param, size='256' } = req.query
|
||||
const { id: param, size = '256' } = req.query
|
||||
const splitted = param.split('.')
|
||||
let ext = splitted[1]
|
||||
const id = splitted[0]
|
||||
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false }).then(el=> el).catch(e=> {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
if(!validated) return
|
||||
const validated = await ImageOptionsSchema.validate({ id, ext, size }, { abortEarly: false })
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
if (!validated) return
|
||||
|
||||
const guild = await get.server.load(id)
|
||||
let img: Buffer
|
||||
if(!guild?.icon) img = await get.images.server.load(DiscordEnpoints.CDN.default(+id % 4))
|
||||
else img = await get.images.server.load(DiscordEnpoints.CDN.guild(id, guild.icon, { format: validated.ext === 'gif' && !guild.icon.startsWith('a_') ? 'png' : validated.ext }))
|
||||
if(!img) {
|
||||
img = await get.images.server.load(DiscordEnpoints.CDN.default(+id % 4, { format: 'png', size: validated.size }))
|
||||
if (!guild?.icon) img = await get.images.server.load(DiscordEnpoints.CDN.default(+id % 4))
|
||||
else
|
||||
img = await get.images.server.load(
|
||||
DiscordEnpoints.CDN.guild(id, guild.icon, {
|
||||
format: validated.ext === 'gif' && !guild.icon.startsWith('a_') ? 'png' : validated.ext,
|
||||
})
|
||||
)
|
||||
if (!img) {
|
||||
img = await get.images.server.load(
|
||||
DiscordEnpoints.CDN.default(+id % 4, { format: 'png', size: validated.size })
|
||||
)
|
||||
ext = 'png'
|
||||
}
|
||||
|
||||
|
||||
res.setHeader('Content-Type', `image/${ext}`)
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400')
|
||||
res.send(img)
|
||||
})
|
||||
|
||||
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
size?: '128' | '256' | '512'
|
||||
size?: '128' | '256' | '512'
|
||||
}
|
||||
}
|
||||
|
||||
export default Icon
|
||||
export default Icon
|
||||
|
||||
@ -1 +1 @@
|
||||
export { default } from './[...404]'
|
||||
export { default } from './[...404]'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { NextApiRequest} from 'next'
|
||||
import { NextApiRequest } from 'next'
|
||||
import rateLimit from 'express-rate-limit'
|
||||
import { EmbedBuilder } from 'discord.js'
|
||||
|
||||
@ -18,43 +18,65 @@ const limiter = rateLimit({
|
||||
keyGenerator: (req) => req.headers.authorization,
|
||||
skip: (req, res) => {
|
||||
res.removeHeader('X-RateLimit-Global')
|
||||
if(!req.headers.authorization) return true
|
||||
if (!req.headers.authorization) return true
|
||||
else return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const BotStats = RequestHandler()
|
||||
.post(limiter)
|
||||
.post(async (req: PostApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.token)
|
||||
if(!bot) return ResponseWrapper(res, { code: 401, version: 1 })
|
||||
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
if (!bot) return ResponseWrapper(res, { code: 401, version: 1 })
|
||||
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, {
|
||||
abortEarly: false,
|
||||
})
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
|
||||
if(!validated) return
|
||||
|
||||
if (!validated) return
|
||||
const botInfo = await get.bot.load(bot)
|
||||
if(!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.', version: 1 })
|
||||
if(botInfo.id !== bot) return ResponseWrapper(res, { code: 403, version: 1 })
|
||||
if (!botInfo)
|
||||
return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.', version: 1 })
|
||||
if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403, version: 1 })
|
||||
const d = await update.updateServer(botInfo.id, validated.servers, undefined)
|
||||
if(d===1 || d===2) return ResponseWrapper(res, { code: 403, message: `서버 수를 ${[null, '1만', '100만'][d]} 이상으로 설정하실 수 없습니다. 문의해주세요.`, version: 1 })
|
||||
if (d === 1 || d === 2)
|
||||
return ResponseWrapper(res, {
|
||||
code: 403,
|
||||
message: `서버 수를 ${
|
||||
[null, '1만', '100만'][d]
|
||||
} 이상으로 설정하실 수 없습니다. 문의해주세요.`,
|
||||
version: 1,
|
||||
})
|
||||
get.bot.clear(bot)
|
||||
await webhookClients.internal.statsLog.send({
|
||||
content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(`${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${validated.servers} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(validated.servers - botInfo.servers)})`, 'diff')}`,
|
||||
embeds: [new EmbedBuilder().setDescription(`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(botInfo.id)}`)]
|
||||
content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(
|
||||
`${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${
|
||||
validated.servers
|
||||
} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(
|
||||
validated.servers - botInfo.servers
|
||||
)})`,
|
||||
'diff'
|
||||
)}`,
|
||||
embeds: [
|
||||
new EmbedBuilder().setDescription(
|
||||
`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(
|
||||
botInfo.id
|
||||
)}`
|
||||
),
|
||||
],
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.', version: 1 })
|
||||
})
|
||||
|
||||
|
||||
interface PostApiRequest extends NextApiRequest {
|
||||
headers: {
|
||||
token: string
|
||||
}
|
||||
body: BotStatUpdate
|
||||
headers: {
|
||||
token: string
|
||||
}
|
||||
body: BotStatUpdate
|
||||
}
|
||||
|
||||
export default BotStats
|
||||
export default BotStats
|
||||
|
||||
@ -5,23 +5,26 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import Yup from '@utils/Yup'
|
||||
import { VOTE_COOLDOWN } from '@utils/Constants'
|
||||
|
||||
const BotVoted = RequestHandler()
|
||||
.get(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.token)
|
||||
if(!bot) return ResponseWrapper(res, { code: 401, version: 1 })
|
||||
const userID = await Yup.string().required().validate(req.query.id).then(el => el).catch(() => null)
|
||||
if(!userID) return ResponseWrapper(res, { code: 400, version: 1 })
|
||||
const result = await get.botVote(userID, bot)
|
||||
return res.json({ code: 200, voted: +new Date() < result + VOTE_COOLDOWN })
|
||||
})
|
||||
const BotVoted = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.token)
|
||||
if (!bot) return ResponseWrapper(res, { code: 401, version: 1 })
|
||||
const userID = await Yup.string()
|
||||
.required()
|
||||
.validate(req.query.id)
|
||||
.then((el) => el)
|
||||
.catch(() => null)
|
||||
if (!userID) return ResponseWrapper(res, { code: 400, version: 1 })
|
||||
const result = await get.botVote(userID, bot)
|
||||
return res.json({ code: 200, voted: +new Date() < result + VOTE_COOLDOWN })
|
||||
})
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
headers: {
|
||||
token: string
|
||||
}
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
headers: {
|
||||
token: string
|
||||
}
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export default BotVoted
|
||||
export default BotVoted
|
||||
|
||||
@ -17,8 +17,8 @@ const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
const validated = await DeveloperBotSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
@ -26,17 +26,24 @@ const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
|
||||
if (!validated) return
|
||||
const bot = await get.bot.load(req.query.id)
|
||||
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
||||
if(validated.webhookURL) {
|
||||
if (!(bot.owners as User[]).find((el) => el.id === user))
|
||||
return ResponseWrapper(res, { code: 403 })
|
||||
if (validated.webhookURL) {
|
||||
const key = await verifyWebhook(validated.webhookURL)
|
||||
if(key === false) {
|
||||
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] })
|
||||
if (key === false) {
|
||||
return ResponseWrapper(res, {
|
||||
code: 400,
|
||||
message: '웹후크 주소를 검증할 수 없습니다.',
|
||||
errors: [
|
||||
'웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.',
|
||||
],
|
||||
})
|
||||
}
|
||||
const client = webhookClients.bot.get(req.query.id)
|
||||
if(client && validated.webhookURL !== client.url) {
|
||||
if (client && validated.webhookURL !== client.url) {
|
||||
destroyWebhookClient(req.query.id, 'bot')
|
||||
}
|
||||
await update.webhook(req.query.id, 'bots', {
|
||||
await update.webhook(req.query.id, 'bots', {
|
||||
url: validated.webhookURL,
|
||||
status: parseWebhookURL(validated.webhookURL) ? WebhookStatus.Discord : WebhookStatus.HTTP,
|
||||
failedSince: null,
|
||||
@ -44,7 +51,7 @@ const BotApplications = RequestHandler().patch(async (req: ApiRequest, res) => {
|
||||
})
|
||||
} else {
|
||||
destroyWebhookClient(req.query.id, 'bot')
|
||||
await update.webhook(req.query.id, 'bots', {
|
||||
await update.webhook(req.query.id, 'bots', {
|
||||
url: null,
|
||||
status: WebhookStatus.None,
|
||||
failedSince: null,
|
||||
|
||||
@ -14,8 +14,8 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
const validated = await ResetTokenSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
@ -23,7 +23,8 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
|
||||
if (!validated) return
|
||||
const bot = await get.bot.load(req.query.id)
|
||||
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
if (!(bot.owners as User[]).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
||||
if (!(bot.owners as User[]).find((el) => el.id === user))
|
||||
return ResponseWrapper(res, { code: 403 })
|
||||
const d = await update.resetBotToken(req.query.id, validated.token)
|
||||
if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
|
||||
return ResponseWrapper(res, { code: 200, data: { token: d } })
|
||||
|
||||
@ -17,8 +17,8 @@ const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) =
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
const validated = await DeveloperServerSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
@ -27,16 +27,22 @@ const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) =
|
||||
const server = await get.serverData(req.query.id)
|
||||
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
||||
if (![server.owner, ...server.admins].includes(user)) return ResponseWrapper(res, { code: 403 })
|
||||
if(validated.webhookURL) {
|
||||
if (validated.webhookURL) {
|
||||
const key = await verifyWebhook(validated.webhookURL)
|
||||
if(key === false) {
|
||||
return ResponseWrapper(res, { code: 400, message: '웹후크 주소를 검증할 수 없습니다.', errors: ['웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.'] })
|
||||
if (key === false) {
|
||||
return ResponseWrapper(res, {
|
||||
code: 400,
|
||||
message: '웹후크 주소를 검증할 수 없습니다.',
|
||||
errors: [
|
||||
'웹후크 주소가 올바른지 확인해주세요.\n웹후크 주소 검증에 대한 자세한 내용은 API 문서를 참고해주세요.',
|
||||
],
|
||||
})
|
||||
}
|
||||
const client = webhookClients.server.get(req.query.id)
|
||||
if(client && validated.webhookURL !== client.url) {
|
||||
if (client && validated.webhookURL !== client.url) {
|
||||
destroyWebhookClient(req.query.id, 'server')
|
||||
}
|
||||
await update.webhook(req.query.id, 'servers', {
|
||||
await update.webhook(req.query.id, 'servers', {
|
||||
url: validated.webhookURL,
|
||||
status: parseWebhookURL(validated.webhookURL) ? WebhookStatus.Discord : WebhookStatus.HTTP,
|
||||
failedSince: null,
|
||||
@ -44,7 +50,7 @@ const ServerApplications = RequestHandler().patch(async (req: ApiRequest, res) =
|
||||
})
|
||||
} else {
|
||||
destroyWebhookClient(req.query.id, 'server')
|
||||
await update.webhook(req.query.id, 'servers', {
|
||||
await update.webhook(req.query.id, 'servers', {
|
||||
url: null,
|
||||
status: WebhookStatus.None,
|
||||
failedSince: null,
|
||||
|
||||
@ -12,8 +12,8 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
const validated = await ResetTokenSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
@ -21,8 +21,14 @@ const ResetApplication = RequestHandler().post(async (req: ApiRequest, res) => {
|
||||
if (!validated) return
|
||||
const server = await get.server.load(req.query.id)
|
||||
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
||||
if(server.state === 'unreachable') return ResponseWrapper(res, { code: 400, message: '서버 정보를 불러올 수 없습니다.', errors: ['서버에서 봇이 추방되었거나, 봇이 오프라인이여서 서버 정보를 갱신할 수 없습니다.'] })
|
||||
if (!(await get.serverOwners(server.id)).find(el => el.id === user)) return ResponseWrapper(res, { code: 403 })
|
||||
if (server.state === 'unreachable')
|
||||
return ResponseWrapper(res, {
|
||||
code: 400,
|
||||
message: '서버 정보를 불러올 수 없습니다.',
|
||||
errors: ['서버에서 봇이 추방되었거나, 봇이 오프라인이여서 서버 정보를 갱신할 수 없습니다.'],
|
||||
})
|
||||
if (!(await get.serverOwners(server.id)).find((el) => el.id === user))
|
||||
return ResponseWrapper(res, { code: 403 })
|
||||
const d = await update.resetServerToken(req.query.id, validated.token)
|
||||
if (!d) return ResponseWrapper(res, { code: 500, message: '무언가 잘못되었습니다.' })
|
||||
return ResponseWrapper(res, { code: 200, data: { token: d } })
|
||||
|
||||
@ -6,10 +6,23 @@ import tracer from 'dd-trace'
|
||||
import { CaptchaVerify, get, put, remove, update } from '@utils/Query'
|
||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { checkToken } from '@utils/Csrf'
|
||||
import { AddBotSubmit, AddBotSubmitSchema, CsrfCaptcha, ManageBot, ManageBotSchema } from '@utils/Yup'
|
||||
import {
|
||||
AddBotSubmit,
|
||||
AddBotSubmitSchema,
|
||||
CsrfCaptcha,
|
||||
ManageBot,
|
||||
ManageBotSchema,
|
||||
} from '@utils/Yup'
|
||||
import RequestHandler from '@utils/RequestHandler'
|
||||
import { User } from '@types'
|
||||
import { checkUserFlag, diff, inspect, makeDiscordCodeblock, objectDiff, serialize } from '@utils/Tools'
|
||||
import {
|
||||
checkUserFlag,
|
||||
diff,
|
||||
inspect,
|
||||
makeDiscordCodeblock,
|
||||
objectDiff,
|
||||
serialize,
|
||||
} from '@utils/Tools'
|
||||
import { discordLog, getMainGuild, webhookClients } from '@utils/DiscordBot'
|
||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||
|
||||
@ -21,7 +34,7 @@ const patchLimiter = rateLimit({
|
||||
skip: (_req, res) => {
|
||||
res.removeHeader('X-RateLimit-Global')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
const Bots = RequestHandler()
|
||||
.get(async (req: GetApiRequest, res) => {
|
||||
@ -38,8 +51,8 @@ const Bots = RequestHandler()
|
||||
if (!csrfValidated) return
|
||||
|
||||
const validated = await AddBotSubmitSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
@ -48,7 +61,7 @@ const Bots = RequestHandler()
|
||||
if (validated.id !== req.query.id)
|
||||
return ResponseWrapper(res, { code: 400, errors: ['요청 주소와 Body의 정보가 다릅니다.'] })
|
||||
const captcha = await CaptchaVerify(validated._captcha)
|
||||
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
const result = await put.submitBot(user, validated)
|
||||
if (result === 1)
|
||||
return ResponseWrapper(res, {
|
||||
@ -82,30 +95,53 @@ const Bots = RequestHandler()
|
||||
return ResponseWrapper(res, {
|
||||
code: 403,
|
||||
message: '더 이상 해당 봇에 대한 심사 요청을 하실 수 없습니다.',
|
||||
errors: ['해당 봇은 심사에서 3회 이상 거부되었습니다. 더 이상의 심사를 요청하실 수 없습니다.', '이의 제기를 원하시는 경우 디스코드 서버를 통해 문의해주세요.'],
|
||||
errors: [
|
||||
'해당 봇은 심사에서 3회 이상 거부되었습니다. 더 이상의 심사를 요청하실 수 없습니다.',
|
||||
'이의 제기를 원하시는 경우 디스코드 서버를 통해 문의해주세요.',
|
||||
],
|
||||
})
|
||||
get.botSubmits.clear(user)
|
||||
|
||||
await discordLog('BOT/SUBMIT', user, new EmbedBuilder().setDescription(`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(result.id, result.date)})`), {
|
||||
content: inspect(serialize(result)),
|
||||
format: 'js'
|
||||
})
|
||||
await discordLog(
|
||||
'BOT/SUBMIT',
|
||||
user,
|
||||
new EmbedBuilder().setDescription(
|
||||
`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(
|
||||
result.id,
|
||||
result.date
|
||||
)})`
|
||||
),
|
||||
{
|
||||
content: inspect(serialize(result)),
|
||||
format: 'js',
|
||||
}
|
||||
)
|
||||
const userinfo = await get.user.load(user)
|
||||
await webhookClients.internal.reviewLog.send({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: userinfo.tag === '0' ? `${userinfo.globalName} (@${userinfo.username})` : `${userinfo.username}#${userinfo.tag}`,
|
||||
iconURL: KoreanbotsEndPoints.URL.root + KoreanbotsEndPoints.CDN.avatar(userinfo.id, { format: 'png', size: 256 }),
|
||||
url: KoreanbotsEndPoints.URL.user(userinfo.id)
|
||||
name:
|
||||
userinfo.tag === '0'
|
||||
? `${userinfo.globalName} (@${userinfo.username})`
|
||||
: `${userinfo.username}#${userinfo.tag}`,
|
||||
iconURL:
|
||||
KoreanbotsEndPoints.URL.root +
|
||||
KoreanbotsEndPoints.CDN.avatar(userinfo.id, { format: 'png', size: 256 }),
|
||||
url: KoreanbotsEndPoints.URL.user(userinfo.id),
|
||||
})
|
||||
.setTitle('대기 중')
|
||||
.setColor(Colors.Grey)
|
||||
.setDescription(`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(result.id, result.date)})`)
|
||||
.setTimestamp()
|
||||
]
|
||||
.setDescription(
|
||||
`[${result.id}/${result.date}](${KoreanbotsEndPoints.URL.submittedBot(
|
||||
result.id,
|
||||
result.date
|
||||
)})`
|
||||
)
|
||||
.setTimestamp(),
|
||||
],
|
||||
})
|
||||
tracer.trace('botSubmits.submitted', span => {
|
||||
tracer.trace('botSubmits.submitted', (span) => {
|
||||
span.setTag('id', result.id)
|
||||
span.setTag('date', result.date)
|
||||
span.setTag('user', userinfo.id)
|
||||
@ -116,68 +152,114 @@ const Bots = RequestHandler()
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const bot = await get.bot.load(req.query.id)
|
||||
if(!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
if((bot.owners as User[])[0].id !== user) return ResponseWrapper(res, { code: 403 })
|
||||
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
if ((bot.owners as User[])[0].id !== user) return ResponseWrapper(res, { code: 403 })
|
||||
const userInfo = await get.user.load(user)
|
||||
if(['reported', 'blocked', 'archived'].includes(bot.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 봇은 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] })
|
||||
if (
|
||||
['reported', 'blocked', 'archived'].includes(bot.state) &&
|
||||
!checkUserFlag(userInfo?.flags, 'staff')
|
||||
)
|
||||
return ResponseWrapper(res, {
|
||||
code: 403,
|
||||
message: '해당 봇은 수정할 수 없습니다.',
|
||||
errors: ['오류라고 생각되면 문의해주세요.'],
|
||||
})
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
const captcha = await CaptchaVerify(req.body._captcha)
|
||||
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
if(req.body.name !== bot.name) return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' })
|
||||
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
if (req.body.name !== bot.name)
|
||||
return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' })
|
||||
await remove.bot(bot.id)
|
||||
await getMainGuild().members.cache.get(bot.id)?.kick('봇 삭제됨.')
|
||||
get.user.clear(user)
|
||||
await discordLog('BOT/DELETE', user, (new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`)),
|
||||
await discordLog(
|
||||
'BOT/DELETE',
|
||||
user,
|
||||
new EmbedBuilder().setDescription(
|
||||
`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`
|
||||
),
|
||||
{
|
||||
content: inspect(bot),
|
||||
format: 'js'
|
||||
format: 'js',
|
||||
}
|
||||
)
|
||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 삭제했습니다.' })
|
||||
})
|
||||
.patch(patchLimiter).patch(async (req: PatchApiRequest, res) => {
|
||||
.patch(patchLimiter)
|
||||
.patch(async (req: PatchApiRequest, res) => {
|
||||
const bot = await get.bot.load(req.query.id)
|
||||
if(!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const userInfo = await get.user.load(user)
|
||||
if(['reported', 'blocked', 'archived'].includes(bot.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 봇은 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] })
|
||||
if(!(bot.owners as User[]).find(el => el.id === user) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403 })
|
||||
if (
|
||||
['reported', 'blocked', 'archived'].includes(bot.state) &&
|
||||
!checkUserFlag(userInfo?.flags, 'staff')
|
||||
)
|
||||
return ResponseWrapper(res, {
|
||||
code: 403,
|
||||
message: '해당 봇은 수정할 수 없습니다.',
|
||||
errors: ['오류라고 생각되면 문의해주세요.'],
|
||||
})
|
||||
if (
|
||||
!(bot.owners as User[]).find((el) => el.id === user) &&
|
||||
!checkUserFlag(userInfo?.flags, 'staff')
|
||||
)
|
||||
return ResponseWrapper(res, { code: 403 })
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
|
||||
const validated = await ManageBotSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
|
||||
if (!validated) return
|
||||
|
||||
|
||||
const result = await update.bot(req.query.id, validated)
|
||||
if(result === 0) return ResponseWrapper(res, { code: 400 })
|
||||
if (result === 0) return ResponseWrapper(res, { code: 400 })
|
||||
else {
|
||||
get.bot.clear(req.query.id)
|
||||
const embed = new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`)
|
||||
const diffData = objectDiff(
|
||||
{ prefix: bot.prefix, library: bot.lib, web: bot.web, git: bot.git, url: bot.url, discord: bot.discord, intro: bot.intro, category: JSON.stringify(bot.category) },
|
||||
{ prefix: validated.prefix, library: validated.library, web: validated.website, git: validated.git, url: validated.url, discord: validated.discord, intro: validated.intro, category: JSON.stringify(validated.category) }
|
||||
const embed = new EmbedBuilder().setDescription(
|
||||
`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`
|
||||
)
|
||||
diffData.forEach(d => {
|
||||
embed.addFields({name: d[0], value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff')
|
||||
})
|
||||
})
|
||||
await discordLog('BOT/EDIT', user, embed,
|
||||
const diffData = objectDiff(
|
||||
{
|
||||
content: `--- 설명\n${diff(bot.desc, validated.desc, true)}`,
|
||||
format: 'diff'
|
||||
prefix: bot.prefix,
|
||||
library: bot.lib,
|
||||
web: bot.web,
|
||||
git: bot.git,
|
||||
url: bot.url,
|
||||
discord: bot.discord,
|
||||
intro: bot.intro,
|
||||
category: JSON.stringify(bot.category),
|
||||
},
|
||||
{
|
||||
prefix: validated.prefix,
|
||||
library: validated.library,
|
||||
web: validated.website,
|
||||
git: validated.git,
|
||||
url: validated.url,
|
||||
discord: validated.discord,
|
||||
intro: validated.intro,
|
||||
category: JSON.stringify(validated.category),
|
||||
}
|
||||
)
|
||||
diffData.forEach((d) => {
|
||||
embed.addFields({
|
||||
name: d[0],
|
||||
value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff'),
|
||||
})
|
||||
})
|
||||
await discordLog('BOT/EDIT', user, embed, {
|
||||
content: `--- 설명\n${diff(bot.desc, validated.desc, true)}`,
|
||||
format: 'diff',
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
interface GetApiRequest extends NextApiRequest {
|
||||
@ -195,7 +277,7 @@ interface PatchApiRequest extends GetApiRequest {
|
||||
}
|
||||
|
||||
interface DeleteApiRequest extends GetApiRequest {
|
||||
body: CsrfCaptcha & { name: string } | null
|
||||
body: (CsrfCaptcha & { name: string }) | null
|
||||
}
|
||||
|
||||
export default Bots
|
||||
|
||||
@ -11,40 +11,69 @@ import { discordLog } from '@utils/DiscordBot'
|
||||
import { EmbedBuilder } from 'discord.js'
|
||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||
|
||||
const BotOwners = RequestHandler()
|
||||
.patch(async (req: PostApiRequest, res) => {
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const userinfo = await get.user.load(user)
|
||||
const bot = await get.bot.load(req.query.id)
|
||||
if(!bot) return ResponseWrapper(res, { code: 404 })
|
||||
if((bot.owners as User[])[0].id !== user && !checkUserFlag(userinfo.flags, 'staff')) return ResponseWrapper(res, { code: 403 })
|
||||
if(['reported', 'blocked', 'archived'].includes(bot.state) && !checkUserFlag(userinfo.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 봇은 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] })
|
||||
const validated = await EditBotOwnerSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
if(!validated) return
|
||||
const csrfValidated = checkToken(req, res, validated._csrf)
|
||||
if (!csrfValidated) return
|
||||
const captcha = await CaptchaVerify(validated._captcha)
|
||||
if(!captcha) return
|
||||
const userFetched: User[] = await Promise.all(validated.owners.map((u: string) => get.user.load(u)))
|
||||
if(userFetched.indexOf(null) !== -1) return ResponseWrapper(res, { code: 400, message: '올바르지 않은 유저 ID를 포함하고 있습니다.' })
|
||||
if(userFetched.length > 1 && userFetched[0].id !== (bot.owners as User[])[0].id) return ResponseWrapper(res, { code: 400, errors: ['소유자를 이전할 때는 다른 관리자를 포함할 수 없습니다.'] })
|
||||
await update.botOwners(bot.id, validated.owners)
|
||||
get.user.clear(user)
|
||||
await discordLog('BOT/OWNERS', userinfo.id, (new EmbedBuilder().setDescription(`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`)), null, makeDiscordCodeblock(diff(JSON.stringify(bot.owners.map(el => el.id)), JSON.stringify(validated.owners)), 'diff'))
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
})
|
||||
const BotOwners = RequestHandler().patch(async (req: PostApiRequest, res) => {
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const userinfo = await get.user.load(user)
|
||||
const bot = await get.bot.load(req.query.id)
|
||||
if (!bot) return ResponseWrapper(res, { code: 404 })
|
||||
if ((bot.owners as User[])[0].id !== user && !checkUserFlag(userinfo.flags, 'staff'))
|
||||
return ResponseWrapper(res, { code: 403 })
|
||||
if (
|
||||
['reported', 'blocked', 'archived'].includes(bot.state) &&
|
||||
!checkUserFlag(userinfo.flags, 'staff')
|
||||
)
|
||||
return ResponseWrapper(res, {
|
||||
code: 403,
|
||||
message: '해당 봇은 수정할 수 없습니다.',
|
||||
errors: ['오류라고 생각되면 문의해주세요.'],
|
||||
})
|
||||
const validated = await EditBotOwnerSchema.validate(req.body, { abortEarly: false })
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
if (!validated) return
|
||||
const csrfValidated = checkToken(req, res, validated._csrf)
|
||||
if (!csrfValidated) return
|
||||
const captcha = await CaptchaVerify(validated._captcha)
|
||||
if (!captcha) return
|
||||
const userFetched: User[] = await Promise.all(
|
||||
validated.owners.map((u: string) => get.user.load(u))
|
||||
)
|
||||
if (userFetched.indexOf(null) !== -1)
|
||||
return ResponseWrapper(res, {
|
||||
code: 400,
|
||||
message: '올바르지 않은 유저 ID를 포함하고 있습니다.',
|
||||
})
|
||||
if (userFetched.length > 1 && userFetched[0].id !== (bot.owners as User[])[0].id)
|
||||
return ResponseWrapper(res, {
|
||||
code: 400,
|
||||
errors: ['소유자를 이전할 때는 다른 관리자를 포함할 수 없습니다.'],
|
||||
})
|
||||
await update.botOwners(bot.id, validated.owners)
|
||||
get.user.clear(user)
|
||||
await discordLog(
|
||||
'BOT/OWNERS',
|
||||
userinfo.id,
|
||||
new EmbedBuilder().setDescription(
|
||||
`${bot.name} - <@${bot.id}> ([${bot.id}](${KoreanbotsEndPoints.URL.bot(bot.id)}))`
|
||||
),
|
||||
null,
|
||||
makeDiscordCodeblock(
|
||||
diff(JSON.stringify(bot.owners.map((el) => el.id)), JSON.stringify(validated.owners)),
|
||||
'diff'
|
||||
)
|
||||
)
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
})
|
||||
|
||||
interface PostApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
},
|
||||
body: EditBotOwner
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
body: EditBotOwner
|
||||
}
|
||||
|
||||
export default BotOwners
|
||||
export default BotOwners
|
||||
|
||||
@ -4,7 +4,7 @@ import rateLimit from 'express-rate-limit'
|
||||
import { get } from '@utils/Query'
|
||||
import RequestHandler from '@utils/RequestHandler'
|
||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { ReportSchema, Report} from '@utils/Yup'
|
||||
import { ReportSchema, Report } from '@utils/Yup'
|
||||
import { webhookClients } from '@utils/DiscordBot'
|
||||
import { checkToken } from '@utils/Csrf'
|
||||
|
||||
@ -18,36 +18,40 @@ const limiter = rateLimit({
|
||||
skip: (_req, res) => {
|
||||
res.removeHeader('X-RateLimit-Global')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const BotReport = RequestHandler().post(limiter)
|
||||
const BotReport = RequestHandler()
|
||||
.post(limiter)
|
||||
.post(async (req: PostApiRequest, res) => {
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if(!user) return ResponseWrapper(res, { code: 401 })
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const bot = await get.bot.load(req.query.id)
|
||||
if(!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
if(!req.body) return ResponseWrapper(res, { code: 400 })
|
||||
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
||||
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
|
||||
if(!validated) return
|
||||
await webhookClients.internal.reportChannel.send({ threadName: `봇-${bot.id}`, content: `Reported by <@${user}> (${user})\nReported **${bot.name}** <@${bot.id}> (${bot.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``, allowedMentions: { parse: ['users'] }})
|
||||
|
||||
if (!validated) return
|
||||
await webhookClients.internal.reportChannel.send({
|
||||
threadName: `봇-${bot.id}`,
|
||||
content: `Reported by <@${user}> (${user})\nReported **${bot.name}** <@${bot.id}> (${bot.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``,
|
||||
allowedMentions: { parse: ['users'] },
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
|
||||
})
|
||||
|
||||
|
||||
interface PostApiRequest extends NextApiRequest {
|
||||
body: Report | null
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
body: Report | null
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export default BotReport
|
||||
export default BotReport
|
||||
|
||||
@ -21,9 +21,9 @@ const limiter = rateLimit({
|
||||
keyGenerator: (req) => req.headers.authorization,
|
||||
skip: (req, res) => {
|
||||
res.removeHeader('X-RateLimit-Global')
|
||||
if(!req.headers.authorization) return true
|
||||
if (!req.headers.authorization) return true
|
||||
else return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const patchLimiter = rateLimit({
|
||||
@ -36,27 +36,36 @@ const patchLimiter = rateLimit({
|
||||
skip: (_req, res) => {
|
||||
res.removeHeader('X-RateLimit-Global')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const BotStats = RequestHandler().post(limiter)
|
||||
const BotStats = RequestHandler()
|
||||
.post(limiter)
|
||||
.post(async (req: PostApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if(!bot) return ResponseWrapper(res, { code: 401 })
|
||||
if(!req.body) return ResponseWrapper(res, { code: 400 })
|
||||
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
if (!bot) return ResponseWrapper(res, { code: 401 })
|
||||
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
||||
const validated: BotStatUpdate = await BotStatUpdateSchema.validate(req.body, {
|
||||
abortEarly: false,
|
||||
})
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
|
||||
if(!validated) return
|
||||
if (!validated) return
|
||||
const botInfo = await get.bot.load(req.query.id)
|
||||
if(!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
if(botInfo.id !== bot) return ResponseWrapper(res, { code: 403 })
|
||||
if (!botInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
if (botInfo.id !== bot) return ResponseWrapper(res, { code: 403 })
|
||||
const d = await update.updateServer(botInfo.id, validated.servers, validated.shards)
|
||||
if(d===1 || d===2) return ResponseWrapper(res, { code: 403, message: `서버 수를 ${[null, '1만', '100만'][d]} 이상으로 설정하실 수 없습니다. 문의해주세요.` })
|
||||
if (d === 1 || d === 2)
|
||||
return ResponseWrapper(res, {
|
||||
code: 403,
|
||||
message: `서버 수를 ${
|
||||
[null, '1만', '100만'][d]
|
||||
} 이상으로 설정하실 수 없습니다. 문의해주세요.`,
|
||||
})
|
||||
get.bot.clear(req.query.id)
|
||||
if (validated.servers && botInfo.servers !== validated.servers) {
|
||||
sendWebhook(botInfo, {
|
||||
@ -67,37 +76,54 @@ const BotStats = RequestHandler().post(limiter)
|
||||
before: botInfo.servers,
|
||||
after: validated.servers,
|
||||
},
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
await webhookClients.internal.statsLog.send({
|
||||
content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(`${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${validated.servers} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(validated.servers - botInfo.servers)})`, 'diff')}`,
|
||||
embeds: [new EmbedBuilder().setDescription(`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(botInfo.id)}))`)]
|
||||
content: `[BOT/STATS] <@${botInfo.id}> (${botInfo.id})\n${makeDiscordCodeblock(
|
||||
`${botInfo.servers > validated.servers ? '-' : '+'} ${botInfo.servers} -> ${
|
||||
validated.servers
|
||||
} (${botInfo.servers > validated.servers ? '▼' : '▲'}${Math.abs(
|
||||
validated.servers - botInfo.servers
|
||||
)})`,
|
||||
'diff'
|
||||
)}`,
|
||||
embeds: [
|
||||
new EmbedBuilder().setDescription(
|
||||
`${botInfo.name} - <@${botInfo.id}> ([${botInfo.id}](${KoreanbotsEndPoints.URL.bot(
|
||||
botInfo.id
|
||||
)}))`
|
||||
),
|
||||
],
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.'})
|
||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 업데이트 했습니다.' })
|
||||
})
|
||||
.patch(patchLimiter).patch(async (req: ApiRequest, res) => {
|
||||
.patch(patchLimiter)
|
||||
.patch(async (req: ApiRequest, res) => {
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const userinfo = await get.user.load(user)
|
||||
const bot = await get.bot.load(req.query.id)
|
||||
if(!bot) return ResponseWrapper(res, { code: 404 })
|
||||
if(!(bot.owners as User[]).find(el => el.id === user) && !checkUserFlag(userinfo.flags, 'staff')) return ResponseWrapper(res, { code: 403 })
|
||||
if (!bot) return ResponseWrapper(res, { code: 404 })
|
||||
if (
|
||||
!(bot.owners as User[]).find((el) => el.id === user) &&
|
||||
!checkUserFlag(userinfo.flags, 'staff')
|
||||
)
|
||||
return ResponseWrapper(res, { code: 403 })
|
||||
get.bot.clear(req.query.id)
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
})
|
||||
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
interface PostApiRequest extends ApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
body: BotStatUpdate
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
body: BotStatUpdate
|
||||
}
|
||||
|
||||
export default BotStats
|
||||
export default BotStats
|
||||
|
||||
@ -12,29 +12,37 @@ import { WebhookType } from '@types'
|
||||
const BotVote = RequestHandler()
|
||||
.get(async (req: GetApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if(!bot) return ResponseWrapper(res, { code: 401 })
|
||||
if(req.query.id !== bot) return ResponseWrapper(res, { code: 403 })
|
||||
const userID = await Yup.string().required().label('userID').validate(req.query.userID).then(el => el).catch(e => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
if(!userID) return ResponseWrapper(res, { code: 400 })
|
||||
if (!bot) return ResponseWrapper(res, { code: 401 })
|
||||
if (req.query.id !== bot) return ResponseWrapper(res, { code: 403 })
|
||||
const userID = await Yup.string()
|
||||
.required()
|
||||
.label('userID')
|
||||
.validate(req.query.userID)
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
if (!userID) return ResponseWrapper(res, { code: 400 })
|
||||
const result = await get.botVote(userID, bot)
|
||||
return ResponseWrapper(res, { code: 200, data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result } })
|
||||
return ResponseWrapper(res, {
|
||||
code: 200,
|
||||
data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result },
|
||||
})
|
||||
})
|
||||
.post(async (req: PostApiRequest, res) => {
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if(!user) return ResponseWrapper(res, { code: 401 })
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const bot = await get.bot.load(req.query.id)
|
||||
if (!bot) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 봇입니다.' })
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
const captcha = await CaptchaVerify(req.body._captcha)
|
||||
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
|
||||
const vote = await put.voteBot(user, bot.id)
|
||||
if(vote === null) return ResponseWrapper(res, { code: 401 })
|
||||
else if(vote === true) {
|
||||
if (vote === null) return ResponseWrapper(res, { code: 401 })
|
||||
else if (vote === true) {
|
||||
sendWebhook(bot, {
|
||||
type: 'bot',
|
||||
data: {
|
||||
@ -42,19 +50,18 @@ const BotVote = RequestHandler()
|
||||
type: WebhookType.HeartChange,
|
||||
before: bot.votes,
|
||||
after: bot.votes + 1,
|
||||
userId: user
|
||||
userId: user,
|
||||
},
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
}
|
||||
else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
|
||||
} else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
|
||||
})
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GetApiRequest extends ApiRequest {
|
||||
@ -65,8 +72,8 @@ interface GetApiRequest extends ApiRequest {
|
||||
}
|
||||
interface PostApiRequest extends ApiRequest {
|
||||
body: {
|
||||
_captcha: string
|
||||
_csrf: string
|
||||
}
|
||||
_captcha: string
|
||||
_csrf: string
|
||||
}
|
||||
}
|
||||
export default BotVote
|
||||
export default BotVote
|
||||
|
||||
@ -9,4 +9,4 @@ const NewList = RequestHandler().get(async (_req, res) => {
|
||||
return ResponseWrapper<List<Bot>>(res, { code: 200, data: result })
|
||||
})
|
||||
|
||||
export default NewList
|
||||
export default NewList
|
||||
|
||||
@ -6,14 +6,20 @@ import { Bot, List } from '@types'
|
||||
import Yup from '@utils/Yup'
|
||||
|
||||
const VotesList = RequestHandler().get(async (req, res) => {
|
||||
const page = await Yup.number().positive().integer().notRequired().default(1).label('페이지').validate(req.query.page)
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
const page = await Yup.number()
|
||||
.positive()
|
||||
.integer()
|
||||
.notRequired()
|
||||
.default(1)
|
||||
.label('페이지')
|
||||
.validate(req.query.page)
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
})
|
||||
if(!page) return
|
||||
if (!page) return
|
||||
const result = await get.list.votes.load(page)
|
||||
return ResponseWrapper<List<Bot>>(res, { code: 200, data: result })
|
||||
})
|
||||
|
||||
export default VotesList
|
||||
export default VotesList
|
||||
|
||||
@ -8,36 +8,47 @@ import { get, update } from '@utils/Query'
|
||||
import { DiscordBot, webhookClients } from '@utils/DiscordBot'
|
||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||
|
||||
const ApproveBotSubmit = RequestHandler()
|
||||
.post(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if(bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
const submit = await get.botSubmit.load(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
||||
if(!submit) return ResponseWrapper(res, { code: 404 })
|
||||
if(submit.state !== 0) return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
|
||||
const result = await update.approveBotSubmission(submit.id, submit.date)
|
||||
if(!result) return ResponseWrapper(res, { code: 400 })
|
||||
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
||||
get.bot.clear(req.query.id)
|
||||
const embed = new EmbedBuilder().setTitle('승인').setColor(Colors.Green).setDescription(`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(submit.id, submit.date)})`).setTimestamp()
|
||||
if(req.body.reviewer) embed.addFields({name: '📃 정보', value: `심사자: ${req.body.reviewer}`})
|
||||
await webhookClients.internal.reviewLog.send({embeds: [embed]})
|
||||
tracer.trace('botSubmits.approve', span => {
|
||||
span.setTag('id', submit.id)
|
||||
span.setTag('date', submit.date)
|
||||
span.setTag('reviewer', req.body.reviewer)
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
const ApproveBotSubmit = RequestHandler().post(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
const submit = await get.botSubmit.load(
|
||||
JSON.stringify({ id: req.query.id, date: req.query.date })
|
||||
)
|
||||
if (!submit) return ResponseWrapper(res, { code: 404 })
|
||||
if (submit.state !== 0)
|
||||
return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
|
||||
const result = await update.approveBotSubmission(submit.id, submit.date)
|
||||
if (!result) return ResponseWrapper(res, { code: 400 })
|
||||
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
||||
get.bot.clear(req.query.id)
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('승인')
|
||||
.setColor(Colors.Green)
|
||||
.setDescription(
|
||||
`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(
|
||||
submit.id,
|
||||
submit.date
|
||||
)})`
|
||||
)
|
||||
.setTimestamp()
|
||||
if (req.body.reviewer) embed.addFields({ name: '📃 정보', value: `심사자: ${req.body.reviewer}` })
|
||||
await webhookClients.internal.reviewLog.send({ embeds: [embed] })
|
||||
tracer.trace('botSubmits.approve', (span) => {
|
||||
span.setTag('id', submit.id)
|
||||
span.setTag('date', submit.date)
|
||||
span.setTag('reviewer', req.body.reviewer)
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
})
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
date: string
|
||||
}
|
||||
body: {
|
||||
query: {
|
||||
id: string
|
||||
date: string
|
||||
}
|
||||
body: {
|
||||
reviewer?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ApproveBotSubmit
|
||||
export default ApproveBotSubmit
|
||||
|
||||
@ -8,40 +8,71 @@ import { get, update } from '@utils/Query'
|
||||
import { DiscordBot, webhookClients } from '@utils/DiscordBot'
|
||||
import { BotSubmissionDenyReasonPresetsName, KoreanbotsEndPoints } from '@utils/Constants'
|
||||
|
||||
const DenyBotSubmit = RequestHandler()
|
||||
.post(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if(bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
const submit = await get.botSubmit.load(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
||||
if(!submit) return ResponseWrapper(res, { code: 404 })
|
||||
if(submit.state !== 0) return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
|
||||
await update.denyBotSubmission(submit.id, submit.date, req.body.reason)
|
||||
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
||||
const embed = new EmbedBuilder().setTitle('거부').setColor(Colors.Red).setDescription(`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(submit.id, submit.date)})`).setTimestamp()
|
||||
if(req.body.reviewer || req.body.reason) embed.addFields({name: '📃 정보', value: `${req.body.reason ? `사유: ${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`: ''}${req.body.reviewer ? `심사자: ${req.body.reviewer}` : ''}`})
|
||||
await webhookClients.internal.reviewLog.send({embeds: [embed]})
|
||||
const openEmbed = new EmbedBuilder().setTitle('거부').setColor(Colors.Red).setDescription(`<@${submit.id}> (${submit.id})`).setTimestamp()
|
||||
if(req.body.reason) openEmbed.addFields({name: '📃 사유', value: `${req.body.reason ? `${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`: '없음'}`})
|
||||
await webhookClients.internal.openReviewLog.send({embeds: [openEmbed]})
|
||||
tracer.trace('botSubmits.deny', span => {
|
||||
span.setTag('id', submit.id)
|
||||
span.setTag('date', submit.date)
|
||||
span.setTag('reviewer', req.body.reviewer)
|
||||
span.setTag('reason', BotSubmissionDenyReasonPresetsName[req.body.reason] || 'OTHER')
|
||||
span.setTag('_raw_reason', req.body.reason)
|
||||
const DenyBotSubmit = RequestHandler().post(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
const submit = await get.botSubmit.load(
|
||||
JSON.stringify({ id: req.query.id, date: req.query.date })
|
||||
)
|
||||
if (!submit) return ResponseWrapper(res, { code: 404 })
|
||||
if (submit.state !== 0)
|
||||
return ResponseWrapper(res, { code: 400, message: '대기 중이지 않은 아이디입니다.' })
|
||||
await update.denyBotSubmission(submit.id, submit.date, req.body.reason)
|
||||
get.botSubmit.clear(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('거부')
|
||||
.setColor(Colors.Red)
|
||||
.setDescription(
|
||||
`[${submit.id}/${submit.date}](${KoreanbotsEndPoints.URL.submittedBot(
|
||||
submit.id,
|
||||
submit.date
|
||||
)})`
|
||||
)
|
||||
.setTimestamp()
|
||||
if (req.body.reviewer || req.body.reason)
|
||||
embed.addFields({
|
||||
name: '📃 정보',
|
||||
value: `${
|
||||
req.body.reason
|
||||
? `사유: ${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`
|
||||
: ''
|
||||
}${req.body.reviewer ? `심사자: ${req.body.reviewer}` : ''}`,
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
await webhookClients.internal.reviewLog.send({ embeds: [embed] })
|
||||
const openEmbed = new EmbedBuilder()
|
||||
.setTitle('거부')
|
||||
.setColor(Colors.Red)
|
||||
.setDescription(`<@${submit.id}> (${submit.id})`)
|
||||
.setTimestamp()
|
||||
if (req.body.reason)
|
||||
openEmbed.addFields({
|
||||
name: '📃 사유',
|
||||
value: `${
|
||||
req.body.reason
|
||||
? `${BotSubmissionDenyReasonPresetsName[req.body.reason] || req.body.reason}\n`
|
||||
: '없음'
|
||||
}`,
|
||||
})
|
||||
await webhookClients.internal.openReviewLog.send({ embeds: [openEmbed] })
|
||||
tracer.trace('botSubmits.deny', (span) => {
|
||||
span.setTag('id', submit.id)
|
||||
span.setTag('date', submit.date)
|
||||
span.setTag('reviewer', req.body.reviewer)
|
||||
span.setTag('reason', BotSubmissionDenyReasonPresetsName[req.body.reason] || 'OTHER')
|
||||
span.setTag('_raw_reason', req.body.reason)
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
})
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
date: string
|
||||
}
|
||||
body: {
|
||||
reason?: string
|
||||
query: {
|
||||
id: string
|
||||
date: string
|
||||
}
|
||||
body: {
|
||||
reason?: string
|
||||
reviewer: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DenyBotSubmit
|
||||
|
||||
@ -5,20 +5,21 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { get } from '@utils/Query'
|
||||
import { DiscordBot } from '@utils/DiscordBot'
|
||||
|
||||
const BotSubmit = RequestHandler()
|
||||
.get(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if(bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
const submit = await get.botSubmit.load(JSON.stringify({ id: req.query.id, date: req.query.date }))
|
||||
if(!submit) return ResponseWrapper(res, { code: 404 })
|
||||
return ResponseWrapper(res, { code: 200, data: submit })
|
||||
})
|
||||
const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
const submit = await get.botSubmit.load(
|
||||
JSON.stringify({ id: req.query.id, date: req.query.date })
|
||||
)
|
||||
if (!submit) return ResponseWrapper(res, { code: 404 })
|
||||
return ResponseWrapper(res, { code: 200, data: submit })
|
||||
})
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
query: {
|
||||
id: string
|
||||
date: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BotSubmit
|
||||
export default BotSubmit
|
||||
|
||||
@ -5,17 +5,16 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { get } from '@utils/Query'
|
||||
import { DiscordBot } from '@utils/DiscordBot'
|
||||
|
||||
const BotSubmit = RequestHandler()
|
||||
.get(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if(bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
return ResponseWrapper(res, { code: 200, data: await get.botSubmitHistory(req.query.id) })
|
||||
})
|
||||
const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
return ResponseWrapper(res, { code: 200, data: await get.botSubmitHistory(req.query.id) })
|
||||
})
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export default BotSubmit
|
||||
export default BotSubmit
|
||||
|
||||
@ -5,19 +5,18 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { get } from '@utils/Query'
|
||||
import { DiscordBot } from '@utils/DiscordBot'
|
||||
|
||||
const BotSubmit = RequestHandler()
|
||||
.get(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if(bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
const submits = await get.botSubmitList()
|
||||
const submit = submits.find((el, n) => el.id === req.query.id || n+1 === Number(req.query.id))
|
||||
if(!submit) return ResponseWrapper(res, { code: 404 })
|
||||
return ResponseWrapper(res, { code: 200, data: submit })
|
||||
})
|
||||
const BotSubmit = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
const submits = await get.botSubmitList()
|
||||
const submit = submits.find((el, n) => el.id === req.query.id || n + 1 === Number(req.query.id))
|
||||
if (!submit) return ResponseWrapper(res, { code: 404 })
|
||||
return ResponseWrapper(res, { code: 200, data: submit })
|
||||
})
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
export default BotSubmit
|
||||
export default BotSubmit
|
||||
|
||||
@ -3,12 +3,11 @@ import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { get } from '@utils/Query'
|
||||
import { DiscordBot } from '@utils/DiscordBot'
|
||||
|
||||
const BotSubmits = RequestHandler()
|
||||
.get(async (req, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if(bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
const submits = await get.botSubmitList()
|
||||
return ResponseWrapper(res, { code: 200, data: submits })
|
||||
})
|
||||
const BotSubmits = RequestHandler().get(async (req, res) => {
|
||||
const bot = await get.BotAuthorization(req.headers.authorization)
|
||||
if (bot !== DiscordBot.user.id) return ResponseWrapper(res, { code: 403 })
|
||||
const submits = await get.botSubmitList()
|
||||
return ResponseWrapper(res, { code: 200, data: submits })
|
||||
})
|
||||
|
||||
export default BotSubmits
|
||||
export default BotSubmits
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import RequestHandler from '@utils/RequestHandler'
|
||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
|
||||
const BotSubmits = RequestHandler()
|
||||
.get(async (_req, res) => {
|
||||
return ResponseWrapper(res, { code: 403, message: 'Private API' })
|
||||
})
|
||||
const BotSubmits = RequestHandler().get(async (_req, res) => {
|
||||
return ResponseWrapper(res, { code: 403, message: 'Private API' })
|
||||
})
|
||||
|
||||
export default BotSubmits
|
||||
export default BotSubmits
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
import { NextApiHandler } from 'next'
|
||||
import { getMainGuild } from '@utils/DiscordBot'
|
||||
import RequestHandler from '@utils/RequestHandler'
|
||||
|
||||
@ -9,8 +9,8 @@ import { Bot, Server, List } from '@types'
|
||||
|
||||
const Search = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: 1 })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
})
|
||||
if (!validated) return
|
||||
@ -27,7 +27,10 @@ const Search = RequestHandler().get(async (req: ApiRequest, res) => {
|
||||
} catch {
|
||||
return ResponseWrapper(res, { code: 400, message: '검색 문법이 잘못되었습니다.' })
|
||||
}
|
||||
return ResponseWrapper<{ bots: Bot[], servers: Server[] }>(res, { code: 200, data: { bots: botResult?.data || [], servers: serverResult?.data || [] } })
|
||||
return ResponseWrapper<{ bots: Bot[]; servers: Server[] }>(res, {
|
||||
code: 200,
|
||||
data: { bots: botResult?.data || [], servers: serverResult?.data || [] },
|
||||
})
|
||||
})
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
|
||||
@ -8,9 +8,12 @@ import { SearchQuerySchema } from '@utils/Yup'
|
||||
import { Bot, List } from '@types'
|
||||
|
||||
const SearchBots = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
|
||||
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: req.query.page })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
const validated = await SearchQuerySchema.validate({
|
||||
q: req.query.q || req.query.query,
|
||||
page: req.query.page,
|
||||
})
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
})
|
||||
if (!validated) return
|
||||
|
||||
@ -8,9 +8,12 @@ import { SearchQuerySchema } from '@utils/Yup'
|
||||
import { Server, List } from '@types'
|
||||
|
||||
const SearchServers = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse) => {
|
||||
const validated = await SearchQuerySchema.validate({ q: req.query.q || req.query.query, page: req.query.page })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
const validated = await SearchQuerySchema.validate({
|
||||
q: req.query.q || req.query.query,
|
||||
page: req.query.page,
|
||||
})
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
})
|
||||
if (!validated) return
|
||||
|
||||
@ -5,9 +5,22 @@ import { EmbedBuilder, RESTJSONErrorCodes } from 'discord.js'
|
||||
import { CaptchaVerify, get, put, remove, update } from '@utils/Query'
|
||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { checkToken } from '@utils/Csrf'
|
||||
import { AddServerSubmitSchema, AddServerSubmit, CsrfCaptcha, ManageServerSchema, ManageServer } from '@utils/Yup'
|
||||
import {
|
||||
AddServerSubmitSchema,
|
||||
AddServerSubmit,
|
||||
CsrfCaptcha,
|
||||
ManageServerSchema,
|
||||
ManageServer,
|
||||
} from '@utils/Yup'
|
||||
import RequestHandler from '@utils/RequestHandler'
|
||||
import { checkUserFlag, diff, inspect, makeDiscordCodeblock, objectDiff, serialize } from '@utils/Tools'
|
||||
import {
|
||||
checkUserFlag,
|
||||
diff,
|
||||
inspect,
|
||||
makeDiscordCodeblock,
|
||||
objectDiff,
|
||||
serialize,
|
||||
} from '@utils/Tools'
|
||||
import { DiscordBot, discordLog } from '@utils/DiscordBot'
|
||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||
|
||||
@ -19,14 +32,14 @@ const patchLimiter = rateLimit({
|
||||
skip: (_req, res) => {
|
||||
res.removeHeader('X-RateLimit-Global')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
const Servers = RequestHandler()
|
||||
.get(async (req: GetApiRequest, res) => {
|
||||
const server = await get.server.load(req.query.id)
|
||||
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버 입니다.' })
|
||||
else {
|
||||
return ResponseWrapper(res, { code: 200, data: server })
|
||||
return ResponseWrapper(res, { code: 200, data: server })
|
||||
}
|
||||
})
|
||||
.post(async (req: PostApiRequest, res) => {
|
||||
@ -36,20 +49,20 @@ const Servers = RequestHandler()
|
||||
if (!csrfValidated) return
|
||||
|
||||
const validated = await AddServerSubmitSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
|
||||
if (!validated) return
|
||||
const captcha = await CaptchaVerify(validated._captcha)
|
||||
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
const result = await put.submitServer(user, req.query.id, validated)
|
||||
if (result === 1)
|
||||
return ResponseWrapper(res, {
|
||||
code: 400,
|
||||
message: '이미 등록된 서버 입니다.'
|
||||
message: '이미 등록된 서버 입니다.',
|
||||
})
|
||||
else if (result === 2)
|
||||
return ResponseWrapper(res, {
|
||||
@ -57,7 +70,7 @@ const Servers = RequestHandler()
|
||||
message: '봇이 초대되지 않았습니다.',
|
||||
errors: [
|
||||
'서버에 봇이 초대되지 않았습니다.',
|
||||
'이미 봇을 초대하셨다면, 잠시 후 다시 시도해주세요.'
|
||||
'이미 봇을 초대하셨다면, 잠시 후 다시 시도해주세요.',
|
||||
],
|
||||
})
|
||||
else if (result === 3)
|
||||
@ -66,7 +79,7 @@ const Servers = RequestHandler()
|
||||
message: '서버의 관리자가 아닙니다.',
|
||||
errors: [
|
||||
'해당 서버를 등록할 권한이 없습니다.',
|
||||
'서버에서 관리자 권한이 있으신지 확인해주세요.'
|
||||
'서버에서 관리자 권한이 있으신지 확인해주세요.',
|
||||
],
|
||||
})
|
||||
else if (result === 4)
|
||||
@ -75,14 +88,21 @@ const Servers = RequestHandler()
|
||||
message: '올바르지 않은 초대 코드 입니다.',
|
||||
errors: [
|
||||
'올바른 초대코드를 입력하셨는지 확인해주세요.',
|
||||
'만료되지 않는 초대코드인지 확인해주세요.'
|
||||
'만료되지 않는 초대코드인지 확인해주세요.',
|
||||
],
|
||||
})
|
||||
get.user.clear(user)
|
||||
await discordLog('SERVER/SUBMIT', user, new EmbedBuilder().setDescription(`[${req.query.id}](${KoreanbotsEndPoints.URL.server(req.query.id)})`), {
|
||||
content: inspect(serialize(validated)),
|
||||
format: 'js'
|
||||
})
|
||||
await discordLog(
|
||||
'SERVER/SUBMIT',
|
||||
user,
|
||||
new EmbedBuilder().setDescription(
|
||||
`[${req.query.id}](${KoreanbotsEndPoints.URL.server(req.query.id)})`
|
||||
),
|
||||
{
|
||||
content: inspect(serialize(validated)),
|
||||
format: 'js',
|
||||
}
|
||||
)
|
||||
|
||||
return ResponseWrapper(res, { code: 200, data: result })
|
||||
})
|
||||
@ -90,74 +110,120 @@ const Servers = RequestHandler()
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const server = await get.server.load(req.query.id)
|
||||
if(!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버 입니다.' })
|
||||
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버 입니다.' })
|
||||
const data = await get.serverData(req.query.id)
|
||||
if((!data || server.state === 'unreachable') && (await DiscordBot.fetchInvite(server.invite).catch((e) => e.code !== RESTJSONErrorCodes.UnknownInvite))) return ResponseWrapper(res, { code: 400, message: '해당 서버의 정보를 불러올 수 없습니다.', errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'] })
|
||||
if(![data.owner, ...data.admins].includes(user)) return ResponseWrapper(res, { code: 403 })
|
||||
if (
|
||||
(!data || server.state === 'unreachable') &&
|
||||
(await DiscordBot.fetchInvite(server.invite).catch(
|
||||
(e) => e.code !== RESTJSONErrorCodes.UnknownInvite
|
||||
))
|
||||
)
|
||||
return ResponseWrapper(res, {
|
||||
code: 400,
|
||||
message: '해당 서버의 정보를 불러올 수 없습니다.',
|
||||
errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'],
|
||||
})
|
||||
if (![data.owner, ...data.admins].includes(user)) return ResponseWrapper(res, { code: 403 })
|
||||
const userInfo = await get.user.load(user)
|
||||
if(['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 서버는 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] })
|
||||
if (['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff'))
|
||||
return ResponseWrapper(res, {
|
||||
code: 403,
|
||||
message: '해당 서버는 수정할 수 없습니다.',
|
||||
errors: ['오류라고 생각되면 문의해주세요.'],
|
||||
})
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
const captcha = await CaptchaVerify(req.body._captcha)
|
||||
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
if(req.body.name !== server.name) return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' })
|
||||
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
if (req.body.name !== server.name)
|
||||
return ResponseWrapper(res, { code: 400, message: '봇 이름을 입력해주세요.' })
|
||||
await remove.server(server.id)
|
||||
get.user.clear(user)
|
||||
await discordLog('SERVER/DELETE', user, (new EmbedBuilder().setDescription(`${server.name} - [${server.id}](${KoreanbotsEndPoints.URL.bot(server.id)}))`)),
|
||||
await discordLog(
|
||||
'SERVER/DELETE',
|
||||
user,
|
||||
new EmbedBuilder().setDescription(
|
||||
`${server.name} - [${server.id}](${KoreanbotsEndPoints.URL.bot(server.id)}))`
|
||||
),
|
||||
{
|
||||
content: inspect(server),
|
||||
format: 'js'
|
||||
format: 'js',
|
||||
}
|
||||
)
|
||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 삭제했습니다.' })
|
||||
})
|
||||
.patch(patchLimiter).patch(async (req: PatchApiRequest, res) => {
|
||||
.patch(patchLimiter)
|
||||
.patch(async (req: PatchApiRequest, res) => {
|
||||
const server = await get.server.load(req.query.id)
|
||||
if(!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
||||
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const userInfo = await get.user.load(user)
|
||||
const data = await get.serverData(req.query.id)
|
||||
if(!data || server.state === 'unreachable') return ResponseWrapper(res, { code: 400, message: '해당 서버의 정보를 불러올 수 없습니다.', errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'] })
|
||||
if(![data.owner, ...data.admins].includes(user) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403 })
|
||||
if(['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff')) return ResponseWrapper(res, { code: 403, message: '해당 서버는 수정할 수 없습니다.', errors: ['오류라고 생각되면 문의해주세요.'] })
|
||||
if (!data || server.state === 'unreachable')
|
||||
return ResponseWrapper(res, {
|
||||
code: 400,
|
||||
message: '해당 서버의 정보를 불러올 수 없습니다.',
|
||||
errors: ['봇이 추방되었거나, 오프라인이 아닌지 확인하시고 다시 시도해주세요.'],
|
||||
})
|
||||
if (![data.owner, ...data.admins].includes(user) && !checkUserFlag(userInfo?.flags, 'staff'))
|
||||
return ResponseWrapper(res, { code: 403 })
|
||||
if (['reported', 'blocked'].includes(server.state) && !checkUserFlag(userInfo?.flags, 'staff'))
|
||||
return ResponseWrapper(res, {
|
||||
code: 403,
|
||||
message: '해당 서버는 수정할 수 없습니다.',
|
||||
errors: ['오류라고 생각되면 문의해주세요.'],
|
||||
})
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
|
||||
const validated = await ManageServerSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
|
||||
if (!validated) return
|
||||
const invite = await DiscordBot.fetchInvite(validated.invite).catch(() => null)
|
||||
if(invite?.guild.id !== server.id || invite.expiresAt) return ResponseWrapper(res, { code: 400, message: '올바르지 않은 초대코드입니다.', errors: ['입력하신 초대코드가 올바르지 않습니다. 올바른 초대코드를 입력했는지 다시 한 번 확인해주세요.', '만료되지 않는 초대코드인지 확인해주세요.'] })
|
||||
|
||||
if (invite?.guild.id !== server.id || invite.expiresAt)
|
||||
return ResponseWrapper(res, {
|
||||
code: 400,
|
||||
message: '올바르지 않은 초대코드입니다.',
|
||||
errors: [
|
||||
'입력하신 초대코드가 올바르지 않습니다. 올바른 초대코드를 입력했는지 다시 한 번 확인해주세요.',
|
||||
'만료되지 않는 초대코드인지 확인해주세요.',
|
||||
],
|
||||
})
|
||||
|
||||
const result = await update.server(req.query.id, validated)
|
||||
|
||||
if(result === 0) return ResponseWrapper(res, { code: 400 })
|
||||
|
||||
if (result === 0) return ResponseWrapper(res, { code: 400 })
|
||||
else {
|
||||
get.server.clear(req.query.id)
|
||||
const embed = new EmbedBuilder().setDescription(`${server.name} - ([${server.id}](${KoreanbotsEndPoints.URL.server(server.id)}))`)
|
||||
const embed = new EmbedBuilder().setDescription(
|
||||
`${server.name} - ([${server.id}](${KoreanbotsEndPoints.URL.server(server.id)}))`
|
||||
)
|
||||
const diffData = objectDiff(
|
||||
{ intro: server.intro, invite: server.invite, category: JSON.stringify(server.category) },
|
||||
{ intro: validated.intro, invite: validated.invite, category: JSON.stringify(validated.category) },
|
||||
)
|
||||
diffData.forEach(d => {
|
||||
embed.addFields({name: d[0], value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff')
|
||||
})
|
||||
})
|
||||
await discordLog('SERVER/EDIT', user, embed,
|
||||
{
|
||||
content: `--- 설명\n${diff(server.desc, validated.desc, true)}`,
|
||||
format: 'diff'
|
||||
intro: validated.intro,
|
||||
invite: validated.invite,
|
||||
category: JSON.stringify(validated.category),
|
||||
}
|
||||
)
|
||||
diffData.forEach((d) => {
|
||||
embed.addFields({
|
||||
name: d[0],
|
||||
value: makeDiscordCodeblock(diff(d[1][0] || '', d[1][1] || ''), 'diff'),
|
||||
})
|
||||
})
|
||||
await discordLog('SERVER/EDIT', user, embed, {
|
||||
content: `--- 설명\n${diff(server.desc, validated.desc, true)}`,
|
||||
format: 'diff',
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
interface GetApiRequest extends NextApiRequest {
|
||||
@ -175,7 +241,7 @@ interface PatchApiRequest extends GetApiRequest {
|
||||
}
|
||||
|
||||
interface DeleteApiRequest extends GetApiRequest {
|
||||
body: CsrfCaptcha & { name: string } | null
|
||||
body: (CsrfCaptcha & { name: string }) | null
|
||||
}
|
||||
|
||||
export default Servers
|
||||
|
||||
@ -4,17 +4,16 @@ import RequestHandler from '@utils/RequestHandler'
|
||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { get } from '@utils/Query'
|
||||
|
||||
const ServerOwners = RequestHandler()
|
||||
.get(async (req: GetApiRequest, res) => {
|
||||
const owners = await get.serverOwners(req.query.id)
|
||||
if(!owners) return ResponseWrapper(res, { code: 404 })
|
||||
return ResponseWrapper(res, { code: 200, data: owners })
|
||||
})
|
||||
const ServerOwners = RequestHandler().get(async (req: GetApiRequest, res) => {
|
||||
const owners = await get.serverOwners(req.query.id)
|
||||
if (!owners) return ResponseWrapper(res, { code: 404 })
|
||||
return ResponseWrapper(res, { code: 200, data: owners })
|
||||
})
|
||||
|
||||
interface GetApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export default ServerOwners
|
||||
export default ServerOwners
|
||||
|
||||
@ -4,7 +4,7 @@ import rateLimit from 'express-rate-limit'
|
||||
import { get } from '@utils/Query'
|
||||
import RequestHandler from '@utils/RequestHandler'
|
||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { ReportSchema, Report} from '@utils/Yup'
|
||||
import { ReportSchema, Report } from '@utils/Yup'
|
||||
import { webhookClients } from '@utils/DiscordBot'
|
||||
import { checkToken } from '@utils/Csrf'
|
||||
|
||||
@ -18,36 +18,40 @@ const limiter = rateLimit({
|
||||
skip: (_req, res) => {
|
||||
res.removeHeader('X-RateLimit-Global')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const ServerReport = RequestHandler().post(limiter)
|
||||
const ServerReport = RequestHandler()
|
||||
.post(limiter)
|
||||
.post(async (req: PostApiRequest, res) => {
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if(!user) return ResponseWrapper(res, { code: 401 })
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const server = await get.server.load(req.query.id)
|
||||
if(!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
||||
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
if(!req.body) return ResponseWrapper(res, { code: 400 })
|
||||
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
||||
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
|
||||
if(!validated) return
|
||||
await webhookClients.internal.reportChannel.send({ threadName: `서버-${server.id}`, content: `Reported by <@${user}> (${user})\nReported **${server.name}** (${server.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``, allowedMentions: { parse: ['users'] }})
|
||||
|
||||
if (!validated) return
|
||||
await webhookClients.internal.reportChannel.send({
|
||||
threadName: `서버-${server.id}`,
|
||||
content: `Reported by <@${user}> (${user})\nReported **${server.name}** (${server.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``,
|
||||
allowedMentions: { parse: ['users'] },
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
|
||||
})
|
||||
|
||||
|
||||
interface PostApiRequest extends NextApiRequest {
|
||||
body: Report | null
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
body: Report | null
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export default ServerReport
|
||||
export default ServerReport
|
||||
|
||||
@ -12,49 +12,56 @@ import { WebhookType } from '@types'
|
||||
const ServerVote = RequestHandler()
|
||||
.get(async (req: GetApiRequest, res) => {
|
||||
const server = await get.ServerAuthorization(req.headers.authorization)
|
||||
if(!server) return ResponseWrapper(res, { code: 401 })
|
||||
if(req.query.id !== server) return ResponseWrapper(res, { code: 403 })
|
||||
const userID = await Yup.string().required().label('userID').validate(req.query.userID).then(el => el).catch(e => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
if(!userID) return ResponseWrapper(res, { code: 400 })
|
||||
if (!server) return ResponseWrapper(res, { code: 401 })
|
||||
if (req.query.id !== server) return ResponseWrapper(res, { code: 403 })
|
||||
const userID = await Yup.string()
|
||||
.required()
|
||||
.label('userID')
|
||||
.validate(req.query.userID)
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
if (!userID) return ResponseWrapper(res, { code: 400 })
|
||||
const result = await get.vote(userID, server, 'server')
|
||||
return ResponseWrapper(res, { code: 200, data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result } })
|
||||
return ResponseWrapper(res, {
|
||||
code: 200,
|
||||
data: { voted: +new Date() < result + VOTE_COOLDOWN, lastVote: result },
|
||||
})
|
||||
})
|
||||
.post(async (req: PostApiRequest, res) => {
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if(!user) return ResponseWrapper(res, { code: 401 })
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const server = await get.server.load(req.query.id)
|
||||
if (!server) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 서버입니다.' })
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
const captcha = await CaptchaVerify(req.body._captcha)
|
||||
if(!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
if (!captcha) return ResponseWrapper(res, { code: 400, message: '캡챠 검증에 실패하였습니다.' })
|
||||
|
||||
const vote = await put.voteServer(user, server.id)
|
||||
if(vote === null) return ResponseWrapper(res, { code: 401 })
|
||||
else if(vote === true) {
|
||||
if (vote === null) return ResponseWrapper(res, { code: 401 })
|
||||
else if (vote === true) {
|
||||
sendWebhook(server, {
|
||||
type: 'server',
|
||||
type: 'server',
|
||||
data: {
|
||||
guildId: server.id,
|
||||
type: WebhookType.HeartChange,
|
||||
before: server.votes,
|
||||
after: server.votes + 1,
|
||||
userId: user
|
||||
userId: user,
|
||||
},
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200 })
|
||||
}
|
||||
else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
|
||||
} else return ResponseWrapper(res, { code: 429, data: { retryAfter: vote } })
|
||||
})
|
||||
|
||||
interface ApiRequest extends NextApiRequest {
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GetApiRequest extends ApiRequest {
|
||||
@ -65,8 +72,8 @@ interface GetApiRequest extends ApiRequest {
|
||||
}
|
||||
interface PostApiRequest extends ApiRequest {
|
||||
body: {
|
||||
_captcha: string
|
||||
_csrf: string
|
||||
}
|
||||
_captcha: string
|
||||
_csrf: string
|
||||
}
|
||||
}
|
||||
export default ServerVote
|
||||
export default ServerVote
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { NextApiRequest} from 'next'
|
||||
import { NextApiRequest } from 'next'
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
import { get } from '@utils/Query'
|
||||
import RequestHandler from '@utils/RequestHandler'
|
||||
import ResponseWrapper from '@utils/ResponseWrapper'
|
||||
import { ReportSchema, Report} from '@utils/Yup'
|
||||
import { ReportSchema, Report } from '@utils/Yup'
|
||||
import { webhookClients } from '@utils/DiscordBot'
|
||||
import { checkToken } from '@utils/Csrf'
|
||||
|
||||
@ -17,36 +17,46 @@ const limiter = rateLimit({
|
||||
skip: (_req, res) => {
|
||||
res.removeHeader('X-RateLimit-Global')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const UserReport = RequestHandler().post(limiter)
|
||||
const UserReport = RequestHandler()
|
||||
.post(limiter)
|
||||
.post(async (req: PostApiRequest, res) => {
|
||||
const user = await get.Authorization(req.cookies.token)
|
||||
if(!user) return ResponseWrapper(res, { code: 401 })
|
||||
if (!user) return ResponseWrapper(res, { code: 401 })
|
||||
const userInfo = await get.user.load(req.query.id)
|
||||
if(!userInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 유저입니다.' })
|
||||
if (!userInfo) return ResponseWrapper(res, { code: 404, message: '존재하지 않는 유저입니다.' })
|
||||
const csrfValidated = checkToken(req, res, req.body._csrf)
|
||||
if (!csrfValidated) return
|
||||
if(!req.body) return ResponseWrapper(res, { code: 400 })
|
||||
if (!req.body) return ResponseWrapper(res, { code: 400 })
|
||||
const validated: Report = await ReportSchema.validate(req.body, { abortEarly: false })
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
|
||||
if(!validated) return
|
||||
await webhookClients.internal.reportChannel.send({ threadName: `유저-${userInfo.id}`, content: `Reported by <@${user}> (${user})\nReported **${userInfo.tag === '0' ? userInfo.globalName + ' @' + userInfo.username : userInfo.username + '#' + userInfo.tag}** <@${userInfo.id}> (${userInfo.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${req.body.description}\`\`\``, allowedMentions: { parse: ['users'] }})
|
||||
|
||||
if (!validated) return
|
||||
await webhookClients.internal.reportChannel.send({
|
||||
threadName: `유저-${userInfo.id}`,
|
||||
content: `Reported by <@${user}> (${user})\nReported **${
|
||||
userInfo.tag === '0'
|
||||
? userInfo.globalName + ' @' + userInfo.username
|
||||
: userInfo.username + '#' + userInfo.tag
|
||||
}** <@${userInfo.id}> (${userInfo.id})\nCategory ${req.body.category}\nDesc\n\`\`\`${
|
||||
req.body.description
|
||||
}\`\`\``,
|
||||
allowedMentions: { parse: ['users'] },
|
||||
})
|
||||
return ResponseWrapper(res, { code: 200, message: '성공적으로 처리되었습니다.' })
|
||||
})
|
||||
|
||||
|
||||
interface PostApiRequest extends NextApiRequest {
|
||||
body: Report | null
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
body: Report | null
|
||||
query: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export default UserReport
|
||||
export default UserReport
|
||||
|
||||
@ -20,8 +20,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
|
||||
scale,
|
||||
icon,
|
||||
})
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
@ -34,8 +34,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
|
||||
const userImage = !data.avatar
|
||||
? null
|
||||
: await get.images.user.load(
|
||||
DiscordEnpoints.CDN.user(data.id, data.avatar, { format: 'png', size: 128 })
|
||||
)
|
||||
DiscordEnpoints.CDN.user(data.id, data.avatar, { format: 'png', size: 128 })
|
||||
)
|
||||
const img =
|
||||
userImage ||
|
||||
(await get.images.user.load(
|
||||
|
||||
@ -20,8 +20,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
|
||||
scale,
|
||||
icon,
|
||||
})
|
||||
.then(el => el)
|
||||
.catch(e => {
|
||||
.then((el) => el)
|
||||
.catch((e) => {
|
||||
ResponseWrapper(res, { code: 400, errors: e.errors })
|
||||
return null
|
||||
})
|
||||
@ -34,8 +34,8 @@ const Widget = RequestHandler().get(async (req: ApiRequest, res: NextApiResponse
|
||||
const userImage = !data.icon
|
||||
? null
|
||||
: await get.images.user.load(
|
||||
DiscordEnpoints.CDN.guild(data.id, data.icon, { format: 'png', size: 128 })
|
||||
)
|
||||
DiscordEnpoints.CDN.guild(data.id, data.icon, { format: 'png', size: 128 })
|
||||
)
|
||||
const img =
|
||||
userImage ||
|
||||
(await get.images.user.load(
|
||||
|
||||
@ -37,290 +37,535 @@ const Modal = dynamic(() => import('@components/Modal'))
|
||||
const Captcha = dynamic(() => import('@components/Captcha'))
|
||||
const Login = dynamic(() => import('@components/Login'))
|
||||
|
||||
const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }) => {
|
||||
const [ data, setData ] = useState(null)
|
||||
const [ adminModal, setAdminModal ] = useState(false)
|
||||
const [ transferModal, setTransferModal ] = useState(false)
|
||||
const [ deleteModal, setDeleteModal ] = useState(false)
|
||||
const ManageBotPage: NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme }) => {
|
||||
const [data, setData] = useState(null)
|
||||
const [adminModal, setAdminModal] = useState(false)
|
||||
const [transferModal, setTransferModal] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
async function submitBot(value: ManageBot) {
|
||||
const res = await Fetch(`/bots/${bot.id}`, { method: 'PATCH', body: JSON.stringify(cleanObject<ManageBot>(value)) })
|
||||
const res = await Fetch(`/bots/${bot.id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(cleanObject<ManageBot>(value)),
|
||||
})
|
||||
setData(res)
|
||||
}
|
||||
|
||||
async function getUser(id: string) {
|
||||
const u = await Fetch<User>(`/users/${encodeURIComponent(id)}`)
|
||||
if(u.code === 200 && u.data) return u.data
|
||||
if (u.code === 200 && u.data) return u.data
|
||||
else return null
|
||||
}
|
||||
|
||||
if(!bot) return <NotFound />
|
||||
if(!user) return <Login>
|
||||
<NextSeo title='봇 정보 수정하기' description='봇의 정보를 수정합니다.'/>
|
||||
</Login>
|
||||
if(!(bot.owners as User[]).find(el => el.id === user.id) && !checkUserFlag(user.flags, 'staff')) return <Forbidden />
|
||||
if (!bot) return <NotFound />
|
||||
if (!user)
|
||||
return (
|
||||
<Login>
|
||||
<NextSeo title='봇 정보 수정하기' description='봇의 정보를 수정합니다.' />
|
||||
</Login>
|
||||
)
|
||||
if (
|
||||
!(bot.owners as User[]).find((el) => el.id === user.id) &&
|
||||
!checkUserFlag(user.flags, 'staff')
|
||||
)
|
||||
return <Forbidden />
|
||||
return (
|
||||
<Container paddingTop className='pt-5 pb-10'>
|
||||
<NextSeo title={`${bot.name} 수정하기`} description='봇의 정보를 수정합니다.'/>
|
||||
<h1 className='text-3xl font-bold mb-8'>봇 관리하기</h1>
|
||||
<Formik initialValues={cleanObject({
|
||||
agree: false,
|
||||
id: bot.id,
|
||||
prefix: bot.prefix,
|
||||
library: bot.lib,
|
||||
category: bot.category,
|
||||
intro: bot.intro,
|
||||
desc: bot.desc,
|
||||
website: bot.web,
|
||||
url: bot.url,
|
||||
git: bot.git,
|
||||
discord: bot.discord,
|
||||
_csrf: csrfToken
|
||||
})}
|
||||
validationSchema={ManageBotSchema}
|
||||
onSubmit={submitBot}>
|
||||
<Container paddingTop className='pb-10 pt-5'>
|
||||
<NextSeo title={`${bot.name} 수정하기`} description='봇의 정보를 수정합니다.' />
|
||||
<h1 className='mb-8 text-3xl font-bold'>봇 관리하기</h1>
|
||||
<Formik
|
||||
initialValues={cleanObject({
|
||||
agree: false,
|
||||
id: bot.id,
|
||||
prefix: bot.prefix,
|
||||
library: bot.lib,
|
||||
category: bot.category,
|
||||
intro: bot.intro,
|
||||
desc: bot.desc,
|
||||
website: bot.web,
|
||||
url: bot.url,
|
||||
git: bot.git,
|
||||
discord: bot.discord,
|
||||
_csrf: csrfToken,
|
||||
})}
|
||||
validationSchema={ManageBotSchema}
|
||||
onSubmit={submitBot}
|
||||
>
|
||||
{({ errors, touched, values, setFieldTouched, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className='md:flex text-center md:text-left'>
|
||||
<DiscordAvatar userID={bot.id} className='md:mx-1 mx-auto rounded-full'/>
|
||||
<div className='md:w-2/3 px-8 py-6'>
|
||||
<h1 className='text-3xl font-bold'>{bot.name}#{bot.tag}</h1>
|
||||
<div className='text-center md:flex md:text-left'>
|
||||
<DiscordAvatar userID={bot.id} className='mx-auto rounded-full md:mx-1' />
|
||||
<div className='px-8 py-6 md:w-2/3'>
|
||||
<h1 className='text-3xl font-bold'>
|
||||
{bot.name}#{bot.tag}
|
||||
</h1>
|
||||
<h2>ID: {bot.id}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
data ? data.code === 200 ? <div className='mt-4'>
|
||||
<Redirect to={makeBotURL(bot)}>
|
||||
<Message type='success'>
|
||||
<h2 className='text-lg font-extrabold'>정보를 저장했습니다.</h2>
|
||||
<p>반영까지는 시간이 조금 걸릴 수 있습니다!</p>
|
||||
{data ? (
|
||||
data.code === 200 ? (
|
||||
<div className='mt-4'>
|
||||
<Redirect to={makeBotURL(bot)}>
|
||||
<Message type='success'>
|
||||
<h2 className='text-lg font-extrabold'>정보를 저장했습니다.</h2>
|
||||
<p>반영까지는 시간이 조금 걸릴 수 있습니다!</p>
|
||||
</Message>
|
||||
</Redirect>
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-4'>
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-extrabold'>
|
||||
{data.message || '오류가 발생했습니다.'}
|
||||
</h2>
|
||||
<ul className='list-inside list-disc'>
|
||||
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||
</ul>
|
||||
</Message>
|
||||
</Redirect>
|
||||
|
||||
</div> : <div className='mt-4'>
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-extrabold'>{data.message || '오류가 발생했습니다.'}</h2>
|
||||
<ul className='list-disc list-inside'>
|
||||
{data.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||
</ul>
|
||||
</Message>
|
||||
</div> : ''
|
||||
}
|
||||
<Label For='prefix' label='접두사' labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)' error={errors.prefix && touched.prefix ? errors.prefix : null} short required>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<Label
|
||||
For='prefix'
|
||||
label='접두사'
|
||||
labelDesc='봇의 사용시 앞 쪽에 붙은 기호를 의미합니다. (Prefix)'
|
||||
error={errors.prefix && touched.prefix ? errors.prefix : null}
|
||||
short
|
||||
required
|
||||
>
|
||||
<Input name='prefix' placeholder='!' />
|
||||
</Label>
|
||||
<Label For='library' label='라이브러리' labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.' short required error={errors.library && touched.library ? errors.library : null}>
|
||||
<Select value={{ label: bot.lib, value: bot.lib }} options={library.map(el=> ({ label: el, value: el }))} handleChange={(value) => setFieldValue('library', value.value)} handleTouch={() => setFieldTouched('library', true)} />
|
||||
<Label
|
||||
For='library'
|
||||
label='라이브러리'
|
||||
labelDesc='봇에 사용된 라이브러리를 선택해주세요. 해당되는 라이브러리가 없다면 기타를 선택해주세요.'
|
||||
short
|
||||
required
|
||||
error={errors.library && touched.library ? errors.library : null}
|
||||
>
|
||||
<Select
|
||||
value={{ label: bot.lib, value: bot.lib }}
|
||||
options={library.map((el) => ({ label: el, value: el }))}
|
||||
handleChange={(value) => setFieldValue('library', value.value)}
|
||||
handleTouch={() => setFieldTouched('library', true)}
|
||||
/>
|
||||
</Label>
|
||||
<Label For='category' label='카테고리' labelDesc='봇에 해당되는 카테고리를 선택해주세요' required error={errors.category && touched.category ? errors.category as string : null}>
|
||||
<Selects options={botCategories.map(el=> ({ label: el, value: el }))} handleChange={(value) => {
|
||||
setFieldValue('category', value.map(v=> v.value))
|
||||
}} handleTouch={() => setFieldTouched('category', true)} values={values.category as string[]} setValues={(value) => setFieldValue('category', value)} />
|
||||
<span className='text-gray-400 mt-1 text-sm'>봇 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요. <strong>반드시 해당되는 카테고리만 선택해주세요.</strong></span>
|
||||
<Label
|
||||
For='category'
|
||||
label='카테고리'
|
||||
labelDesc='봇에 해당되는 카테고리를 선택해주세요'
|
||||
required
|
||||
error={errors.category && touched.category ? (errors.category as string) : null}
|
||||
>
|
||||
<Selects
|
||||
options={botCategories.map((el) => ({ label: el, value: el }))}
|
||||
handleChange={(value) => {
|
||||
setFieldValue(
|
||||
'category',
|
||||
value.map((v) => v.value)
|
||||
)
|
||||
}}
|
||||
handleTouch={() => setFieldTouched('category', true)}
|
||||
values={values.category as string[]}
|
||||
setValues={(value) => setFieldValue('category', value)}
|
||||
/>
|
||||
<span className='mt-1 text-sm text-gray-400'>
|
||||
봇 카드에는 앞 3개의 카테고리만 표시됩니다. 드래그하여 카테고리를 정렬하세요.{' '}
|
||||
<strong>반드시 해당되는 카테고리만 선택해주세요.</strong>
|
||||
</span>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Label For='website' label='웹사이트' labelDesc='봇의 웹사이트를 작성해주세요.' error={errors.website && touched.website ? errors.website : null}>
|
||||
<Label
|
||||
For='website'
|
||||
label='웹사이트'
|
||||
labelDesc='봇의 웹사이트를 작성해주세요.'
|
||||
error={errors.website && touched.website ? errors.website : null}
|
||||
>
|
||||
<Input name='website' placeholder='https://koreanbots.dev' />
|
||||
</Label>
|
||||
<Label For='git' label='Git URL' labelDesc='봇 소스코드의 Git 주소를 입력해주세요 (오픈소스인 경우)' error={errors.git && touched.git ? errors.git : null}>
|
||||
<Input name='git' placeholder='https://github.com/koreanbots/koreanbots'/>
|
||||
<Label
|
||||
For='git'
|
||||
label='Git URL'
|
||||
labelDesc='봇 소스코드의 Git 주소를 입력해주세요 (오픈소스인 경우)'
|
||||
error={errors.git && touched.git ? errors.git : null}
|
||||
>
|
||||
<Input name='git' placeholder='https://github.com/koreanbots/koreanbots' />
|
||||
</Label>
|
||||
<Label For='inviteLink' label='초대링크' labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.' error={errors.url && touched.url ? errors.url : null}>
|
||||
<Input name='url' placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0' />
|
||||
<span className='text-gray-400 mt-1 text-sm'>
|
||||
<Label
|
||||
For='inviteLink'
|
||||
label='초대링크'
|
||||
labelDesc='봇의 초대링크입니다. 비워두시면 자동으로 생성합니다.'
|
||||
error={errors.url && touched.url ? errors.url : null}
|
||||
>
|
||||
<Input
|
||||
name='url'
|
||||
placeholder='https://discord.com/oauth2/authorize?client_id=653534001742741552&scope=bot&permissions=0'
|
||||
/>
|
||||
<span className='mt-1 text-sm text-gray-400'>
|
||||
<Link
|
||||
href='/calculator'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:text-blue-400'>
|
||||
이곳
|
||||
</Link>에서 초대링크를 생성하실 수 있습니다!
|
||||
className='text-blue-500 hover:text-blue-400'
|
||||
>
|
||||
이곳
|
||||
</Link>
|
||||
에서 초대링크를 생성하실 수 있습니다!
|
||||
</span>
|
||||
</Label>
|
||||
<Label For='discord' label='지원 디스코드 서버' labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)' error={errors.discord && touched.discord ? errors.discord : null} short>
|
||||
<Label
|
||||
For='discord'
|
||||
label='지원 디스코드 서버'
|
||||
labelDesc='봇의 지원 디스코드 서버를 입력해주세요. (봇에 대해 도움을 받을 수 있는 공간입니다.)'
|
||||
error={errors.discord && touched.discord ? errors.discord : null}
|
||||
short
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
discord.gg/<Input name='discord' placeholder='JEh53MQ' />
|
||||
discord.gg/
|
||||
<Input name='discord' placeholder='JEh53MQ' />
|
||||
</div>
|
||||
</Label>
|
||||
<Divider />
|
||||
<Label For='intro' label='봇 소개' labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)' error={errors.intro && touched.intro ? errors.intro : null} required>
|
||||
<Label
|
||||
For='intro'
|
||||
label='봇 소개'
|
||||
labelDesc='봇을 소개할 수 있는 간단한 설명을 적어주세요. (최대 60자)'
|
||||
error={errors.intro && touched.intro ? errors.intro : null}
|
||||
required
|
||||
>
|
||||
<Input name='intro' placeholder='국내 봇을 한 곳에서.' />
|
||||
</Label>
|
||||
<Label For='intro' label='봇 설명' labelDesc={<>봇을 자세하게 설명해주세요! (최대 1500자)<br/>마크다운을 지원합니다!</>} error={errors.desc && touched.desc ? errors.desc : null} required>
|
||||
<TextArea name='desc' placeholder='봇에 대해 최대한 자세히 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.desc} setValue={(value) => setFieldValue('desc', value)} max={1500} />
|
||||
<Label
|
||||
For='intro'
|
||||
label='봇 설명'
|
||||
labelDesc={
|
||||
<>
|
||||
봇을 자세하게 설명해주세요! (최대 1500자)
|
||||
<br />
|
||||
마크다운을 지원합니다!
|
||||
</>
|
||||
}
|
||||
error={errors.desc && touched.desc ? errors.desc : null}
|
||||
required
|
||||
>
|
||||
<TextArea
|
||||
name='desc'
|
||||
placeholder='봇에 대해 최대한 자세히 설명해주세요!'
|
||||
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||
value={values.desc}
|
||||
setValue={(value) => setFieldValue('desc', value)}
|
||||
max={1500}
|
||||
/>
|
||||
</Label>
|
||||
<Label For='preview' label='설명 미리보기' labelDesc='다음 결과는 실제와 다를 수 있습니다'>
|
||||
<Label
|
||||
For='preview'
|
||||
label='설명 미리보기'
|
||||
labelDesc='다음 결과는 실제와 다를 수 있습니다'
|
||||
>
|
||||
<Segment>
|
||||
<Markdown text={values.desc} />
|
||||
</Segment>
|
||||
</Label>
|
||||
<Divider />
|
||||
<p className='text-base mt-2 mb-5'>
|
||||
<span className='text-red-500 font-semibold'> *</span> = 필수 항목
|
||||
<p className='mb-5 mt-2 text-base'>
|
||||
<span className='font-semibold text-red-500'> *</span> = 필수 항목
|
||||
</p>
|
||||
<Button type='submit' onClick={() => window.scrollTo({ top: 0 })}>
|
||||
<>
|
||||
<i className='far fa-save'/> 저장
|
||||
<i className='far fa-save' /> 저장
|
||||
</>
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
{
|
||||
(checkUserFlag(user.flags, 'staff') || (bot.owners as User[])[0].id === user.id) && <div className='py-4'>
|
||||
{(checkUserFlag(user.flags, 'staff') || (bot.owners as User[])[0].id === user.id) && (
|
||||
<div className='py-4'>
|
||||
<Divider />
|
||||
<h2 className='text-2xl font-semibold pb-2'>위험구역</h2>
|
||||
<h2 className='pb-2 text-2xl font-semibold'>위험구역</h2>
|
||||
<Segment>
|
||||
<div className='lg:flex items-center'>
|
||||
<div className='items-center lg:flex'>
|
||||
<div className='grow py-1'>
|
||||
<h3 className='text-lg font-semibold'>관리자 수정</h3>
|
||||
<p className='text-gray-400'>봇의 관리자를 추가하거나 삭제합니다.</p>
|
||||
</div>
|
||||
<Button onClick={() => setAdminModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-user-cog' /> 관리자 수정</Button>
|
||||
<Modal full header='관리자 수정' isOpen={adminModal} dark={theme === 'dark'} onClose={() => setAdminModal(false)} closeIcon>
|
||||
<Formik initialValues={{ owners: (bot.owners as User[]), id: '', _captcha: '' }} onSubmit={async (v) => {
|
||||
const res = await Fetch(`/bots/${bot.id}/owners`, { method: 'PATCH', body: JSON.stringify({
|
||||
_captcha: v._captcha,
|
||||
_csrf: csrfToken,
|
||||
owners: v.owners.map(el => el.id)
|
||||
}) })
|
||||
if(res.code === 200) {
|
||||
alert('성공적으로 수정했습니다.')
|
||||
router.push(makeBotURL(bot))
|
||||
} else {
|
||||
alert(res.message)
|
||||
setAdminModal(false)
|
||||
}
|
||||
}}>
|
||||
{
|
||||
({ values, setFieldValue }) => <Form>
|
||||
<Button
|
||||
onClick={() => setAdminModal(true)}
|
||||
className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
|
||||
>
|
||||
<i className='fas fa-user-cog' /> 관리자 수정
|
||||
</Button>
|
||||
<Modal
|
||||
full
|
||||
header='관리자 수정'
|
||||
isOpen={adminModal}
|
||||
dark={theme === 'dark'}
|
||||
onClose={() => setAdminModal(false)}
|
||||
closeIcon
|
||||
>
|
||||
<Formik
|
||||
initialValues={{ owners: bot.owners as User[], id: '', _captcha: '' }}
|
||||
onSubmit={async (v) => {
|
||||
const res = await Fetch(`/bots/${bot.id}/owners`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
_captcha: v._captcha,
|
||||
_csrf: csrfToken,
|
||||
owners: v.owners.map((el) => el.id),
|
||||
}),
|
||||
})
|
||||
if (res.code === 200) {
|
||||
alert('성공적으로 수정했습니다.')
|
||||
router.push(makeBotURL(bot))
|
||||
} else {
|
||||
alert(res.message)
|
||||
setAdminModal(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form>
|
||||
<Message type='warning'>
|
||||
<p>소유자는 삭제할 수 없습니다. 소유권을 이전하고 싶으시다면 소유권 이전을 사용해주세요.</p>
|
||||
<p>
|
||||
소유자는 삭제할 수 없습니다. 소유권을 이전하고 싶으시다면 소유권 이전을
|
||||
사용해주세요.
|
||||
</p>
|
||||
</Message>
|
||||
<div className='py-4'>
|
||||
<h2 className='text-md my-1'>이전하실 유저 ID를 입력해주세요.</h2>
|
||||
<div className='flex flex-wrap'>
|
||||
{
|
||||
(values.owners as User[]).map((el, n) => <Tag className='flex items-center' text={<>
|
||||
<DiscordAvatar userID={el.id} size={128} className='w-6 h-6 mr-1 rounded-full' /> {el.tag === '0' ? `${el.globalName} (@${el.username})` : `${el.username}#${el.tag}`}
|
||||
{
|
||||
n !== 0 && <button className='ml-0.5 hover:text-red-500' onClick={() => {
|
||||
setFieldValue('owners', (() => {
|
||||
const arr = [...values.owners]
|
||||
arr.splice(n, 1)
|
||||
return arr
|
||||
})())
|
||||
}}>
|
||||
<i className='fas fa-times' />
|
||||
</button>
|
||||
{(values.owners as User[]).map((el, n) => (
|
||||
<Tag
|
||||
className='flex items-center'
|
||||
text={
|
||||
<>
|
||||
<DiscordAvatar
|
||||
userID={el.id}
|
||||
size={128}
|
||||
className='mr-1 h-6 w-6 rounded-full'
|
||||
/>{' '}
|
||||
{el.tag === '0'
|
||||
? `${el.globalName} (@${el.username})`
|
||||
: `${el.username}#${el.tag}`}
|
||||
{n !== 0 && (
|
||||
<button
|
||||
className='ml-0.5 hover:text-red-500'
|
||||
onClick={() => {
|
||||
setFieldValue(
|
||||
'owners',
|
||||
(() => {
|
||||
const arr = [...values.owners]
|
||||
arr.splice(n, 1)
|
||||
return arr
|
||||
})()
|
||||
)
|
||||
}}
|
||||
>
|
||||
<i className='fas fa-times' />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
</>} key={el.id} />)
|
||||
}
|
||||
key={el.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='grow pr-2'>
|
||||
<Input name='id' placeholder='추가할 유저 ID' />
|
||||
</div>
|
||||
<Button className='w-16 bg-discord-blurple' onClick={async () => {
|
||||
if(values.owners.find(el => el.id === values.id)) return alert('이미 존재하는 유저입니다.')
|
||||
const user = await getUser(values.id)
|
||||
const arr = [...values.owners]
|
||||
if(!user) return alert('올바르지 않은 유저입니다.')
|
||||
else {
|
||||
arr.push(user)
|
||||
setFieldValue('owners', arr)
|
||||
setFieldValue('id', '')
|
||||
}
|
||||
}}>
|
||||
<Button
|
||||
className='w-16 bg-discord-blurple'
|
||||
onClick={async () => {
|
||||
if (values.owners.find((el) => el.id === values.id))
|
||||
return alert('이미 존재하는 유저입니다.')
|
||||
const user = await getUser(values.id)
|
||||
const arr = [...values.owners]
|
||||
if (!user) return alert('올바르지 않은 유저입니다.')
|
||||
else {
|
||||
arr.push(user)
|
||||
setFieldValue('owners', arr)
|
||||
setFieldValue('id', '')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className='fas fa-user-plus text-white' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} />
|
||||
<Button disabled={!values._captcha} className={`mt-2 bg-red-500 text-white ${!values._captcha ? 'opacity-80' : 'hover:opacity-80'}`} type='submit'><i className='fas fa-save text-sm' /> 저장</Button>
|
||||
<Captcha
|
||||
dark={theme === 'dark'}
|
||||
onVerify={(k) => setFieldValue('_captcha', k)}
|
||||
/>
|
||||
<Button
|
||||
disabled={!values._captcha}
|
||||
className={`mt-2 bg-red-500 text-white ${
|
||||
!values._captcha ? 'opacity-80' : 'hover:opacity-80'
|
||||
}`}
|
||||
type='submit'
|
||||
>
|
||||
<i className='fas fa-save text-sm' /> 저장
|
||||
</Button>
|
||||
</Form>
|
||||
}
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='lg:flex items-center'>
|
||||
<div className='items-center lg:flex'>
|
||||
<div className='grow py-1'>
|
||||
<h3 className='text-lg font-semibold'>소유권 이전</h3>
|
||||
<p className='text-gray-400'>봇의 소유권을 이전합니다. 소유권을 이전하게 되면 소유권을 잃게 됩니다.</p>
|
||||
<p className='text-gray-400'>
|
||||
봇의 소유권을 이전합니다. 소유권을 이전하게 되면 소유권을 잃게 됩니다.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setTransferModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-exchange-alt' /> 소유권 이전</Button>
|
||||
<Modal full header={`${bot.name} 소유권 이전하기`} isOpen={transferModal} dark={theme === 'dark'} onClose={() => setTransferModal(false)} closeIcon>
|
||||
<Formik initialValues={{ ownerID: '', name: '', _captcha: '' }} onSubmit={async (v) => {
|
||||
const res = await Fetch(`/bots/${bot.id}/owners`, { method: 'PATCH', body: JSON.stringify({
|
||||
_captcha: v._captcha,
|
||||
_csrf: csrfToken,
|
||||
owners: [ v.ownerID ]
|
||||
}) })
|
||||
if(res.code === 200) {
|
||||
alert('성공적으로 소유권을 이전했습니다.')
|
||||
router.push('/')
|
||||
} else {
|
||||
alert(res.message)
|
||||
setTransferModal(false)
|
||||
}
|
||||
}}>
|
||||
{
|
||||
({ values, setFieldValue }) => <Form>
|
||||
<Button
|
||||
onClick={() => setTransferModal(true)}
|
||||
className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
|
||||
>
|
||||
<i className='fas fa-exchange-alt' /> 소유권 이전
|
||||
</Button>
|
||||
<Modal
|
||||
full
|
||||
header={`${bot.name} 소유권 이전하기`}
|
||||
isOpen={transferModal}
|
||||
dark={theme === 'dark'}
|
||||
onClose={() => setTransferModal(false)}
|
||||
closeIcon
|
||||
>
|
||||
<Formik
|
||||
initialValues={{ ownerID: '', name: '', _captcha: '' }}
|
||||
onSubmit={async (v) => {
|
||||
const res = await Fetch(`/bots/${bot.id}/owners`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
_captcha: v._captcha,
|
||||
_csrf: csrfToken,
|
||||
owners: [v.ownerID],
|
||||
}),
|
||||
})
|
||||
if (res.code === 200) {
|
||||
alert('성공적으로 소유권을 이전했습니다.')
|
||||
router.push('/')
|
||||
} else {
|
||||
alert(res.message)
|
||||
setTransferModal(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form>
|
||||
<Message type='warning'>
|
||||
<h2 className='text-2xl font-bold'>주의해주세요!</h2>
|
||||
<p>봇의 소유권을 이전하게 되면 봇의 소유자 권한을 이전하게 되며, 본인을 포함한 모든 관리자가 해당 봇에 대한 권한을 잃게됩니다.</p>
|
||||
<p>
|
||||
봇의 소유권을 이전하게 되면 봇의 소유자 권한을 이전하게 되며, 본인을
|
||||
포함한 모든 관리자가 해당 봇에 대한 권한을 잃게됩니다.
|
||||
</p>
|
||||
</Message>
|
||||
<div className='py-4'>
|
||||
<h2 className='text-md my-1'>이전하실 유저 ID를 입력해주세요.</h2>
|
||||
<Input name='ownerID' placeholder='이전할 유저 ID' />
|
||||
<Divider />
|
||||
<h2 className='text-md my-1'>계속 하시려면 <strong>{bot.name}</strong>{getJosaPicker('을')(bot.name)} 입력해주세요.</h2>
|
||||
<h2 className='text-md my-1'>
|
||||
계속 하시려면 <strong>{bot.name}</strong>
|
||||
{getJosaPicker('을')(bot.name)} 입력해주세요.
|
||||
</h2>
|
||||
<Input name='name' placeholder={bot.name} />
|
||||
</div>
|
||||
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} />
|
||||
<Button disabled={!values.ownerID || values.name !== bot.name || !values._captcha} className={`mt-4 bg-red-500 text-white ${!values.ownerID ||values.name !== bot.name || !values._captcha ? 'opacity-80' : 'hover:opacity-80'}`} type='submit'><i className='fas fa-exchange-alt' /> 소유권 이전</Button>
|
||||
<Captcha
|
||||
dark={theme === 'dark'}
|
||||
onVerify={(k) => setFieldValue('_captcha', k)}
|
||||
/>
|
||||
<Button
|
||||
disabled={!values.ownerID || values.name !== bot.name || !values._captcha}
|
||||
className={`mt-4 bg-red-500 text-white ${
|
||||
!values.ownerID || values.name !== bot.name || !values._captcha
|
||||
? 'opacity-80'
|
||||
: 'hover:opacity-80'
|
||||
}`}
|
||||
type='submit'
|
||||
>
|
||||
<i className='fas fa-exchange-alt' /> 소유권 이전
|
||||
</Button>
|
||||
</Form>
|
||||
}
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='lg:flex items-center'>
|
||||
<div className='items-center lg:flex'>
|
||||
<div className='grow py-1'>
|
||||
<h3 className='text-lg font-semibold'>봇 삭제하기</h3>
|
||||
<p className='text-gray-400'>봇을 삭제하게 되면 되돌릴 수 없습니다.</p>
|
||||
</div>
|
||||
<Button onClick={() => setDeleteModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-trash' /> 봇 삭제하기</Button>
|
||||
<Modal full header={`${bot.name} 삭제하기`} isOpen={deleteModal} dark={theme === 'dark'} onClose={() => setDeleteModal(false)} closeIcon>
|
||||
<Formik initialValues={{ name: '', _captcha: '', _csrf: csrfToken }} onSubmit={async (v) => {
|
||||
const res = await Fetch(`/bots/${bot.id}`, { method: 'DELETE', body: JSON.stringify(v) })
|
||||
if(res.code === 200) {
|
||||
alert('성공적으로 삭제하였습니다.')
|
||||
redirectTo(router, '/')
|
||||
}
|
||||
else alert(res.message)
|
||||
}}>
|
||||
{
|
||||
({ values, setFieldValue }) => <Form>
|
||||
<Button
|
||||
onClick={() => setDeleteModal(true)}
|
||||
className='lg:w-1/8 h-10 bg-red-500 text-white hover:opacity-80'
|
||||
>
|
||||
<i className='fas fa-trash' /> 봇 삭제하기
|
||||
</Button>
|
||||
<Modal
|
||||
full
|
||||
header={`${bot.name} 삭제하기`}
|
||||
isOpen={deleteModal}
|
||||
dark={theme === 'dark'}
|
||||
onClose={() => setDeleteModal(false)}
|
||||
closeIcon
|
||||
>
|
||||
<Formik
|
||||
initialValues={{ name: '', _captcha: '', _csrf: csrfToken }}
|
||||
onSubmit={async (v) => {
|
||||
const res = await Fetch(`/bots/${bot.id}`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(v),
|
||||
})
|
||||
if (res.code === 200) {
|
||||
alert('성공적으로 삭제하였습니다.')
|
||||
redirectTo(router, '/')
|
||||
} else alert(res.message)
|
||||
}}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form>
|
||||
<Message type='warning'>
|
||||
<p>봇을 삭제하게 되면 되돌릴 수 없습니다.<br/>하트 수를 포함한 모든 봇 정보가 영구적으로 삭제됩니다.</p>
|
||||
<p>계속 하시려면 봇의 이름 <strong>{bot.name}</strong>{getJosaPicker('을')(bot.name)} 입력해주세요.</p>
|
||||
<p>
|
||||
봇을 삭제하게 되면 되돌릴 수 없습니다.
|
||||
<br />
|
||||
하트 수를 포함한 모든 봇 정보가 영구적으로 삭제됩니다.
|
||||
</p>
|
||||
<p>
|
||||
계속 하시려면 봇의 이름 <strong>{bot.name}</strong>
|
||||
{getJosaPicker('을')(bot.name)} 입력해주세요.
|
||||
</p>
|
||||
</Message>
|
||||
<div className='py-4'>
|
||||
<Input name='name' placeholder={bot.name} />
|
||||
</div>
|
||||
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} />
|
||||
<Button disabled={values.name !== bot.name || !values._captcha} className={`mt-4 bg-red-500 text-white ${values.name !== bot.name || !values._captcha ? 'opacity-80' : 'hover:opacity-80'}`} type='submit'><i className='fas fa-trash' /> 삭제</Button>
|
||||
<Captcha
|
||||
dark={theme === 'dark'}
|
||||
onVerify={(k) => setFieldValue('_captcha', k)}
|
||||
/>
|
||||
<Button
|
||||
disabled={values.name !== bot.name || !values._captcha}
|
||||
className={`mt-4 bg-red-500 text-white ${
|
||||
values.name !== bot.name || !values._captcha
|
||||
? 'opacity-80'
|
||||
: 'hover:opacity-80'
|
||||
}`}
|
||||
type='submit'
|
||||
>
|
||||
<i className='fas fa-trash' /> 삭제
|
||||
</Button>
|
||||
</Form>
|
||||
}
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</div>
|
||||
</Segment>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -328,7 +573,13 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
||||
export const getServerSideProps = async (ctx: Context) => {
|
||||
const parsed = parseCookie(ctx.req)
|
||||
const user = await get.Authorization(parsed?.token)
|
||||
return { props: { bot: await get.bot.load(ctx.query.id), user: await get.user.load(user || ''), csrfToken: getToken(ctx.req, ctx.res) } }
|
||||
return {
|
||||
props: {
|
||||
bot: await get.bot.load(ctx.query.id),
|
||||
user: await get.user.load(user || ''),
|
||||
csrfToken: getToken(ctx.req, ctx.res),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface ManageBotProps {
|
||||
@ -346,4 +597,4 @@ interface Query extends ParsedUrlQuery {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default ManageBotPage
|
||||
export default ManageBotPage
|
||||
|
||||
@ -29,7 +29,7 @@ const Owner = dynamic(() => import('@components/Owner'))
|
||||
const Segment = dynamic(() => import('@components/Segment'))
|
||||
const LongButton = dynamic(() => import('@components/LongButton'))
|
||||
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
||||
const Markdown = dynamic(() => import ('@components/Markdown'))
|
||||
const Markdown = dynamic(() => import('@components/Markdown'))
|
||||
const Message = dynamic(() => import('@components/Message'))
|
||||
const Button = dynamic(() => import('@components/Button'))
|
||||
const TextArea = dynamic(() => import('@components/Form/TextArea'))
|
||||
@ -39,302 +39,411 @@ const NSFW = dynamic(() => import('@components/NSFW'))
|
||||
const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken }) => {
|
||||
const bg = checkBotFlag(data?.flags, 'trusted') && data?.banner
|
||||
const router = useRouter()
|
||||
const [ nsfw, setNSFW ] = useState<boolean>()
|
||||
const [ reportModal, setReportModal ] = useState(false)
|
||||
const [ reportRes, setReportRes ] = useState<ResponseProps<unknown>>(null)
|
||||
const [nsfw, setNSFW] = useState<boolean>()
|
||||
const [reportModal, setReportModal] = useState(false)
|
||||
const [reportRes, setReportRes] = useState<ResponseProps<unknown>>(null)
|
||||
useEffect(() => {
|
||||
setNSFW(localStorage.nsfw)
|
||||
}, [])
|
||||
if (!data?.id) return <NotFound />
|
||||
return (
|
||||
<div style={bg ? { background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${data.bg}") center top / cover no-repeat fixed` } : {}}>
|
||||
<div
|
||||
style={
|
||||
bg
|
||||
? {
|
||||
background: `linear-gradient(to right, rgba(34, 36, 38, 0.68), rgba(34, 36, 38, 0.68)), url("${data.bg}") center top / cover no-repeat fixed`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Container paddingTop className='py-10'>
|
||||
<NextSeo
|
||||
title={data.name}
|
||||
description={data.intro}
|
||||
twitter={{
|
||||
cardType: 'summary_large_image'
|
||||
cardType: 'summary_large_image',
|
||||
}}
|
||||
openGraph={{
|
||||
images: [
|
||||
{
|
||||
url: KoreanbotsEndPoints.OG.bot(data.id, data.name, data.intro, data.category, [formatNumber(data.votes), formatNumber(data.servers)]),
|
||||
url: KoreanbotsEndPoints.OG.bot(data.id, data.name, data.intro, data.category, [
|
||||
formatNumber(data.votes),
|
||||
formatNumber(data.servers),
|
||||
]),
|
||||
width: 2048,
|
||||
height: 1170,
|
||||
alt: 'Bot Preview Image'
|
||||
}
|
||||
]
|
||||
alt: 'Bot Preview Image',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
{
|
||||
data.state === 'blocked' ? <div className='pb-40'>
|
||||
{data.state === 'blocked' ? (
|
||||
<div className='pb-40'>
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-extrabold'>해당 봇은 관리자에 의해 삭제되었습니다.</h2>
|
||||
</Message>
|
||||
</div>
|
||||
: data.category.includes('NSFW') && !nsfw ? <NSFW onClick={() => setNSFW(true)} onDisableClick={() => localStorage.nsfw = true} />
|
||||
: <>
|
||||
<div className='w-full pb-2'>
|
||||
{
|
||||
data.state === 'private' ? <Message type='info'>
|
||||
<h2 className='text-lg font-extrabold'>해당 봇은 특수목적 봇이므로 초대하실 수 없습니다.</h2>
|
||||
<p>해당 봇은 공개 사용이 목적이 아닌 특수목적봇입니다. 따라서 따로 초대하실 수 없습니다.</p>
|
||||
</Message> :
|
||||
data.state === 'reported' ?
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-extrabold'>해당 봇은 신고가 접수되어, 관리자에 의해 잠금 상태입니다.</h2>
|
||||
<p>해당 봇 사용에 주의해주세요.</p>
|
||||
<p>봇 소유자분은 <Link href='/guidelines' className='text-blue-500 hover:text-blue-400'>가이드라인</Link>에 대한 위반사항을 확인해주시고 <Link href='/discord' className='text-blue-500 hover:text-blue-400'>디스코드 서버</Link>로 문의해주세요.</p>
|
||||
</Message> : ''
|
||||
}
|
||||
</div>
|
||||
<div className='lg:flex w-full'>
|
||||
<div className='w-full text-center lg:w-2/12'>
|
||||
<DiscordAvatar
|
||||
userID={data.id}
|
||||
size={256}
|
||||
className='w-full rounded-full'
|
||||
/>
|
||||
</div>
|
||||
<div className='grow px-5 py-12 w-full text-center lg:w-5/12 lg:text-left'>
|
||||
<Tag
|
||||
circular
|
||||
text={
|
||||
<>
|
||||
<i className={`fas fa-circle text-${Status[data.status]?.color}`} />{' '}
|
||||
{Status[data.status]?.text}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<h1 className='mb-2 mt-3 text-4xl font-bold' style={bg ? { color: 'white' } : {}}>
|
||||
{data.name}{' '}
|
||||
{checkBotFlag(data.flags, 'trusted') ? (
|
||||
<Tooltip placement='bottom' overlay='해당 봇은 한국 디스코드 리스트에서 엄격한 기준을 통과한 봇입니다!'>
|
||||
<span className='text-koreanbots-blue text-3xl'>
|
||||
<i className='fas fa-award' />
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : ''}
|
||||
</h1>
|
||||
<p className={`${bg ? 'text-gray-300' : 'dark:text-gray-300 text-gray-800'} text-base`}>{data.intro}</p>
|
||||
</div>
|
||||
<div className='w-full lg:w-1/4'>
|
||||
{
|
||||
data.state === 'ok' && <LongButton
|
||||
newTab
|
||||
href={`/bots/${router.query.id}/invite`}
|
||||
>
|
||||
<h4 className='whitespace-nowrap'>
|
||||
<i className='fas fa-user-plus text-discord-blurple' /> 초대하기
|
||||
</h4>
|
||||
</LongButton>
|
||||
}
|
||||
<Link href={`/bots/${router.query.id}/vote`} legacyBehavior>
|
||||
<LongButton>
|
||||
<h4>
|
||||
<i className='fas fa-heart text-red-600' /> 하트 추가
|
||||
</h4>
|
||||
<span className='ml-1 px-2 text-center text-black dark:text-gray-400 text-sm bg-little-white-hover dark:bg-very-black rounded-lg'>
|
||||
{formatNumber(data.votes)}
|
||||
</span>
|
||||
</LongButton>
|
||||
) : data.category.includes('NSFW') && !nsfw ? (
|
||||
<NSFW onClick={() => setNSFW(true)} onDisableClick={() => (localStorage.nsfw = true)} />
|
||||
) : (
|
||||
<>
|
||||
<div className='w-full pb-2'>
|
||||
{data.state === 'private' ? (
|
||||
<Message type='info'>
|
||||
<h2 className='text-lg font-extrabold'>
|
||||
해당 봇은 특수목적 봇이므로 초대하실 수 없습니다.
|
||||
</h2>
|
||||
<p>
|
||||
해당 봇은 공개 사용이 목적이 아닌 특수목적봇입니다. 따라서 따로 초대하실 수
|
||||
없습니다.
|
||||
</p>
|
||||
</Message>
|
||||
) : data.state === 'reported' ? (
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-extrabold'>
|
||||
해당 봇은 신고가 접수되어, 관리자에 의해 잠금 상태입니다.
|
||||
</h2>
|
||||
<p>해당 봇 사용에 주의해주세요.</p>
|
||||
<p>
|
||||
봇 소유자분은{' '}
|
||||
<Link href='/guidelines' className='text-blue-500 hover:text-blue-400'>
|
||||
가이드라인
|
||||
</Link>
|
||||
{
|
||||
((data.owners as User[]).find(el => el.id === user?.id) || checkUserFlag(user?.flags, 'staff')) && <LongButton href={`/bots/${data.id}/edit`}>
|
||||
<h4>
|
||||
<i className='fas fa-cogs' /> 관리하기
|
||||
</h4>
|
||||
</LongButton>
|
||||
}
|
||||
{
|
||||
((data.owners as User[]).find(el => el.id === user?.id) || checkUserFlag(user?.flags, 'staff')) && <LongButton onClick={async() => {
|
||||
const res = await Fetch(`/bots/${data.id}/stats`, { method: 'PATCH'} )
|
||||
if(res.code !== 200) return alert(res.message)
|
||||
else window.location.reload()
|
||||
}}>
|
||||
<h4>
|
||||
<i className='fas fa-sync' /> 정보 갱신하기
|
||||
</h4>
|
||||
</LongButton>
|
||||
}
|
||||
에 대한 위반사항을 확인해주시고{' '}
|
||||
<Link href='/discord' className='text-blue-500 hover:text-blue-400'>
|
||||
디스코드 서버
|
||||
</Link>
|
||||
로 문의해주세요.
|
||||
</p>
|
||||
</Message>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<div className='w-full lg:flex'>
|
||||
<div className='w-full text-center lg:w-2/12'>
|
||||
<DiscordAvatar userID={data.id} size={256} className='w-full rounded-full' />
|
||||
</div>
|
||||
<div className='w-full grow px-5 py-12 text-center lg:w-5/12 lg:text-left'>
|
||||
<Tag
|
||||
circular
|
||||
text={
|
||||
<>
|
||||
<i className={`fas fa-circle text-${Status[data.status]?.color}`} />{' '}
|
||||
{Status[data.status]?.text}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<h1 className='mb-2 mt-3 text-4xl font-bold' style={bg ? { color: 'white' } : {}}>
|
||||
{data.name}{' '}
|
||||
{checkBotFlag(data.flags, 'trusted') ? (
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
overlay='해당 봇은 한국 디스코드 리스트에서 엄격한 기준을 통과한 봇입니다!'
|
||||
>
|
||||
<span className='text-3xl text-koreanbots-blue'>
|
||||
<i className='fas fa-award' />
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</h1>
|
||||
<p
|
||||
className={`${
|
||||
bg ? 'text-gray-300' : 'text-gray-800 dark:text-gray-300'
|
||||
} text-base`}
|
||||
>
|
||||
{data.intro}
|
||||
</p>
|
||||
</div>
|
||||
<div className='w-full lg:w-1/4'>
|
||||
{data.state === 'ok' && (
|
||||
<LongButton newTab href={`/bots/${router.query.id}/invite`}>
|
||||
<h4 className='whitespace-nowrap'>
|
||||
<i className='fas fa-user-plus text-discord-blurple' /> 초대하기
|
||||
</h4>
|
||||
</LongButton>
|
||||
)}
|
||||
<Link href={`/bots/${router.query.id}/vote`} legacyBehavior>
|
||||
<LongButton>
|
||||
<h4>
|
||||
<i className='fas fa-heart text-red-600' /> 하트 추가
|
||||
</h4>
|
||||
<span className='ml-1 rounded-lg bg-little-white-hover px-2 text-center text-sm text-black dark:bg-very-black dark:text-gray-400'>
|
||||
{formatNumber(data.votes)}
|
||||
</span>
|
||||
</LongButton>
|
||||
</Link>
|
||||
{((data.owners as User[]).find((el) => el.id === user?.id) ||
|
||||
checkUserFlag(user?.flags, 'staff')) && (
|
||||
<LongButton href={`/bots/${data.id}/edit`}>
|
||||
<h4>
|
||||
<i className='fas fa-cogs' /> 관리하기
|
||||
</h4>
|
||||
</LongButton>
|
||||
)}
|
||||
{((data.owners as User[]).find((el) => el.id === user?.id) ||
|
||||
checkUserFlag(user?.flags, 'staff')) && (
|
||||
<LongButton
|
||||
onClick={async () => {
|
||||
const res = await Fetch(`/bots/${data.id}/stats`, { method: 'PATCH' })
|
||||
if (res.code !== 200) return alert(res.message)
|
||||
else window.location.reload()
|
||||
}}
|
||||
>
|
||||
<h4>
|
||||
<i className='fas fa-sync' /> 정보 갱신하기
|
||||
</h4>
|
||||
</LongButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Divider className='px-5' />
|
||||
<div className='hidden lg:block'>
|
||||
<Advertisement />
|
||||
</div>
|
||||
<div className='lg:flex lg:flex-row-reverse' style={bg ? { color: 'white' } : {}}>
|
||||
<div className='mb-1 w-full lg:w-1/4'>
|
||||
<h2 className='3xl mb-2 font-bold'>정보</h2>
|
||||
<div className='grid grid-cols-2 gap-4 rounded-sm bg-little-white px-4 py-4 text-black dark:bg-discord-black dark:text-gray-400'>
|
||||
<div>
|
||||
<i className='far fa-flag' /> 접두사
|
||||
</div>
|
||||
<div className='markdown-body text-black dark:text-gray-400'>
|
||||
<code>{data.prefix}</code>
|
||||
</div>
|
||||
<div>
|
||||
<i className='fas fa-users' /> 서버수
|
||||
</div>
|
||||
<div>{data.servers || 'N/A'}</div>
|
||||
{data.shards && data.servers > 1500 && (
|
||||
<>
|
||||
<div>
|
||||
<i className='fas fa-sitemap' /> 샤드수
|
||||
</div>
|
||||
<div>{data.shards}</div>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<i className='fas fa-calendar-day' /> 봇 생성일
|
||||
</div>
|
||||
<div>{Day(date).fromNow(false)}</div>
|
||||
{checkBotFlag(data.flags, 'verified') ? (
|
||||
<Tooltip overlay='해당 봇은 디스코드측에서 인증된 봇입니다.'>
|
||||
<div className='col-span-2'>
|
||||
<i className='fas fa-check text-discord-blurple' /> 디스코드 인증됨
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<Divider className='px-5' />
|
||||
<div className='hidden lg:block'>
|
||||
<Advertisement />
|
||||
<h2 className='3xl mb-2 mt-2 font-bold'>카테고리</h2>
|
||||
<div className='flex flex-wrap'>
|
||||
{data.category.map((el) => (
|
||||
<Tag key={el} text={el} href={`/bots/categories/${el}`} />
|
||||
))}
|
||||
</div>
|
||||
<div className='lg:flex lg:flex-row-reverse' style={bg ? { color: 'white' } : {}}>
|
||||
<div className='mb-1 w-full lg:w-1/4'>
|
||||
<h2 className='3xl mb-2 font-bold'>정보</h2>
|
||||
<div className='grid gap-4 grid-cols-2 px-4 py-4 text-black dark:text-gray-400 dark:bg-discord-black bg-little-white rounded-sm'>
|
||||
<div>
|
||||
<i className='far fa-flag' /> 접두사
|
||||
</div>
|
||||
<div className='markdown-body text-black dark:text-gray-400'>
|
||||
<code>{data.prefix}</code>
|
||||
</div>
|
||||
<div>
|
||||
<i className='fas fa-users' /> 서버수
|
||||
</div>
|
||||
<div>{data.servers || 'N/A'}</div>
|
||||
{
|
||||
data.shards && data.servers > 1500 && <>
|
||||
<div>
|
||||
<i className='fas fa-sitemap' /> 샤드수
|
||||
</div>
|
||||
<div>{data.shards}</div>
|
||||
</>
|
||||
}
|
||||
<div>
|
||||
<i className='fas fa-calendar-day' /> 봇 생성일
|
||||
</div>
|
||||
<div>{Day(date).fromNow(false)}</div>
|
||||
{
|
||||
checkBotFlag(data.flags, 'verified') ?
|
||||
<Tooltip overlay='해당 봇은 디스코드측에서 인증된 봇입니다.'>
|
||||
<div className='col-span-2'>
|
||||
<i className='fas fa-check text-discord-blurple' /> 디스코드 인증됨
|
||||
<h2 className='3xl mb-2 mt-2 font-bold'>제작자</h2>
|
||||
{(data.owners as User[]).map((el) => (
|
||||
<Owner
|
||||
key={el.id}
|
||||
id={el.id}
|
||||
tag={el.tag}
|
||||
globalName={el.globalName}
|
||||
username={el.username}
|
||||
/>
|
||||
))}
|
||||
<div className='list grid'>
|
||||
<Link
|
||||
href={`/bots/${router.query.id}/report`}
|
||||
className='cursor-pointer text-red-600 hover:underline'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<i className='far fa-flag' />
|
||||
신고하기
|
||||
</Link>
|
||||
<Modal
|
||||
header={`${data.name}#${data.tag} 신고하기`}
|
||||
closeIcon
|
||||
isOpen={reportModal}
|
||||
onClose={() => {
|
||||
setReportModal(false)
|
||||
setReportRes(null)
|
||||
}}
|
||||
full
|
||||
dark={theme === 'dark'}
|
||||
>
|
||||
{reportRes?.code === 200 ? (
|
||||
<Message type='success'>
|
||||
<h2 className='text-lg font-semibold'>성공적으로 신고하였습니다!</h2>
|
||||
<p>
|
||||
더 자세한 설명이 필요할 수 있습니다!{' '}
|
||||
<a className='text-blue-600 hover:text-blue-500' href='/discord'>
|
||||
공식 디스코드
|
||||
</a>
|
||||
에 참여해주세요
|
||||
</p>
|
||||
</Message>
|
||||
) : (
|
||||
<Formik
|
||||
onSubmit={async (body) => {
|
||||
const res = await Fetch(`/bots/${data.id}/report`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
setReportRes(res)
|
||||
}}
|
||||
validationSchema={ReportSchema}
|
||||
initialValues={{
|
||||
category: null,
|
||||
description: '',
|
||||
_csrf: csrfToken,
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, values, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className='mb-5'>
|
||||
{reportRes && (
|
||||
<div className='my-5'>
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
|
||||
<ul className='list-disc'>
|
||||
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
)}
|
||||
<h3 className='font-bold'>신고 구분</h3>
|
||||
<p className='mb-1 text-sm text-gray-400'>
|
||||
해당되는 항목을 선택해주세요.
|
||||
</p>
|
||||
{reportCats.map((el) => (
|
||||
<div key={el}>
|
||||
<label>
|
||||
<Field
|
||||
type='radio'
|
||||
name='category'
|
||||
value={el}
|
||||
className='mr-1.5 py-2'
|
||||
/>
|
||||
{el}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
<div className='mt-1 text-xs font-light text-red-500'>
|
||||
{errors.category && touched.category
|
||||
? (errors.category as string)
|
||||
: null}
|
||||
</div>
|
||||
<h3 className='mt-2 font-bold'>설명</h3>
|
||||
<p className='mb-1 text-sm text-gray-400'>
|
||||
신고하시는 내용을 자세하게 설명해주세요.
|
||||
</p>
|
||||
<TextArea
|
||||
name='description'
|
||||
placeholder='최대한 자세하게 설명해주세요!'
|
||||
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||
value={values.description}
|
||||
setValue={(value) => setFieldValue('description', value)}
|
||||
/>
|
||||
<div className='mt-1 text-xs font-light text-red-500'>
|
||||
{errors.description && touched.description
|
||||
? errors.description
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
<h2 className='3xl mb-2 mt-2 font-bold'>카테고리</h2>
|
||||
<div className='flex flex-wrap'>
|
||||
{data.category.map(el => (
|
||||
<Tag key={el} text={el} href={`/bots/categories/${el}`} />
|
||||
))}
|
||||
</div>
|
||||
<h2 className='3xl mb-2 mt-2 font-bold'>제작자</h2>
|
||||
{(data.owners as User[]).map(el => (
|
||||
<Owner
|
||||
key={el.id}
|
||||
id={el.id}
|
||||
tag={el.tag}
|
||||
globalName={el.globalName}
|
||||
username={el.username}
|
||||
<div className='text-right'>
|
||||
<Button
|
||||
className='bg-gray-500 text-white hover:opacity-90'
|
||||
onClick={() => setReportModal(false)}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='bg-red-500 text-white hover:opacity-90'
|
||||
>
|
||||
제출
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</Modal>
|
||||
{data.discord && (
|
||||
<a
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
className='text-discord-blurple hover:underline'
|
||||
href={`https://discord.gg/${data.discord}`}
|
||||
>
|
||||
<i className='fab fa-discord' />
|
||||
디스코드 서버
|
||||
</a>
|
||||
)}
|
||||
{data.web && (
|
||||
<a
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:underline'
|
||||
href={data.web}
|
||||
>
|
||||
<i className='fas fa-globe' />
|
||||
웹사이트
|
||||
</a>
|
||||
)}
|
||||
{data.git && (
|
||||
<a
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
className='hover:underline'
|
||||
href={data.git}
|
||||
>
|
||||
<i
|
||||
className={`fab fa-${git[new URL(data.git).hostname]?.icon ?? 'git-alt'}`}
|
||||
/>
|
||||
))}
|
||||
<div className='list grid'>
|
||||
<Link
|
||||
href={`/bots/${router.query.id}/report`}
|
||||
className='text-red-600 hover:underline cursor-pointer'
|
||||
aria-hidden='true'>
|
||||
|
||||
<i className='far fa-flag' />신고하기
|
||||
</Link>
|
||||
<Modal header={`${data.name}#${data.tag} 신고하기`} closeIcon isOpen={reportModal} onClose={() => {
|
||||
setReportModal(false)
|
||||
setReportRes(null)
|
||||
}} full dark={theme === 'dark'}>
|
||||
{
|
||||
reportRes?.code === 200 ? <Message type='success'>
|
||||
<h2 className='text-lg font-semibold'>성공적으로 신고하였습니다!</h2>
|
||||
<p>더 자세한 설명이 필요할 수 있습니다! <a className='text-blue-600 hover:text-blue-500' href='/discord'>공식 디스코드</a>에 참여해주세요</p>
|
||||
</Message> : <Formik onSubmit={async (body) => {
|
||||
const res = await Fetch(`/bots/${data.id}/report`, { method: 'POST', body: JSON.stringify(body) })
|
||||
setReportRes(res)
|
||||
}} validationSchema={ReportSchema} initialValues={{
|
||||
category: null,
|
||||
description: '',
|
||||
_csrf: csrfToken
|
||||
}}>
|
||||
{
|
||||
({ errors, touched, values, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className='mb-5'>
|
||||
{
|
||||
reportRes && <div className='my-5'>
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
|
||||
<ul className='list-disc'>
|
||||
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
}
|
||||
<h3 className='font-bold'>신고 구분</h3>
|
||||
<p className='text-gray-400 text-sm mb-1'>해당되는 항목을 선택해주세요.</p>
|
||||
{
|
||||
reportCats.map(el =>
|
||||
<div key={el}>
|
||||
<label>
|
||||
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
|
||||
{el}
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category as string: null}</div>
|
||||
<h3 className='font-bold mt-2'>설명</h3>
|
||||
<p className='text-gray-400 text-sm mb-1'>신고하시는 내용을 자세하게 설명해주세요.</p>
|
||||
<TextArea name='description' placeholder='최대한 자세하게 설명해주세요!' theme={theme === 'dark' ? 'dark' : 'light'} value={values.description} setValue={(value) => setFieldValue('description', value)} />
|
||||
<div className='mt-1 text-red-500 text-xs font-light'>{errors.description && touched.description ? errors.description : null}</div>
|
||||
</div>
|
||||
<div className='text-right'>
|
||||
<Button className='bg-gray-500 hover:opacity-90 text-white' onClick={()=> setReportModal(false)}>취소</Button>
|
||||
<Button type='submit' className='bg-red-500 hover:opacity-90 text-white'>제출</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
}
|
||||
</Modal>
|
||||
{data.discord && (
|
||||
<a
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
className='text-discord-blurple hover:underline'
|
||||
href={`https://discord.gg/${data.discord}`}
|
||||
>
|
||||
<i className='fab fa-discord' />
|
||||
디스코드 서버
|
||||
</a>
|
||||
)}
|
||||
{data.web && (
|
||||
<a
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
className='text-blue-500 hover:underline'
|
||||
href={data.web}
|
||||
>
|
||||
<i className='fas fa-globe' />
|
||||
웹사이트
|
||||
</a>
|
||||
)}
|
||||
{data.git && (
|
||||
<a
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
className='hover:underline'
|
||||
href={data.git}
|
||||
>
|
||||
<i className={`fab fa-${git[new URL(data.git).hostname]?.icon ?? 'git-alt'}`} />
|
||||
{git[new URL(data.git).hostname]?.text ?? 'Git'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<Advertisement size='tall' />
|
||||
</div>
|
||||
<div className='w-full lg:pr-5 lg:w-3/4'>
|
||||
{
|
||||
checkBotFlag(data.flags, 'hackerthon') ? <Segment className='mt-10'>
|
||||
<h1 className='text-3xl font-semibold'>
|
||||
<i className='fas fa-trophy mr-4 my-2 text-amber-300' /> 해당 봇은 한국 디스코드 리스트 해커톤 수상작품입니다!
|
||||
</h1>
|
||||
<p>해당 봇은 한국 디스코드 리스트 주최로 진행되었던 "한국 디스코드 리스트 제1회 해커톤"에서 우수한 성적을 거둔 봇입니다.</p>
|
||||
<p>자세한 내용은 <a className='text-blue-500 hover:text-blue-400' href='https://blog.koreanbots.dev/first-hackathon-results/'>해당 글</a>을 확인해주세요.</p>
|
||||
</Segment> : ''
|
||||
}
|
||||
<Segment className='my-4'>
|
||||
<Markdown text={desc}/>
|
||||
</Segment>
|
||||
<Advertisement />
|
||||
</div>
|
||||
{git[new URL(data.git).hostname]?.text ?? 'Git'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
<Advertisement size='tall' />
|
||||
</div>
|
||||
<div className='w-full lg:w-3/4 lg:pr-5'>
|
||||
{checkBotFlag(data.flags, 'hackerthon') ? (
|
||||
<Segment className='mt-10'>
|
||||
<h1 className='text-3xl font-semibold'>
|
||||
<i className='fas fa-trophy my-2 mr-4 text-amber-300' /> 해당 봇은 한국
|
||||
디스코드 리스트 해커톤 수상작품입니다!
|
||||
</h1>
|
||||
<p>
|
||||
해당 봇은 한국 디스코드 리스트 주최로 진행되었던 "한국 디스코드 리스트 제1회
|
||||
해커톤"에서 우수한 성적을 거둔 봇입니다.
|
||||
</p>
|
||||
<p>
|
||||
자세한 내용은{' '}
|
||||
<a
|
||||
className='text-blue-500 hover:text-blue-400'
|
||||
href='https://blog.koreanbots.dev/first-hackathon-results/'
|
||||
>
|
||||
해당 글
|
||||
</a>
|
||||
을 확인해주세요.
|
||||
</p>
|
||||
</Segment>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<Segment className='my-4'>
|
||||
<Markdown text={desc} />
|
||||
</Segment>
|
||||
<Advertisement />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
@ -343,20 +452,25 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
|
||||
export const getServerSideProps = async (ctx: Context) => {
|
||||
const parsed = parseCookie(ctx.req)
|
||||
const data = await get.bot.load(ctx.query.id)
|
||||
if(!data) return {
|
||||
props: {
|
||||
data
|
||||
if (!data)
|
||||
return {
|
||||
props: {
|
||||
data,
|
||||
},
|
||||
}
|
||||
}
|
||||
const desc = await get.botDescSafe(data.id)
|
||||
const user = await get.Authorization(parsed?.token)
|
||||
if((checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) && data.vanity && data.vanity !== ctx.query.id) {
|
||||
if (
|
||||
(checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) &&
|
||||
data.vanity &&
|
||||
data.vanity !== ctx.query.id
|
||||
) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/bots/${data.vanity}`,
|
||||
permanent: true
|
||||
permanent: true,
|
||||
},
|
||||
props: {}
|
||||
props: {},
|
||||
}
|
||||
}
|
||||
return {
|
||||
@ -365,7 +479,7 @@ export const getServerSideProps = async (ctx: Context) => {
|
||||
desc,
|
||||
date: Number(SnowflakeUtil.deconstruct(data.id ?? '0').timestamp),
|
||||
user: await get.user.load(user || ''),
|
||||
csrfToken: getToken(ctx.req, ctx.res)
|
||||
csrfToken: getToken(ctx.req, ctx.res),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,14 +7,26 @@ const Invite: NextPage = () => <NotFound />
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const data = await get.bot.load(ctx.query.id as string)
|
||||
if(!data) return { props: {} }
|
||||
const record = await Bots.updateOne({ _id: data.id, 'inviteMetrix.day': getYYMMDD() }, { $inc: { 'inviteMetrix.$.count': 1 } })
|
||||
if(record.matchedCount === 0) await Bots.findByIdAndUpdate(data.id, { $push: { inviteMetrix: { count: 1 } } }, { upsert: true })
|
||||
if (!data) return { props: {} }
|
||||
const record = await Bots.updateOne(
|
||||
{ _id: data.id, 'inviteMetrix.day': getYYMMDD() },
|
||||
{ $inc: { 'inviteMetrix.$.count': 1 } }
|
||||
)
|
||||
if (record.matchedCount === 0)
|
||||
await Bots.findByIdAndUpdate(
|
||||
data.id,
|
||||
{ $push: { inviteMetrix: { count: 1 } } },
|
||||
{ upsert: true }
|
||||
)
|
||||
ctx.res.statusCode = 307
|
||||
ctx.res.setHeader('Location', data.url || `https://discordapp.com/oauth2/authorize?client_id=${data.id}&scope=bot&permissions=0`)
|
||||
ctx.res.setHeader(
|
||||
'Location',
|
||||
data.url ||
|
||||
`https://discordapp.com/oauth2/authorize?client_id=${data.id}&scope=bot&permissions=0`
|
||||
)
|
||||
return {
|
||||
props: {}
|
||||
props: {},
|
||||
}
|
||||
}
|
||||
|
||||
export default Invite
|
||||
export default Invite
|
||||
|
||||
@ -19,94 +19,144 @@ import { getJosaPicker } from 'josa'
|
||||
import { reportCats } from '@utils/Constants'
|
||||
import { NextSeo } from 'next-seo'
|
||||
|
||||
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
const Message = dynamic(() => import('@components/Message'))
|
||||
const Login = dynamic(() => import('@components/Login'))
|
||||
|
||||
const ReportBot: NextPage<ReportBotProps> = ({ data, user, csrfToken }) => {
|
||||
const [ reportRes, setReportRes ] = useState<ResponseProps<unknown>>(null)
|
||||
if(!data?.id) return <NotFound />
|
||||
if(!user) return <Login>
|
||||
<NextSeo title='신고하기' />
|
||||
</Login>
|
||||
const [reportRes, setReportRes] = useState<ResponseProps<unknown>>(null)
|
||||
if (!data?.id) return <NotFound />
|
||||
if (!user)
|
||||
return (
|
||||
<Login>
|
||||
<NextSeo title='신고하기' />
|
||||
</Login>
|
||||
)
|
||||
return (
|
||||
<Container paddingTop className='py-10'>
|
||||
<NextSeo title={`${data.name} 신고하기`} />
|
||||
<Link href={makeBotURL(data)} className='text-blue-500 hover:opacity-80'>
|
||||
<i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.name}</strong>{getJosaPicker('로')(data.name)}돌아가기
|
||||
<i className='fas fa-arrow-left mb-3 mt-3' /> <strong>{data.name}</strong>
|
||||
{getJosaPicker('로')(data.name)}돌아가기
|
||||
</Link>
|
||||
{
|
||||
reportRes?.code === 200 ? <Message type='success'>
|
||||
{reportRes?.code === 200 ? (
|
||||
<Message type='success'>
|
||||
<h2 className='text-lg font-semibold'>성공적으로 제출하였습니다!</h2>
|
||||
<p>더 자세한 설명이 필요할 수 있습니다. <strong>반드시 <a className='text-blue-600 hover:text-blue-500' href='/discord'>공식 디스코드</a>에 참여해주세요!!</strong></p>
|
||||
</Message> : <Formik onSubmit={async (body) => {
|
||||
const res = await Fetch(`/bots/${data.id}/report`, { method: 'POST', body: JSON.stringify(body) })
|
||||
setReportRes(res)
|
||||
}} validationSchema={ReportSchema} initialValues={{
|
||||
category: null,
|
||||
description: '',
|
||||
_csrf: csrfToken
|
||||
}}>
|
||||
{
|
||||
({ errors, touched, values, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className='mb-5'>
|
||||
{
|
||||
reportRes && <div className='my-5'>
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
|
||||
<ul className='list-disc'>
|
||||
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
}
|
||||
<h3 className='font-bold'>신고 구분</h3>
|
||||
<p className='text-gray-400 text-sm mb-1'>해당되는 항목을 선택해주세요.</p>
|
||||
{
|
||||
reportCats.map(el =>
|
||||
<div key={el}>
|
||||
<label>
|
||||
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
|
||||
{el}
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='mt-1 text-red-500 text-xs font-light'>{errors.category && touched.category ? errors.category as string: null}</div>
|
||||
{
|
||||
values.category && <>
|
||||
{
|
||||
{
|
||||
[reportCats[2]]: <Message type='info'>
|
||||
<h3 className='font-bold text-xl'>본인 혹은 다른 사람이 위험에 처해 있나요?</h3>
|
||||
<p>당신은 소중한 사람입니다.</p>
|
||||
<p className='list-disc list-item list-inside'>자살예방상담전화 1393 | 청소년전화 1388</p>
|
||||
</Message>,
|
||||
[reportCats[5]]: <DMCA values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />,
|
||||
[reportCats[6]]: <Message type='warning'>
|
||||
<h3 className='font-bold text-xl'>디스코드 약관을 위반사항을 신고하시려고요?</h3>
|
||||
<p><a className='text-blue-400' target='_blank' rel='noreferrer' href='http://dis.gd/report'>디스코드 문의</a>를 통해 직접 디스코드에 신고하실 수도 있습니다.</p>
|
||||
</Message>
|
||||
|
||||
}[values.category]
|
||||
}
|
||||
{
|
||||
!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].includes(values.category) && <>
|
||||
<h3 className='font-bold mt-2'>설명</h3>
|
||||
<p className='text-gray-400 text-sm mb-1'>최대한 자세하게 기재해주세요.</p>
|
||||
<TextField values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} />
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
<p>
|
||||
더 자세한 설명이 필요할 수 있습니다.{' '}
|
||||
<strong>
|
||||
반드시{' '}
|
||||
<a className='text-blue-600 hover:text-blue-500' href='/discord'>
|
||||
공식 디스코드
|
||||
</a>
|
||||
에 참여해주세요!!
|
||||
</strong>
|
||||
</p>
|
||||
</Message>
|
||||
) : (
|
||||
<Formik
|
||||
onSubmit={async (body) => {
|
||||
const res = await Fetch(`/bots/${data.id}/report`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
setReportRes(res)
|
||||
}}
|
||||
validationSchema={ReportSchema}
|
||||
initialValues={{
|
||||
category: null,
|
||||
description: '',
|
||||
_csrf: csrfToken,
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, values, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className='mb-5'>
|
||||
{reportRes && (
|
||||
<div className='my-5'>
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-semibold'>{reportRes.message}</h2>
|
||||
<ul className='list-disc'>
|
||||
{reportRes.errors?.map((el, n) => <li key={n}>{el}</li>)}
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
)}
|
||||
<h3 className='font-bold'>신고 구분</h3>
|
||||
<p className='mb-1 text-sm text-gray-400'>해당되는 항목을 선택해주세요.</p>
|
||||
{reportCats.map((el) => (
|
||||
<div key={el}>
|
||||
<label>
|
||||
<Field type='radio' name='category' value={el} className='mr-1.5 py-2' />
|
||||
{el}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
<div className='mt-1 text-xs font-light text-red-500'>
|
||||
{errors.category && touched.category ? (errors.category as string) : null}
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
{values.category && (
|
||||
<>
|
||||
{
|
||||
{
|
||||
[reportCats[2]]: (
|
||||
<Message type='info'>
|
||||
<h3 className='text-xl font-bold'>
|
||||
본인 혹은 다른 사람이 위험에 처해 있나요?
|
||||
</h3>
|
||||
<p>당신은 소중한 사람입니다.</p>
|
||||
<p className='list-item list-inside list-disc'>
|
||||
자살예방상담전화 1393 | 청소년전화 1388
|
||||
</p>
|
||||
</Message>
|
||||
),
|
||||
[reportCats[5]]: (
|
||||
<DMCA
|
||||
values={values}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
setFieldValue={setFieldValue}
|
||||
/>
|
||||
),
|
||||
[reportCats[6]]: (
|
||||
<Message type='warning'>
|
||||
<h3 className='text-xl font-bold'>
|
||||
디스코드 약관을 위반사항을 신고하시려고요?
|
||||
</h3>
|
||||
<p>
|
||||
<a
|
||||
className='text-blue-400'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
href='http://dis.gd/report'
|
||||
>
|
||||
디스코드 문의
|
||||
</a>
|
||||
를 통해 직접 디스코드에 신고하실 수도 있습니다.
|
||||
</p>
|
||||
</Message>
|
||||
),
|
||||
}[values.category]
|
||||
}
|
||||
{!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].includes(values.category) && (
|
||||
<>
|
||||
<h3 className='mt-2 font-bold'>설명</h3>
|
||||
<p className='mb-1 text-sm text-gray-400'>최대한 자세하게 기재해주세요.</p>
|
||||
<TextField
|
||||
values={values}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
setFieldValue={setFieldValue}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
}
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -115,28 +165,28 @@ export const getServerSideProps = async (ctx: Context) => {
|
||||
const parsed = parseCookie(ctx.req)
|
||||
const data = await get.bot.load(ctx.query.id)
|
||||
const user = await get.Authorization(parsed?.token)
|
||||
|
||||
|
||||
return {
|
||||
props: {
|
||||
csrfToken: getToken(ctx.req, ctx.res),
|
||||
data,
|
||||
user: await get.user.load(user || '')
|
||||
user: await get.user.load(user || ''),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface ReportBotProps {
|
||||
csrfToken: string
|
||||
data: Bot
|
||||
user: User
|
||||
data: Bot
|
||||
user: User
|
||||
}
|
||||
|
||||
interface Context extends CsrfContext {
|
||||
query: URLQuery
|
||||
query: URLQuery
|
||||
}
|
||||
|
||||
interface URLQuery extends ParsedUrlQuery {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default ReportBot
|
||||
export default ReportBot
|
||||
|
||||
@ -19,7 +19,6 @@ import { getJosaPicker } from 'josa'
|
||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||
import { NextSeo } from 'next-seo'
|
||||
|
||||
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
|
||||
const Button = dynamic(() => import('@components/Button'))
|
||||
@ -30,76 +29,122 @@ const Login = dynamic(() => import('@components/Login'))
|
||||
const Message = dynamic(() => import('@components/Message'))
|
||||
|
||||
const VoteBot: NextPage<VoteBotProps> = ({ data, user, theme, csrfToken }) => {
|
||||
const [ votingStatus, setVotingStatus ] = useState(0)
|
||||
const [ result, setResult ] = useState<ResponseProps<{retryAfter?: number}>>(null)
|
||||
const [votingStatus, setVotingStatus] = useState(0)
|
||||
const [result, setResult] = useState<ResponseProps<{ retryAfter?: number }>>(null)
|
||||
const router = useRouter()
|
||||
if(!data?.id) return <NotFound />
|
||||
if(!user) return <Login>
|
||||
<NextSeo title={data.name} description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`} openGraph={{
|
||||
images: [
|
||||
{
|
||||
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
|
||||
width: 256,
|
||||
height: 256,
|
||||
alt: 'Bot Avatar'
|
||||
}
|
||||
]
|
||||
}} />
|
||||
</Login>
|
||||
|
||||
if((checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) && data.vanity && data.vanity !== router.query.id) router.push(`/bots/${data.vanity}/vote?csrfToken=${csrfToken}`)
|
||||
if (!data?.id) return <NotFound />
|
||||
if (!user)
|
||||
return (
|
||||
<Login>
|
||||
<NextSeo
|
||||
title={data.name}
|
||||
description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`}
|
||||
openGraph={{
|
||||
images: [
|
||||
{
|
||||
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
|
||||
width: 256,
|
||||
height: 256,
|
||||
alt: 'Bot Avatar',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</Login>
|
||||
)
|
||||
|
||||
if (
|
||||
(checkBotFlag(data.flags, 'trusted') || checkBotFlag(data.flags, 'partnered')) &&
|
||||
data.vanity &&
|
||||
data.vanity !== router.query.id
|
||||
)
|
||||
router.push(`/bots/${data.vanity}/vote?csrfToken=${csrfToken}`)
|
||||
return (
|
||||
<Container paddingTop className='py-10'>
|
||||
<NextSeo title={data.name} description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`} openGraph={{
|
||||
images: [
|
||||
{
|
||||
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
|
||||
width: 256,
|
||||
height: 256,
|
||||
alt: 'Bot Avatar'
|
||||
}
|
||||
]
|
||||
}} />
|
||||
{
|
||||
data.state === 'blocked' ? <div className='pb-40'>
|
||||
<NextSeo
|
||||
title={data.name}
|
||||
description={`한국 디스코드 리스트에서 ${data.name}에 투표하세요.`}
|
||||
openGraph={{
|
||||
images: [
|
||||
{
|
||||
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
|
||||
width: 256,
|
||||
height: 256,
|
||||
alt: 'Bot Avatar',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
{data.state === 'blocked' ? (
|
||||
<div className='pb-40'>
|
||||
<Message type='error'>
|
||||
<h2 className='text-lg font-extrabold'>해당 봇은 관리자에 의해 삭제되었습니다.</h2>
|
||||
</Message>
|
||||
</div> : <>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Advertisement />
|
||||
<Link href={makeBotURL(data)} className='text-blue-500 hover:opacity-80'>
|
||||
<i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.name}</strong>{getJosaPicker('로')(data.name)}돌아가기
|
||||
<i className='fas fa-arrow-left mb-3 mt-3' /> <strong>{data.name}</strong>
|
||||
{getJosaPicker('로')(data.name)}돌아가기
|
||||
</Link>
|
||||
<Segment className='mb-16 py-8'>
|
||||
<div className='text-center'>
|
||||
<DiscordAvatar userID={data.id} className='mx-auto w-52 h-52 bg-white mb-4 rounded-full' />
|
||||
<Tag text={<span><i className='fas fa-heart text-red-600' /> {data.votes}</span>} dark />
|
||||
<h1 className='text-3xl font-bold mt-3'>{data.name}</h1>
|
||||
<DiscordAvatar
|
||||
userID={data.id}
|
||||
className='mx-auto mb-4 h-52 w-52 rounded-full bg-white'
|
||||
/>
|
||||
<Tag
|
||||
text={
|
||||
<span>
|
||||
<i className='fas fa-heart text-red-600' /> {data.votes}
|
||||
</span>
|
||||
}
|
||||
dark
|
||||
/>
|
||||
<h1 className='mt-3 text-3xl font-bold'>{data.name}</h1>
|
||||
<h4 className='text-md mt-1'>12시간마다 다시 투표하실 수 있습니다.</h4>
|
||||
<div className='inline-block mt-2'>
|
||||
{
|
||||
votingStatus === 0 ? <Button onClick={()=> setVotingStatus(1)}>
|
||||
<><i className='far fa-heart text-red-600'/> 하트 추가</>
|
||||
</Button>
|
||||
: votingStatus === 1 ? <Captcha dark={theme === 'dark'} onVerify={async (key) => {
|
||||
const res = await Fetch<{ retryAfter: number }|unknown>(`/bots/${data.id}/vote`, { method: 'POST', body: JSON.stringify({ _csrf: csrfToken, _captcha: key }) })
|
||||
<div className='mt-2 inline-block'>
|
||||
{votingStatus === 0 ? (
|
||||
<Button onClick={() => setVotingStatus(1)}>
|
||||
<>
|
||||
<i className='far fa-heart text-red-600' /> 하트 추가
|
||||
</>
|
||||
</Button>
|
||||
) : votingStatus === 1 ? (
|
||||
<Captcha
|
||||
dark={theme === 'dark'}
|
||||
onVerify={async (key) => {
|
||||
const res = await Fetch<{ retryAfter: number } | unknown>(
|
||||
`/bots/${data.id}/vote`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ _csrf: csrfToken, _captcha: key }),
|
||||
}
|
||||
)
|
||||
setResult(res)
|
||||
setVotingStatus(2)
|
||||
}}
|
||||
/>
|
||||
: result.code === 200 ? <h2 className='text-2xl font-bold'>해당 봇에 투표했습니다!</h2>
|
||||
: result.code === 429 ? <>
|
||||
<h2 className='text-2xl font-bold'>이미 해당 봇에 투표하였습니다.</h2>
|
||||
<h4 className='text-md mt-1'>{Day(+new Date() + result.data?.retryAfter).fromNow()} 다시 투표하실 수 있습니다.</h4>
|
||||
</>
|
||||
: <p>{result.message}</p>
|
||||
}
|
||||
/>
|
||||
) : result.code === 200 ? (
|
||||
<h2 className='text-2xl font-bold'>해당 봇에 투표했습니다!</h2>
|
||||
) : result.code === 429 ? (
|
||||
<>
|
||||
<h2 className='text-2xl font-bold'>이미 해당 봇에 투표하였습니다.</h2>
|
||||
<h4 className='text-md mt-1'>
|
||||
{Day(+new Date() + result.data?.retryAfter).fromNow()} 다시 투표하실 수
|
||||
있습니다.
|
||||
</h4>
|
||||
</>
|
||||
) : (
|
||||
<p>{result.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Segment>
|
||||
<Advertisement /></>
|
||||
}
|
||||
<Advertisement />
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -108,30 +153,30 @@ export const getServerSideProps = async (ctx: Context) => {
|
||||
const parsed = parseCookie(ctx.req)
|
||||
const data = await get.bot.load(ctx.query.id)
|
||||
const user = await get.Authorization(parsed?.token)
|
||||
|
||||
|
||||
return {
|
||||
props: {
|
||||
csrfToken: getToken(ctx.req, ctx.res),
|
||||
data,
|
||||
user: await get.user.load(user || '')
|
||||
user: await get.user.load(user || ''),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface VoteBotProps {
|
||||
csrfToken: string
|
||||
vote: boolean
|
||||
data: Bot
|
||||
user: User
|
||||
vote: boolean
|
||||
data: Bot
|
||||
user: User
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
interface Context extends CsrfContext {
|
||||
query: URLQuery
|
||||
query: URLQuery
|
||||
}
|
||||
|
||||
interface URLQuery extends ParsedUrlQuery {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default VoteBot
|
||||
export default VoteBot
|
||||
|
||||
@ -20,62 +20,88 @@ const Markdown = dynamic(() => import('@components/Markdown'))
|
||||
const NSFW = dynamic(() => import('@components/NSFW'))
|
||||
|
||||
const Category: NextPage<CategoryProps> = ({ data, query }) => {
|
||||
const [ nsfw, setNSFW ] = useState<boolean>()
|
||||
const [nsfw, setNSFW] = useState<boolean>()
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
setNSFW(localStorage.nsfw)
|
||||
}, [])
|
||||
if(!data || data.data.length === 0 || data.totalPage < Number(query.page)) return <NotFound message={data?.data.length === 0 ? '해당 카테고리에 해당되는 봇이 존재하지 않습니다.' : null} />
|
||||
return <>
|
||||
<Hero type='bots' header={`${query.category} 카테고리 봇들`} description={`다양한 "${query.category}" 카테고리의 봇들을 만나보세요.`} />
|
||||
{
|
||||
query.category === 'NSFW' && !nsfw ? <NSFW onClick={() => setNSFW(true)} onDisableClick={() => localStorage.nsfw = true} />
|
||||
: <Container>
|
||||
{
|
||||
router.query.category === '빗금 명령어' && <Segment className='mb-4'>
|
||||
<h1 className='text-2xl font-bold pt-3.5 pb-1'>빗금 명령어</h1>
|
||||
<Markdown text={'빗금 명렁어는 디스코드 채팅창에 `/` 를 입력하여 사용할 수 있습니다.'} />
|
||||
if (!data || data.data.length === 0 || data.totalPage < Number(query.page))
|
||||
return (
|
||||
<NotFound
|
||||
message={
|
||||
data?.data.length === 0 ? '해당 카테고리에 해당되는 봇이 존재하지 않습니다.' : null
|
||||
}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<Hero
|
||||
type='bots'
|
||||
header={`${query.category} 카테고리 봇들`}
|
||||
description={`다양한 "${query.category}" 카테고리의 봇들을 만나보세요.`}
|
||||
/>
|
||||
{query.category === 'NSFW' && !nsfw ? (
|
||||
<NSFW onClick={() => setNSFW(true)} onDisableClick={() => (localStorage.nsfw = true)} />
|
||||
) : (
|
||||
<Container>
|
||||
{router.query.category === '빗금 명령어' && (
|
||||
<Segment className='mb-4'>
|
||||
<h1 className='pb-1 pt-3.5 text-2xl font-bold'>빗금 명령어</h1>
|
||||
<Markdown
|
||||
text={'빗금 명렁어는 디스코드 채팅창에 `/` 를 입력하여 사용할 수 있습니다.'}
|
||||
/>
|
||||
</Segment>
|
||||
}
|
||||
)}
|
||||
<Advertisement />
|
||||
<ResponsiveGrid>
|
||||
{
|
||||
data.data.map(bot => <BotCard key={bot.id} bot={bot} /> )
|
||||
}
|
||||
{data.data.map((bot) => (
|
||||
<BotCard key={bot.id} bot={bot} />
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
<Paginator totalPage={data.totalPage} currentPage={data.currentPage} pathname={`/bots/categories/${query.category}`} />
|
||||
<Paginator
|
||||
totalPage={data.totalPage}
|
||||
currentPage={data.currentPage}
|
||||
pathname={`/bots/categories/${query.category}`}
|
||||
/>
|
||||
<Advertisement />
|
||||
</Container>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (ctx: Context) => {
|
||||
let data: List<Bot>
|
||||
if(!ctx.query.page) ctx.query.page = '1'
|
||||
const validate = await botCategoryListArgumentSchema.validate(ctx.query).then(el => el).catch(() => null)
|
||||
if(!validate || isNaN(Number(ctx.query.page))) data = null
|
||||
else data = await get.list.category.load(JSON.stringify({ page: Number(ctx.query.page), category: ctx.query.category }))
|
||||
if (!ctx.query.page) ctx.query.page = '1'
|
||||
const validate = await botCategoryListArgumentSchema
|
||||
.validate(ctx.query)
|
||||
.then((el) => el)
|
||||
.catch(() => null)
|
||||
if (!validate || isNaN(Number(ctx.query.page))) data = null
|
||||
else
|
||||
data = await get.list.category.load(
|
||||
JSON.stringify({ page: Number(ctx.query.page), category: ctx.query.category })
|
||||
)
|
||||
return {
|
||||
props: {
|
||||
data,
|
||||
query: ctx.query
|
||||
}
|
||||
query: ctx.query,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface CategoryProps {
|
||||
data: List<Bot>
|
||||
query: URLQuery
|
||||
data: List<Bot>
|
||||
query: URLQuery
|
||||
}
|
||||
|
||||
interface Context extends NextPageContext {
|
||||
query: URLQuery
|
||||
query: URLQuery
|
||||
}
|
||||
|
||||
interface URLQuery extends ParsedUrlQuery {
|
||||
category: string
|
||||
page?: string
|
||||
category: string
|
||||
page?: string
|
||||
}
|
||||
|
||||
export default Category
|
||||
export default Category
|
||||
|
||||
@ -4,32 +4,56 @@ import { NextSeo } from 'next-seo'
|
||||
|
||||
import { botCategories, botCategoryIcon } from '@utils/Constants'
|
||||
|
||||
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
||||
const Tag = dynamic(() => import('@components/Tag'))
|
||||
const Segment = dynamic(() => import('@components/Segment'))
|
||||
|
||||
const Categories:NextPage = () => {
|
||||
return <Container paddingTop>
|
||||
<NextSeo title='전체 카테고리' description='한국 디스코드 리스트의 전체 카테고리입니다.' />
|
||||
<h1 className='text-2xl font-bold mt-2 mb-5'>전체 카테고리</h1>
|
||||
<Segment className='mb-10'>
|
||||
<div className='text-center flex flex-wrap mt-1.5'>
|
||||
{
|
||||
botCategories.map(t => <Tag key={t} text={<>
|
||||
{
|
||||
{ '빗금 명령어': <span className='fa-stack' style={{ fontSize: '1em', height: '1.2em', lineHeight: '1em', width: '20px', verticalAlign: 'middle' }}>
|
||||
<i className='fas fa-square fa-stack-1x fa-md' />
|
||||
<i className='fas fa-slash fa-rotate-90 fa-xs fa-stack-1x fa-inverse' style={{ fontSize: '0.3rem' }} />
|
||||
</span> }[t] ?? <i className={botCategoryIcon[t]} />
|
||||
} {t}
|
||||
</>} href={`/bots/categories/${t}`} dark bigger /> )
|
||||
}
|
||||
</div>
|
||||
</Segment>
|
||||
<Advertisement />
|
||||
</Container>
|
||||
const Categories: NextPage = () => {
|
||||
return (
|
||||
<Container paddingTop>
|
||||
<NextSeo title='전체 카테고리' description='한국 디스코드 리스트의 전체 카테고리입니다.' />
|
||||
<h1 className='mb-5 mt-2 text-2xl font-bold'>전체 카테고리</h1>
|
||||
<Segment className='mb-10'>
|
||||
<div className='mt-1.5 flex flex-wrap text-center'>
|
||||
{botCategories.map((t) => (
|
||||
<Tag
|
||||
key={t}
|
||||
text={
|
||||
<>
|
||||
{{
|
||||
'빗금 명령어': (
|
||||
<span
|
||||
className='fa-stack'
|
||||
style={{
|
||||
fontSize: '1em',
|
||||
height: '1.2em',
|
||||
lineHeight: '1em',
|
||||
width: '20px',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
<i className='fas fa-square fa-stack-1x fa-md' />
|
||||
<i
|
||||
className='fas fa-slash fa-rotate-90 fa-xs fa-stack-1x fa-inverse'
|
||||
style={{ fontSize: '0.3rem' }}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
}[t] ?? <i className={botCategoryIcon[t]} />}{' '}
|
||||
{t}
|
||||
</>
|
||||
}
|
||||
href={`/bots/categories/${t}`}
|
||||
dark
|
||||
bigger
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Segment>
|
||||
<Advertisement />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Categories
|
||||
export default Categories
|
||||
|
||||
@ -18,49 +18,54 @@ const Index: NextPage<IndexProps> = ({ votes, newBots, trusted }) => {
|
||||
<Hero type='bots' />
|
||||
<Container className='pb-10'>
|
||||
<Advertisement />
|
||||
<h1 className='text-3xl font-bold mt-10 mb-2'>
|
||||
<h1 className='mb-2 mt-10 text-3xl font-bold'>
|
||||
<i className='far fa-heart mr-3 text-pink-600' /> 하트 랭킹
|
||||
</h1>
|
||||
<p className='text-base'>하트를 많이 받은 봇들의 순위입니다!</p>
|
||||
<ResponsiveGrid>
|
||||
{
|
||||
votes.data.map(bot=> <BotCard key={bot.id} bot={bot} />)
|
||||
}
|
||||
{votes.data.map((bot) => (
|
||||
<BotCard key={bot.id} bot={bot} />
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
<Paginator totalPage={votes.totalPage} currentPage={votes.currentPage} pathname='/bots/list/votes' />
|
||||
<Paginator
|
||||
totalPage={votes.totalPage}
|
||||
currentPage={votes.currentPage}
|
||||
pathname='/bots/list/votes'
|
||||
/>
|
||||
<Advertisement />
|
||||
<h1 className='text-3xl font-bold mb-2'>
|
||||
<h1 className='mb-2 text-3xl font-bold'>
|
||||
<i className='fa fa-check mr-3 mt-10 text-emerald-500' /> 신뢰된 봇
|
||||
</h1>
|
||||
<p className='text-base'>한국 디스코드 리스트에서 인증받은 신뢰할 수 있는 봇들입니다!!</p>
|
||||
<ResponsiveGrid>
|
||||
{
|
||||
trusted.data.slice(0, 4).map(bot=> <BotCard key={bot.id} bot={bot} />)
|
||||
}
|
||||
{trusted.data.slice(0, 4).map((bot) => (
|
||||
<BotCard key={bot.id} bot={bot} />
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
<h1 className='text-3xl font-bold mt-20 mb-2'>
|
||||
<h1 className='mb-2 mt-20 text-3xl font-bold'>
|
||||
<i className='far fa-star mr-3 text-amber-500' /> 새로운 봇
|
||||
</h1>
|
||||
<p className='text-base'>최근에 한국 디스코드 리스트에 추가된 따끈따끈한 봇입니다.</p>
|
||||
<ResponsiveGrid>
|
||||
{
|
||||
newBots.data.slice(0, 4).map(bot=> <BotCard key={bot.id} bot={bot} />)
|
||||
}
|
||||
{newBots.data.slice(0, 4).map((bot) => (
|
||||
<BotCard key={bot.id} bot={bot} />
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
<LongButton href='/bots/list/new' center>더보기</LongButton>
|
||||
<LongButton href='/bots/list/new' center>
|
||||
더보기
|
||||
</LongButton>
|
||||
<Advertisement />
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = async() => {
|
||||
export const getServerSideProps = async () => {
|
||||
const votes = await Query.get.list.votes.load(1)
|
||||
const newBots = await Query.get.list.new.load(1)
|
||||
const trusted = await Query.get.list.trusted.load(1)
|
||||
|
||||
return { props: { votes, newBots, trusted }}
|
||||
|
||||
return { props: { votes, newBots, trusted } }
|
||||
}
|
||||
|
||||
interface IndexProps {
|
||||
|
||||
@ -10,32 +10,38 @@ const BotCard = dynamic(() => import('@components/BotCard'))
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid'))
|
||||
|
||||
const New:NextPage<NewProps> = ({ data }) => {
|
||||
return <>
|
||||
<Hero type='bots' header='새로운 봇' description='최근에 한국 디스코드 리스트에 추가된 봇들입니다!' />
|
||||
<Container className='pb-10'>
|
||||
<Advertisement />
|
||||
<ResponsiveGrid>
|
||||
{
|
||||
data.data.map(bot => <BotCard key={bot.id} bot={bot} /> )
|
||||
}
|
||||
</ResponsiveGrid>
|
||||
<Advertisement />
|
||||
</Container>
|
||||
</>
|
||||
const New: NextPage<NewProps> = ({ data }) => {
|
||||
return (
|
||||
<>
|
||||
<Hero
|
||||
type='bots'
|
||||
header='새로운 봇'
|
||||
description='최근에 한국 디스코드 리스트에 추가된 봇들입니다!'
|
||||
/>
|
||||
<Container className='pb-10'>
|
||||
<Advertisement />
|
||||
<ResponsiveGrid>
|
||||
{data.data.map((bot) => (
|
||||
<BotCard key={bot.id} bot={bot} />
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
<Advertisement />
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = async () => {
|
||||
const data = await get.list.new.load(1)
|
||||
return {
|
||||
props: {
|
||||
data
|
||||
}
|
||||
data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface NewProps {
|
||||
data: List<Bot>
|
||||
data: List<Bot>
|
||||
}
|
||||
|
||||
export default New
|
||||
export default New
|
||||
|
||||
@ -4,7 +4,7 @@ import dynamic from 'next/dynamic'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
|
||||
import { Bot, List } from '@types'
|
||||
import { get }from '@utils/Query'
|
||||
import { get } from '@utils/Query'
|
||||
|
||||
import NotFound from '../../404'
|
||||
import { PageCount } from '@utils/Yup'
|
||||
@ -16,40 +16,49 @@ const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid'))
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
const Paginator = dynamic(() => import('@components/Paginator'))
|
||||
|
||||
const Votes:NextPage<VotesProps> = ({ data }) => {
|
||||
const Votes: NextPage<VotesProps> = ({ data }) => {
|
||||
const router = useRouter()
|
||||
if(!data || data.data.length === 0 || data.totalPage < Number(router.query.page)) return <NotFound />
|
||||
return <>
|
||||
<Hero type='bots' header='하트 랭킹' description='하트를 많이 받은 봇들의 순위입니다!'/>
|
||||
<section id='list'>
|
||||
<Container className='pb-10'>
|
||||
<Advertisement />
|
||||
<ResponsiveGrid>
|
||||
{
|
||||
data.data.map(bot => <BotCard key={bot.id} bot={bot} /> )
|
||||
}
|
||||
</ResponsiveGrid>
|
||||
<Paginator totalPage={data.totalPage} currentPage={data.currentPage} pathname='/bots/list/votes' />
|
||||
<Advertisement />
|
||||
</Container>
|
||||
</section>
|
||||
</>
|
||||
if (!data || data.data.length === 0 || data.totalPage < Number(router.query.page))
|
||||
return <NotFound />
|
||||
return (
|
||||
<>
|
||||
<Hero type='bots' header='하트 랭킹' description='하트를 많이 받은 봇들의 순위입니다!' />
|
||||
<section id='list'>
|
||||
<Container className='pb-10'>
|
||||
<Advertisement />
|
||||
<ResponsiveGrid>
|
||||
{data.data.map((bot) => (
|
||||
<BotCard key={bot.id} bot={bot} />
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
<Paginator
|
||||
totalPage={data.totalPage}
|
||||
currentPage={data.currentPage}
|
||||
pathname='/bots/list/votes'
|
||||
/>
|
||||
<Advertisement />
|
||||
</Container>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const getServerSideProps = async (ctx:Context) => {
|
||||
export const getServerSideProps = async (ctx: Context) => {
|
||||
let data: List<Bot>
|
||||
if(!ctx.query.page) ctx.query.page = '1'
|
||||
const validate = await PageCount.validate(ctx.query.page).then(el => el).catch(() => null)
|
||||
if(!validate || isNaN(Number(ctx.query.page))) data = null
|
||||
if (!ctx.query.page) ctx.query.page = '1'
|
||||
const validate = await PageCount.validate(ctx.query.page)
|
||||
.then((el) => el)
|
||||
.catch(() => null)
|
||||
if (!validate || isNaN(Number(ctx.query.page))) data = null
|
||||
else data = await get.list.votes.load(Number(ctx.query.page))
|
||||
return {
|
||||
props: {
|
||||
data
|
||||
}
|
||||
data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface VotesProps {
|
||||
data: List<Bot>
|
||||
data: List<Bot>
|
||||
}
|
||||
|
||||
interface Context extends NextPageContext {
|
||||
|
||||
@ -8,7 +8,6 @@ import { get } from '@utils/Query'
|
||||
import { SearchQuerySchema } from '@utils/Yup'
|
||||
import { KoreanbotsEndPoints } from '@utils/Constants'
|
||||
|
||||
|
||||
const Hero = dynamic(() => import('@components/Hero'))
|
||||
const Advertisement = dynamic(() => import('@components/Advertisement'))
|
||||
const BotCard = dynamic(() => import('@components/BotCard'))
|
||||
@ -18,75 +17,96 @@ const Paginator = dynamic(() => import('@components/Paginator'))
|
||||
const LongButton = dynamic(() => import('@components/LongButton'))
|
||||
const Redirect = dynamic(() => import('@components/Redirect'))
|
||||
|
||||
const SearchComponent: FC<{data: List<Bot>, query: URLQuery }> = ({ data, query }) => {
|
||||
return <div className='py-10'>
|
||||
{ !data || data.data.length === 0 ? <h1 className='text-3xl font-bold text-center py-20'>검색 결과가 없습니다.</h1> :
|
||||
<>
|
||||
<ResponsiveGrid>
|
||||
{
|
||||
data.data.map(el => <BotCard key={el.id} bot={el as Bot} /> )
|
||||
}
|
||||
</ResponsiveGrid>
|
||||
<Paginator totalPage={data.totalPage} currentPage={data.currentPage} pathname='/search' searchParams={query} />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
const SearchComponent: FC<{ data: List<Bot>; query: URLQuery }> = ({ data, query }) => {
|
||||
return (
|
||||
<div className='py-10'>
|
||||
{!data || data.data.length === 0 ? (
|
||||
<h1 className='py-20 text-center text-3xl font-bold'>검색 결과가 없습니다.</h1>
|
||||
) : (
|
||||
<>
|
||||
<ResponsiveGrid>
|
||||
{data.data.map((el) => (
|
||||
<BotCard key={el.id} bot={el as Bot} />
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
<Paginator
|
||||
totalPage={data.totalPage}
|
||||
currentPage={data.currentPage}
|
||||
pathname='/search'
|
||||
searchParams={query}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const Search:NextPage<SearchProps> = ({ botData, query }) => {
|
||||
if(!query?.q) return <Redirect text={false} to='/' />
|
||||
return <>
|
||||
<Hero type='bots' header={`"${query.q}" 검색 결과`} description={`'${query.q}' 에 대한 검색 결과입니다.`} />
|
||||
<Container>
|
||||
<section id='list'>
|
||||
<Advertisement />
|
||||
<h1 className='text-4xl font-bold'>봇</h1>
|
||||
<SearchComponent data={botData} query={query} />
|
||||
<h1 className='text-2xl font-bold py-10'>서버를 찾으시나요?</h1>
|
||||
<LongButton center href={KoreanbotsEndPoints.URL.searchServer(query.q)}>서버 검색 결과 보기</LongButton>
|
||||
<Advertisement />
|
||||
</section>
|
||||
</Container>
|
||||
</>
|
||||
const Search: NextPage<SearchProps> = ({ botData, query }) => {
|
||||
if (!query?.q) return <Redirect text={false} to='/' />
|
||||
return (
|
||||
<>
|
||||
<Hero
|
||||
type='bots'
|
||||
header={`"${query.q}" 검색 결과`}
|
||||
description={`'${query.q}' 에 대한 검색 결과입니다.`}
|
||||
/>
|
||||
<Container>
|
||||
<section id='list'>
|
||||
<Advertisement />
|
||||
<h1 className='text-4xl font-bold'>봇</h1>
|
||||
<SearchComponent data={botData} query={query} />
|
||||
<h1 className='py-10 text-2xl font-bold'>서버를 찾으시나요?</h1>
|
||||
<LongButton center href={KoreanbotsEndPoints.URL.searchServer(query.q)}>
|
||||
서버 검색 결과 보기
|
||||
</LongButton>
|
||||
<Advertisement />
|
||||
</section>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = async(ctx: Context) => {
|
||||
if(ctx.query.query && !ctx.query.q) ctx.query.q = ctx.query.query
|
||||
if(!ctx.query?.q) {
|
||||
export const getServerSideProps = async (ctx: Context) => {
|
||||
if (ctx.query.query && !ctx.query.q) ctx.query.q = ctx.query.query
|
||||
if (!ctx.query?.q) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/',
|
||||
permanent: true
|
||||
permanent: true,
|
||||
},
|
||||
props: {}
|
||||
props: {},
|
||||
}
|
||||
}
|
||||
if(!ctx.query.page) ctx.query.page = '1'
|
||||
const validate = await SearchQuerySchema.validate(ctx.query).then(el => el).catch(() => null)
|
||||
if(!validate || isNaN(Number(ctx.query.page))) return { props: { query: ctx.query } }
|
||||
if (!ctx.query.page) ctx.query.page = '1'
|
||||
const validate = await SearchQuerySchema.validate(ctx.query)
|
||||
.then((el) => el)
|
||||
.catch(() => null)
|
||||
if (!validate || isNaN(Number(ctx.query.page))) return { props: { query: ctx.query } }
|
||||
else {
|
||||
return {
|
||||
props: {
|
||||
botData: await get.list.search.load(JSON.stringify({ query: ctx.query.q || '', page: ctx.query.page })).then(el => el).catch(() => null),
|
||||
query: ctx.query
|
||||
}
|
||||
botData: await get.list.search
|
||||
.load(JSON.stringify({ query: ctx.query.q || '', page: ctx.query.page }))
|
||||
.then((el) => el)
|
||||
.catch(() => null),
|
||||
query: ctx.query,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface SearchProps {
|
||||
botData?: List<Bot>
|
||||
query: URLQuery
|
||||
botData?: List<Bot>
|
||||
query: URLQuery
|
||||
}
|
||||
|
||||
interface Context extends NextPageContext {
|
||||
query: URLQuery
|
||||
query: URLQuery
|
||||
}
|
||||
|
||||
interface URLQuery extends ParsedUrlQuery {
|
||||
q?: string
|
||||
q?: string
|
||||
query?: string
|
||||
page?: string
|
||||
page?: string
|
||||
}
|
||||
|
||||
export default Search
|
||||
export default Search
|
||||
|
||||
@ -10,81 +10,112 @@ import { DiscordEnpoints, GuildPermissions } from '@utils/Constants'
|
||||
const Container = dynamic(() => import('@components/Container'))
|
||||
const Input = dynamic(() => import('@components/Form/Input'))
|
||||
|
||||
const Calculator:NextPage<CalculatorProps> = ({ query }) => {
|
||||
const [ value, setValue ] = useState<{[perm: string]: boolean}>({})
|
||||
const Perm = ({ name, perm, yellow }:{ name: string, perm: number, yellow?: boolean }) => {
|
||||
return <li>
|
||||
<label className='inline-flex items-center py-1'>
|
||||
<input className='form-checkbox text-discord-blurple bg-gray-300 h-5 w-5 rounded' type='checkbox' checked={value[perm]} onChange={() => {
|
||||
setValue({ ...value, [perm]: !value[perm] })
|
||||
}} />
|
||||
<span className={`ml-2.5 text-lg ${yellow ? 'text-amber-500' : ''}`}>{name}</span>
|
||||
</label>
|
||||
</li>
|
||||
|
||||
const Calculator: NextPage<CalculatorProps> = ({ query }) => {
|
||||
const [value, setValue] = useState<{ [perm: string]: boolean }>({})
|
||||
const Perm = ({ name, perm, yellow }: { name: string; perm: number; yellow?: boolean }) => {
|
||||
return (
|
||||
<li>
|
||||
<label className='inline-flex items-center py-1'>
|
||||
<input
|
||||
className='form-checkbox h-5 w-5 rounded bg-gray-300 text-discord-blurple'
|
||||
type='checkbox'
|
||||
checked={value[perm]}
|
||||
onChange={() => {
|
||||
setValue({ ...value, [perm]: !value[perm] })
|
||||
}}
|
||||
/>
|
||||
<span className={`ml-2.5 text-lg ${yellow ? 'text-amber-500' : ''}`}>{name}</span>
|
||||
</label>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return <Container paddingTop className='pb-10'>
|
||||
<NextSeo title='봇 초대링크 생성기' description='디스코드 봇 초대링크를 간편하게 생성하세요' openGraph={{
|
||||
title:'봇 초대링크 생성기',
|
||||
description: '디스코드 봇 초대링크를 간편하게 생성하세요'
|
||||
}} />
|
||||
<h1 className='text-4xl font-bold mt-2 mb-4'>봇 초대링크 생성기</h1>
|
||||
<div className='text-2xl font-bold inline-flex items-center'>권한: {String(Object.keys(value).filter(el => value[el]).map(el => Number(el)).reduce((prev, curr) => BigInt(prev) | BigInt(curr), BigInt(0)))}
|
||||
<span className='ml-2 text-lg font-semibold'>= { Object.keys(value).filter(el => value[el]).map(el => `0x${Number(el).toString(16)}`).join(' | ') }</span>
|
||||
</div>
|
||||
<div className='grid gap-2 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 mt-2'>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold'>일반 권한</h2>
|
||||
<ul>
|
||||
{
|
||||
GuildPermissions.general.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
|
||||
}
|
||||
</ul>
|
||||
|
||||
return (
|
||||
<Container paddingTop className='pb-10'>
|
||||
<NextSeo
|
||||
title='봇 초대링크 생성기'
|
||||
description='디스코드 봇 초대링크를 간편하게 생성하세요'
|
||||
openGraph={{
|
||||
title: '봇 초대링크 생성기',
|
||||
description: '디스코드 봇 초대링크를 간편하게 생성하세요',
|
||||
}}
|
||||
/>
|
||||
<h1 className='mb-4 mt-2 text-4xl font-bold'>봇 초대링크 생성기</h1>
|
||||
<div className='inline-flex items-center text-2xl font-bold'>
|
||||
권한:{' '}
|
||||
{String(
|
||||
Object.keys(value)
|
||||
.filter((el) => value[el])
|
||||
.map((el) => Number(el))
|
||||
.reduce((prev, curr) => BigInt(prev) | BigInt(curr), BigInt(0))
|
||||
)}
|
||||
<span className='ml-2 text-lg font-semibold'>
|
||||
={' '}
|
||||
{Object.keys(value)
|
||||
.filter((el) => value[el])
|
||||
.map((el) => `0x${Number(el).toString(16)}`)
|
||||
.join(' | ')}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold'>멤버쉽 권한</h2>
|
||||
<ul>
|
||||
{
|
||||
GuildPermissions.membership.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
|
||||
}
|
||||
</ul>
|
||||
<div className='mt-2 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3'>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold'>일반 권한</h2>
|
||||
<ul>
|
||||
{GuildPermissions.general.map((el) => (
|
||||
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold'>멤버쉽 권한</h2>
|
||||
<ul>
|
||||
{GuildPermissions.membership.map((el) => (
|
||||
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold'>채팅 채널 권한</h2>
|
||||
<ul>
|
||||
{GuildPermissions.channel.map((el) => (
|
||||
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold'>음성 채널 권한</h2>
|
||||
<ul>
|
||||
{GuildPermissions.voice.map((el) => (
|
||||
<Perm key={el.name} name={el.name} perm={el.flag} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold'>고급 권한</h2>
|
||||
<ul>
|
||||
{GuildPermissions.advanced.map((el) => (
|
||||
<Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold'>채팅 채널 권한</h2>
|
||||
<ul>
|
||||
{
|
||||
GuildPermissions.channel.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
|
||||
}
|
||||
</ul>
|
||||
<div className='py-10'>
|
||||
<span className='text-amber-500'>
|
||||
노란색 = 서버에 2단계 인증 필수가 활성화되어있다면, 봇 소유자는{' '}
|
||||
<a href='https://support.discord.com/hc/ko/articles/219576828-2단계-인증-설정하기'>
|
||||
2단계 인증
|
||||
</a>
|
||||
이 완료되어있어야합니다.
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold'>음성 채널 권한</h2>
|
||||
<ul>
|
||||
{
|
||||
GuildPermissions.voice.map(el => <Perm key={el.name} name={el.name} perm={el.flag} />)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold'>고급 권한</h2>
|
||||
<ul>
|
||||
{
|
||||
GuildPermissions.advanced.map(el => <Perm key={el.name} name={el.name} perm={el.flag} yellow={el.twofactor} />)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className='py-10'>
|
||||
<span className='text-amber-500'>노란색 = 서버에 2단계 인증 필수가 활성화되어있다면, 봇 소유자는 <a href='https://support.discord.com/hc/ko/articles/219576828-2단계-인증-설정하기'>2단계 인증</a>이 완료되어있어야합니다.</span>
|
||||
</div>
|
||||
<Formik onSubmit={()=> console.log('Pong?')} initialValues={{
|
||||
id: query.id?.toString() || '',
|
||||
scope: 'bot',
|
||||
redirect: ''
|
||||
}}>
|
||||
{
|
||||
({ values, setFieldValue }) => (
|
||||
<Formik
|
||||
onSubmit={() => console.log('Pong?')}
|
||||
initialValues={{
|
||||
id: query.id?.toString() || '',
|
||||
scope: 'bot',
|
||||
redirect: '',
|
||||
}}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className='grid gap-3 lg:grid-cols-4'>
|
||||
<div>
|
||||
@ -94,7 +125,12 @@ const Calculator:NextPage<CalculatorProps> = ({ query }) => {
|
||||
</div>
|
||||
<div>
|
||||
<h6>스코프 (Scope)</h6>
|
||||
<button onClick={() => setFieldValue('scope', 'bot applications.commands')} className='text-blue-500 hover:text-blue-400'>빗금 명령어(Slash Command) 봇인가요?</button>
|
||||
<button
|
||||
onClick={() => setFieldValue('scope', 'bot applications.commands')}
|
||||
className='text-blue-500 hover:text-blue-400'
|
||||
>
|
||||
빗금 명령어(Slash Command) 봇인가요?
|
||||
</button>
|
||||
<Input name='scope' placeholder='bot' />
|
||||
</div>
|
||||
<div>
|
||||
@ -104,20 +140,37 @@ const Calculator:NextPage<CalculatorProps> = ({ query }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
초대링크: <a rel='noreferrer' target='_blank' href={values.id ? DiscordEnpoints.InviteApplication(values.id, value, values.scope, values.redirect) : null} className='cursor-pointer text-blue-500 hover:text-blue-400'>{DiscordEnpoints.InviteApplication(values.id, value, values.scope, values.redirect)}</a>
|
||||
초대링크:{' '}
|
||||
<a
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
href={
|
||||
values.id
|
||||
? DiscordEnpoints.InviteApplication(
|
||||
values.id,
|
||||
value,
|
||||
values.scope,
|
||||
values.redirect
|
||||
)
|
||||
: null
|
||||
}
|
||||
className='cursor-pointer text-blue-500 hover:text-blue-400'
|
||||
>
|
||||
{DiscordEnpoints.InviteApplication(values.id, value, values.scope, values.redirect)}
|
||||
</a>
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
</Container>
|
||||
)}
|
||||
</Formik>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
return {
|
||||
props: {
|
||||
query: ctx.query
|
||||
}
|
||||
query: ctx.query,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user