🚀 Initial commit for rzmk/smart-brain!

This commit is contained in:
rzmk 2022-01-01 09:37:28 -05:00
commit 48634c33ea
30 changed files with 28624 additions and 0 deletions

23
.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

42
README.md Normal file
View file

@ -0,0 +1,42 @@
<div align="center">
<a href="https://github.com/rzmk/smart-brain">
<img src="smart-brain.png" alt="Server" width="80" height="80">
</a>
<h3 align="center">smart-brain</h3>
Frontend repo for viewing detected faces using Clarifai's AI models API.
<br />
</div>
## Details
The frontend repo of a full stack web application that enables logged-in users to upload images to view detected faces using a face recognition AI model using Clarifai's REST API.
Backend: [https://github.com/rzmk/smart-brain-api](https://github.com/rzmk/smart-brain-api)
## Features
- Login and registration system.
- Upload images to view detected faces.
- Stores user login information in a database and total number of images uploaded by each user.
## Stack
### Frontend
- [React](https://reactjs.org/) (frontend)
- [Tachyons](https://tachyons.io/) (CSS toolkit)
### Backend
- [Node.js](https://nodejs.org/) (server)
- [Express](https://expressjs.com/) (web app framework for Node.js)
- [PostgreSQL](https://www.postgresql.org/) (database)
- [Clarifai](https://www.clarifai.com/) (face recognition AI model API)
### Hosting
- [Heroku](https://www.heroku.com/) (hosting)
## Acknowledgements
- [Zero to Mastery Academy](https://academy.zerotomastery.io/p/complete-web-developer-zero-to-mastery)

27686
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

42
package.json Normal file
View file

@ -0,0 +1,42 @@
{
"name": "smart-brain",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"clarifai": "^2.9.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-parallax-tilt": "^1.5.83",
"react-scripts": "^5.0.0",
"react-tsparticles": "^1.37.5",
"tachyons": "^4.12.0",
"web-vitals": "^2.1.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

62
public/index.html Normal file
View file

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!-- Primary Meta Tags -->
<title>Smart Brain 🧠 | Detect faces with AI!</title>
<meta name="title" content="Smart Brain 🧠 | Detect faces with AI!">
<meta name="description"
content="Upload images to detect faces using Clarifai's face-recognition model AI REST API! Features a registration and login system.">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://rzmk.github.io/smart-brain/">
<meta property="og:title" content="Smart Brain 🧠 | Detect faces with AI!">
<meta property="og:description"
content="Upload images to detect faces using Clarifai's face-recognition model AI REST API! Features a registration and login system.">
<meta property="og:image" content="https://i.imgur.com/KlntPhk.png">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://rzmk.github.io/smart-brain/">
<meta property="twitter:title" content="Smart Brain 🧠 | Detect faces with AI!">
<meta property="twitter:description"
content="Upload images to detect faces using Clarifai's face-recognition model AI REST API! Features a registration and login system.">
<meta property="twitter:image" content="https://i.imgur.com/KlntPhk.png">
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View file

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

BIN
smart-brain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

17
src/App.css Normal file
View file

@ -0,0 +1,17 @@
.App {
text-align: center;
}
.center {
display: flex;
justify-content: center;
}
.particles {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
}

169
src/App.js Normal file
View file

@ -0,0 +1,169 @@
import React, { Component } from "react";
import Navigation from "./components/Navigation/Navigation";
import Signin from "./components/Signin/Signin";
import Register from "./components/Register/Register";
import Logo from "./components/Logo/Logo";
import ImageLinkForm from "./components/ImageLinkForm/ImageLinkForm";
import Rank from "./components/Rank/Rank";
import FaceRecognition from "./components/FaceRecognition/FaceRecognition";
import BackgroundParticles from "./components/BackgroundParticles/BackgroundParticles";
import "./App.css";
const initialState = {
input: "",
imageUrl: "",
box: {},
route: "signin",
isSignedIn: false,
user: {
id: "",
name: "",
email: "",
entries: 0,
joined: "",
},
};
class App extends Component {
constructor() {
super();
this.state = initialState;
}
componentDidMount() {
fetch("BACKEND_URL"); // Wake up backend server
}
loadUser = (data) => {
this.setState({
user: {
id: data.id,
name: data.name,
email: data.email,
entries: data.entries,
joined: data.joined,
},
});
};
calculateFaceLocation = (data) => {
const clarifaiFace =
data.outputs[0].data.regions[0].region_info.bounding_box;
const image = document.getElementById("inputimage");
const width = Number(image.width);
const height = Number(image.height);
return {
leftCol: clarifaiFace.left_col * width,
topRow: clarifaiFace.top_row * height,
rightCol: width - clarifaiFace.right_col * width,
bottomRow: height - clarifaiFace.bottom_row * height,
};
};
displayFaceBox = (box) => {
this.setState({ box: box });
};
onInputChange = (event) => {
this.setState({ input: event.target.value });
};
onButtonSubmit = () => {
this.setState({ imageUrl: this.state.input });
fetch("BACKEND_URL/imageurl", {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
input: this.state.input,
}),
})
.then((response) => response.json())
.then((response) => {
if (response) {
fetch("BACKEND_URL/image", {
method: "put",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: this.state.user.id,
}),
})
.then((response) => response.json())
.then((count) => {
this.setState(
Object.assign(this.state.user, {
entries: count,
})
);
})
.catch(console.log);
}
this.displayFaceBox(this.calculateFaceLocation(response));
})
.catch((err) => console.log(err));
};
onRouteChange = (route) => {
if (route === "signout") {
// Signout Button
this.setState(initialState);
this.setState({ route: "signin" });
} else if (route === "home") {
this.setState({ isSignedIn: true });
this.setState({ route: "home" });
} else {
this.setState({ route: route });
}
};
render() {
const { isSignedIn, imageUrl, route, box } = this.state;
// Functions for particles background
const particlesInit = (main) => {
// console.log(main);
};
const particlesLoaded = (container) => {
// console.log(container);
};
return (
<div className="App">
<Navigation
isSignedIn={isSignedIn}
onRouteChange={this.onRouteChange}
/>
<BackgroundParticles
particlesInit={particlesInit}
particlesLoaded={particlesLoaded}
/>
{route === "home" ? (
<div>
<Logo />
<Rank
name={this.state.user.name}
entries={this.state.user.entries}
/>
<ImageLinkForm
onInputChange={this.onInputChange}
onButtonSubmit={this.onButtonSubmit}
/>
<FaceRecognition box={box} imageUrl={imageUrl} />
</div>
) : route === "signin" ? (
<Signin
loadUser={this.loadUser}
onRouteChange={this.onRouteChange}
/>
) : (
<Register
loadUser={this.loadUser}
onRouteChange={this.onRouteChange}
/>
)}
</div>
);
}
}
export default App;

8
src/App.test.js Normal file
View file

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View file

@ -0,0 +1,57 @@
import Particles from "react-tsparticles";
import React from "react";
const BackgroundParticles = ({ particlesInit, particlesLoaded }) => {
return (
<div>
<Particles
className="particles"
id="tsparticles"
init={particlesInit}
loaded={particlesLoaded}
options={{
fpsLimit: 165,
interactivity: {
events: {
resize: true,
},
},
particles: {
color: {
value: "#ffffff",
},
links: {
color: "#ffffff",
distance: 300,
enable: true,
opacity: 1,
width: 1,
},
collisions: {
enable: true,
},
move: {
direction: "none",
enable: true,
outMode: "bounce",
random: false,
speed: 6,
straight: false,
},
number: {
density: {
enable: true,
value_area: 800,
},
value: 30,
},
},
detectRetina: true,
}}
/>
</div>
);
};
export default BackgroundParticles;

View file

@ -0,0 +1,9 @@
.bounding-box {
position: absolute;
box-shadow: 0 0 0 3px #149df2 inset;
display: flex;
flex-wrap: wrap;
justify-content: center;
cursor: pointer;
/* backdrop-filter: blur(20px); Blur face example */
}

View file

@ -0,0 +1,29 @@
import React from "react";
import "./FaceRecognition.css";
const FaceRecognition = ({ imageUrl, box }) => {
return (
<div className="center ma">
<div className="absolute mt2">
<img
id="inputimage"
src={imageUrl}
alt=""
width="500px"
height="auto"
/>
<div
className="bounding-box"
style={{
top: box.topRow,
right: box.rightCol,
bottom: box.bottomRow,
left: box.leftCol,
}}
></div>
</div>
</div>
);
};
export default FaceRecognition;

View file

@ -0,0 +1,49 @@
.form {
width: 700px;
background: radial-gradient(
circle farthest-side at 0% 50%,
#89fffd 23.5%,
rgba(240, 166, 17, 0) 0
)
21px 30px,
radial-gradient(
circle farthest-side at 0% 50%,
#89ccff 24%,
rgba(240, 166, 17, 0) 0
)
19px 30px,
linear-gradient(
#89fffd 14%,
rgba(240, 166, 17, 0) 0,
rgba(240, 166, 17, 0) 85%,
#89fffd 0
)
0 0,
linear-gradient(
150deg,
#89fffd 24%,
#89ccff 0,
#89ccff 26%,
rgba(240, 166, 17, 0) 0,
rgba(240, 166, 17, 0) 74%,
#89ccff 0,
#89ccff 76%,
#89fffd 0
)
0 0,
linear-gradient(
30deg,
#89fffd 24%,
#89ccff 0,
#89ccff 26%,
rgba(240, 166, 17, 0) 0,
rgba(240, 166, 17, 0) 74%,
#89ccff 0,
#89ccff 76%,
#89fffd 0
)
0 0,
linear-gradient(90deg, #89ccff 2%, #89fffd 0, #89fffd 98%, #89ccff 0%) 0
0 #89fffd;
background-size: 40px 60px;
}

View file

@ -0,0 +1,31 @@
import React from "react";
import "./ImageLinkForm.css";
const ImageLinkForm = ({ onInputChange, onButtonSubmit }) => {
return (
<div>
<p className="f3">
{
"Paste an image URL and click 'Detect' to detect faces in your pictures. Give it a try!"
}
</p>
<div className="center">
<div className="form center pa4 br3 shadow-5">
<input
className="f4 pa2 w-70 center"
type="text"
onChange={onInputChange}
/>
<button
className="w-30 grow f4 link ph3 pv2 dib white bg-light-purple"
onClick={onButtonSubmit}
>
Detect
</button>
</div>
</div>
</div>
);
};
export default ImageLinkForm;

View file

@ -0,0 +1,18 @@
.Tilt {
box-shadow: 10px 10px 5px 0px rgba(0, 0, 0, 0.5);
-webkit-box-shadow: 10px 10px 5px 0px rgba(0, 0, 0, 0.5);
-moz-box-shadow: 10px 10px 5px 0px rgba(0, 0, 0, 0.5);
width: 200px;
margin: 0 auto;
background: #ef32d9; /* fallback for old browsers */
background: -webkit-linear-gradient(
to right,
#89fffd,
#ef32d9
); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(
to right,
#89fffd,
#ef32d9
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}

View file

@ -0,0 +1,16 @@
import React from "react";
import Tilt from "react-parallax-tilt";
import "./Logo.css";
import brain from "./brain.svg";
const Logo = () => {
return (
<div className={{ justifyContent: "center" }}>
<Tilt className="Tilt">
<img className="pa3" src={brain} alt="Logo" />
</Tilt>
</div>
);
};
export default Logo;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#efefef;}.cls-2{fill:#424242;}.cls-3{fill:#89ffff;}</style></defs><title>Brainstorming</title><g id="Brainstorming"><path class="cls-1" d="M19.51,22.67A2,2,0,0,1,23,24a1,1,0,0,0,2,0,2,2,0,0,1,3.49-1.33,1,1,0,0,0,1.41.08c1.66-1.49-3.16-4.51-5.9-1.37-2.76-3.16-7.56-.11-5.9,1.37A1,1,0,0,0,19.51,22.67Z"/><path class="cls-1" d="M24,29a5,5,0,0,0-9,3,1,1,0,0,0,2,0,3,3,0,0,1,6,0,1,1,0,0,0,2,0,3,3,0,0,1,6,0,1,1,0,0,0,2,0A5,5,0,0,0,24,29Z"/><path class="cls-2" d="M42.31,21a6.94,6.94,0,0,0,.28-5.36,1,1,0,1,0-1.88.68,5,5,0,0,1-.52,4.39,1,1,0,0,0,.26,1.37,6,6,0,0,1-4.3,10.84,1,1,0,0,0-.3,2,7.78,7.78,0,0,0,3-.15,6,6,0,0,1-8,6.81,1,1,0,0,0-1.31.55,1,1,0,0,0,.55,1.31,7.39,7.39,0,0,0,1.46.42,4,4,0,0,1-6.53.75V2.79a3,3,0,0,1,4.9,1.47,1,1,0,1,0,1.94-.5A5,5,0,0,0,24,1h0a5,5,0,0,0-7.77,2.49A8,8,0,0,0,11,11.07,7,7,0,0,0,5.69,21a8,8,0,0,0,1.55,13A8,8,0,0,0,14.33,44,6,6,0,0,0,24,46.47,6,6,0,0,0,33.67,44a8,8,0,0,0,7.09-9.91A8,8,0,0,0,42.31,21ZM23,44.61a4,4,0,0,1-6.53-.75,7.39,7.39,0,0,0,1.46-.42,1,1,0,1,0-.76-1.86,6,6,0,0,1-8-6.81,7.78,7.78,0,0,0,3,.15,1,1,0,0,0-.3-2A6,6,0,0,1,5,27a6,6,0,0,1,2.55-4.9,1,1,0,0,0,.26-1.37,5,5,0,0,1,5.86-7.45,1,1,0,0,0,.66-1.88A6.78,6.78,0,0,0,13,11.08a6,6,0,0,1,4.34-5.85,1,1,0,0,0,.71-.78A3,3,0,0,1,23,2.79Z"/><path class="cls-1" d="M19.5,34a1,1,0,0,0-1,1,2,2,0,0,1-2,2,1,1,0,0,0-1,1c0,2.09,5,.81,5-3A1,1,0,0,0,19.5,34Z"/><path class="cls-1" d="M13,27a2,2,0,0,1-2-2,1,1,0,0,0-1-1c-2.09,0-.81,5,3,5a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M39,25a1,1,0,0,0-2,0,2,2,0,0,1-2,2,1,1,0,0,0-1,1C34,30.09,39,28.81,39,25Z"/><path class="cls-1" d="M16,17c0-2.09-5-.81-5,3a1,1,0,0,0,2,0,2,2,0,0,1,2-2A1,1,0,0,0,16,17Z"/><circle class="cls-1" cx="45" cy="45" r="2"/><path class="cls-3" d="M44,3H30a2,2,0,0,0-2,2V15a2,2,0,0,0,2,2h1v2a1,1,0,0,0,1,1c.48,0,.42-.06,4.33-3H44a2,2,0,0,0,2-2V5A2,2,0,0,0,44,3ZM32,7h6a1,1,0,0,1,0,2H32a1,1,0,0,1,0-2Zm10,6H32a1,1,0,0,1,0-2H42a1,1,0,0,1,0,2Z"/><path class="cls-1" d="M11.65,2.24C11.28,1.92,11.27,2,8,2V1A1,1,0,0,0,7,0H1A1,1,0,0,0,0,1V5A1,1,0,0,0,1,6H7A1,1,0,0,0,8,5V4h2.63c7.23,6.2,6.84,6,7.37,6a1,1,0,0,0,.65-1.76Z"/><path class="cls-1" d="M45,42a3,3,0,0,0-2.82,2H39.45l-7.7-8.66a1,1,0,0,0-1.5,1.32l8,9A1,1,0,0,0,39,46h3.18A3,3,0,1,0,45,42Z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,36 @@
import React from "react";
const Navigation = ({ onRouteChange, isSignedIn }) => {
if (isSignedIn) {
return (
<nav style={{ display: "flex", justifyContent: "flex-end" }}>
<p
onClick={() => onRouteChange("signout")}
className="f3 link dim black underline pa3 pointer"
>
Sign Out
</p>
</nav>
);
} else {
return (
<nav style={{ display: "flex", justifyContent: "flex-end" }}>
<p
onClick={() => onRouteChange("signin")}
className="f3 link dim black underline pa3 pointer"
>
Sign In
</p>
<p
onClick={() => onRouteChange("register")}
className="f3 link dim black underline pa3 pointer"
>
Register
</p>
)
</nav>
);
}
};
export default Navigation;

View file

@ -0,0 +1,14 @@
import React from "react";
const Rank = ({ name, entries }) => {
return (
<div>
<div className="white f3 mt4">
{`${name}, your total number of image uploads is...`}
</div>
<div className="white f1">{entries}</div>
</div>
);
};
export default Rank;

View file

@ -0,0 +1,117 @@
import React from "react";
class Register extends React.Component {
constructor(props) {
super(props);
this.state = {
email: "",
password: "",
name: "",
};
}
onNameChange = (event) => {
this.setState({ name: event.target.value });
};
onEmailChange = (event) => {
this.setState({ email: event.target.value });
};
onPasswordChange = (event) => {
this.setState({ password: event.target.value });
};
onSubmitRegister = () => {
fetch("BACKEND_URL/register", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: this.state.email,
password: this.state.password,
name: this.state.name,
}),
})
.then((response) => response.json())
.then((user) => {
if (user?.id) {
this.props.loadUser(user);
this.props.onRouteChange("home");
}
});
};
render() {
return (
<article className="br3 ba dark-gray b--black-10 mv4 w-100 w-50-m w-25-l mw6 shadow-5 center">
<main className="pa4 black-80">
<div className="measure">
<fieldset
id="sign_up"
className="ba b--transparent ph0 mh0"
>
<legend className="f1 fw6 ph0 mh0">Register</legend>
<div className="mt3">
<label
className="db fw6 lh-copy f4"
htmlFor="name"
>
Name
</label>
<input
onChange={this.onNameChange}
className="pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100"
type="name"
name="name"
id="name"
/>
</div>
<div className="mt3">
<label
className="db fw6 lh-copy f4"
htmlFor="email-address"
>
Email
</label>
<input
onChange={this.onEmailChange}
className="pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100"
type="email"
name="email"
id="email"
/>
</div>
<div className="mv3">
<label
className="db fw6 lh-copy f4"
htmlFor="password"
>
Password
</label>
<input
onChange={this.onPasswordChange}
className="b pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100"
type="password"
name="password"
id="password"
/>
</div>
</fieldset>
<div className="">
<input
onClick={this.onSubmitRegister}
className="b ph3 pv2 input-reset ba b--black bg-transparent grow pointer f5 dib"
type="submit"
value="Register"
/>
</div>
</div>
</main>
</article>
);
}
}
export default Register;

View file

@ -0,0 +1,106 @@
import React from "react";
class Signin extends React.Component {
constructor(props) {
super(props);
this.state = {
signInEmail: "",
signInPassword: "",
};
}
onEmailChange = (event) => {
this.setState({ signInEmail: event.target.value });
};
onPasswordChange = (event) => {
this.setState({ signInPassword: event.target.value });
};
onSubmitSignIn = () => {
fetch("BACKEND_URL/signin", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: this.state.signInEmail,
password: this.state.signInPassword,
}),
})
.then((response) => response.json())
.then((user) => {
if (user?.id) {
this.props.loadUser(user);
this.props.onRouteChange("home");
}
});
};
render() {
const { onRouteChange } = this.props;
return (
<article className="br3 ba dark-gray b--black-10 mv4 w-100 w-50-m w-25-l mw6 shadow-5 center">
<main className="pa4 black-80">
<div className="measure">
<fieldset
id="sign_up"
className="ba b--transparent ph0 mh0"
>
<legend className="f1 fw6 ph0 mh0">Sign In</legend>
<div className="mt3">
<label
className="db fw6 lh-copy f4"
htmlFor="email"
>
Email
</label>
<input
onChange={this.onEmailChange}
className="pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100"
type="email"
name="email"
id="email"
/>
</div>
<div className="mv3">
<label
className="db fw6 lh-copy f4"
htmlFor="password"
>
Password
</label>
<input
onChange={this.onPasswordChange}
className="b pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100"
type="password"
name="password"
id="password"
/>
</div>
</fieldset>
<div className="">
<input
onClick={this.onSubmitSignIn}
className="b ph3 pv2 input-reset ba b--black bg-transparent grow pointer f5 dib"
type="submit"
value="Sign in"
/>
</div>
<div className="lh-copy mt3">
<p
onClick={() => onRouteChange("register")}
href="#0"
className="f6 link dim black db pointer"
>
Register
</p>
</div>
</div>
</main>
</article>
);
}
}
export default Signin;

28
src/index.css Normal file
View file

@ -0,0 +1,28 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #ef32d9; /* fallback for old browsers */
background: -webkit-linear-gradient(
to right,
#89fffd,
#ef32d9
); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(
to right,
#89fffd,
#ef32d9
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}
button {
cursor: pointer;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

18
src/index.js Normal file
View file

@ -0,0 +1,18 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import "tachyons";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

13
src/reportWebVitals.js Normal file
View file

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
src/setupTests.js Normal file
View file

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';