Skip to content

Commit

Permalink
Merge pull request #7 from amirsaeed671/login-flow
Browse files Browse the repository at this point in the history
login validation added
  • Loading branch information
amirsaeed671 committed Jul 29, 2020
2 parents 05b8b94 + 9e0273e commit 24bbc6d
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 28 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Expand Up @@ -16,6 +16,7 @@ module.exports = {
"global-require": "off",
"no-debugger": "off",
"no-console": "off",
"no-param-reassign": "off",
},
plugins: ["prettier"],
settings: {
Expand Down
1 change: 1 addition & 0 deletions public/index.html
Expand Up @@ -7,6 +7,7 @@
<title>Gist Uploader</title>
</head>
<body class="bg-pink-500">
<div id="modal-root"></div>
<div id="root"></div>
</body>
</html>
34 changes: 12 additions & 22 deletions src/Form.js
Expand Up @@ -4,6 +4,8 @@ import useForm from "custom-hooks/useForm";
import TextInput from "common/text-input";
import FormHeader from "common/form-header";
import Checkbox from "common/checkbox";
import TextArea from "common/text-area";
import Button from "common/button";

function Form({ onSubmit, loader }) {
const [
Expand Down Expand Up @@ -68,30 +70,18 @@ function Form({ onSubmit, loader }) {
</div>
</div>
<div className="mb-6">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="content"
>
Content/Code
<textarea
className="shadow h-64 resize-none appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="content"
required
value={content}
onChange={(e) => setFormValue({ content: e.target.value })}
type="text"
placeholder="Write you content/code here"
/>
</label>
<TextArea
id="content"
required
label="Content/Code"
value={content}
onChange={(e) => setFormValue({ content: e.target.value })}
type="text"
placeholder="Write you content/code here"
/>
</div>
<div className="flex items-center justify-between">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
disabled={loader}
type="submit"
>
Create Gist
</button>
<Button disabled={loader} label="Create Gist" />
</div>
</form>
</div>
Expand Down
26 changes: 22 additions & 4 deletions src/Login.js
@@ -1,24 +1,42 @@
import React, { useState, useRef, useEffect } from "react";
import React, { useState, useRef, useEffect, lazy } from "react";
import githubIcon from "assets/GitHub-Mark-32px.png";
import { useHistory } from "react-router-dom";
import api from "utils/api";

const Modal = lazy(() => import("common/modal"));
const Popup = lazy(() => import("common/popup"));

function Login() {
const [token, setToken] = useState("");
const [error, setError] = useState(false);
const history = useHistory();
const inputRef = useRef(null);

useEffect(() => {
inputRef.current.focus();
}, []);

const handleSubmit = (e) => {
const handleSubmit = async (e) => {
e.preventDefault();
localStorage.setItem("token", token);
history.push("/gists");
try {
localStorage.setItem("token", token);
await api.get("user");
history.push("/gists");
} catch {
setError(true);
setTimeout(() => {
setError(false);
}, 2000);
}
};

return (
<div className="w-full">
{error && (
<Modal>
<Popup />
</Modal>
)}
<form
onSubmit={handleSubmit}
className="flex flex-col h-screen justify-center items-center"
Expand Down
25 changes: 25 additions & 0 deletions src/common/button.js
@@ -0,0 +1,25 @@
import React from "react";
import Proptypes from "prop-types";

function Button({ disabled, label }) {
return (
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
disabled={disabled}
type="submit"
>
{label}
</button>
);
}

Button.propTypes = {
label: Proptypes.string.isRequired,
disabled: Proptypes.bool,
};

Button.defaultProps = {
disabled: false,
};

export default Button;
31 changes: 31 additions & 0 deletions src/common/modal.js
@@ -0,0 +1,31 @@
import React from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";

const modalRoot = document.getElementById("modal-root");

class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement("div");
}

componentDidMount() {
modalRoot.appendChild(this.el);
}

componentWillUnmount() {
modalRoot.removeChild(this.el);
}

render() {
const { children } = this.props;
return ReactDOM.createPortal(children, this.el);
}
}

Modal.propTypes = {
children: PropTypes.node.isRequired,
};

export default Modal;
34 changes: 34 additions & 0 deletions src/common/popup.js
@@ -0,0 +1,34 @@
import React from "react";

function Popup() {
return (
<div className="w-screen in-animation mx-auto container flex justify-center">
<div
className="bg-teal-100 border-t-4 fixed mt-6 ml-6 border-teal-500 rounded-b text-teal-900 px-4 py-3 shadow-md"
role="alert"
>
<div className="flex">
<div className="py-1">
<svg
className="fill-current h-6 w-6 text-teal-500 mr-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z" />
</svg>
</div>
<div>
<p className="font-bold">
Access token is incorrect! (Unauthorized)
</p>
<p className="text-sm">
Make sure you&apos;ve entered the correct access token.
</p>
</div>
</div>
</div>
</div>
);
}

export default Popup;
36 changes: 36 additions & 0 deletions src/common/text-area.js
@@ -0,0 +1,36 @@
import React from "react";
import Proptypes from "prop-types";

function TextArea({ label, id, value, onChange, placeholder, required }) {
return (
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor={id}>
{label}
<textarea
className="shadow h-64 resize-none appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id={id}
required={required}
value={value}
onChange={onChange}
placeholder={placeholder}
/>
</label>
);
}

TextArea.propTypes = {
value: Proptypes.string.isRequired,
onChange: Proptypes.string.isRequired,
id: Proptypes.string,
label: Proptypes.string,
placeholder: Proptypes.string,
required: Proptypes.bool,
};

TextArea.defaultProps = {
id: "",
label: "",
placeholder: "",
required: false,
};

export default TextArea;
29 changes: 29 additions & 0 deletions src/custom-hooks/useFetch.js
@@ -0,0 +1,29 @@
import { useState, useEffect } from "react";
import fetchGists from "observables/fetch-gists";

function useFetch() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
const observer = {
next: (gists) => {
setData(gists);
setLoading(false);
},
error: (err) => {
setLoading(false);
setError(err);
},
complete: () => {
setLoading(false);
},
};
fetchGists().subscribe(observer);
}, []);

return [data, loading, error];
}

export default useFetch;
12 changes: 12 additions & 0 deletions src/index.css
Expand Up @@ -5,6 +5,18 @@ html, body, #root {
padding: 0;
}

@keyframes enter {
from {
transform: scale(0.8);
}
to {
transform: scale(1);
}
}
.in-animation {
animation: enter 0.5s ease-in-out;
}

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
9 changes: 9 additions & 0 deletions src/observables/fetch-gists.js
@@ -0,0 +1,9 @@
import { from } from "rxjs";
import { pluck } from "rxjs/operators";
import api from "utils/api";

function fetchGists() {
return from(api("gists")).pipe(pluck("data"));
}

export default fetchGists;
29 changes: 29 additions & 0 deletions src/utils/api.js
@@ -0,0 +1,29 @@
import axios from "axios";

const baseURL = "https://api.github.com/";

const api = axios.create({
baseURL,
});

api.interceptors.request.use(
function requestCB(config) {
const token = localStorage.getItem("token");
config.headers.Authorization = `token ${token}`;
return config;
},
function reqErrorCB(error) {
return Promise.reject(error);
}
);

api.interceptors.response.use(
function responseCB(response = {}) {
return response.data;
},
function resErrorCB(error) {
return Promise.reject(error);
}
);

export default api;
2 changes: 2 additions & 0 deletions test/jest-common.js
Expand Up @@ -7,7 +7,9 @@ module.exports = {
"test",
path.join(__dirname, "../src"),
"custom-hooks",
"observables",
"common",
"utils",
],
moduleNameMapper: {
"\\.css$": require.resolve("./style-mock.js"),
Expand Down
4 changes: 2 additions & 2 deletions webpack.config.js
@@ -1,5 +1,4 @@
const path = require("path");
const { EnvironmentPlugin } = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const DotenvWebpackPlugin = require("dotenv-webpack");
const { merge } = require("webpack-merge");
Expand All @@ -17,7 +16,9 @@ module.exports = ({ mode, presets } = { mode: "production", presets: [] }) => {
"test",
path.join(__dirname, "src"),
"custom-hooks",
"observables",
"common",
"utils",
],
},
entry: {
Expand Down Expand Up @@ -72,7 +73,6 @@ module.exports = ({ mode, presets } = { mode: "production", presets: [] }) => {
filename: "index.html",
}),
new DotenvWebpackPlugin(),
new EnvironmentPlugin(["CLIENT_ID", "CLIENT_SECRET"]),
],
},
modeConfig(mode),
Expand Down

0 comments on commit 24bbc6d

Please sign in to comment.