added contact support
This commit is contained in:
parent
61a3aa4ece
commit
53a97ce121
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
@ -14,7 +14,9 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run flow && npm run mocha",
|
"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",
|
"build": "webpack --env production && flow-copy-source src build",
|
||||||
"mocha": "mocha --require @babel/register --require babel-polyfill --slow 30 --throw-deprecation --use_strict",
|
"mocha": "mocha --require @babel/register --require babel-polyfill --slow 30 --throw-deprecation --use_strict",
|
||||||
"clean": "rm -rf build",
|
"clean": "rm -rf build",
|
||||||
|
@ -44,14 +46,16 @@
|
||||||
"flow-copy-source": "^2.0.2",
|
"flow-copy-source": "^2.0.2",
|
||||||
"flow-typed": "^2.4.0",
|
"flow-typed": "^2.4.0",
|
||||||
"mocha": "^5.1.0",
|
"mocha": "^5.1.0",
|
||||||
|
"nodemon": "^1.18.9",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"url-loader": "^1.1.2",
|
"url-loader": "^1.1.2",
|
||||||
"webpack": "^4.20.2",
|
"webpack": "^4.20.2",
|
||||||
"webpack-cli": "^3.1.2",
|
"webpack-cli": "^3.1.2",
|
||||||
"webpack-dev-server": "^3.1.9",
|
"webpack-dev-server": "^3.1.14",
|
||||||
"webpack-notifier": "^1.7.0"
|
"webpack-notifier": "^1.7.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/node": "^7.2.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.6",
|
"@fortawesome/fontawesome-svg-core": "^1.2.6",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.4.1",
|
"@fortawesome/free-brands-svg-icons": "^5.4.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.4.1",
|
"@fortawesome/free-solid-svg-icons": "^5.4.1",
|
||||||
|
@ -63,8 +67,13 @@
|
||||||
"react-autosize-textarea": "^5.0.0",
|
"react-autosize-textarea": "^5.0.0",
|
||||||
"react-dom": "^16.5.2",
|
"react-dom": "^16.5.2",
|
||||||
"react-intersection-observer": "^6.2.3",
|
"react-intersection-observer": "^6.2.3",
|
||||||
|
"react-redux": "^6.0.1",
|
||||||
"react-router": "^4.3.1",
|
"react-router": "^4.3.1",
|
||||||
"react-router-dom": "^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"
|
"styled-components": "^3.4.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
//@flow
|
||||||
|
|
||||||
|
export { send_message } from "./message"
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
26
src/app.js
26
src/app.js
|
@ -1,10 +1,12 @@
|
||||||
//@flow
|
//@flow
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Header, Projects, Contact } from "./components"
|
import { Header, Projects, Contact } from "./components"
|
||||||
import { Body, Section, content } from "./elements"
|
import { Body, content } from "./elements"
|
||||||
import NoscriptWarning from "./noscript"
|
import NoscriptWarning from "./noscript"
|
||||||
import { ThemeProvider } from "styled-components"
|
import { ThemeProvider } from "styled-components"
|
||||||
import { Dark } from "./themes"
|
import { Dark } from "./themes"
|
||||||
|
import { Provider } from "react-redux"
|
||||||
|
import store from "./store"
|
||||||
|
|
||||||
const projects = {
|
const projects = {
|
||||||
title: "My Projects",
|
title: "My Projects",
|
||||||
|
@ -15,13 +17,15 @@ const contact = {
|
||||||
body: () => <Contact />
|
body: () => <Contact />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => <ThemeProvider theme={ Dark }>
|
export default () => <Provider store={ store }>
|
||||||
<Body>
|
<ThemeProvider theme={ Dark }>
|
||||||
<NoscriptWarning />
|
<Body>
|
||||||
<Header />
|
<NoscriptWarning />
|
||||||
{ content([
|
<Header />
|
||||||
projects,
|
{ content([
|
||||||
contact
|
projects,
|
||||||
]) }
|
contact
|
||||||
</Body>
|
]) }
|
||||||
</ThemeProvider>
|
</Body>
|
||||||
|
</ThemeProvider>
|
||||||
|
</Provider>
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
//@flow
|
//@flow
|
||||||
import React, { Fragment, Component } from "react"
|
import React, { Component } from "react"
|
||||||
import { Section, Button } from "../elements"
|
import { Button } from "../elements"
|
||||||
import styled from "styled-components"
|
import styled from "styled-components"
|
||||||
import textarea from "react-autosize-textarea"
|
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 */
|
/* Styling */
|
||||||
/************************************************************/
|
/************************************************************/
|
||||||
|
|
||||||
const BaseInput = () => `
|
const BaseInput = ({ validation }: { validation: boolean }) => `
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: #888 solid;
|
border-bottom: #888 solid;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
@ -20,6 +24,9 @@ const BaseInput = () => `
|
||||||
:focus {
|
:focus {
|
||||||
border-bottom: white solid;
|
border-bottom: white solid;
|
||||||
}
|
}
|
||||||
|
:invalid {
|
||||||
|
${ validation ? "border-bottom: red solid;" : "" }
|
||||||
|
}
|
||||||
`
|
`
|
||||||
const Input = styled.input`
|
const Input = styled.input`
|
||||||
${ BaseInput }
|
${ BaseInput }
|
||||||
|
@ -35,7 +42,7 @@ const Email = styled(Input)`
|
||||||
|
|
||||||
const Message = styled(textarea)`
|
const Message = styled(textarea)`
|
||||||
${ BaseInput }
|
${ BaseInput }
|
||||||
grid-area: message;
|
grid-area: text;
|
||||||
grid-column-end: span 2;
|
grid-column-end: span 2;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -59,14 +66,13 @@ const Send = styled(Button)`
|
||||||
const Form = styled.form`
|
const Form = styled.form`
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
|
||||||
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 1fr 1fr auto auto;
|
grid-template-rows: 1fr 1fr auto auto;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"name"
|
"name"
|
||||||
"email"
|
"email"
|
||||||
"message"
|
"text"
|
||||||
"send";
|
"send";
|
||||||
|
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
|
@ -76,29 +82,85 @@ const Form = styled.form`
|
||||||
grid-template-rows: 1fr auto auto;
|
grid-template-rows: 1fr auto auto;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"name email"
|
"name email"
|
||||||
"message message"
|
"text text"
|
||||||
"send send";
|
"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 */
|
/* Contact Form Component */
|
||||||
/************************************************************/
|
/************************************************************/
|
||||||
|
|
||||||
type ContactProps = { }
|
type State = "IDLE" | "PENDING" | "FULFILLED" | "REJECTED"
|
||||||
|
|
||||||
|
type ContactProps = { state: State, dispatch: Function }
|
||||||
|
|
||||||
type ContactState = {
|
type ContactState = {
|
||||||
name: string,
|
name: string,
|
||||||
email: string,
|
email: string,
|
||||||
message: string,
|
text: string,
|
||||||
busy: boolean
|
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 = {
|
state = {
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
message: "",
|
text: "",
|
||||||
busy: false
|
validation: false
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInput(name: string) {
|
handleInput(name: string) {
|
||||||
|
@ -110,76 +172,82 @@ export default class Contact extends Component<ContactProps, ContactState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkText(text: string) {
|
activateValidation() {
|
||||||
return text.length > 0
|
this.setState({ validation: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEmail(email: string) {
|
async handleSubmit(event: SyntheticInputEvent<HTMLButtonElement>) {
|
||||||
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>) {
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
//get data
|
|
||||||
const { name, email, message } = this.state
|
|
||||||
|
|
||||||
this.setState({ busy: true })
|
const { name, email, text } = this.state
|
||||||
|
await this.props.dispatch(send_message({ name, email, text }))
|
||||||
//validate data
|
if(this.props.state === "FULFILLED") {
|
||||||
if(!this.checkText(name)) {
|
this.setState({ name: "", email: "", text: "" })
|
||||||
//TODO
|
|
||||||
}
|
}
|
||||||
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() {
|
render() {
|
||||||
const { name, email, message, busy } = this.state
|
const { name, email, text, validation } = this.state
|
||||||
return <Form
|
const { state } = this.props
|
||||||
action="/contact"
|
|
||||||
method="post"
|
let body
|
||||||
onSubmit={ this.handleSubmit.bind(this) }
|
switch(state) {
|
||||||
>
|
case "PENDING":
|
||||||
<Name
|
case "IDLE":
|
||||||
placeholder="Your Name"
|
body = <Form
|
||||||
name="name"
|
action="/contact"
|
||||||
type="text"
|
method="post"
|
||||||
value={ name }
|
onSubmit={ this.handleSubmit.bind(this) }
|
||||||
onChange={ this.handleInput("name").bind(this) }
|
>
|
||||||
/>
|
<Name
|
||||||
<Email
|
placeholder="Your Name"
|
||||||
placeholder="Your Email"
|
name="name"
|
||||||
name="email"
|
type="text"
|
||||||
type="email"
|
required={ true }
|
||||||
value={ email }
|
value={ name }
|
||||||
onChange={ this.handleInput("email").bind(this) }
|
onChange={ this.handleInput("name").bind(this) }
|
||||||
/>
|
validation={ validation ? 1 : 0 }
|
||||||
<Message
|
disabled={ state === "PENDING" }
|
||||||
rows={ 10 }
|
/>
|
||||||
placeholder="Your Message"
|
<Email
|
||||||
name="message"
|
placeholder="Your Email"
|
||||||
type="text"
|
name="email"
|
||||||
value={ message }
|
type="email"
|
||||||
onChange={ this.handleInput("message").bind(this) }
|
required={ true }
|
||||||
/>
|
value={ email }
|
||||||
<Send busy={ busy } type="submit">Send</Send>
|
onChange={ this.handleInput("email").bind(this) }
|
||||||
</Form>
|
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)
|
||||||
|
|
|
@ -13,4 +13,5 @@ export default styled.div.attrs({
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
color: white;
|
||||||
`
|
`
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
//@flow
|
||||||
|
|
||||||
|
import styled from "styled-components"
|
||||||
|
|
||||||
|
export const Container = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
|
@ -160,7 +160,7 @@ export const ProjectContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
`
|
`
|
||||||
|
|
|
@ -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()
|
|
@ -0,0 +1,8 @@
|
||||||
|
//@flow
|
||||||
|
|
||||||
|
import { combineReducers } from "redux"
|
||||||
|
import message from "./message"
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
message
|
||||||
|
})
|
|
@ -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" }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
@ -102,13 +102,19 @@ const development = config => Object.assign({ }, config(), {
|
||||||
devServer: {
|
devServer: {
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
stats: "errors-only",
|
stats: "errors-only",
|
||||||
|
|
||||||
host: process.env.HOST,
|
host: process.env.HOST,
|
||||||
port: 5000,
|
port: 5000,
|
||||||
compress: true,
|
compress: true,
|
||||||
overlay: true,
|
overlay: true,
|
||||||
hot: 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",
|
devtool: "source-map",
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
Loading…
Reference in New Issue