chore: initial commit
This commit is contained in:
23
pages/404.tsx
Normal file
23
pages/404.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { ErrorPage } from 'components/ErrorPage'
|
||||
|
||||
const Error404: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title='Thream | 404' />
|
||||
|
||||
<ErrorPage message={t('errors:page-not-found')} statusCode={404} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
return { props: {} }
|
||||
}
|
||||
|
||||
export default Error404
|
23
pages/500.tsx
Normal file
23
pages/500.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { ErrorPage } from 'components/ErrorPage'
|
||||
|
||||
const Error500: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title='Thream | 500' />
|
||||
|
||||
<ErrorPage message={t('errors:server-error')} statusCode={500} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
return { props: {} }
|
||||
}
|
||||
|
||||
export default Error500
|
101
pages/_app.tsx
Normal file
101
pages/_app.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { useEffect } from 'react'
|
||||
import { AppProps } from 'next/app'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { ThemeProvider } from 'contexts/Theme'
|
||||
import { cookies } from 'utils/cookies'
|
||||
|
||||
import '@fontsource/poppins/400.css'
|
||||
import '@fontsource/poppins/600.css'
|
||||
import '@fontsource/poppins/700.css'
|
||||
|
||||
import '@fontsource/roboto/400.css'
|
||||
import '@fontsource/roboto/700.css'
|
||||
|
||||
import 'normalize.css/normalize.css'
|
||||
|
||||
const ThreamApp = ({ Component, pageProps }: AppProps): JSX.Element => {
|
||||
const { lang } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
cookies.set('NEXT_LOCALE', lang)
|
||||
}, [lang])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThemeProvider>
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
|
||||
<style jsx global>
|
||||
{`
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html {
|
||||
font-size: 62.5%; /* 1 rem = 10px; 10px/16px = 62.5% */
|
||||
}
|
||||
body {
|
||||
--default-font-size: 1.6rem;
|
||||
--color-background-primary: #212121;
|
||||
--color-background-secondary: #292d3e;
|
||||
--color-background-tertiary: #202331;
|
||||
--color-primary: #27b05e;
|
||||
--color-secondary: #fff;
|
||||
--color-tertiary: #7c818f;
|
||||
--color-success: #90ee90;
|
||||
--color-error: #ff7f7f;
|
||||
--color-shadow: rgba(255, 255, 255, 0.2);
|
||||
&.theme-dark {
|
||||
--color-background-primary: #212121;
|
||||
--color-secondary: #fff;
|
||||
--color-success: #90ee90;
|
||||
--color-error: #ff7f7f;
|
||||
--color-shadow: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
&.theme-light {
|
||||
--color-background-primary: #fff;
|
||||
--color-secondary: #181818;
|
||||
--color-success: #1e4620;
|
||||
--color-error: #ee1b1b;
|
||||
--color-shadow: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
background-color: var(--color-background-primary);
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--default-font-size);
|
||||
font-family: 'Poppins', 'Arial', 'sans-serif';
|
||||
font-weight: 400;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
#__next {
|
||||
max-width: 100%;
|
||||
}
|
||||
a {
|
||||
font-family: 'Roboto', 'Arial', 'sans-serif';
|
||||
font-size: 14px;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgba(36, 38, 45, 0.4);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThreamApp
|
44
pages/application/[guildId]/[channelId].tsx
Normal file
44
pages/application/[guildId]/[channelId].tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Head } from 'components/Head'
|
||||
import { authenticationFromServerSide } from 'utils/authentication'
|
||||
import { Application, ApplicationProps } from 'components/Application'
|
||||
import { Messages } from 'components/Messages'
|
||||
import { Messages as MessagesType, MessagesProvider } from 'contexts/Messages'
|
||||
import { Guild } from 'contexts/Guilds'
|
||||
import { SendMessage } from 'components/SendMessage'
|
||||
|
||||
export interface ChannelPageProps extends ApplicationProps {
|
||||
guild: Guild
|
||||
messages: MessagesType
|
||||
channelId: string
|
||||
guildId: string
|
||||
}
|
||||
|
||||
const ChannelPage: React.FC<ChannelPageProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Head title={props.guild.name} />
|
||||
<Application authentication={props.authentication} guilds={props.guilds}>
|
||||
<MessagesProvider messages={props.messages} channelId={props.channelId}>
|
||||
<Messages />
|
||||
<SendMessage channelId={props.channelId} />
|
||||
</MessagesProvider>
|
||||
</Application>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = authenticationFromServerSide({
|
||||
shouldBeAuthenticated: true,
|
||||
fetchData: async (api, context) => {
|
||||
const { channelId, guildId } = context.params as {
|
||||
channelId: string
|
||||
guildId: string
|
||||
}
|
||||
const { data: guild } = await api.get(`/guilds/${guildId}`)
|
||||
const { data: messages } = await api.get(`/channels/${channelId}/messages`)
|
||||
const { data: guilds } = await api.get('/guilds')
|
||||
return { guild, guilds, messages, channelId, guildId }
|
||||
}
|
||||
})
|
||||
|
||||
export default ChannelPage
|
24
pages/application/index.tsx
Normal file
24
pages/application/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { Head } from 'components/Head'
|
||||
import { authenticationFromServerSide } from 'utils/authentication'
|
||||
import { Application, ApplicationProps } from 'components/Application'
|
||||
|
||||
const ApplicationPage: React.FC<ApplicationProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Head title='Thream | Application' />
|
||||
<Application authentication={props.authentication} guilds={props.guilds}>
|
||||
<p>Main Content</p>
|
||||
</Application>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = authenticationFromServerSide({
|
||||
shouldBeAuthenticated: true,
|
||||
fetchData: async (api) => {
|
||||
const { data } = await api.get('/guilds')
|
||||
return { guilds: data }
|
||||
}
|
||||
})
|
||||
|
||||
export default ApplicationPage
|
68
pages/authentication/forgot-password.tsx
Normal file
68
pages/authentication/forgot-password.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Input } from 'components/design/Input'
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
import { FormState } from 'components/Authentication/FormState'
|
||||
import { Container } from 'components/design/Container'
|
||||
import { AuthenticationFormLayout } from 'components/Authentication/AuthenticationFormLayout'
|
||||
import { emailSchema } from 'components/Authentication/AuthenticationForm'
|
||||
import { useForm } from 'hooks/useForm'
|
||||
import { api } from 'utils/api'
|
||||
import { authenticationFromServerSide } from 'utils/authentication'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
const ForgotPassword: React.FC = () => {
|
||||
const {
|
||||
getErrorMessages,
|
||||
formState,
|
||||
message,
|
||||
handleChange,
|
||||
handleSubmit
|
||||
} = useForm({
|
||||
validatorSchema: emailSchema
|
||||
})
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title={`Thream | ${t('authentication:forgot-password')}`} />
|
||||
|
||||
<Header />
|
||||
<Container>
|
||||
<AuthenticationFormLayout
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit(async (formData) => {
|
||||
await api.post(
|
||||
`/users/resetPassword?redirectURI=${window.location.origin}/authentication/reset-password`,
|
||||
formData
|
||||
)
|
||||
return await t('authentication:success-forgot-password')
|
||||
})}
|
||||
link={
|
||||
<p>
|
||||
<Link href='/authentication/signin'>
|
||||
<a>{t('authentication:already-know-password')}</a>
|
||||
</Link>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
errors={getErrorMessages('email')}
|
||||
type='email'
|
||||
placeholder='Email'
|
||||
name='email'
|
||||
label='Email'
|
||||
/>
|
||||
</AuthenticationFormLayout>
|
||||
<FormState state={formState} message={message} />
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = authenticationFromServerSide({
|
||||
shouldBeAuthenticated: false
|
||||
})
|
||||
|
||||
export default ForgotPassword
|
75
pages/authentication/reset-password.tsx
Normal file
75
pages/authentication/reset-password.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { Input } from 'components/design/Input'
|
||||
import { Header } from 'components/Header'
|
||||
import { FormState } from 'components/Authentication/FormState'
|
||||
import { Container } from 'components/design/Container'
|
||||
import { AuthenticationFormLayout } from 'components/Authentication/AuthenticationFormLayout'
|
||||
import { passwordSchema } from 'components/Authentication/AuthenticationForm'
|
||||
import { useForm } from 'hooks/useForm'
|
||||
import { api } from 'utils/api'
|
||||
import { authenticationFromServerSide } from 'utils/authentication'
|
||||
|
||||
const ResetPassword: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
getErrorMessages,
|
||||
formState,
|
||||
message,
|
||||
handleChange,
|
||||
handleSubmit
|
||||
} = useForm({
|
||||
validatorSchema: passwordSchema
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title={`Thream | ${t('authentication:reset-password')}`} />
|
||||
|
||||
<Header />
|
||||
<Container>
|
||||
<AuthenticationFormLayout
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit(async (formData) => {
|
||||
await api.put('/users/resetPassword', {
|
||||
...formData,
|
||||
tempToken: router.query.tempToken
|
||||
})
|
||||
await router.push('/authentication/signin')
|
||||
return null
|
||||
})}
|
||||
>
|
||||
<Input
|
||||
errors={getErrorMessages('password')}
|
||||
type='password'
|
||||
placeholder='Password'
|
||||
name='password'
|
||||
label='Password'
|
||||
/>
|
||||
</AuthenticationFormLayout>
|
||||
<FormState state={formState} message={message} />
|
||||
</Container>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.signin-link {
|
||||
font-size: 16px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = authenticationFromServerSide({
|
||||
shouldBeAuthenticated: false
|
||||
})
|
||||
|
||||
export default ResetPassword
|
38
pages/authentication/signin.tsx
Normal file
38
pages/authentication/signin.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { Authentication as AuthenticationComponent } from 'components/Authentication'
|
||||
import { api } from 'utils/api'
|
||||
import {
|
||||
Authentication,
|
||||
authenticationFromServerSide,
|
||||
Tokens
|
||||
} from 'utils/authentication'
|
||||
|
||||
const Signin: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title={`Thream | ${t('authentication:signin')}`} />
|
||||
<AuthenticationComponent
|
||||
mode='signin'
|
||||
onSubmit={async (formData) => {
|
||||
const { data } = await api.post<Tokens>('/users/signin', formData)
|
||||
const authentication = new Authentication(data)
|
||||
authentication.signin()
|
||||
await router.push('/application')
|
||||
return null
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = authenticationFromServerSide({
|
||||
shouldBeAuthenticated: false
|
||||
})
|
||||
|
||||
export default Signin
|
34
pages/authentication/signup.tsx
Normal file
34
pages/authentication/signup.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { Authentication } from 'components/Authentication'
|
||||
import { api } from 'utils/api'
|
||||
import { useTheme } from 'contexts/Theme'
|
||||
import { authenticationFromServerSide } from 'utils/authentication'
|
||||
|
||||
const Signup: React.FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const { t, lang } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title={`Thream | ${t('authentication:signup')}`} />
|
||||
<Authentication
|
||||
mode='signup'
|
||||
onSubmit={async (formData) => {
|
||||
await api.post(
|
||||
`/users/signup?redirectURI=${window.location.origin}/authentication/signin`,
|
||||
{ ...formData, language: lang, theme }
|
||||
)
|
||||
return await t('authentication:success-signup')
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps = authenticationFromServerSide({
|
||||
shouldBeAuthenticated: false
|
||||
})
|
||||
|
||||
export default Signup
|
138
pages/index.tsx
Normal file
138
pages/index.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import Translation from 'next-translate/Trans'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
import { Container } from 'components/design/Container'
|
||||
import { Button } from 'components/design/Button'
|
||||
import { SocialMediaButton } from 'components/design/SocialMediaButton'
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head />
|
||||
|
||||
<Header />
|
||||
<Container className='home-container'>
|
||||
<section id='about'>
|
||||
<div className='application__image'>
|
||||
<Link href='/authentication/signup'>
|
||||
<a>
|
||||
<Image
|
||||
width={351}
|
||||
height={341}
|
||||
src='/images/svg/design/home.svg'
|
||||
alt={"Thream's chat app"}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className='title'>Thream</h1>
|
||||
<div className='paragraph'>
|
||||
<Translation
|
||||
i18nKey='home:description'
|
||||
components={[<strong key='bold' />]}
|
||||
/>
|
||||
<div className='buttons'>
|
||||
<Link href='/authentication/signup'>
|
||||
<a className='get-started-link'>
|
||||
<Button>{t('home:get-started')}</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<a
|
||||
href='https://github.com/Thream'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='github-link'
|
||||
>
|
||||
<SocialMediaButton socialMedia='GitHub' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Container>
|
||||
|
||||
<style jsx global>
|
||||
{`
|
||||
@media (max-width: 338px) {
|
||||
.get-started-link {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.get-started-link > button {
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
}
|
||||
.home-container {
|
||||
align-items: center;
|
||||
margin-top: 50px;
|
||||
text-align: justify;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<style jsx>
|
||||
{`
|
||||
#about {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 740px;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
}
|
||||
.application__image {
|
||||
margin-right: 25px;
|
||||
max-width: 300px;
|
||||
}
|
||||
.title {
|
||||
font-weight: 400;
|
||||
}
|
||||
.paragraph {
|
||||
font-family: 'Roboto', 'sans-serif';
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 32px;
|
||||
max-width: 500px;
|
||||
}
|
||||
strong {
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.buttons {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.get-started-link {
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
.github-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 600px) and (max-height: 700px) {
|
||||
.application__image:first-child {
|
||||
width: 65%;
|
||||
}
|
||||
.paragraph {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
return { props: {} }
|
||||
}
|
||||
|
||||
export default Home
|
Reference in New Issue
Block a user