feat: Login Page

This commit is contained in:
Anoop M D 2022-03-07 20:02:39 +05:30
parent a17b9f4b73
commit 3856f244f5
15 changed files with 430 additions and 11 deletions

43
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@fortawesome/react-fontawesome": "^0.1.16",
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.64.0",
"codemirror-graphql": "^1.2.5",
@ -22,7 +23,7 @@
"graphql": "^16.2.0",
"graphql-request": "^3.7.0",
"idb": "^7.0.0",
"immer": "^9.0.7",
"immer": "^9.0.12",
"lodash": "^4.17.21",
"markdown-it": "^12.2.0",
"mousetrap": "^1.6.5",
@ -5101,6 +5102,14 @@
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
"dev": true
},
"node_modules/axios": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
"integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
"dependencies": {
"follow-redirects": "^1.14.8"
}
},
"node_modules/babel-loader": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz",
@ -8157,6 +8166,25 @@
"node": ">=4"
}
},
"node_modules/follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@ -20118,6 +20146,14 @@
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
"dev": true
},
"axios": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
"integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
"requires": {
"follow-redirects": "^1.14.8"
}
},
"babel-loader": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz",
@ -22491,6 +22527,11 @@
"locate-path": "^2.0.0"
}
},
"follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",

View File

@ -22,6 +22,7 @@
"@fortawesome/react-fontawesome": "^0.1.16",
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"axios": "^0.26.0",
"classnames": "^2.3.1",
"codemirror": "^5.64.0",
"codemirror-graphql": "^1.2.5",
@ -33,7 +34,7 @@
"graphql": "^16.2.0",
"graphql-request": "^3.7.0",
"idb": "^7.0.0",
"immer": "^9.0.7",
"immer": "^9.0.12",
"lodash": "^4.17.21",
"markdown-it": "^12.2.0",
"mousetrap": "^1.6.5",

27
renderer/api/base.js Normal file
View File

@ -0,0 +1,27 @@
import axios from "axios";
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API
});
apiClient.interceptors.request.use((config) => {
const headers = {
'Content-Type': 'application/json'
};
return ({
...config,
headers: headers
});
}, error => Promise.reject(error));
apiClient.interceptors.response.use((response) =>
response,
async (error) => {
return Promise.reject(error.response ? error.response.data : error);
}
);
const { get, post, put, delete: destroy } = apiClient;
export { get, post, put, destroy };

13
renderer/api/identity.js Normal file
View File

@ -0,0 +1,13 @@
import { get, post, put } from './base';
const IdentityApi = {
whoami: () =>get('v1/user/whoami'),
signup: (params) =>post('v1/user/register', params),
login: (params) =>post('v1/user/login', params),
signout: () => post('v1/user/logout'),
getProfile: () =>get('v1/user/profile'),
updateProfile: (params) =>put('v1/user/profile', params),
updateUsername: (params) =>put('v1/user/username', params)
};
export default IdentityApi;

View File

@ -3,6 +3,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
background-color: rgb(44, 44, 44);
color: rgba(255, 255, 255, 0.5);
min-height: 100vh;
.menu-item {
padding: 0.6rem;

View File

@ -1,4 +1,5 @@
import React from 'react';
import Link from 'next/link';
import { IconCode, IconStack, IconGitPullRequest, IconUser, IconUsers, IconSettings,IconBuilding } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
@ -7,7 +8,9 @@ const MenuBar = () => {
<StyledWrapper className="h-full flex flex-col">
<div className="flex flex-col">
<div className="menu-item active">
<IconCode size={28} strokeWidth={1.5}/>
<Link href="/">
<IconCode size={28} strokeWidth={1.5}/>
</Link>
</div>
<div className="menu-item">
<IconStack size={28} strokeWidth={1.5}/>
@ -25,7 +28,9 @@ const MenuBar = () => {
<IconBuilding size={28} strokeWidth={1.5}/>
</div> */}
<div className="menu-item">
<IconUser size={28} strokeWidth={1.5}/>
<Link href="/login">
<IconUser size={28} strokeWidth={1.5}/>
</Link>
</div>
<div className="menu-item">
<IconSettings size={28} strokeWidth={1.5}/>

View File

@ -5,6 +5,7 @@
"baseUrl": "./",
"paths": {
"components/*": ["src/components/*"],
"api/*": ["src/api/*"],
"pageComponents/*": ["src/pageComponents/*"],
"providers/*": ["src/providers/*"]
}

View File

@ -0,0 +1,41 @@
import styled from 'styled-components';
const Wrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 100vh;
.form-container {
max-width: 350px;
border-radius: 4px;
border: 1px #ddd solid;
button.continue-btn {
font-size: 16px;
padding-top: 8x;
padding-bottom: 8px;
min-height: 38px;
align-items: center;
color: #212529;
background: #e2e6ea;
border: solid 1px #dae0e5;
}
.field-error {
font-size: 0.875rem;
}
a {
color: var(--color-text-link);
}
.error-msg {
font-size: 15px;
color: rgb(192 69 8);
}
}
`;
export default Wrapper;

View File

@ -0,0 +1,153 @@
import React, { useState } from "react";
import * as Yup from 'yup';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useAuth } from 'providers/Auth';
import IdentityApi from 'api/identity';
import { useFormik } from 'formik';
import StyledWrapper from './StyledWrapper';
const Login = () => {
const router = useRouter();
const [authState, authDispatch] = useAuth();
const {
currentUser
} = authState;
const [loggingIn, setLoggingIn] = useState(false);
const [loginError, setLoginError] = useState(false);
const formik = useFormik({
initialValues: {
email: '',
password: '',
},
validationSchema: Yup.object({
email: Yup.string()
.required('Email is required'),
password: Yup.string()
.required('Password is required')
}),
onSubmit: (values, { resetForm }) => {
setLoggingIn(true);
IdentityApi
.login({
email: values.email,
password: values.password
})
.then((response) => {
authDispatch({
type: 'LOGIN_SUCCESS',
user: response.data
});
})
.catch((error) => {
setLoggingIn(false);
setLoginError(true);
});
},
});
if(authState.isLoading) {
return null;
};
if(currentUser) {
router.push('/home');
return null;
};
return (
<StyledWrapper>
<div className="flex flex-col justify-center cursor-pointer items-center mt-10">
<div style={{fontSize: '3rem'}}>
<svg id="emoji" width="50" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#F4AA41" stroke="none" d="M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z"/>
<polygon fill="#EA5A47" stroke="none" points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"/>
<polygon fill="#3F3F3F" stroke="none" points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<path fill="#000000" stroke="none" d="M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875"/>
<path fill="#000000" stroke="none" d="M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"/>
<path fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2" d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632"/>
<line x1="36.2078" x2="36.2078" y1="47.3393" y2="44.3093" fill="none" stroke="#000000" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="10" strokeWidth="2"/>
</g>
</svg>
</div>
<div className="font-semibold" style={{fontSize: '2rem'}}>grafnode</div>
<div className="mt-1">Opensource API Collection Collaboration</div>
</div>
<form onSubmit={formik.handleSubmit}>
<div className="flex justify-center flex-col form-container mx-auto mt-10 p-5">
<div className="text-2xl mt-3 mb-5">Login</div>
<div>
<label htmlFor="email" className="pb-2 pt-3 block font-semibold">Email</label>
<input id="email" name="email" type="text"
className="appearance-none rounded relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-blue-600 focus:z-10 sm:text-sm"
placeholder="Email"
onChange={formik.handleChange}
onFocus={() => setLoginError(false)}
onBlur={formik.handleBlur}
value={formik.values.email}
/>
{formik.touched.email && formik.errors.email ? (
<div className="field-error error-msg">{formik.errors.email}</div>
) : null}
</div>
<div>
<label htmlFor="password" className="pb-2 pt-4 block font-semibold">Password</label>
<input id="password" name="password" type="password" autoComplete="password"
className="appearance-none rounded relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-blue-600 focus:z-10 sm:text-sm"
placeholder="Password"
onChange={formik.handleChange}
onFocus={() => setLoginError(false)}
onBlur={formik.handleBlur}
value={formik.values.password}
/>
{formik.touched.password && formik.errors.password ? (
<div className="field-error error-msg">{formik.errors.password}</div>
) : null}
</div>
<div className="pt-6">
{ loggingIn ? (
<button disabled={true} className="continue-btn relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<div className="dot-elastic"/>
</button>
) :
<div className="text-center">
<button type="submit" className="continue-btn mb-4 relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Continue
</button>
</div>
}
{loginError ? (
<div className="field-error error-msg text-red-500 ml-1 mt-1">Invalid Credentials</div>
) : null}
</div>
<div className="sign-in-container mt-2">
</div>
<div className="mt-2">
No account? <Link href="/signup">Create one!</Link>
</div>
</div>
</form>
</StyledWrapper>
)
};
export default Login;

View File

@ -1,5 +1,6 @@
import { StoreProvider } from 'providers/Store';
import { HotkeysProvider } from 'providers/Hotkeys';
import { AuthProvider } from 'providers/Auth';
import '../styles/globals.css'
import 'tailwindcss/dist/tailwind.min.css';
@ -20,11 +21,13 @@ function SafeHydrate({ children }) {
function MyApp({ Component, pageProps }) {
return (
<SafeHydrate>
<HotkeysProvider>
<StoreProvider>
<Component {...pageProps} />
</StoreProvider>
</HotkeysProvider>
<AuthProvider>
<HotkeysProvider>
<StoreProvider>
<Component {...pageProps} />
</StoreProvider>
</HotkeysProvider>
</AuthProvider>
</SafeHydrate>
);
}

View File

@ -16,5 +16,5 @@ export default function Home() {
<Main />
</main>
</div>
)
}
);
};

26
renderer/pages/login.js Normal file
View File

@ -0,0 +1,26 @@
import Head from 'next/head';
import Login from 'pageComponents/Login';
import MenuBar from 'components/Sidebar/MenuBar';
import GlobalStyle from '../globalStyles';
export default function Home() {
return (
<div>
<Head>
<title>grafnode</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<GlobalStyle />
<main>
<div className="flex flex-row h-full">
<MenuBar />
<div className="flex flex-grow h-full">
<Login />
</div>
</div>
</main>
</div>
);
};

View File

@ -0,0 +1,63 @@
import React, { useEffect, useReducer } from 'react';
import { useRouter } from 'next/router';
import IdentityApi from 'api/identity';
import reducer from './reducer';
const AuthContext = React.createContext();
const initialState = {
isLoading: true,
lastStateTransition: null,
currentUser: null
};
export const AuthProvider = props => {
const router = useRouter();
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
IdentityApi
.whoami()
.then((response) => {
let data = response.data;
dispatch({
type: 'WHOAMI_SUCCESS',
user : {
id: data.id,
name: data.name,
username: data.username
}
});
})
.catch((error) => {
dispatch({
type: 'WHOAMI_ERROR',
error: error
});
});
}, []);
useEffect(() => {
if(state.lastStateTransition === 'LOGIN_SUCCESS') {
router.push('/home');
}
if(state.lastStateTransition === 'WHOAMI_ERROR') {
// Todo: decide action
// router.push('/login');
}
}, [state]);
return <AuthContext.Provider value={[state, dispatch]} {...props} />;
};
export const useAuth = () => {
const context = React.useContext(AuthContext);
if (context === undefined) {
throw new Error(`useAuth must be used within a AuthProvider`);
}
return context;
};
export default AuthProvider;

View File

@ -0,0 +1,43 @@
import produce from 'immer';
const reducer = (state, action) => {
switch (action.type) {
case 'WHOAMI_SUCCESS': {
return produce(state, (draft) => {
draft.isLoading = false;
draft.currentUser = action.user;
draft.lastStateTransition = 'WHOAMI_SUCCESS';
});
}
case 'WHOAMI_ERROR': {
return produce(state, (draft) => {
draft.isLoading = false;
draft.currentUser = null;
draft.lastStateTransition = 'WHOAMI_ERROR';
});
}
case 'LOGIN_SUCCESS': {
return produce(state, (draft) => {
draft.isLoading = false;
draft.currentUser = action.user;
draft.lastStateTransition = 'LOGIN_SUCCESS';
});
}
case 'LOGOUT_SUCCESS': {
return produce(state, (draft) => {
draft.isLoading = false;
draft.currentUser = null;
draft.lastStateTransition = 'LOGOUT_SUCCESS';
});
}
default: {
return state;
}
}
};
export default reducer;

View File

@ -10,6 +10,7 @@
--color-layout-border: #dedede;
--color-codemirror-border: #efefef;
--color-codemirror-background: rgb(243, 243, 243);
--color-text-link: #1663bb;
}
html, body {