Style and i18nize signup

This commit is contained in:
Amit Jakubowicz 2019-10-13 19:01:52 +02:00
parent 370f2c608b
commit 3defe9a5b7
10 changed files with 294 additions and 147 deletions

3
@types/index.d.ts vendored
View file

@ -1 +1,4 @@
export type EventStatus = "confirmed" | "canceled"
declare module "*.png" {
}

View file

@ -5,6 +5,10 @@ import {ITextFieldProps, TextField as OUITextField} from 'office-ui-fabric-react
export interface TextFieldProps extends ITextFieldProps {}
const TextField = (props: TextFieldProps) => {
return <OUITextField {...props} />
}
export default styled(TextField)``
export default styled(TextField)`
border-radius: 8px;
border-color: #5E8036;
`

View file

@ -1,66 +1,76 @@
import {css, Global} from "@emotion/core"
import { css, Global } from "@emotion/core"
import styled from "@emotion/styled"
import {MessageCenterDisplay} from "qpa-message-center"
import { MessageCenterDisplay } from "qpa-message-center"
import * as React from "react"
import Footer from "./Footer"
import Header from "./Header/Header"
import Routes from "./Routes"
import * as intl from 'react-intl-universal'
import {Helmet} from 'react-helmet'
import AppMessages from './App.msg.json'
import * as intl from "react-intl-universal"
import { Helmet } from "react-helmet"
import AppMessages from "./App.msg.json"
const App = () => {
intl.init({
currentLocale: 'es-ES',
locales: {
'en-GB': AppMessages.en,
'es-ES': AppMessages.es
}
})
return (
<Root>
<Helmet>
<title>{ intl.get('APP_TITLE')}</title>
</Helmet>
<Global
styles={css`
body {
margin: 0;
height: 100vh;
}
#app {
height: 100%;
}
`}
/>
<StyledHeader/>
<Content>
<Routes />
</Content>
<MessageCenterDisplay/>
<StyledFooter/>
</Root>
)
intl.init({
currentLocale: "es-ES",
locales: {
"en-GB": AppMessages.en,
"es-ES": AppMessages.es,
},
})
return (
<Root>
<Helmet>
<title>{intl.get("APP_TITLE")}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Helmet>
<Global
styles={css`
body {
margin: 0;
height: 100vh;
--sansserif: "Segoe UI Web (East European)", Segoe UI, -apple-system, BlinkMacSystemFont, Roboto, Helvetica Neue, sans-serif;
font-family: var(--sansserif);
}
#app {
height: 100%;
}
`}
/>
<StyledHeader />
<Content>
<Routes />
</Content>
<MessageCenterDisplay />
<StyledFooter />
</Root>
)
}
const Root = styled.div`
display: grid;
height: 100%;
grid-template-columns: 1fr;
grid-template-rows: [header] 48px [body] 1fr [footer] 32px;
grid-template-columns:
[left-start full-start] minmax(0, 240px)
[left-end content-start] minmax(320px, 1200px)
[content-end right-start] minmax(0, 240px)
[right-end full-end];
grid-template-rows:
[header-start] 48px
[header-end center-start] 1fr
[center-end footer-start] 32px
[footer-end];
`
const Content = styled.div`
grid-row: body;
display: flex;
flex-direction: row;
justify-self: center;
grid-row: center;
grid-column: content;
`
const StyledFooter = styled(Footer)`
grid-row: footer;
grid-column: full;
`
const StyledHeader = styled(Header)`
grid-column: full;
grid-row: header;
`
export default App

View file

@ -0,0 +1,32 @@
{
"es-ES": {
"form-error-no-name": "Por favor, introduze un nombre",
"form-error-name-too-short": "El Nombre tiene que ser más largo",
"form-error-no-email": "Por favor, introduze un email",
"form-error-legal-email": "Por favor, introduze una dirección valida de email",
"signup-error": "Error en crear una cuenta, por favor intenta más tarde otra vez",
"signup-success": "Cuenta creada correctamente, mira en tu email",
"email-taken": "Esta email ya tiene una cuenta. Quieres iniciar una sesión?",
"go-to-calendar": "Ir al calendario",
"your-email": "Tu email",
"your-name": "Tu nombre",
"sign-up": "Registrarse",
"signup-form-title": "Para poder entrar to propios eventos, tienes que registrarte. Solo tendrías que darnos un nombre y una dirección email, dónde reciber correos electronico. Una vez tengamos esta información, de mandamos una invitación a tú email",
"already-have-account-login": "Ya tengo una cuenta. ¡Inicia Sesión!"
},
"en-GB": {
"form-error-no-name": "Please type in a name",
"form-error-name-too-short": "Name has to be longer",
"form-error-no-email": "Please type in an email",
"form-error-legal-email": "Please enter a legal email",
"signup-error": "Error signin up. Please try later",
"signup-success": "Sign up succeeded. Please check your email",
"email-taken": "Email is taken. Maybe try to log in?",
"go-to-calendar": "Go to calendar",
"your-email": "Your email",
"your-name": "Your name",
"sign-up": "Sign Up",
"signup-form-title": "In order to insert your own event, please sign up. You only have to give us a name, and an email where we can reach you. Once these are set, we will send an invitation to your email.",
"already-have-account-login": "I already have an account, go to login!"
}
}

View file

@ -1,9 +1,13 @@
import styled from "@emotion/styled"
import {Field, Form, Formik} from "formik"
import {Button, Label, Spinner, TextField} from "qpa-components"
import {useMessageCenter} from "qpa-message-center"
import { Field, Form, Formik } from "formik"
import { Button, Label, Spinner, TextField } from "qpa-components"
import { useMessageCenter } from "qpa-message-center"
import * as React from "react"
import {Link} from "react-router-dom"
import { hot } from "react-hot-loader"
import { Link } from "react-router-dom"
import Logo from "../LOGO.png"
import intl from "react-intl-universal"
import messages from "./Signup.msg.json"
interface SignupFormData {
email: string
@ -11,94 +15,165 @@ interface SignupFormData {
}
class SignupFormik extends Formik<SignupFormData> {}
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const Signup = () => {
intl.load(messages)
const { addMessage } = useMessageCenter()
const [loading, setLoading] = React.useState(false)
const [success, setSuccess] = React.useState(null)
const [error, setError] = React.useState(null)
const [emailTaken, setEmailTaken] = React.useState(false)
if (success) {
return <Label>Sign up was successful. Please check your email or <Link to="/">go to calendar</Link></Label>
}
return (
<SignupFormik
initialValues={{name: "", email: ""}}
onSubmit={(values, {setFieldError}) => {
fetch("/api/signup", {
method: "post",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
name: values.name,
email: values.email,
}),
}).then((res) => {
if (res.status === 200) {
setSuccess(true)
addMessage({
type: "success",
text: "Sign up succeeded. Please check your email",
<Root>
<LogoHolder>
<img src={Logo} />
</LogoHolder>
{success ? (
<Label>
{intl.get("signup-success")}{" "}
<Link to="/">{intl.get("go-to-calendar")}</Link>
</Label>
) : (
<SignupFormik
initialValues={{ name: "", email: "" }}
validate={(values: SignupFormData) => {
const errors: any = {}
if (!values.name) {
errors.name = intl.get("form-error-no-name")
}
if (values.name.length < 4) {
errors.name = intl.get("form-error-name-too-short")
}
if (!values.email) {
errors.email = intl.get("form-error-no-email")
}
if (!emailRegex.test(values.email)) {
errors.email = intl.get("form-error-legal-email")
}
return errors
}}
onSubmit={(values, { setFieldError }) => {
fetch("/api/signup", {
method: "post",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
name: values.name,
email: values.email,
}),
})
return
} else {
addMessage({
type: "error",
text: "Error signin up. Please try later",
})
}
.then(res => {
if (res.status === 200) {
setSuccess(true)
addMessage({
type: "success",
text: intl.get("signup-success"),
})
return
} else {
addMessage({
type: "error",
text: intl.get("signup-error"),
})
}
if (res.status === 409) {
setFieldError("email", "Email is taken. Maybe try to log in?")
}
}).catch((e) => {
addMessage({
type: "error",
text: `Error signin up. Please try later. ${e.message}`,
})
})
}}
>
{
({values, isValid}) => (
<SForm>
<p css={{gridArea: "1/1/1/4"}}>
In order to insert your own event, please sign up.
You only have to give us a name, and and email where we can reach you.
Once these are set, we will send an invitation to your email.
</p>
<Field name="name">
{
({field}) => <STextField placeholder="Your name" {...field} css={{gridArea: "2/2/2/3"}}/>
}
</Field>
<Field name="email" css={{gridRow: 2}}>
{
({field}) => <STextField placeholder="Your email" {...field} css={{gridArea: "3/2/3/4"}}/>
}
</Field>
<Button type="submit" disabled={!isValid || loading} css={{gridArea: "4/2/5/3", marginTop: 12}}>{
loading ? <Spinner /> : "Sign up"
}</Button>
</SForm>
)
}
</SignupFormik>
if (res.status === 409) {
setFieldError("email", intl.get("email-taken"))
}
})
.catch(e => {
addMessage({
type: "error",
text: `${intl.get('signup-error')} ${e.message}`,
})
})
}}
>
{({ values, isValid, errors, touched }) => (
<SForm>
<Title>
{
intl.get('signup-form-title')
}
</Title>
<Fields>
<Field name="name">
{({ field }) => (
<TextField errorMessage={touched.name && errors.name} placeholder={intl.get('your-name')} {...field} />
)}
</Field>
<Field name="email" css={{ gridRow: 2 }}>
{({ field }) => (
<TextField errorMessage={touched.email && errors.email} placeholder={intl.get("your-email")} {...field} />
)}
</Field>
</Fields>
<SButton type="submit" disabled={!isValid || loading}>
{loading ? <Spinner /> : intl.get("sign-up")}
</SButton>
<GoToLogin to="/login">
{
intl.get('already-have-account-login')
}
</GoToLogin>
</SForm>
)}
</SignupFormik>
)}
</Root>
)
}
const Root = styled.div`
display: flex;
flex-direction: column;
`
const LogoHolder = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
margin: 20px;
`
const SForm = styled(Form)`
height: 100%;
display: grid;
grid-template-columns: 1fr 240px 1fr;
max-width: 640px;
grid-template-columns:
[full-start] auto
[center-start] 200px
[center-end] auto
[full-end];
grid-template-rows:
[title-start] 120px
[title-end fields-start] 120px
[fields-end button-start] 32px
[button-end bottom-start] 48px
[bottom-end]
;
`
const Title = styled.div`
grid-column: full;
grid-row: title;
`
const Fields = styled.div`
grid-column: full;
grid-row: fields;
> *:not(:first-child) {
margin-top: 24px;
}
`
const SButton = styled(Button)`
grid-row: button;
grid-column: center;
`
const STextField = styled(TextField)`
width: 240px;
const GoToLogin = styled(Link)`
grid-row: bottom;
grid-column: center;
font-size: 12px;
margin-top: 4px;
`
export default Signup
export default hot(module)(Signup)

View file

@ -11,7 +11,7 @@ const Footer = (props: Props) => (
)
const Root = styled.div`
background: rgba(0, 0, 0, 0.1);
background: #FFAD00;
`
export default Footer

View file

@ -43,7 +43,7 @@ const Title = styled.div`
flex: 1;
`
const Root = styled.div`
background: rgba(0, 0, 0, 0.1);
background: #5E8036;
display: flex;
flex-direction: row;
padding-right: 14px;

BIN
packages/qpa/App/LOGO.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -2,26 +2,39 @@ import styled from "@emotion/styled"
import addMonths from "date-fns/add_months"
import endOfMonth from "date-fns/end_of_month"
import startOfMonth from "date-fns/start_of_month"
import {PrimaryButton} from "qpa-components"
import { PrimaryButton } from "qpa-components"
import * as React from "react"
import {hot} from "react-hot-loader"
import {RouteComponentProps, withRouter} from "react-router"
import { hot } from "react-hot-loader"
import { RouteComponentProps, withRouter } from "react-router"
import RangedCalendar from "./RangedCalendar"
interface Props extends RouteComponentProps<{month?: string}> {
interface Props extends RouteComponentProps<{ month?: string }> {
className?: string
}
const now = new Date()
const MONTH_NAMES = ["january", "february", "march", "april", "may", "june",
"july", "august", "september", "october", "november", "december"]
const MONTH_NAMES = [
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
]
const Calendar = (props: Props) => {
const month = props.match.params.month
const currentDateOfMonth = (month && MONTH_NAMES.includes(month)) ? (
now.setMonth(MONTH_NAMES.indexOf(month))
) : now
const currentDateOfMonth =
month && MONTH_NAMES.includes(month)
? now.setMonth(MONTH_NAMES.indexOf(month))
: now
const from = startOfMonth(currentDateOfMonth)
const to = endOfMonth(currentDateOfMonth)
@ -34,19 +47,29 @@ const Calendar = (props: Props) => {
return (
<Root>
<Controls>
<PrimaryButton title="Previous"
onClick={() => {
props.history.push(`/${monthBeforeName}`)
}}>{"<"}{monthBeforeName}</PrimaryButton>
<ThisMonth>{ MONTH_NAMES[from.getMonth()] }</ThisMonth>
<PrimaryButton title="Next" onClick={() => {
props.history.push(`/${monthAfterName}`)
}}>{monthAfterName}{">"}</PrimaryButton>
<PrimaryButton
title="Previous"
onClick={() => {
props.history.push(`/${monthBeforeName}`)
}}
>
{"<"}
{monthBeforeName}
</PrimaryButton>
<ThisMonth>{MONTH_NAMES[from.getMonth()]}</ThisMonth>
<PrimaryButton
title="Next"
onClick={() => {
props.history.push(`/${monthAfterName}`)
}}
>
{monthAfterName}
{">"}
</PrimaryButton>
</Controls>
<RangedCalendar from={from} to={to} className={props.className}/>
<RangedCalendar from={from} to={to} className={props.className} />
</Root>
)
}
const Controls = styled.div`
@ -56,16 +79,16 @@ const Controls = styled.div`
const ThisMonth = styled.div`
font-weight: 600;
color: rgba(0,0,0,.6);
font-size: 24px;
color: rgba(0, 0, 0, 0.6);
font-size: 24px;
`
const Root = styled.div`
min-width: 480px;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
${Controls} {
margin-bottom: 24px;
}

View file

@ -49,7 +49,7 @@ const config: webpack.Configuration = {
},
},
{
test: /\.(woff|woff2)$/i,
test: /\.(woff|woff2|png)$/i,
use: [
{
loader: "url-loader",