added contact support

This commit is contained in:
Arwed Mett 2019-02-20 15:33:44 +01:00
parent 61a3aa4ece
commit 53a97ce121
Signed by: Pfeifenjoy
GPG Key ID: 86943827297DA9FC
14 changed files with 281 additions and 8667 deletions

8575
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,9 @@
},
"scripts": {
"test": "npm run flow && npm run mocha",
"start": "webpack-dev-server --env development",
"dev-server": "webpack-dev-server --env development",
"start": "nodemon --exec npm run babel-node",
"babel-node": "npx babel-node src/local-instance.js --source-maps",
"build": "webpack --env production && flow-copy-source src build",
"mocha": "mocha --require @babel/register --require babel-polyfill --slow 30 --throw-deprecation --use_strict",
"clean": "rm -rf build",
@ -44,14 +46,16 @@
"flow-copy-source": "^2.0.2",
"flow-typed": "^2.4.0",
"mocha": "^5.1.0",
"nodemon": "^1.18.9",
"raw-loader": "^0.5.1",
"url-loader": "^1.1.2",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.9",
"webpack-dev-server": "^3.1.14",
"webpack-notifier": "^1.7.0"
},
"dependencies": {
"@babel/node": "^7.2.2",
"@fortawesome/fontawesome-svg-core": "^1.2.6",
"@fortawesome/free-brands-svg-icons": "^5.4.1",
"@fortawesome/free-solid-svg-icons": "^5.4.1",
@ -63,8 +67,13 @@
"react-autosize-textarea": "^5.0.0",
"react-dom": "^16.5.2",
"react-intersection-observer": "^6.2.3",
"react-redux": "^6.0.1",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"redux": "^4.0.1",
"redux-logger": "^3.0.6",
"redux-promise-middleware": "^6.1.0",
"redux-thunk": "^2.3.0",
"styled-components": "^3.4.10"
}
}

3
src/action/index.js Normal file
View File

@ -0,0 +1,3 @@
//@flow
export { send_message } from "./message"

19
src/action/message.js Normal file
View File

@ -0,0 +1,19 @@
//@flow
export type message_t = {
email: string;
name: string;
text: string;
};
export const send_message = (payload: message_t) => ({
type: "SEND_MESSAGE",
payload: fetch("api/message/create", {
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
})
})

View File

@ -1,10 +1,12 @@
//@flow
import React from "react"
import { Header, Projects, Contact } from "./components"
import { Body, Section, content } from "./elements"
import { Body, content } from "./elements"
import NoscriptWarning from "./noscript"
import { ThemeProvider } from "styled-components"
import { Dark } from "./themes"
import { Provider } from "react-redux"
import store from "./store"
const projects = {
title: "My Projects",
@ -15,13 +17,15 @@ const contact = {
body: () => <Contact />
}
export default () => <ThemeProvider theme={ Dark }>
<Body>
<NoscriptWarning />
<Header />
{ content([
projects,
contact
]) }
</Body>
</ThemeProvider>
export default () => <Provider store={ store }>
<ThemeProvider theme={ Dark }>
<Body>
<NoscriptWarning />
<Header />
{ content([
projects,
contact
]) }
</Body>
</ThemeProvider>
</Provider>

View File

@ -1,14 +1,18 @@
//@flow
import React, { Fragment, Component } from "react"
import { Section, Button } from "../elements"
import React, { Component } from "react"
import { Button } from "../elements"
import styled from "styled-components"
import textarea from "react-autosize-textarea"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"
import { connect } from "react-redux"
import { send_message } from "../action"
/************************************************************/
/* Styling */
/************************************************************/
const BaseInput = () => `
const BaseInput = ({ validation }: { validation: boolean }) => `
border: none;
border-bottom: #888 solid;
padding: 5px;
@ -20,6 +24,9 @@ const BaseInput = () => `
:focus {
border-bottom: white solid;
}
:invalid {
${ validation ? "border-bottom: red solid;" : "" }
}
`
const Input = styled.input`
${ BaseInput }
@ -35,7 +42,7 @@ const Email = styled(Input)`
const Message = styled(textarea)`
${ BaseInput }
grid-area: message;
grid-area: text;
grid-column-end: span 2;
resize: vertical;
box-sizing: border-box;
@ -59,14 +66,13 @@ const Send = styled(Button)`
const Form = styled.form`
display: grid;
width: 100%;
max-width: 400px;
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr auto auto;
grid-template-areas:
"name"
"email"
"message"
"text"
"send";
grid-gap: 10px;
@ -76,29 +82,85 @@ const Form = styled.form`
grid-template-rows: 1fr auto auto;
grid-template-areas:
"name email"
"message message"
"text text"
"send send";
}
margin-top: 10px;
margin-bottom: 10px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
`
/************************************************************/
/* Successfully send */
/************************************************************/
const SendIcon = styled(FontAwesomeIcon)`
color: white;
min-width: 50px;
min-height: 50px;
margin: 10px;
`
const SendText = styled.div`
color: white;
font-size: 10pt;
`
const SendContainer = styled.div`
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
distplay: flex;
height: 250px;
`
const Success = () => <SendContainer>
<SendIcon icon={ faCheck } />
<SendText>Success</SendText>
</SendContainer>
/************************************************************/
/* Failure send */
/************************************************************/
const Fail = () => <SendContainer>
<SendIcon icon={ faTimes } />
<SendText>Unknown Failure</SendText>
</SendContainer>
/************************************************************/
/* Contact Form Component */
/************************************************************/
type ContactProps = { }
type State = "IDLE" | "PENDING" | "FULFILLED" | "REJECTED"
type ContactProps = { state: State, dispatch: Function }
type ContactState = {
name: string,
email: string,
message: string,
busy: boolean
text: string,
validation: boolean
}
export default class Contact extends Component<ContactProps, ContactState> {
/************************************************************/
/* Container */
/************************************************************/
const Container = styled.div`
min-height: 250px;
width: 100%;
padding: 0;
margin: 0;
`
export class Contact extends Component<ContactProps, ContactState> {
state = {
name: "",
email: "",
message: "",
busy: false
text: "",
validation: false
}
handleInput(name: string) {
@ -110,76 +172,82 @@ export default class Contact extends Component<ContactProps, ContactState> {
}
}
checkText(text: string) {
return text.length > 0
activateValidation() {
this.setState({ validation: true })
}
checkEmail(email: string) {
return /^(([^<>()\[\]\\.,;:\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,}))$/.test(email)
}
handleSubmit(event: SyntheticInputEvent<HTMLButtonElement>) {
async handleSubmit(event: SyntheticInputEvent<HTMLButtonElement>) {
event.preventDefault()
//get data
const { name, email, message } = this.state
this.setState({ busy: true })
//validate data
if(!this.checkText(name)) {
//TODO
const { name, email, text } = this.state
await this.props.dispatch(send_message({ name, email, text }))
if(this.props.state === "FULFILLED") {
this.setState({ name: "", email: "", text: "" })
}
if(!this.checkEmail(email)) {
//TODO
}
if(!this.checkText(message)) {
//TODO
}
//TODO send data
//TODO handle response
// - set new state (clear)
// - unset busy
this.setState({
name: "",
email: "",
message: "",
busy: false
})
}
render() {
const { name, email, message, busy } = this.state
return <Form
action="/contact"
method="post"
onSubmit={ this.handleSubmit.bind(this) }
>
<Name
placeholder="Your Name"
name="name"
type="text"
value={ name }
onChange={ this.handleInput("name").bind(this) }
/>
<Email
placeholder="Your Email"
name="email"
type="email"
value={ email }
onChange={ this.handleInput("email").bind(this) }
/>
<Message
rows={ 10 }
placeholder="Your Message"
name="message"
type="text"
value={ message }
onChange={ this.handleInput("message").bind(this) }
/>
<Send busy={ busy } type="submit">Send</Send>
</Form>
const { name, email, text, validation } = this.state
const { state } = this.props
let body
switch(state) {
case "PENDING":
case "IDLE":
body = <Form
action="/contact"
method="post"
onSubmit={ this.handleSubmit.bind(this) }
>
<Name
placeholder="Your Name"
name="name"
type="text"
required={ true }
value={ name }
onChange={ this.handleInput("name").bind(this) }
validation={ validation ? 1 : 0 }
disabled={ state === "PENDING" }
/>
<Email
placeholder="Your Email"
name="email"
type="email"
required={ true }
value={ email }
onChange={ this.handleInput("email").bind(this) }
validation={ validation ? 1 : 0 }
disabled={ state === "PENDING" }
/>
<Message
rows={ 10 }
placeholder="Your Message"
name="text"
type="text"
required={ true }
value={ text }
onChange={ this.handleInput("text").bind(this) }
validation={ validation ? 1 : 0 }
disabled={ state === "PENDING" }
/>
<Send
busy={ state === "PENDING" }
type="submit"
onClick={ this.activateValidation.bind(this) }
>
Send
</Send>
</Form>
break
case "FULFILLED":
body = <Success />
break
default:
body = <Fail />
}
return <Container>{ body }</Container>
}
}
export default connect(state => ({ state: state.message.state }))(Contact)

View File

@ -13,4 +13,5 @@ export default styled.div.attrs({
flex-direction: column;
align-items: center;
font-family: monospace;
color: white;
`

9
src/elements/contact.js Normal file
View File

@ -0,0 +1,9 @@
//@flow
import styled from "styled-components"
export const Container = styled.div`
width: 100%;
display: flex;
justify-content: center;
`

View File

@ -160,7 +160,7 @@ export const ProjectContainer = styled.div`
display: flex;
width: 100%;
flex-direction: row;
justify-content: flex-start;
justify-content: center;
flex-wrap: wrap;
align-items: stretch;
`

13
src/local-instance.js Normal file
View File

@ -0,0 +1,13 @@
//@flow
import express from "express"
import router from "./router"
async function launch() {
const app = express()
app.use(router)
app.listen(5000)
}
launch()

8
src/reducer/index.js Normal file
View File

@ -0,0 +1,8 @@
//@flow
import { combineReducers } from "redux"
import message from "./message"
export default combineReducers({
message
})

32
src/reducer/message.js Normal file
View File

@ -0,0 +1,32 @@
//@flow
type state_t = {
state: "IDLE" | "PENDING" | "FULFILLED" | "FAIL"
}
type action_t = {
type: "SEND_MESSAGE_PENDING"
} | {
type: "SEND_MESSAGE_FULFILLED",
payload: Response
} | {
type: "SEND_MESSAGE_REJECTED",
payload: Error
}
export default (_: state_t, action: action_t) => {
switch(action.type) {
case "SEND_MESSAGE_PENDING":
return { state: "PENDING" }
case "SEND_MESSAGE_FULFILLED":
if(action.payload.ok) {
return { state: "FULFILLED" }
} else {
return { state: "REJECTED" }
}
case "SEND_MESSAGE_REJECTED":
return { state: "REJECTED" }
default:
return { state: "IDLE" }
}
}

17
src/store.js Normal file
View File

@ -0,0 +1,17 @@
//@flow
import { createStore, applyMiddleware } from "redux"
import reducers from "./reducer"
import thunk from "redux-thunk"
import logger from "redux-logger"
import promise from "redux-promise-middleware"
const middleware = (() => {
if(process.env.NODE_ENV !== "production") {
return applyMiddleware(promise, thunk, logger)
} else {
return applyMiddleware(promise, thunk)
}
})()
export default createStore(reducers, middleware)

View File

@ -102,13 +102,19 @@ const development = config => Object.assign({ }, config(), {
devServer: {
historyApiFallback: true,
stats: "errors-only",
host: process.env.HOST,
port: 5000,
compress: true,
overlay: true,
hot: true,
contentBase: path.resolve(__dirname, "build/static")
contentBase: path.resolve(__dirname, "build/static"),
https: true,
proxy: {
"/api": {
"target": "https://localhost:6000",
secure: false
}
}
},
devtool: "source-map",
plugins: [