🚀 Initial commit for rzmk/smart-brain!
This commit is contained in:
commit
48634c33ea
30 changed files with 28624 additions and 0 deletions
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal 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
42
README.md
Normal 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
27686
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
42
package.json
Normal file
42
package.json
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
62
public/index.html
Normal file
62
public/index.html
Normal 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
BIN
public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal 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
3
public/robots.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
BIN
smart-brain.png
Normal file
BIN
smart-brain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
17
src/App.css
Normal file
17
src/App.css
Normal 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
169
src/App.js
Normal 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
8
src/App.test.js
Normal 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();
|
||||||
|
});
|
||||||
57
src/components/BackgroundParticles/BackgroundParticles.js
Normal file
57
src/components/BackgroundParticles/BackgroundParticles.js
Normal 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;
|
||||||
9
src/components/FaceRecognition/FaceRecognition.css
Normal file
9
src/components/FaceRecognition/FaceRecognition.css
Normal 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 */
|
||||||
|
}
|
||||||
29
src/components/FaceRecognition/FaceRecognition.js
Normal file
29
src/components/FaceRecognition/FaceRecognition.js
Normal 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;
|
||||||
49
src/components/ImageLinkForm/ImageLinkForm.css
Normal file
49
src/components/ImageLinkForm/ImageLinkForm.css
Normal 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;
|
||||||
|
}
|
||||||
31
src/components/ImageLinkForm/ImageLinkForm.js
Normal file
31
src/components/ImageLinkForm/ImageLinkForm.js
Normal 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;
|
||||||
18
src/components/Logo/Logo.css
Normal file
18
src/components/Logo/Logo.css
Normal 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+ */
|
||||||
|
}
|
||||||
16
src/components/Logo/Logo.js
Normal file
16
src/components/Logo/Logo.js
Normal 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;
|
||||||
1
src/components/Logo/brain.svg
Normal file
1
src/components/Logo/brain.svg
Normal 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 |
36
src/components/Navigation/Navigation.js
Normal file
36
src/components/Navigation/Navigation.js
Normal 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;
|
||||||
14
src/components/Rank/Rank.js
Normal file
14
src/components/Rank/Rank.js
Normal 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;
|
||||||
117
src/components/Register/Register.js
Normal file
117
src/components/Register/Register.js
Normal 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;
|
||||||
106
src/components/Signin/Signin.js
Normal file
106
src/components/Signin/Signin.js
Normal 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
28
src/index.css
Normal 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
18
src/index.js
Normal 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
13
src/reportWebVitals.js
Normal 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
5
src/setupTests.js
Normal 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';
|
||||||
Loading…
Add table
Add a link
Reference in a new issue