🚀 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