added contact support
This commit is contained in:
parent
61a3aa4ece
commit
53a97ce121
8575
package-lock.json
generated
8575
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -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
3
src/action/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
//@flow
|
||||
|
||||
export { send_message } from "./message"
|
19
src/action/message.js
Normal file
19
src/action/message.js
Normal 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)
|
||||
})
|
||||
})
|
26
src/app.js
26
src/app.js
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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
9
src/elements/contact.js
Normal file
@ -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;
|
||||
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
13
src/local-instance.js
Normal 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
8
src/reducer/index.js
Normal 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
32
src/reducer/message.js
Normal 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
17
src/store.js
Normal 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)
|
@ -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: [
|
||||
|
Loading…
Reference in New Issue
Block a user