Improved Report and changed email address (#440)

* feat: added report page for bot

* feat: added report page for user

* feat: blocking user reporting self

* feat: changed emails

* refactor: changed category handler style
This commit is contained in:
Junseo Park 2021-07-04 12:23:23 +09:00 committed by GitHub
parent 084a90977a
commit 2ee7151a06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 528 additions and 240 deletions

View File

@ -6,4 +6,4 @@
## English ## English
Please [mail](mailto:koreanbots.dev@gmail.com) us! Please [mail](mailto:team@koreanbots.dev) us!

View File

@ -0,0 +1,78 @@
import { FC, useState } from 'react'
import dynamic from 'next/dynamic'
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-green-400 fas fa-check-circle mr-1' />}
{text}
</>
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 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>
<p> , !</p>
</> : ''
}
</div>
}
interface ReportValues {
category: string | null
description: string
_csrf: string
}
interface ReportTemplateProps {
values?: ReportValues
errors?: FormikErrors<ReportValues>
touched?: FormikTouched<ReportValues>
setFieldValue?(field: string, value: unknown): void
}

View File

@ -16,7 +16,7 @@ import { get } from '@utils/Query'
import Day from '@utils/Day' import Day from '@utils/Day'
import { ReportSchema } from '@utils/Yup' import { ReportSchema } from '@utils/Yup'
import Fetch from '@utils/Fetch' import Fetch from '@utils/Fetch'
import { checkBotFlag, checkUserFlag, formatNumber, parseCookie, redirectTo } from '@utils/Tools' import { checkBotFlag, checkUserFlag, formatNumber, parseCookie } from '@utils/Tools'
import { getToken } from '@utils/Csrf' import { getToken } from '@utils/Csrf'
import NotFound from '../../404' import NotFound from '../../404'
@ -217,15 +217,11 @@ const Bots: NextPage<BotsProps> = ({ data, desc, date, user, theme, csrfToken })
/> />
))} ))}
<div className='list grid'> <div className='list grid'>
<a className='text-red-600 hover:underline cursor-pointer' onClick={() => { <Link href={`/bots/${router.query.id}/report`}>
if(!user) { <a className='text-red-600 hover:underline cursor-pointer' aria-hidden='true'>
localStorage.redirectTo = window.location.href <i className='far fa-flag' />
redirectTo(router, 'login') </a>
} </Link>
else setReportModal(true)
}} aria-hidden='true'>
<i className='far fa-flag' />
</a>
<Modal header={`${data.name}#${data.tag} 신고하기`} closeIcon isOpen={reportModal} onClose={() => { <Modal header={`${data.name}#${data.tag} 신고하기`} closeIcon isOpen={reportModal} onClose={() => {
setReportModal(false) setReportModal(false)
setReportRes(null) setReportRes(null)

140
pages/bots/[id]/report.tsx Normal file
View File

@ -0,0 +1,140 @@
import { NextPage } from 'next'
import Link from 'next/link'
import dynamic from 'next/dynamic'
import { Field, Form, Formik } from 'formik'
import { Bot, CsrfContext, ResponseProps, User } from '@types'
import { get } from '@utils/Query'
import { makeBotURL, parseCookie } from '@utils/Tools'
import { ParsedUrlQuery } from 'querystring'
import NotFound from 'pages/404'
import { getToken } from '@utils/Csrf'
import { DMCA, TextField } from '@components/ReportTemplate'
import { useState } from 'react'
import Fetch from '@utils/Fetch'
import { ReportSchema } from '@utils/Yup'
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>
return <Container paddingTop className='py-10'>
<NextSeo title={`${data.name} 신고하기`} />
<Link href={makeBotURL(data)}>
<a className='text-blue-500 hover:opacity-80'><i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.name}</strong>{getJosaPicker('로')(data.name)} </a>
</Link>
{
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 : 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} />
</>
}
</>
}
</div>
</Form>
)
}
</Formik>
}
</Container>
}
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 || '')
},
}
}
interface ReportBotProps {
csrfToken: string
data: Bot
user: User
}
interface Context extends CsrfContext {
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
id: string
}
export default ReportBot

View File

@ -9,6 +9,8 @@ const Docs = dynamic(() => import('@components/Docs'))
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar')) const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
const Button = dynamic(() => import('@components/Button')) const Button = dynamic(() => import('@components/Button'))
const BODY = '중요도:\n설명:\n\n영향을 줄 수 있는 경우:'
const Security: NextPage<SecurityProps> = ({ bugReports }) => { const Security: NextPage<SecurityProps> = ({ bugReports }) => {
return <Docs return <Docs
header='버그 바운티 프로그램' header='버그 바운티 프로그램'
@ -59,7 +61,7 @@ const Security: NextPage<SecurityProps> = ({ bugReports }) => {
</ul> </ul>
<div className='text-center py-36'> <div className='text-center py-36'>
<h1 className='text-3xl font-bold mb-6'> ?</h1> <h1 className='text-3xl font-bold mb-6'> ?</h1>
<Button href='mailto:koreanbots.dev@gmail.com'></Button> <Button href={`mailto:team@koreanbots.dev?subject=[Security] &body=${encodeURI(BODY)}`}></Button>
</div> </div>
</Docs> </Docs>
} }

View File

@ -1,219 +0,0 @@
import { NextPage, NextPageContext } from 'next'
import { useState } from 'react'
import dynamic from 'next/dynamic'
import { SnowflakeUtil } from 'discord.js'
import { ParsedUrlQuery } from 'querystring'
import { josa } from 'josa'
import { Field, Form, Formik } from 'formik'
import { Bot, User, ResponseProps, Theme } from '@types'
import { get } from '@utils/Query'
import { checkUserFlag, parseCookie, redirectTo } from '@utils/Tools'
import { getToken } from '@utils/Csrf'
import Fetch from '@utils/Fetch'
import { ReportSchema } from '@utils/Yup'
import NotFound from '../404'
import { KoreanbotsEndPoints, reportCats } from '@utils/Constants'
import { NextSeo } from 'next-seo'
import { useRouter } from 'next/router'
const Container = dynamic(() => import('@components/Container'))
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
const Divider = dynamic(() => import('@components/Divider'))
const BotCard = dynamic(() => import('@components/BotCard'))
const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid'))
const Tag = dynamic(() => import('@components/Tag'))
const Advertisement = dynamic(() => import('@components/Advertisement'))
const Tooltip = dynamic(() => import('@components/Tooltip'))
const Message = dynamic(() => import('@components/Message'))
const Modal = dynamic(() => import('@components/Modal'))
const Button = dynamic(() => import('@components/Button'))
const TextArea = dynamic(() => import('@components/Form/TextArea'))
const Users: NextPage<UserProps> = ({ user, data, csrfToken, theme }) => {
const router = useRouter()
const [ reportModal, setReportModal ] = useState(false)
const [ reportRes, setReportRes ] = useState<ResponseProps<null>>(null)
if (!data?.id) return <NotFound />
return (
<Container paddingTop className='py-10'>
<NextSeo
title={data.username}
description={data.bots.length === 0 ? `${data.username}님의 프로필입니다.` : josa(
`${(data.bots as Bot[])
.slice(0, 5)
.map(el => el.name)
.join(', ')}#{} .`
)}
openGraph={{
images: [{
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
width: 256,
height: 256,
alt: 'User Avatar'
}]
}}
/>
<div className='lg:flex'>
<div className='w-3/5 mx-auto text-center lg:w-1/6'>
<DiscordAvatar
size={512}
userID={data.id}
className='w-full'
/>
</div>
<div className='flex-grow px-5 py-10 w-full text-center lg:w-5/12 lg:text-left'>
<div>
<div className='lg:flex mt-3 mb-1 '>
<h1 className='text-4xl font-bold'>{data.username}</h1>
<span className='ml-0.5 text-gray-400 text-3xl font-semibold mt-1'>#{data.tag}</span>
</div>
<div className='badges flex mb-2 justify-center lg:justify-start'>
{checkUserFlag(data.flags, 'staff') && (
<Tooltip text='한국 디스코드봇 리스트 스탭입니다.' direction='left'>
<div className='pr-5 text-koreanbots-blue text-2xl'>
<i className='fas fa-hammer' />
</div>
</Tooltip>
)}
{checkUserFlag(data.flags, 'bughunter') && (
<Tooltip text='버그를 많이 제보해주신 분입니다.' direction='left'>
<div className='pr-5 text-green-500 text-2xl'>
<i className='fas fa-bug' />
</div>
</Tooltip>
)}
</div>
{data.github && (
<Tag
newTab
text={
<>
<i className='fab fa-github' /> {data.github}
</>
}
github
href={`https://github.com/${data.github}`}
/>
)}
<div className='list-none mt-2'>
<a className='text-red-600 hover:underline cursor-pointer' onClick={() => {
if(!user) {
localStorage.redirectTo = window.location.href
redirectTo(router, 'login')
}
else setReportModal(true)
}} aria-hidden='true'>
<i className='far fa-flag' />
</a>
</div>
<Modal header={user.id === data.id ? '자기 자신은 신고할 수 없습니다.' : `${data.username}#${data.tag} 신고하기`} closeIcon isOpen={reportModal} onClose={() => {
setReportModal(false)
setReportRes(null)
}} full dark={theme === 'dark'}>
{
user.id === data.id ? <div className='text-center py-20'>
<h2 className='text-xl font-semibold'>
" 현명한 조언을 해주는 것은 자기 이외에는 없다. "
</h2>
</div> :
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<null>(`/users/${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 : 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>
</div>
</div>
</div>
<Divider />
<h2 className='mt-8 text-3xl font-bold'> </h2>
{data.bots.length === 0 ? <h2 className='text-xl'> .</h2> :
<ResponsiveGrid>
{
(data.bots as Bot[]).map((bot: Bot) => (
<BotCard key={bot.id} bot={bot} />
))
}
</ResponsiveGrid>
}
<Advertisement />
</Container>
)
}
export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
const user = await get.Authorization(parsed?.token) || ''
const data = await get.user.load(ctx.query.id)
return { props: { user: await get.user.load(user) || {}, data, date: SnowflakeUtil.deconstruct(data?.id ?? '0')?.date?.toJSON(), csrfToken: getToken(ctx.req, ctx.res) } }
}
interface UserProps {
user: User
data: User
csrfToken: string
theme: Theme
}
interface Context extends NextPageContext {
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
id: string
}
export default Users

145
pages/users/[id]/index.tsx Normal file
View File

@ -0,0 +1,145 @@
import { NextPage, NextPageContext } from 'next'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { SnowflakeUtil } from 'discord.js'
import { ParsedUrlQuery } from 'querystring'
import { josa } from 'josa'
import { Bot, User, Theme } from '@types'
import { get } from '@utils/Query'
import { checkUserFlag, parseCookie } from '@utils/Tools'
import { getToken } from '@utils/Csrf'
import NotFound from '../../404'
import { KoreanbotsEndPoints } from '@utils/Constants'
import { NextSeo } from 'next-seo'
import { useRouter } from 'next/router'
const Container = dynamic(() => import('@components/Container'))
const DiscordAvatar = dynamic(() => import('@components/DiscordAvatar'))
const Divider = dynamic(() => import('@components/Divider'))
const BotCard = dynamic(() => import('@components/BotCard'))
const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid'))
const Tag = dynamic(() => import('@components/Tag'))
const Advertisement = dynamic(() => import('@components/Advertisement'))
const Tooltip = dynamic(() => import('@components/Tooltip'))
const Users: NextPage<UserProps> = ({ user, data }) => {
const router = useRouter()
if (!data?.id) return <NotFound />
return (
<Container paddingTop className='py-10'>
<NextSeo
title={data.username}
description={data.bots.length === 0 ? `${data.username}님의 프로필입니다.` : josa(
`${(data.bots as Bot[])
.slice(0, 5)
.map(el => el.name)
.join(', ')}#{} .`
)}
openGraph={{
images: [{
url: KoreanbotsEndPoints.CDN.avatar(data.id, { format: 'png', size: 256 }),
width: 256,
height: 256,
alt: 'User Avatar'
}]
}}
/>
<div className='lg:flex'>
<div className='w-3/5 mx-auto text-center lg:w-1/6'>
<DiscordAvatar
size={512}
userID={data.id}
className='w-full'
/>
</div>
<div className='flex-grow px-5 py-10 w-full text-center lg:w-5/12 lg:text-left'>
<div>
<div className='lg:flex mt-3 mb-1 '>
<h1 className='text-4xl font-bold'>{data.username}</h1>
<span className='ml-0.5 text-gray-400 text-3xl font-semibold mt-1'>#{data.tag}</span>
</div>
<div className='badges flex mb-2 justify-center lg:justify-start'>
{checkUserFlag(data.flags, 'staff') && (
<Tooltip text='한국 디스코드봇 리스트 스탭입니다.' direction='left'>
<div className='pr-5 text-koreanbots-blue text-2xl'>
<i className='fas fa-hammer' />
</div>
</Tooltip>
)}
{checkUserFlag(data.flags, 'bughunter') && (
<Tooltip text='버그를 많이 제보해주신 분입니다.' direction='left'>
<div className='pr-5 text-green-500 text-2xl'>
<i className='fas fa-bug' />
</div>
</Tooltip>
)}
</div>
{data.github && (
<Tag
newTab
text={
<>
<i className='fab fa-github' /> {data.github}
</>
}
github
href={`https://github.com/${data.github}`}
/>
)}
{
user?.id !== data.id && <div className='list-none mt-2'>
<Link href={`/users/${router.query.id}/report`}>
<a className='text-red-600 hover:underline cursor-pointer' aria-hidden='true'>
<i className='far fa-flag' />
</a>
</Link>
</div>
}
</div>
</div>
</div>
<Divider />
<h2 className='mt-8 text-3xl font-bold'> </h2>
{data.bots.length === 0 ? <h2 className='text-xl'> .</h2> :
<ResponsiveGrid>
{
(data.bots as Bot[]).map((bot: Bot) => (
<BotCard key={bot.id} bot={bot} />
))
}
</ResponsiveGrid>
}
<Advertisement />
</Container>
)
}
export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
const user = await get.Authorization(parsed?.token) || ''
const data = await get.user.load(ctx.query.id)
return { props: { user: await get.user.load(user) || {}, data, date: SnowflakeUtil.deconstruct(data?.id ?? '0')?.date?.toJSON(), csrfToken: getToken(ctx.req, ctx.res) } }
}
interface UserProps {
user: User
data: User
csrfToken: string
theme: Theme
}
interface Context extends NextPageContext {
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
id: string
}
export default Users

143
pages/users/[id]/report.tsx Normal file
View File

@ -0,0 +1,143 @@
import { NextPage } from 'next'
import Link from 'next/link'
import dynamic from 'next/dynamic'
import { Field, Form, Formik } from 'formik'
import { CsrfContext, ResponseProps, User } from '@types'
import { get } from '@utils/Query'
import { makeUserURL, parseCookie } from '@utils/Tools'
import { ParsedUrlQuery } from 'querystring'
import NotFound from 'pages/404'
import { getToken } from '@utils/Csrf'
import { DMCA, TextField } from '@components/ReportTemplate'
import { useState } from 'react'
import Fetch from '@utils/Fetch'
import { ReportSchema } from '@utils/Yup'
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 ReportUser: NextPage<ReportUserProps> = ({ data, user, csrfToken }) => {
const [ reportRes, setReportRes ] = useState<ResponseProps<unknown>>(null)
if(!data?.id) return <NotFound />
if(!user) return <Login>
<NextSeo title='신고하기' />
</Login>
if(user?.id === data.id) return <>
<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-6xl font-semibold'>..!</h1>
<p className='text-gray-400 text-lg mt-2'> .</p>
</div>
</div>
</>
return <Container paddingTop className='py-10'>
<NextSeo title={`${data.username} 신고하기`} />
<Link href={makeUserURL(data)}>
<a className='text-blue-500 hover:opacity-80'><i className='fas fa-arrow-left mt-3 mb-3' /> <strong>{data.username}</strong>{getJosaPicker('로')(data.username)} </a>
</Link>
{
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(`/users/${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 : null}</div>
{
values.category && <>
{
values.category === '오픈소스 라이선스, 저작권 위반 등 권리 침해' ? <DMCA values={values} errors={errors} touched={touched} setFieldValue={setFieldValue} /> :
values.category === '괴롭힘, 모욕, 명예훼손' ? <>
<Message type='info'>
<h3 className='font-bold text-xl'> ?</h3>
<p> .</p>
<p className='list-disc list-item list-inside'> 1393 | 1388</p>
</Message>
</> : ''
}
{
!['오픈소스 라이선스, 저작권 위반 등 권리 침해'].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} />
</>
}
</>
}
</div>
</Form>
)
}
</Formik>
}
</Container>
}
export const getServerSideProps = async (ctx: Context) => {
const parsed = parseCookie(ctx.req)
const data = await get.user.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 || '')
},
}
}
interface ReportUserProps {
csrfToken: string
data: User
user: User
}
interface Context extends CsrfContext {
query: URLQuery
}
interface URLQuery extends ParsedUrlQuery {
id: string
}
export default ReportUser

View File

@ -204,13 +204,13 @@ export type Category =
| '검색' | '검색'
export type ReportCategory = export type ReportCategory =
| '불법' | '위법'
| '봇을 이용한 테러'
| '괴롭힘, 모욕, 명예훼손' | '괴롭힘, 모욕, 명예훼손'
| '스팸, 도배, 의미없는 텍스트' | '스팸, 도배, 의미없는 텍스트'
| '폭력, 자해, 테러 옹호하거나 조장하는 컨텐츠' | '폭력, 자해, 테러 옹호하거나 조장하는 컨텐츠'
| '라이선스혹은 권리 침해' | '오픈소스 라이선스, 저작권 위반 등 권리 침해'
| 'Discord ToS 위반' | 'Discord ToS를 위반하거나 한국 디스코드봇 리스트 가이드라인 위반'
| 'Koreanbots 가이드라인 위반'
| '기타' | '기타'
export type ListType = 'VOTE' | 'TRUSTED' | 'NEW' | 'PARTNERED' | 'CATEGORY' | 'SEARCH' export type ListType = 'VOTE' | 'TRUSTED' | 'NEW' | 'PARTNERED' | 'CATEGORY' | 'SEARCH'

View File

@ -120,13 +120,13 @@ export const categoryIcon = {
} }
export const reportCats = [ export const reportCats = [
'불법', '위법',
'봇을 이용한 테러',
'괴롭힘, 모욕, 명예훼손', '괴롭힘, 모욕, 명예훼손',
'스팸, 도배, 의미없는 텍스트', '스팸, 도배, 의미없는 텍스트',
'폭력, 자해, 테러 옹호하거나 조장하는 컨텐츠', '폭력, 자해, 테러 옹호하거나 조장하는 컨텐츠',
'라이선스혹은 권리 침해', '오픈소스 라이선스, 저작권 위반 등 권리 침해',
'Discord ToS 위반', 'Discord ToS를 위반하거나 한국 디스코드봇 리스트 가이드라인 위반',
'Koreanbots 가이드라인 위반',
'기타', '기타',
] ]

View File

@ -51,10 +51,13 @@ export function makeImageURL(root:string, { format='png', size=256 }:ImageOption
return `${root}.${format}?size=${size}` return `${root}.${format}?size=${size}`
} }
export function makeBotURL({id, vanity, flags=0}: { flags?: number, vanity?:string, id: string }): string { export function makeBotURL({ id, vanity, flags=0 }: { flags?: number, vanity?:string, id: string }): string {
return `/bots/${(checkBotFlag(flags, 'trusted') || checkBotFlag(flags, 'partnered')) && vanity ? vanity : id}` return `/bots/${(checkBotFlag(flags, 'trusted') || checkBotFlag(flags, 'partnered')) && vanity ? vanity : id}`
} }
export function makeUserURL({ id }: { id: string }): string {
return `/users/${id}`
}
export function serialize<T>(data: T): T { export function serialize<T>(data: T): T {
return JSON.parse(JSON.stringify(data)) return JSON.parse(JSON.stringify(data))
} }