- switched from styled-components to less - included all images in project and removed the github reference - nicer loading button -> use circle notch now
This commit is contained in:
parent
fdae79e6a5
commit
ab54675c82
3
.babelrc
3
.babelrc
@ -3,8 +3,5 @@
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-flow",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
]
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
[ignore]
|
||||
.*/build/.*
|
||||
.*/node_modules/styled-components/.*
|
||||
|
||||
[include]
|
||||
|
||||
@ -12,6 +13,7 @@ flow-typed
|
||||
module.use_strict=true
|
||||
suppress_type=$FlowIgnore
|
||||
module.name_mapper.extension='html' -> '<PROJECT_ROOT>/flow/stub/file-loader.js'
|
||||
module.name_mapper.extension='less' -> '<PROJECT_ROOT>/flow/stub/file-loader.js'
|
||||
module.name_mapper.extension='asc' -> '<PROJECT_ROOT>/flow/stub/file-loader.js'
|
||||
module.name_mapper.extension='ejs' -> '<PROJECT_ROOT>/flow/stub/raw-loader.js'
|
||||
|
||||
|
20
package.json
20
package.json
@ -15,12 +15,12 @@
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --env development",
|
||||
"test:flow": "flow --quiet",
|
||||
"test:lint": "eslint src",
|
||||
"test:lint": "eslint src test",
|
||||
"test:mocha": "mocha --use_strict --require @babel/register --require babel-polyfill",
|
||||
"test": "npm run test:lint && npm run test:flow && npm run test:mocha",
|
||||
"babel-node": "npx babel-node src/local-instance.js --source-maps",
|
||||
"build:webpack": "webpack --env production",
|
||||
"build:flow": "flow-copy-source src build",
|
||||
"build:webpack": "webpack --env production",
|
||||
"build:flow": "flow-copy-source src build",
|
||||
"build": "npm run build:webpack && npm run build:flow",
|
||||
"clean": "rm -rf build"
|
||||
},
|
||||
@ -29,19 +29,19 @@
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.2",
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/polyfill": "^7.0.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/register": "^7.0.0",
|
||||
"@babel/node": "^7.2.2",
|
||||
"babel-eslint": "^9.0.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
"eslint": "^5.6.0",
|
||||
"eslint-plugin-flowtype": "^2.46.1",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"eslint-plugin-flowtype": "^3.4.2",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"file-loader": "^2.0.0",
|
||||
"flow-bin": "^0.69.0",
|
||||
"flow-bin": "^0.93.0",
|
||||
"flow-copy-source": "^2.0.2",
|
||||
"flow-typed": "^2.4.0",
|
||||
"mocha": "^5.1.0",
|
||||
@ -52,14 +52,16 @@
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"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",
|
||||
"@fortawesome/react-fontawesome": "^0.1.3",
|
||||
"css-loader": "^2.1.0",
|
||||
"ejs": "^2.6.1",
|
||||
"express": "^4.16.4",
|
||||
"intersection-observer": "^0.5.0",
|
||||
"less": "^3.9.0",
|
||||
"less-loader": "^4.1.0",
|
||||
"react": "^16.5.2",
|
||||
"react-autosize-textarea": "^5.0.0",
|
||||
"react-dom": "^16.5.2",
|
||||
@ -71,6 +73,6 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-promise-middleware": "^6.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"styled-components": "^3.4.10"
|
||||
"style-loader": "^0.23.1"
|
||||
}
|
||||
}
|
||||
|
16
src/action/action.js
Normal file
16
src/action/action.js
Normal file
@ -0,0 +1,16 @@
|
||||
//@flow
|
||||
|
||||
export type async_action_type = "SEND_MESSAGE"
|
||||
export type static_action_type = "SEND_MESSAGE"
|
||||
|
||||
export type AsyncAction = {|
|
||||
type: async_action_type,
|
||||
payload: Promise<*>
|
||||
|}
|
||||
|
||||
export type StaticAction<T> = {|
|
||||
type: static_action_type,
|
||||
payload: T
|
||||
|}
|
||||
|
||||
export type Action<T> = AsyncAction | StaticAction<T>
|
@ -1,3 +1,4 @@
|
||||
//@flow
|
||||
|
||||
export { send_message } from "./message"
|
||||
export type * from "./action"
|
||||
|
@ -1,12 +1,14 @@
|
||||
//@flow
|
||||
|
||||
import type { AsyncAction } from "./action"
|
||||
|
||||
export type message_t = {
|
||||
email: string;
|
||||
name: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const send_message = (payload: message_t) => ({
|
||||
export const send_message = (payload: message_t): AsyncAction => ({
|
||||
type: "SEND_MESSAGE",
|
||||
payload: fetch("api/message/create", {
|
||||
method: "POST",
|
||||
|
36
src/app.js
36
src/app.js
@ -1,31 +1,27 @@
|
||||
//@flow
|
||||
import React from "react"
|
||||
import React, { Fragment } from "react"
|
||||
import { Header, Projects, Contact } from "./components"
|
||||
import { Body, content } from "./elements"
|
||||
import { 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",
|
||||
body: () => <Projects />
|
||||
body: () => <Projects />,
|
||||
hash: "projects"
|
||||
}
|
||||
const contact = {
|
||||
title: "Contact",
|
||||
body: () => <Contact />
|
||||
body: () => <Contact />,
|
||||
hash: "contact"
|
||||
}
|
||||
|
||||
export default () => <Provider store={ store }>
|
||||
<ThemeProvider theme={ Dark }>
|
||||
<Body>
|
||||
<NoscriptWarning />
|
||||
<Header />
|
||||
{ content([
|
||||
projects,
|
||||
contact
|
||||
]) }
|
||||
</Body>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
export default () => //<ThemeProvider theme={ Dark }>
|
||||
<Fragment>
|
||||
<NoscriptWarning />
|
||||
<Header />
|
||||
{ content([
|
||||
projects,
|
||||
contact
|
||||
]) }
|
||||
</Fragment>
|
||||
//</ThemeProvider>
|
||||
|
@ -1,121 +1,39 @@
|
||||
//@flow
|
||||
import React, { Component } from "react"
|
||||
import React, { Component, Fragment } 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"
|
||||
import {
|
||||
ContactInput as Input,
|
||||
ContactTextarea as Textarea
|
||||
} from "../elements"
|
||||
|
||||
/************************************************************/
|
||||
/* Styling */
|
||||
/************************************************************/
|
||||
|
||||
const BaseInput = ({ validation }: { validation: boolean }) => `
|
||||
border: none;
|
||||
border-bottom: #888 solid;
|
||||
padding: 5px;
|
||||
background-color: inherit;
|
||||
outline: none;
|
||||
transition: 0.3s ease;
|
||||
color: white;
|
||||
const Name = (props: *) => <Input className="name" { ...props } />
|
||||
|
||||
:focus {
|
||||
border-bottom: white solid;
|
||||
}
|
||||
:invalid {
|
||||
${ validation ? "border-bottom: red solid;" : "" }
|
||||
}
|
||||
`
|
||||
const Input = styled.input`
|
||||
${ BaseInput }
|
||||
`
|
||||
const Email = (props: *) => <Input className="email" { ...props } />
|
||||
|
||||
const Name = styled(Input)`
|
||||
grid-area: name;
|
||||
`
|
||||
const Message = (props: *) => <Textarea className="message" { ...props } />
|
||||
|
||||
const Email = styled(Input)`
|
||||
grid-area: email;
|
||||
`
|
||||
const Submit = (props: *) => <Button className="submit" { ...props } />
|
||||
|
||||
const Message = styled(textarea)`
|
||||
${ BaseInput }
|
||||
grid-area: text;
|
||||
grid-column-end: span 2;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
`
|
||||
const Form = (props: *) => <form { ...props } />
|
||||
|
||||
const Send = styled(Button)`
|
||||
grid-area: send;
|
||||
justify-self: center;
|
||||
background-color: #666;
|
||||
border: dotted 1px white;
|
||||
color: white;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 10pt;
|
||||
`
|
||||
|
||||
const Form = styled.form`
|
||||
display: grid;
|
||||
width: 100%;
|
||||
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr auto auto;
|
||||
grid-template-areas:
|
||||
"name"
|
||||
"email"
|
||||
"text"
|
||||
"send";
|
||||
|
||||
grid-gap: 10px;
|
||||
|
||||
@media screen and (min-width: 500px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr auto auto;
|
||||
grid-template-areas:
|
||||
"name email"
|
||||
"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 SendIcon = (props: *) => <FontAwesomeIcon className="icon" { ...props } />
|
||||
|
||||
const SendText = (props: *) => <div className="text" { ...props } />
|
||||
|
||||
const SendContainer = (props: *) => <div className="send" { ...props } />
|
||||
|
||||
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>
|
||||
@ -144,23 +62,17 @@ type ContactState = {
|
||||
validation: boolean
|
||||
}
|
||||
|
||||
/************************************************************/
|
||||
/* Container */
|
||||
/************************************************************/
|
||||
|
||||
const Container = styled.div`
|
||||
min-height: 250px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
export class Contact extends Component<ContactProps, ContactState> {
|
||||
state = {
|
||||
name: "",
|
||||
email: "",
|
||||
text: "",
|
||||
validation: false
|
||||
|
||||
constructor(props: ContactProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
name: "",
|
||||
email: "",
|
||||
text: "",
|
||||
validation: false
|
||||
}
|
||||
}
|
||||
|
||||
handleInput(name: string) {
|
||||
@ -197,6 +109,7 @@ export class Contact extends Component<ContactProps, ContactState> {
|
||||
body = <Form
|
||||
action="/contact"
|
||||
method="post"
|
||||
className="contact"
|
||||
onSubmit={ this.handleSubmit.bind(this) }
|
||||
>
|
||||
<Name
|
||||
@ -206,7 +119,7 @@ export class Contact extends Component<ContactProps, ContactState> {
|
||||
required={ true }
|
||||
value={ name }
|
||||
onChange={ this.handleInput("name").bind(this) }
|
||||
validation={ validation ? 1 : 0 }
|
||||
validation={ validation }
|
||||
disabled={ state === "PENDING" }
|
||||
/>
|
||||
<Email
|
||||
@ -216,7 +129,7 @@ export class Contact extends Component<ContactProps, ContactState> {
|
||||
required={ true }
|
||||
value={ email }
|
||||
onChange={ this.handleInput("email").bind(this) }
|
||||
validation={ validation ? 1 : 0 }
|
||||
validation={ validation }
|
||||
disabled={ state === "PENDING" }
|
||||
/>
|
||||
<Message
|
||||
@ -227,16 +140,16 @@ export class Contact extends Component<ContactProps, ContactState> {
|
||||
required={ true }
|
||||
value={ text }
|
||||
onChange={ this.handleInput("text").bind(this) }
|
||||
validation={ validation ? 1 : 0 }
|
||||
validation={ validation }
|
||||
disabled={ state === "PENDING" }
|
||||
/>
|
||||
<Send
|
||||
<Submit
|
||||
busy={ state === "PENDING" }
|
||||
type="submit"
|
||||
onClick={ this.activateValidation.bind(this) }
|
||||
>
|
||||
Send
|
||||
</Send>
|
||||
</Submit>
|
||||
</Form>
|
||||
break
|
||||
case "FULFILLED":
|
||||
@ -246,7 +159,7 @@ export class Contact extends Component<ContactProps, ContactState> {
|
||||
body = <Fail />
|
||||
}
|
||||
|
||||
return <Container>{ body }</Container>
|
||||
return <Fragment>{ body }</Fragment>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
import React from "react"
|
||||
import { Header, HeaderImage, HeaderName } from "../elements"
|
||||
import Links from "./links"
|
||||
import References from "./references"
|
||||
|
||||
export default () => <Header>
|
||||
<HeaderImage />
|
||||
<HeaderName>Arwed Mett</HeaderName>
|
||||
<Links />
|
||||
<References />
|
||||
</Header>
|
||||
|
||||
|
BIN
src/components/images/github-chrome-fullname.png
Normal file
BIN
src/components/images/github-chrome-fullname.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
src/components/images/java-compiler.gif
Normal file
BIN
src/components/images/java-compiler.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 543 KiB |
BIN
src/components/images/theseus.gif
Normal file
BIN
src/components/images/theseus.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 945 KiB |
@ -1,36 +0,0 @@
|
||||
//@flow
|
||||
|
||||
import React from "react"
|
||||
import { Container, Icon, Link } from "../elements/header/links"
|
||||
import { library } from "@fortawesome/fontawesome-svg-core"
|
||||
import { faKey } from "@fortawesome/free-solid-svg-icons"
|
||||
import { faGithub, faLinkedin, faStackOverflow } from "@fortawesome/free-brands-svg-icons"
|
||||
import pubKey from "./arwed-mett.pub.asc"
|
||||
|
||||
library.add(faGithub)
|
||||
library.add(faLinkedin)
|
||||
library.add(faStackOverflow)
|
||||
library.add(faKey)
|
||||
|
||||
export default () => <Container>
|
||||
<Link
|
||||
icon={ <Icon icon={ [ "fab", "github" ] } /> }
|
||||
label="Github"
|
||||
href="https://github.com/Pfeifenjoy"
|
||||
/>
|
||||
<Link
|
||||
icon={ <Icon icon={ [ "fab", "linkedin" ] } /> }
|
||||
label="Linkedin"
|
||||
href="https://www.linkedin.com/in/arwed-mett-4b5784123/"
|
||||
/>
|
||||
<Link
|
||||
icon={ <Icon icon={ [ "fab", "stack-overflow" ] } /> }
|
||||
label="Stack Overflow"
|
||||
href="https://stackoverflow.com/users/4399651/arwed-mett?tab=profile"
|
||||
/>
|
||||
<Link
|
||||
icon={ <Icon icon={ faKey } /> }
|
||||
label="GPG - Public Key"
|
||||
href={ pubKey }
|
||||
/>
|
||||
</Container>
|
@ -2,13 +2,16 @@
|
||||
|
||||
import React from "react"
|
||||
import { ProjectContainer, Project } from "../elements"
|
||||
import theseus_image from "./images/theseus.gif"
|
||||
import java_compiler_image from "./images/java-compiler.gif"
|
||||
import github_chrome_fullname_image from "./images/github-chrome-fullname.png"
|
||||
|
||||
export default () => <ProjectContainer>
|
||||
<Project
|
||||
title="Theseus"
|
||||
href="https://github.com/Pfeifenjoy/Theseus"
|
||||
description="Adventure game written in the context of my software engineering lecture."
|
||||
img="https://github.com/Pfeifenjoy/Theseus/raw/master/theseus.gif"
|
||||
img={ theseus_image }
|
||||
/>
|
||||
<Project
|
||||
title="Chat"
|
||||
@ -24,12 +27,12 @@ export default () => <ProjectContainer>
|
||||
title="github-chrome-fullname"
|
||||
href="https://github.com/Pfeifenjoy/github-chrome-fullname"
|
||||
description="Chrome extension to display full-name(s) instead of SAP D- / I-User in GitHub Enterprise."
|
||||
img="https://github.com/cgrail/github-chrome-fullname/raw/master/chrome-store-screenshot.png"
|
||||
img={ github_chrome_fullname_image }
|
||||
/>
|
||||
<Project
|
||||
title="Small Java Compiler"
|
||||
href="https://github.com/Pfeifenjoy/compilerbau-WS17-18"
|
||||
description="A small Java compiler developed during my compiler construction lecture."
|
||||
img="https://github.com/Pfeifenjoy/compilerbau-WS17-18/blob/master/figs/usage.gif?raw=true"
|
||||
img={ java_compiler_image }
|
||||
/>
|
||||
</ProjectContainer>
|
||||
|
39
src/components/references.js
Normal file
39
src/components/references.js
Normal file
@ -0,0 +1,39 @@
|
||||
//@flow
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
ReferenceContainer as Container,
|
||||
ReferenceIcon as Icon,
|
||||
Reference
|
||||
} from "../elements/header"
|
||||
import {
|
||||
Key,
|
||||
Github,
|
||||
Linkedin,
|
||||
StackOverflow
|
||||
} from "../icon"
|
||||
|
||||
import pubKey from "./arwed-mett.pub.asc"
|
||||
|
||||
export default () => <Container>
|
||||
<Reference
|
||||
icon={ Icon(<Github />) }
|
||||
label="Github"
|
||||
href="https://github.com/Pfeifenjoy"
|
||||
/>
|
||||
<Reference
|
||||
icon={ Icon(<Linkedin />) }
|
||||
label="Linkedin"
|
||||
href="https://www.linkedin.com/in/arwed-mett-4b5784123/"
|
||||
/>
|
||||
<Reference
|
||||
icon={ Icon(<StackOverflow />) }
|
||||
label="Stack Overflow"
|
||||
href="https://stackoverflow.com/users/4399651/arwed-mett?tab=profile"
|
||||
/>
|
||||
<Reference
|
||||
icon={ Icon(<Key />) }
|
||||
label="GPG - Public Key"
|
||||
href={ pubKey }
|
||||
/>
|
||||
</Container>
|
@ -1,17 +0,0 @@
|
||||
//@flow
|
||||
|
||||
import styled from "styled-components"
|
||||
|
||||
export default styled.div.attrs({
|
||||
backgroundcolor: props => props.theme.backgroundcolor || "#111111"
|
||||
})`
|
||||
background-color: ${ props => props.backgroundcolor };
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-family: monospace;
|
||||
color: white;
|
||||
`
|
@ -1,3 +0,0 @@
|
||||
//@flow
|
||||
|
||||
export { default as Body } from "./body"
|
@ -1,59 +1,19 @@
|
||||
//@flow
|
||||
|
||||
import React, { Component, Fragment } from "react"
|
||||
import type { ElementProps, Node } from "react"
|
||||
import { library } from "@fortawesome/fontawesome-svg-core"
|
||||
import { faCircle } from "@fortawesome/free-solid-svg-icons"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import styled, { keyframes } from "styled-components"
|
||||
import type { Node } from "react"
|
||||
import { CircleNotch } from "../../icon"
|
||||
|
||||
library.add(faCircle)
|
||||
|
||||
/************************************************************/
|
||||
/* Loader */
|
||||
/************************************************************/
|
||||
const LoaderKeyframes = keyframes`
|
||||
from {
|
||||
transform: translateX(-50%) translateY(-50%) rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(-50%) translateY(-50%) rotate(360deg);
|
||||
}
|
||||
`
|
||||
|
||||
const LoaderStyle = styled(FontAwesomeIcon)`
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 90%;
|
||||
max-height: 200px;
|
||||
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
animation: ${ LoaderKeyframes } 2s linear infinite;
|
||||
`
|
||||
|
||||
const Loader = () => <LoaderStyle icon="circle" />
|
||||
|
||||
/************************************************************/
|
||||
/* Button Style */
|
||||
/************************************************************/
|
||||
|
||||
const Base = styled.button`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
type props_t = {
|
||||
type ButtonProps = {
|
||||
busy: boolean,
|
||||
children: ?Node
|
||||
} | ElementProps<typeof Base>
|
||||
}
|
||||
|
||||
/************************************************************/
|
||||
/* Button logic */
|
||||
/************************************************************/
|
||||
|
||||
export default class Button extends Component<props_t, { }> {
|
||||
export default class Button extends Component<ButtonProps, { }> {
|
||||
render() {
|
||||
const { busy, children, ...props } = this.props
|
||||
|
||||
@ -62,10 +22,10 @@ export default class Button extends Component<props_t, { }> {
|
||||
<span style={{ visibility: "hidden" }}>
|
||||
{ children }
|
||||
</span>
|
||||
<Loader />
|
||||
<CircleNotch className="loader" />
|
||||
</Fragment>
|
||||
|
||||
return <Base { ...props }>{ busy ? Busy: children }</Base>
|
||||
return <button { ...props }>{ busy ? Busy : children }</button>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
//@flow
|
||||
|
||||
import styled from "styled-components"
|
||||
|
||||
export const Container = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
4
src/elements/contact/index.js
Normal file
4
src/elements/contact/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
//@flow
|
||||
|
||||
export { default as Input } from "./input"
|
||||
export { default as Textarea } from "./textarea"
|
10
src/elements/contact/input.js
Normal file
10
src/elements/contact/input.js
Normal file
@ -0,0 +1,10 @@
|
||||
//@flow
|
||||
|
||||
import React from "react"
|
||||
|
||||
export default
|
||||
({ validation, className, ...props }: { validation: boolean, className: string }) =>
|
||||
<input
|
||||
className={ className + (validation ? " validate" : "") }
|
||||
{ ...props }
|
||||
/>
|
11
src/elements/contact/textarea.js
Normal file
11
src/elements/contact/textarea.js
Normal file
@ -0,0 +1,11 @@
|
||||
//@flow
|
||||
|
||||
import React from "react"
|
||||
import textarea from "react-autosize-textarea"
|
||||
|
||||
export default
|
||||
({ validation, className, ...props }: { validation: boolean, className: string }) =>
|
||||
<textarea
|
||||
className={ className + (validation ? " validate" : "" )}
|
||||
{ ...props }
|
||||
/>
|
@ -3,52 +3,21 @@
|
||||
import React, { Component, Fragment } from "react"
|
||||
import { Section } from "."
|
||||
import type { SectionDescription } from "."
|
||||
import styled from "styled-components"
|
||||
import Observer from "react-intersection-observer"
|
||||
|
||||
const Link = styled.a`
|
||||
text-decoration: inherit;
|
||||
color: white;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
const Link = (props: *) => <a className="section-link" { ...props } />
|
||||
|
||||
transition: 0.3s ease;
|
||||
:hover {
|
||||
background-color: #777;
|
||||
|
||||
}
|
||||
`
|
||||
|
||||
const Header = styled.header.attrs({
|
||||
backgroundcolor: props => props.theme.backgroundcolor || "#111111"
|
||||
})`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
transition: 0.5s ease;
|
||||
width: ${ props => props.stuck ? "100%" : "60%" };
|
||||
z-index: 5;
|
||||
margin: 20px auto;
|
||||
min-height: 40px;
|
||||
border-bottom: solid white;
|
||||
background-color: ${ props => props.backgroundcolor };
|
||||
`
|
||||
|
||||
/**
|
||||
* create a hash based on the section title
|
||||
*/
|
||||
const get_hash = (section: SectionDescription, key: number) =>
|
||||
key + "-" + section.title.toLocaleLowerCase().split(" ").join("-")
|
||||
const Navigation = ({ stuck, ...rest }: { stuck: boolean }) =>
|
||||
<nav
|
||||
className={ stuck ? "stuck" : "" }
|
||||
{ ...rest }
|
||||
/>
|
||||
|
||||
/**
|
||||
* create a link to the section, according to the name and key.
|
||||
*/
|
||||
const create_link = (section: SectionDescription, key: number) =>
|
||||
<Link href={ "#" + get_hash(section, key) } key={ key }>{ section.title }</Link>
|
||||
<Link href={ "#" + section.hash } key={ key }>{ section.title }</Link>
|
||||
|
||||
/**
|
||||
* create a link for every section.
|
||||
@ -61,7 +30,7 @@ const generate_links = (sections: Array<SectionDescription>) => sections.map(cre
|
||||
const create_section = (description: SectionDescription, key: number) =>
|
||||
<Section
|
||||
description={ description }
|
||||
hash={ get_hash(description, key) }
|
||||
hash={ description.hash }
|
||||
key={ key }
|
||||
/>
|
||||
|
||||
@ -71,11 +40,16 @@ const create_section = (description: SectionDescription, key: number) =>
|
||||
const generate_sections = (descriptions: Array<SectionDescription>) =>
|
||||
descriptions.map(create_section)
|
||||
|
||||
type TopProps = { sections: Array<SectionDescription> }
|
||||
type TopState = { stuck: boolean }
|
||||
|
||||
class Top extends Component<{ sections: Array<SectionDescription> }, { stuck: boolean }> {
|
||||
class Top extends Component<TopProps, TopState> {
|
||||
|
||||
state = {
|
||||
stuck: false
|
||||
constructor(props: TopProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
stuck: false
|
||||
}
|
||||
}
|
||||
|
||||
sentinel(in_view: boolean) {
|
||||
@ -88,9 +62,9 @@ class Top extends Component<{ sections: Array<SectionDescription> }, { stuck: bo
|
||||
return <Fragment>
|
||||
<Observer onChange={ this.sentinel.bind(this) }>
|
||||
</Observer>
|
||||
<Header stuck={ stuck }>
|
||||
<Navigation stuck={ stuck }>
|
||||
{ generate_links(sections) }
|
||||
</Header>
|
||||
</Navigation>
|
||||
</Fragment>
|
||||
}
|
||||
}
|
||||
|
5
src/elements/header/header.js
Normal file
5
src/elements/header/header.js
Normal file
@ -0,0 +1,5 @@
|
||||
//@flow
|
||||
|
||||
import React from "react"
|
||||
|
||||
export default (props: *) => <header className="header" { ...props } />
|
@ -1,19 +1,12 @@
|
||||
//@flow
|
||||
|
||||
import styled from "styled-components"
|
||||
import React from "react"
|
||||
import src from "./image.jpg"
|
||||
|
||||
export default styled.img.attrs({
|
||||
src,
|
||||
size: props => props.size || "200px",
|
||||
bordercolor: props => props.theme.textcolor || "#FFFFFF",
|
||||
backgroundcolor: props => props.theme.backgroundcolor || "#111111"
|
||||
})`
|
||||
border-radius: 100%;
|
||||
border: solid 2px;
|
||||
border-color: ${ props => props.bordercolor };
|
||||
margin-top: 6em;
|
||||
width: ${ props => props.size };
|
||||
height: ${ props => props.size };
|
||||
background-color: ${ props => props.backgroundcolor };
|
||||
`
|
||||
export default (props: *) =>
|
||||
<img
|
||||
className="image"
|
||||
src={ src }
|
||||
{ ...props }
|
||||
/>
|
||||
|
||||
|
@ -1,13 +1,11 @@
|
||||
//@flow
|
||||
|
||||
import styled from "styled-components"
|
||||
|
||||
export default styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
export { default as default } from "./header"
|
||||
export { default as Name } from "./name"
|
||||
|
||||
export { default as Image } from "./image"
|
||||
export {
|
||||
Reference,
|
||||
Container as ReferenceContainer,
|
||||
Label as ReferenceLabel,
|
||||
Icon as ReferenceIcon
|
||||
} from "./references"
|
||||
|
@ -1,8 +0,0 @@
|
||||
//@flow
|
||||
|
||||
import styled from "styled-components"
|
||||
|
||||
export default styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
@ -1,10 +0,0 @@
|
||||
//@flow
|
||||
|
||||
import styled from "styled-components"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
|
||||
export default styled(FontAwesomeIcon)`
|
||||
color: ${ props => props.theme.textColor || "#FFFFFF" };
|
||||
margin-right: 5px;
|
||||
`
|
||||
|
@ -1,9 +0,0 @@
|
||||
//@flow
|
||||
|
||||
import styled from "styled-components"
|
||||
|
||||
export default styled.span`
|
||||
color: ${ props => props.theme.textColor || "#FFFFFF" };
|
||||
font-family: ${ props => props.theme.fontFamily || "monospace" };
|
||||
font-size: 8pt;
|
||||
`
|
@ -1,22 +0,0 @@
|
||||
//@flow
|
||||
|
||||
import React from "react"
|
||||
import type { Node } from "react"
|
||||
import styled from "styled-components"
|
||||
import Label from "./label"
|
||||
|
||||
const Wrapper = styled.a`
|
||||
color: ${ props => props.theme.textColor };
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
margin: 5px;
|
||||
`
|
||||
|
||||
export default (props: { icon?: Node, label: string, href: string }) =>
|
||||
<Wrapper
|
||||
href={ props.href }
|
||||
target="_blank">
|
||||
{ props.icon }
|
||||
<Label>{ props.label }</Label>
|
||||
</Wrapper>
|
@ -1,10 +1,6 @@
|
||||
//@flow
|
||||
|
||||
import styled from "styled-components"
|
||||
import { Title } from "../text"
|
||||
import React from "react"
|
||||
import { Title } from ".."
|
||||
|
||||
const Name = styled(Title)`
|
||||
margin: 20px;
|
||||
`
|
||||
|
||||
export default Name
|
||||
export default (props: *) => <Title className="name" { ...props } />
|
||||
|
5
src/elements/header/references/container.js
Normal file
5
src/elements/header/references/container.js
Normal file
@ -0,0 +1,5 @@
|
||||
//@flow
|
||||
|
||||
import React from "react"
|
||||
|
||||
export default (props: *) => <div className="references" { ...props } />
|
5
src/elements/header/references/icon.js
Normal file
5
src/elements/header/references/icon.js
Normal file
@ -0,0 +1,5 @@
|
||||
//@flow
|
||||
|
||||
import React, { type Node } from "react"
|
||||
|
||||
export default (icon: Node) => <span className="icon">{ icon }</span>
|
@ -1,6 +1,6 @@
|
||||
//@flow
|
||||
|
||||
export { default as Container } from "./container"
|
||||
export { default as Label } from "./label"
|
||||
export { default as Icon } from "./icon"
|
||||
export { default as Link } from "./link"
|
||||
|
||||
export { default as Container } from "./container"
|
||||
export { default as Reference } from "./reference"
|
5
src/elements/header/references/label.js
Normal file
5
src/elements/header/references/label.js
Normal file
@ -0,0 +1,5 @@
|
||||
//@flow
|
||||
|
||||
import React from "react"
|
||||
|
||||
export default (props: *) => <span className="label" { ...props } />
|
12
src/elements/header/references/reference.js
Normal file
12
src/elements/header/references/reference.js
Normal file
@ -0,0 +1,12 @@
|
||||
//@flow
|
||||
|
||||
import React, { type Node } from "react"
|
||||
import { Label } from "."
|
||||
|
||||
export default (props: { icon?: Node, label: string, href: string }) =>
|
||||
<a
|
||||
href={ props.href }
|
||||
>
|
||||
{ props.icon }
|
||||
<Label>{ props.label }</Label>
|
||||
</a>
|
@ -6,8 +6,6 @@ export {
|
||||
Name as HeaderName
|
||||
} from "./header"
|
||||
|
||||
export { Body } from "./body"
|
||||
|
||||
export { default as Section } from "./section"
|
||||
export type { SectionDescription } from "./section"
|
||||
|
||||
@ -16,10 +14,13 @@ export {
|
||||
ProjectContainer
|
||||
} from "./project"
|
||||
|
||||
export {
|
||||
H1, H2, Title, P
|
||||
} from "./text"
|
||||
export { default as Title } from "./title"
|
||||
|
||||
export { default as content } from "./content"
|
||||
|
||||
export { default as Button } from "./button"
|
||||
|
||||
export {
|
||||
Input as ContactInput,
|
||||
Textarea as ContactTextarea
|
||||
} from "./contact"
|
||||
|
@ -2,116 +2,24 @@
|
||||
|
||||
import React, { Component } from "react"
|
||||
|
||||
import styled from "styled-components"
|
||||
import { P } from "./text"
|
||||
const Wrapper = (props: *) => <a className="project" { ...props } />
|
||||
|
||||
const height = 200
|
||||
const width = 200
|
||||
const Title = (props: *) => <h2 className="title" { ...props } />
|
||||
|
||||
const WrapperPadding = 20
|
||||
const NavTextSize = 12
|
||||
const NavTextPadding = 5
|
||||
const Description = (props: *) => <p className="description" { ...props } />
|
||||
|
||||
const Wrapper = styled.a`
|
||||
position: relative;
|
||||
border: dashed 1px;
|
||||
padding: ${ WrapperPadding }px;
|
||||
const BackgroundImage = ({ background, ...props }: { background: boolean, src?: string }) =>
|
||||
<img
|
||||
className={ "background-image" + (background ? " background" : "") }
|
||||
{ ...props }
|
||||
/>
|
||||
|
||||
@media (min-width: 500px) {
|
||||
width: ${ width }px;
|
||||
min-height: ${ height }px;
|
||||
}
|
||||
const Content = (props: *) => <div className="content" { ...props } />
|
||||
|
||||
@media (max-width: 500px) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: calc(${ 2 * WrapperPadding }px + ${ NavTextSize }pt + ${ 2 * NavTextPadding }px);
|
||||
}
|
||||
const NavigationContainer = (props: *) =>
|
||||
<div className="navigation-container" { ...props } />
|
||||
|
||||
border-color: ${ props => props.theme.textColor };
|
||||
margin: 15px;
|
||||
|
||||
background-color: ${ props => props.theme.projectTileColor };
|
||||
text-decoration: inherit;
|
||||
color: ${ props => props.theme.textColor };
|
||||
|
||||
&:visited {
|
||||
text-decoration: inherit;
|
||||
color: ${ props => props.theme.projectVisitedColor };
|
||||
border-color: ${ props => props.theme.projectVisitedColor };
|
||||
}
|
||||
`
|
||||
|
||||
Wrapper.defaultProps = {
|
||||
theme: {
|
||||
textColor: "#FFFFFF",
|
||||
projectTileColor: "#222222",
|
||||
projectVisitedColor: "#888888"
|
||||
}
|
||||
}
|
||||
|
||||
const Title = styled.h2`
|
||||
font-family: ${ props => props.theme.fontFamily };
|
||||
font-size: 14pt;
|
||||
`
|
||||
|
||||
Title.defaultProps = {
|
||||
theme: {
|
||||
fontFamily: "monospace"
|
||||
}
|
||||
}
|
||||
|
||||
const Description = styled(P)`
|
||||
margin-top: 10px;
|
||||
color: inherit;
|
||||
`
|
||||
|
||||
const BackgroundImage = styled.img`
|
||||
position: absolute;
|
||||
top: ${ props => props.background ? "5px" : "0" };
|
||||
left: ${ props => props.background ? "5px" : "0" };
|
||||
width: calc(100% - ${ props => props.background ? "10px" : "0px" });
|
||||
height: calc(100% - ${ props => props.background ? "10px" : "0px" });
|
||||
z-index: 1;
|
||||
filter: ${ props => props.background ? "blur(5px) brightness(30%)" : "blur(2px) brightness(50%)" };
|
||||
transition: ${ props => props.background ? "0s" : "0.5s" };
|
||||
|
||||
display: ${ props => props.src ? "" : "none" };
|
||||
`
|
||||
|
||||
const Content = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const NavigationContainer = styled.div`
|
||||
position: absolute;
|
||||
bottom: ${ WrapperPadding }px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
margin-top: ${ WrapperPadding }px;
|
||||
`
|
||||
|
||||
const Navigation = styled.span`
|
||||
color: #FFFFFF;
|
||||
font-size: ${ NavTextSize }pt;
|
||||
background-color: #FF4136;
|
||||
padding: ${ NavTextPadding }px;
|
||||
border-radius: 5px;
|
||||
font-family: ${ props => props.theme.fontFamily };
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
Navigation.defaultProps = {
|
||||
theme: {
|
||||
fontFamily: "monospace"
|
||||
}
|
||||
}
|
||||
const Navigation = (props: *) => <span className="navigation" { ...props } />
|
||||
|
||||
type ProjectProps = {
|
||||
title: string,
|
||||
@ -125,13 +33,11 @@ type ProjectState = {
|
||||
}
|
||||
|
||||
export default class Project extends Component<ProjectProps, ProjectState> {
|
||||
static defaultProps = {
|
||||
description: "",
|
||||
href: ""
|
||||
}
|
||||
|
||||
state = {
|
||||
hover: false
|
||||
constructor(props: ProjectProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
hover: false
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -145,7 +51,7 @@ export default class Project extends Component<ProjectProps, ProjectState> {
|
||||
onMouseLeave={ () => this.setState({ hover: false }) }
|
||||
>
|
||||
<BackgroundImage src={ img } background={ !hover && !!img } />
|
||||
<Content background={ hover && !!img }>
|
||||
<Content>
|
||||
<Title>{ title }</Title>
|
||||
<Description>{ description }</Description>
|
||||
</Content>
|
||||
@ -156,11 +62,5 @@ export default class Project extends Component<ProjectProps, ProjectState> {
|
||||
}
|
||||
}
|
||||
|
||||
export const ProjectContainer = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
`
|
||||
export const ProjectContainer = (props: *) =>
|
||||
<div className="projects" { ...props } />
|
||||
|
@ -2,29 +2,16 @@
|
||||
|
||||
import React from "react"
|
||||
import type { Node } from "react"
|
||||
import styled from "styled-components"
|
||||
import { H1 } from "./text"
|
||||
|
||||
const padding = 20
|
||||
|
||||
const Style = styled.section`
|
||||
min-width: 100px;
|
||||
width: calc(100% - ${ 2 * padding }%);
|
||||
padding-left: ${ padding }%;
|
||||
padding-right: ${ padding }%;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
`
|
||||
|
||||
const Title = H1
|
||||
|
||||
export type SectionDescription = {
|
||||
export type SectionDescription = {|
|
||||
title: string,
|
||||
body: () => Node
|
||||
}
|
||||
body: () => Node,
|
||||
hash: string
|
||||
|}
|
||||
|
||||
export default (props: { description: SectionDescription, hash: string }) => <Style>
|
||||
<Title id={ props.hash || "" }>{ props.description.title }</Title>
|
||||
<br />
|
||||
<div>{ props.description.body() }</div>
|
||||
</Style>
|
||||
export default (props: { description: SectionDescription, hash: string }) =>
|
||||
<section id={ props.hash }>
|
||||
<h1>{ props.description.title }</h1>
|
||||
<br />
|
||||
<div>{ props.description.body() }</div>
|
||||
</section>
|
||||
|
@ -1,24 +0,0 @@
|
||||
//@flow
|
||||
|
||||
import styled from "styled-components"
|
||||
|
||||
const Base = (props: { theme: { textColor: string, fontFamily: string } }) => `
|
||||
color: ${ props.theme.textColor || "#FFFFFF" };
|
||||
font-family: ${ props.theme.fontFamily || "monospace" };
|
||||
`
|
||||
|
||||
export const Title = styled.h1`
|
||||
${ props => Base(props) };
|
||||
font-size: 24pt;
|
||||
`
|
||||
|
||||
export const H1 = styled.h1`
|
||||
${ props => Base(props) };
|
||||
font-size: 20pt;
|
||||
`
|
||||
|
||||
export const H2 = styled.h2`
|
||||
${ props => Base(props) };
|
||||
font-size: 18pt;
|
||||
`
|
||||
|
@ -1,9 +0,0 @@
|
||||
//@flow
|
||||
|
||||
export {
|
||||
H1, H2, Title
|
||||
} from "./headings"
|
||||
|
||||
export {
|
||||
P
|
||||
} from "./text"
|
@ -1,8 +0,0 @@
|
||||
//@flow
|
||||
|
||||
import styled from "styled-components"
|
||||
|
||||
export const P = styled.p`
|
||||
color: ${ props => props.theme.textColor };
|
||||
font-family: ${ props => props.theme.fontFamily };
|
||||
`
|
5
src/elements/title.js
Normal file
5
src/elements/title.js
Normal file
@ -0,0 +1,5 @@
|
||||
//@flow
|
||||
|
||||
import React from "react"
|
||||
|
||||
export default (props: *) => <span className="title" { ...props } />
|
@ -2,8 +2,10 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Homepage - Arwed Mett</title>
|
||||
<title>Arwed Mett</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" |