add react ui anew

This commit is contained in:
cst 2022-06-12 15:33:27 +03:00
parent 7ab1654c16
commit 2b10777de8
35 changed files with 28569 additions and 1 deletions

8
.gitignore vendored
View File

@ -68,13 +68,19 @@ target/
memray-*
# micro
log.txt
# app's db
lessons_learned.db
backend_fastapi/lessons_learned.db
backend_fastapi/build/
# node
# dependencies
frontend_react/node_modules
node_modules
frontend_react/.pnp
frontend_react/pnp.js

1
frontend_react/.env Normal file
View File

@ -0,0 +1 @@
SKIP_PREFLIGHT_CHECK=true

View File

@ -0,0 +1,26 @@
module.exports = {
'env': {
'browser': true,
'node': true,
'es2021': true,
},
'extends': [
'plugin:react/recommended',
'eslint:recommended',
],
'parserOptions': {
'ecmaFeatures': {
'jsx': true,
},
'ecmaVersion': 'latest',
'sourceType': 'module',
},
'plugins': [
'react',
],
'rules': {
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
"react/prop-types": "off"
},
};

23
frontend_react/.gitignore vendored Normal file
View File

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

27062
frontend_react/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
{
"name": "recipe-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@mars/heroku-js-runtime-env": "^3.0.2",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.24.0",
"jwt-decode": "^3.1.2",
"moment": "^2.29.1",
"notistack": "^2.0.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint src/**/*.js src/**/*.jsx",
"lint:fix": "eslint src/**/*.js src/**/*.jsx --fix"
},
"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"
]
},
"devDependencies": {
"autoprefixer": "^10.4.2",
"eslint": "^7.32.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-react": "^7.28.0",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.15"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,48 @@
<!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" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<!-- Google font -->
<link href="https://fonts.googleapis.com/css?family=Montserrat:200,400,700" rel="stylesheet">
<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`.
-->
<title>React App</title>
</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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

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

View File

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

View File

@ -0,0 +1,40 @@
.App {
text-align: center;
height: 100%;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

26
frontend_react/src/App.js Normal file
View File

@ -0,0 +1,26 @@
import React from 'react';
import './App.css';
import {BrowserRouter, Routes, Route} from 'react-router-dom';
import Login from './pages/login';
import SignUp from './pages/sign-up';
import Home from './pages/home';
import LessonDashboard from './pages/my-lessons';
import ErrorPage from './pages/error-page';
const App = () => {
return (
<div className="App bg-black">
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route exact path="/my-lessons" element={<LessonDashboard />} />
<Route exact path="/login" element={<Login />} />
<Route exact path="/sign-up" element={<SignUp />} />
<Route exact={true} path="*" element={<ErrorPage />} />
</Routes>
</BrowserRouter>
</div>
);
};
export default App;

View File

@ -0,0 +1,137 @@
import config from './config';
import jwtDecode from 'jwt-decode';
import * as moment from 'moment';
const axios = require('axios');
class FastAPIClient {
constructor(overrides) {
this.config = {
...config,
...overrides,
};
this.authToken = config.authToken;
this.login = this.login.bind(this);
this.apiClient = this.getApiClient(this.config);
}
/* ----- Authentication & User Operations ----- */
/* Authenticate the user with the backend services.
* The same JWT should be valid for both the api and cms */
login(username, password) {
delete this.apiClient.defaults.headers['Authorization'];
// HACK: This is a hack for scenario where there is no login form
const form_data = new FormData();
const grant_type = 'password';
const item = {grant_type, username, password};
for (const key in item) {
form_data.append(key, item[key]);
}
return this.apiClient
.post('/auth/login', form_data)
.then((resp) => {
localStorage.setItem('token', JSON.stringify(resp.data));
return this.fetchUser();
});
}
fetchUser() {
return this.apiClient.get('/auth/me').then(({data}) => {
localStorage.setItem('user', JSON.stringify(data));
return data;
});
}
register(email, password, fullName) {
const registerData = {
email,
password,
full_name: fullName,
is_active: true,
};
return this.apiClient.post('/auth/signup', registerData).then(
(resp) => {
return resp.data;
});
}
// Logging out is just deleting the jwt.
logout() {
// Add here any other data that needs to be deleted from local storage
// on logout
localStorage.removeItem('token');
localStorage.removeItem('user');
}
/* ----- Client Configuration ----- */
/* Create Axios client instance pointing at the REST api backend */
getApiClient(config) {
const initialConfig = {
baseURL: `${config.apiBasePath}/api/v1`,
};
const client = axios.create(initialConfig);
client.interceptors.request.use(localStorageTokenInterceptor);
return client;
}
getLesson(lessonId) {
return this.apiClient.get(`/lessons/${lessonId}`);
}
getLessons(keyword) {
return this.apiClient.get(`/search/?keyword=${keyword}&max_results=10`).then(({data}) => {
return data;
});
}
getUserLessons() {
return this.apiClient.get(`/lesson/`).then(({data}) => {
return data;
});
}
createLesson(title, content, tags, submitter_id) {
const lessonData = {
title,
content,
tags,
submitter_id: submitter_id,
};
return this.apiClient.post(`/lesson/`, lessonData);
}
deleteLesson(lessonId) {
return this.apiClient.delete(`/lesson/${lessonId}`);
}
}
// every request is intercepted and has auth header injected.
function localStorageTokenInterceptor(config) {
const headers = {};
const tokenString = localStorage.getItem('token');
if (tokenString) {
const token = JSON.parse(tokenString);
const decodedAccessToken = jwtDecode(token.access_token);
const isAccessTokenValid =
moment.unix(decodedAccessToken.exp).toDate() > new Date();
if (isAccessTokenValid) {
headers['Authorization'] = `Bearer ${token.access_token}`;
} else {
alert('Your login session has expired');
}
}
config['headers'] = headers;
return config;
}
export default FastAPIClient;

View File

@ -0,0 +1,20 @@
import React from 'react';
function Button({loading, title, error}) {
return <div>
<button
className={`flex flex-row justify-center items-center w-full bg-teal-600 ${error ? "mt-6" : "" } cursor-pointer hover:bg-teal-700 text-white font-bold py-2 px-4 mb-6 rounded `}
type="submit"
>
{
loading && <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
}
{title}
</button>
</div>
}
export default Button;

View File

@ -0,0 +1,92 @@
import React, {useEffect, useState} from 'react';
import { useNavigate, Link } from 'react-router-dom';
import FastAPIClient from '../../client';
import config from '../../config';
import jwtDecode from "jwt-decode";
import * as moment from "moment";
const client = new FastAPIClient(config);
function DashboardHeader() {
const navigate = useNavigate();
const [isLoggedIn, setIsLoggedIn] = useState(false);
// STATE WHICH WE WILL USE TO TOGGLE THE MENU ON HAMBURGER BUTTON PRESS
const [toggleMenu, setToggleMenu] = useState(false);
useEffect(() => {
const tokenString = localStorage.getItem("token")
if (tokenString) {
const token = JSON.parse(tokenString)
const decodedAccessToken = jwtDecode(token.access_token)
if(moment.unix(decodedAccessToken.exp).toDate() > new Date()){
setIsLoggedIn(true)
}
}
}, [])
const handleLogout = () => {
client.logout();
setIsLoggedIn(false)
navigate('/')
}
const handleLogin = () => {
navigate("/login");
}
let displayButton;
const buttonStyle = "inline-block text-sm px-4 py-2 leading-none border rounded text-white border-white hover:border-transparent hover:text-teal-500 hover:bg-white mt-4 lg:mt-0"
if (isLoggedIn) {
displayButton = <button className={buttonStyle} onClick={() => handleLogout()}>Logout</button>;
} else {
displayButton = <button className={buttonStyle} onClick={() => handleLogin()}>Login</button>;
}
return (
<nav className="flex items-center justify-between flex-wrap bg-teal-500 p-6">
<div className="flex items-center flex-shrink-0 text-white mr-6">
<a href={"/"}><svg className="fill-current h-8 w-8 mr-2" width="54" height="54" viewBox="0 0 54 54"
xmlns="http://www.w3.org/2000/svg">
<path
d="M13.5 22.1c1.8-7.2 6.3-10.8 13.5-10.8 10.8 0 12.15 8.1 17.55 9.45 3.6.9 6.75-.45 9.45-4.05-1.8 7.2-6.3 10.8-13.5 10.8-10.8 0-12.15-8.1-17.55-9.45-3.6-.9-6.75.45-9.45 4.05zM0 38.3c1.8-7.2 6.3-10.8 13.5-10.8 10.8 0 12.15 8.1 17.55 9.45 3.6.9 6.75-.45 9.45-4.05-1.8 7.2-6.3 10.8-13.5 10.8-10.8 0-12.15-8.1-17.55-9.45-3.6-.9-6.75.45-9.45 4.05z"/>
</svg></a>
<span className="font-semibold text-xl tracking-tight">Lessons Learned</span>
</div>
<div className="block lg:hidden">
<button
className="flex items-center px-3 py-2 border rounded text-teal-200 border-teal-400 hover:text-white hover:border-white"
onClick={() => setToggleMenu(!toggleMenu)}>
<svg className="fill-current h-3 w-3" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<title>Menu</title>
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/>
</svg>
</button>
</div>
<div className={`animate-fade-in-down w-full ${toggleMenu ? "block" : "hidden"} flex-grow lg:flex lg:items-center lg:w-auto`}>
<div className="text-sm lg:flex-grow">
<a href={"http://localhost:8001/docs"} target={"_blank"} rel={"noreferrer"}
className="block mt-4 lg:inline-block lg:mt-0 text-teal-200 hover:text-white mx-4">
API Docs
</a>
<Link to="/my-lessons"
className="block mt-4 lg:inline-block lg:mt-0 text-teal-200 hover:text-white mx-4">
My Lessons
</Link>
{!isLoggedIn && <Link
className="block mt-4 lg:inline-block lg:mt-0 text-teal-200 hover:text-white"
to={`/sign-up`}>
Create Account
</Link>}
</div>
<div>
{displayButton}
</div>
</div>
</nav>
);
}
export default DashboardHeader;

View File

@ -0,0 +1,15 @@
import React from 'react';
function Footer() {
return (
<footer className={"text-center p-4 bg-black mt-auto text-white"}>
<div >
&lt;3 to Cod3
<a className="text-white ml-5" href="https://creativespirit.tech">Lessons Learned API</a>
</div>
</footer>
);
}
export default Footer;

View File

@ -0,0 +1,17 @@
import React from 'react';
function FormInput({label, name, error, value, onChange, type = "text"}) {
return <div>
<label className="block mb-2 text-teal-500" htmlFor={name}>{label}</label>
<input
type={type}
name={name}
value={value}
onChange={onChange}
className={`rounded w-full p-2 border-b-2 ${!error ? "mb-6 border-teal-500 " : "border-red-500 "} text-teal-700 outline-none focus:bg-gray-300`}
/>
{error && <span className='mb-3 text-red-500' >{error}</span>}
</div>
}
export default FormInput;

View File

@ -0,0 +1,48 @@
import { useState } from "react";
const Idea = ({ idea}) => {
const init = idea.indexOf(":")
const fin = idea.lastIndexOf("(")
const fen = idea.lastIndexOf(")")
const url = idea.substr(fin+1,fen-fin-1)
const desc = idea.substr(init+1,fin-init-1)
return (
idea && (
<>
<div
className="flex flex-wrap items-center justify-between w-full transition duration-500 ease-in-out transform bg-black border-2 border-gray-600 rounded-lg hover:border-white mb-3"
>
<div className="w-full xl:text-left md:w-2/5 ">
<div className="relative flex flex-col h-full p-8 ">
<h2 className="font-semibold tracking-widest text-white uppercase title-font text-center ">
{desc}
</h2>
</div>
</div>
<div className="w-full xl:w-1/4 md:w-1/2 lg:ml-auto">
<div className="relative flex flex-col h-full p-8">
<h1 className="flex items-end mx-auto text-3xl font-black leading-none text-white ">
<span>View Idea </span>
</h1>
<div className="flex flex-col md:flex-row">
<a
href={url}
className="transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 w-full bg-teal-600 cursor-pointer hover:bg-teal-700 text-white font-bold px-4 py-2 mx-auto mt-3 rounded"
>
{/* className="w-full px-4 py-2 mx-auto mt-3 text-white transition duration-500 ease-in-out transform border border-gray-900 rounded-lg text-md hover:bg-gray-900 focus:shadow-outline focus:outline-none focus:ring-2 ring-offset-current ring-offset-2 focus:border-gray-700 focus:bg-gray-800 "> */}
Visit Site
</a>
</div>
</div>
</div>
</div>
</>
)
);
};
export default Idea;

View File

@ -0,0 +1,22 @@
import React, {useState} from "react";
import Idea from "../Idea";
const IdeaTable = ({ideas}) => {
return (
<>
<div className="sections-list">
{ideas.length && (
ideas.map((idea, i) => (
<Idea key={i} idea={idea} />
))
)}
{!ideas.length && (
<p>No Ideas found!</p>
)}
</div>
</>
)
}
export default IdeaTable;

View File

@ -0,0 +1,59 @@
import React from "react";
const Lesson = ({ lesson, showLessonInfoModal }) => {
return (
lesson && (
<>
<div
onClick={(e) => {showLessonInfoModal() ; e.stopPropagation()}}
className="flex flex-wrap items-end justify-between w-full transition duration-500 ease-in-out transform bg-black border-2 border-gray-600 rounded-lg hover:border-white mb-3"
>
<div className="w-full xl:w-1/4 md:w-1/4">
<div className="relative flex flex-col h-full p-8 ">
<h2 className="mb-4 font-semibold tracking-widest text-white uppercase title-font">
{lesson?.label}
</h2>
<h2 className="items-center mb-2 text-lg font-normal tracking-wide text-white">
<span className="inline-flex items-center justify-center flex-shrink-0 w-5 h-5 mr-2 text-white rounded-full bg-blue-1300">
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
className="w-4 h-4"
viewBox="0 0 24 24"
>
<path d="M20 6L9 17l-5-5"></path>
</svg>
</span>
{lesson?.source}
</h2>
</div>
</div>
<div className="w-full xl:w-1/4 md:w-1/2 lg:ml-auto" style={{zindex: 10000}}>
<div className="relative flex flex-col h-full p-8">
<h1 className="flex items-end mx-auto text-3xl font-black leading-none text-white ">
<span>View Lesson </span>
</h1>
<div className="flex flex-col md:flex-row">
<a
href={`${lesson?.url}`}
onClick={(e) => e.stopPropagation()}
className="transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 bg-teal-600 cursor-pointer hover:bg-teal-700 text-white font-bold px-4 py-2 mx-auto mt-3 rounded"
>
{/* className="w-full px-4 py-2 mx-auto mt-3 text-white transition duration-500 ease-in-out transform border border-gray-900 rounded-lg text-md hover:bg-gray-900 focus:shadow-outline focus:outline-none focus:ring-2 ring-offset-current ring-offset-2 focus:border-gray-700 focus:bg-gray-800 "> */}
Visit Site
</a>
</div>
</div>
</div>
</div>
</>
)
);
};
export default Lesson;

View File

@ -0,0 +1,58 @@
import Lesson from "../Lesson";
import React, {useState} from "react";
import PopupModal from "../Modal/PopupModal";
import FormInput from "../FormInput/FormInput";
const LessonTable = ({lessons}) => {
const [lessonInfoModal, setLessonInfoModal] = useState(false)
return (
<>
<div className="sections-list">
{lessons.length && (
lessons.map((lesson) => (
<Lesson showLessonInfoModal={() => setLessonInfoModal(lesson)} key={lesson.id} lesson={lesson} />
))
)}
{!lessons.length && (
<p>No lessons found!</p>
)}
</div>
{lessonInfoModal && <PopupModal
modalTitle={"Lesson Info"}
onCloseBtnPress={() => {
setLessonInfoModal(false);
}}
>
<div className="mt-4 text-left">
<form className="mt-5">
<FormInput
disabled
type={"text"}
name={"label"}
label={"Label"}
value={lessonInfoModal?.label}
/>
<FormInput
disabled
type={"text"}
name={"url"}
label={"Url"}
value={lessonInfoModal?.url}
/>
<FormInput
disabled
type={"text"}
name={"source"}
label={"Source"}
value={lessonInfoModal?.source}
/>
</form>
</div>
</PopupModal>}
</>
)
}
export default LessonTable;

View File

@ -0,0 +1,9 @@
import React from 'react';
function Loader() {
return <div className='flex justify-center items-center h-screen w-screen bg-white'>
<img src='https://thumbs.gfycat.com/HugeDeliciousArchaeocete-max-1mb.gif' width={'auto'} height={'auto'} />
</div>;
}
export default Loader;

View File

@ -0,0 +1,38 @@
import React from "react";
function PopupModal({ onCloseBtnPress, modalTitle, children }) {
return (
<div className="animate-fade-in-down container flex justify-center mx-auto">
<div className="absolute inset-0 flex items-center justify-center bg-gray-700 bg-opacity-50">
<div
className="rounded max-w-sm p-6 bg-indigo-100 divide-y divide-teal-500"
style={{ minWidth: 400 }}
>
<div className="flex items-center justify-between">
<h3 className="text-2xl text-teal-500">{modalTitle}</h3>
<div className="cursor-pointer">
<svg
onClick={onCloseBtnPress}
xmlns="http://www.w3.org/2000/svg"
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
stroke="rgb(13 148 136)"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</div>
{children}
</div>
</div>
</div>
);
}
export default PopupModal;

View File

@ -0,0 +1,9 @@
import runtimeEnv from '@mars/heroku-js-runtime-env';
const env = runtimeEnv();
const config = {
apiBasePath: env.REACT_APP_API_BASE_PATH || 'http://localhost:8001',
reactAppMode: process.env.REACT_APP_MODE || 'dev',
};
export default config;

View File

@ -0,0 +1,159 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
margin: 0;
padding: 0;
height: 100vh;
}
/* include border and padding in element width and height */
* {
box-sizing: border-box;
}
body {
margin: 0;
flex-direction: column;
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;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
#root {
height: 100%;
}
/* width */
::-webkit-scrollbar {
width: 10px;
}
/* Track */
::-webkit-scrollbar-track {
background: #f1f1f1;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: rgb(13 148 136);
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #555;
}
#notfound {
position: relative;
height: 100vh;
}
#notfound .notfound {
position: absolute;
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.notfound {
max-width: 520px;
width: 100%;
line-height: 1.4;
text-align: center;
}
.notfound .notfound-404 {
position: relative;
height: 200px;
margin: 0px auto 20px;
z-index: -1;
}
.notfound .notfound-404 h1 {
font-family: 'Montserrat', sans-serif;
font-size: 236px;
font-weight: 200;
margin: 0px;
color: "white";
text-transform: uppercase;
position: absolute;
left: 50%;
top: 20%;
-webkit-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.notfound .notfound-404 h2 {
font-family: 'Montserrat', sans-serif;
font-size: 28px;
font-weight: 400;
text-transform: uppercase;
color: white;
background: black;
padding: 10px 5px;
margin: auto;
display: inline-block;
position: absolute;
bottom: 0px;
left: 0;
right: 0;
}
.notfound a {
font-family: 'Montserrat', sans-serif;
display: inline-block;
font-weight: 700;
text-decoration: none;
color: #fff;
text-transform: uppercase;
padding: 13px 23px;
background-color: rgb(20 184 166 / var(--tw-bg-opacity));
font-size: 18px;
-webkit-transition: 0.2s all;
transition: 0.2s all;
}
.notfound a:hover {
color: white;
background: var(--bg-teal-700);
}
@media only screen and (max-width: 767px) {
.notfound .notfound-404 h1 {
font-size: 148px;
}
}
@media only screen and (max-width: 480px) {
.notfound .notfound-404 {
height: 148px;
margin: 0px auto 10px;
}
.notfound .notfound-404 h1 {
font-size: 86px;
}
.notfound .notfound-404 h2 {
font-size: 16px;
}
.notfound a {
padding: 7px 15px;
font-size: 14px;
}
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
);

View File

@ -0,0 +1,36 @@
import React from 'react';
import { Link } from "react-router-dom";
const ErrorPage = () => {
return (
<div className="
flex
items-center
justify-center
w-screen
h-screen
bg-black
"
>
<div className="px-40 py-20 bg-white rounded-md shadow-xl">
<div className="flex flex-col items-center">
<h1 className="font-bold text-blue-600 text-9xl">404</h1>
<h6 className="mb-2 text-2xl font-bold text-center text-gray-800 md:text-3xl">
<span className="text-red-500">Oops!</span> Page not found
</h6>
<p className="mb-8 text-center text-gray-500 md:text-lg">
The page youre looking for doesnt exist.
</p>
<Link
to="/"
className="px-6 py-2 text-sm font-semibold text-blue-800 bg-blue-100"
>Go home</Link >
</div>
</div>
</div>
)
}
export default ErrorPage;

View File

@ -0,0 +1,83 @@
import React, { useEffect, useState } from 'react';
import FastAPIClient from '../../client';
import config from '../../config';
import LessonTable from "../../components/LessonTable"
import DashboardHeader from "../../components/DashboardHeader";
import Footer from "../../components/Footer";
import Loader from '../../components/Loader';
const client = new FastAPIClient(config);
const Home = () => {
const [loading, setLoading] = useState(true)
const [lessons, setLessons] = useState([])
const [searchValue, setSearchValue] = useState("tech")
useEffect(() => {
// Fetch the lessons
fetchLessons()
}, [])
const fetchLessons = (search) => {
if (searchValue?.length <= 0 && search)
return alert("Please Enter Search Text")
// SET THE LOADER TO TURE
setLoading(true)
// GET THE LESSONS FROM THE API
client.getLessons(searchValue).then((data) => {
setLoading(false)
// SET THE LESSONS DATA
setLessons(data?.results)
});
}
if (loading)
return <Loader />
return (
<>
<section className="bg-black ">
<DashboardHeader />
<div className="container px-5 py-12 mx-auto lg:px-20">
<div className="flex flex-col flex-wrap pb-6 mb-12 text-white ">
<h1 className="mb-6 text-3xl font-medium text-white">
Lessons learned
</h1>
{/* <!-- This is an example component --> */}
<div className="container flex justify-center items-center mb-6">
<div className="relative w-full max-w-xs m-auto">
<input
type="text"
onChange={(e) => setSearchValue(e.target.value)}
className={`text-teal-500 z-20 hover:text-teal-700 h-14 w-full max-w-xs m-auto pr-8 pl-5 rounded z-0 focus:shadow focus:outline-none`} placeholder="Search lessons..." />
<div className="absolute top-2 right-2">
<button onClick={() => fetchLessons(true)} className="h-10 w-20 text-white rounded bg-teal-500 hover:bg-teal-600">Search</button>
</div>
</div>
</div>
{/* <p className="text-base leading-relaxed">
Sample lessons...</p> */}
<div className="mainViewport">
<LessonTable
lessons={lessons}
/>
</div>
</div>
</div>
<Footer />
</section>
</>
)
}
export default Home;

View File

@ -0,0 +1,96 @@
import React, {useState} from 'react';
import DashboardHeader from "../../components/DashboardHeader";
import {Link, useNavigate} from "react-router-dom";
import FastAPIClient from '../../client';
import config from '../../config';
import Button from '../../components/Button/Button';
import FormInput from '../../components/FormInput/FormInput';
const client = new FastAPIClient(config);
const Login = () => {
const [error, setError] = useState({email: "", password: ""});
const [loginForm, setLoginForm] = useState({ email: '', password: '' });
const [loading, setLoading] = useState(false)
const navigate = useNavigate()
const onLogin = (e) => {
e.preventDefault();
setError(false);
setLoading(true)
if(loginForm.email.length <= 0)
{
setLoading(false)
return setError({email: "Please Enter Email Address"})
}
if(loginForm.password.length <= 0)
{
setLoading(false)
return setError({password: "Please Enter Password"})
}
client.login(loginForm.email, loginForm.password)
.then( () => {
navigate('/my-recipes')
})
.catch( (err) => {
setLoading(false)
setError(true);
console.err(err)
});
}
return (
<>
<section className="bg-black ">
<DashboardHeader />
<div className="flex items-center justify-center min-h-screen bg-gray-100 text-left ">
<div className="w-full max-w-xs m-auto bg-indigo-100 rounded p-5 shadow-lg">
<header>
{/* <img className="w-20 mx-auto mb-5" src="https://img.icons8.com/fluent/344/year-of-tiger.png" /> */}
<div className="flex items-center justify-center w-20 h-20 mx-auto mb-5 bg-teal-500 rounded-full ">
<svg className=" h-8 w-8" width="54" height="54" viewBox="0 0 54 54" fill='white' xmlns="http://www.w3.org/2000/svg" >
<path d="M13.5 22.1c1.8-7.2 6.3-10.8 13.5-10.8 10.8 0 12.15 8.1 17.55 9.45 3.6.9 6.75-.45 9.45-4.05-1.8 7.2-6.3 10.8-13.5 10.8-10.8 0-12.15-8.1-17.55-9.45-3.6-.9-6.75.45-9.45 4.05zM0 38.3c1.8-7.2 6.3-10.8 13.5-10.8 10.8 0 12.15 8.1 17.55 9.45 3.6.9 6.75-.45 9.45-4.05-1.8 7.2-6.3 10.8-13.5 10.8-10.8 0-12.15-8.1-17.55-9.45-3.6-.9-6.75.45-9.45 4.05z"/>
</svg>
</div>
</header>
<form onSubmit={(e) => onLogin(e)}>
<FormInput
type={"text"}
name={"email"}
label={"Email"}
error={error.email}
value={loginForm.email}
onChange={(e) => setLoginForm({...loginForm, email: e.target.value })}
/>
<FormInput
type={"password"}
name={"password"}
label={"Password"}
error={error.password}
value={loginForm.password}
onChange={(e) => setLoginForm({...loginForm, password: e.target.value })}
/>
<Button
title={"Login"}
loading={loading}
error={error.password}
/>
</form>
<footer>
<Link className="text-teal-700 hover:text-blue-900 text-sm float-right" to="/sign-up">Create Account</Link>
</footer>
</div>
</div>
</section>
</>
)
}
export default Login;

View File

@ -0,0 +1,14 @@
import React from "react"
import {Link} from "react-router-dom"
export const NotLoggedIn = () =>
<div className="flex flex-col bg-black" id="notfound">
<div className="notfound text-white">
<div className="notfound-404">
<h1 className="mt-4">Oops!</h1>
<h2>Login To Access The Page</h2>
</div>
<Link to="/login" className="rounded" >Go TO LOGIN</Link>
</div>
</div>

View File

@ -0,0 +1,200 @@
import React, { useEffect, useState } from "react";
import FastAPIClient from "../../client";
import config from "../../config";
import DashboardHeader from "../../components/DashboardHeader";
import Footer from "../../components/Footer";
import jwtDecode from "jwt-decode";
import * as moment from "moment";
import LessonTable from "../../components/LessonTable";
import FormInput from "../../components/FormInput/FormInput";
import Button from "../../components/Button/Button";
import { NotLoggedIn } from "./NotLoggedIn";
import Loader from "../../components/Loader";
import PopupModal from "../../components/Modal/PopupModal";
const client = new FastAPIClient(config);
const ProfileView = ({ lessons }) => {
return (
<>
<LessonTable
lessons={lessons}
showUpdate={true}
/>
</>
);
};
const LessonDashboard = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [error, setError] = useState({ title: "", content: "", tags: "" });
const [lessonForm, setLessonForm] = useState({
title: "",
content: "",
tags: "",
});
const [showForm, setShowForm] = useState(false);
const [lessons, setLessons] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(true);
useEffect(() => {
fetchUserLessons();
}, []);
const fetchUserLessons = () => {
client.getUserLessons().then((data) => {
setRefreshing(false);
setLessons(data?.results);
});
};
const contentPatternValidation = () => {
return true;
};
const onCreateLesson = (e) => {
e.preventDefault();
setLoading(true);
setError(false);
if (lessonForm.title.length <= 0) {
setLoading(false);
return setError({ title: "Please Enter Lesson Title" });
}
if (lessonForm.content.length <= 0) {
setLoading(false);
return setError({ content: "Please Enter Lesson Content" });
}
if (!contentPatternValidation(lessonForm.content)) {
setLoading(false);
return setError({ content: "Please Enter Valid Content" });
}
if (lessonForm.tags.length <= 0) {
setLoading(false);
return setError({ tags: "Please Enter Lesson Tags" });
}
client.fetchUser().then((user) => {
client
.createLesson(
lessonForm.title,
lessonForm.content,
lessonForm.tags,
user?.id
)
// eslint-disable-next-line no-unused-vars
.then((data) => { // eslint:ignore
fetchUserLessons();
setLoading(false);
setShowForm(false);
});
});
};
useEffect(() => {
const tokenString = localStorage.getItem("token");
if (tokenString) {
const token = JSON.parse(tokenString);
const decodedAccessToken = jwtDecode(token.access_token);
if (moment.unix(decodedAccessToken.exp).toDate() > new Date()) {
setIsLoggedIn(true);
}
}
}, []);
if (refreshing) return !isLoggedIn ? <NotLoggedIn /> : <Loader />;
return (
<>
<section
className="flex flex-col bg-black text-center"
style={{ minHeight: "100vh" }}
>
<DashboardHeader />
<div className="container px-5 pt-6 text-center mx-auto lg:px-20">
{/*TODO - move to component*/}
<h1 className="mb-12 text-3xl font-medium text-white">
Lessons Learned
</h1>
<button
className="my-5 text-white bg-teal-500 p-3 rounded"
onClick={() => {
setShowForm(!showForm);
}}
>
Create Lesson
</button>
<p className="text-base leading-relaxed text-white">Latest lessons</p>
<div className="mainViewport text-white">
{lessons.length && (
<ProfileView
lessons={lessons}
fetchUserLessons={fetchUserLessons}
/>
)}
</div>
</div>
<Footer />
</section>
{showForm && (
<PopupModal
modalTitle={"Create Lesson"}
onCloseBtnPress={() => {
setShowForm(false);
setError({ fullName: "", email: "", password: "" });
}}
>
<div className="mt-4 text-left">
<form className="mt-5" onSubmit={(e) => onCreateLesson(e)}>
<FormInput
type={"text"}
name={"title"}
label={"Title"}
error={error.title}
value={lessonForm.title}
onChange={(e) =>
setLessonForm({ ...lessonForm, title: e.target.value })
}
/>
<FormInput
type={"text"}
name={"content"}
label={"Content"}
error={error.content}
value={lessonForm.content}
onChange={(e) =>
setLessonForm({ ...lessonForm, content: e.target.value })
}
/>
<FormInput
type={"text"}
name={"tags"}
label={"Tags"}
error={error.tags}
value={lessonForm.tags}
onChange={(e) =>
setLessonForm({ ...lessonForm, tags: e.target.value })
}
/>
<Button
loading={loading}
error={error.tags}
title={"Create Lesson"}
/>
</form>
</div>
</PopupModal>
)}
</>
);
};
export default LessonDashboard;

View File

@ -0,0 +1,105 @@
import React, {useState} from 'react';
import DashboardHeader from "../../components/DashboardHeader";
import {Link, useNavigate} from "react-router-dom";
import FastAPIClient from '../../client';
import config from '../../config';
import Button from '../../components/Button/Button';
import FormInput from '../../components/FormInput/FormInput';
const client = new FastAPIClient(config);
const SignUp = () => {
const [error, setError] = useState({ email: '', password: '', fullName: '' });
const [registerForm, setRegisterForm] = useState({ email: '', password: '', fullName: '' });
const [loading, setLoading] = useState(false)
const navigate = useNavigate()
const onRegister = (e) => {
e.preventDefault();
setLoading(true)
setError(false);
if(registerForm.fullName.length <= 0)
{
setLoading(false)
return setError({fullName: "Please Enter Your Full Name"})
}
if(registerForm.email.length <= 0)
{
setLoading(false)
return setError({email: "Please Enter Email Address"})
}
if(registerForm.password.length <= 0)
{
setLoading(false)
return setError({password: "Please Enter Password"})
}
client.register(registerForm.email, registerForm.password, registerForm.fullName)
.then( () => {
navigate('/login')
})
.catch( (err) => {
setLoading(false)
setError(true);
alert(err)
});
}
return (
<>
<section className="bg-black ">
<DashboardHeader />
<div className="flex items-center justify-center min-h-screen bg-gray-100 text-left ">
<div className="w-full max-w-xs m-auto bg-indigo-100 rounded p-5 shadow-lg">
<header>
{/* <img className="w-20 mx-auto mb-5" src="https://img.icons8.com/fluent/344/year-of-tiger.png" /> */}
<div className="flex items-center justify-center w-20 h-20 mx-auto mb-5 bg-teal-500 rounded-full ">
<svg className=" h-8 w-8" width="54" height="54" viewBox="0 0 54 54" fill='white' xmlns="http://www.w3.org/2000/svg" >
<path d="M13.5 22.1c1.8-7.2 6.3-10.8 13.5-10.8 10.8 0 12.15 8.1 17.55 9.45 3.6.9 6.75-.45 9.45-4.05-1.8 7.2-6.3 10.8-13.5 10.8-10.8 0-12.15-8.1-17.55-9.45-3.6-.9-6.75.45-9.45 4.05zM0 38.3c1.8-7.2 6.3-10.8 13.5-10.8 10.8 0 12.15 8.1 17.55 9.45 3.6.9 6.75-.45 9.45-4.05-1.8 7.2-6.3 10.8-13.5 10.8-10.8 0-12.15-8.1-17.55-9.45-3.6-.9-6.75.45-9.45 4.05z"/>
</svg>
</div>
</header>
<form onSubmit={(e) => onRegister(e)}>
<FormInput
type={"text"}
name={"fullName"}
label={"Full Name"}
error={error.fullName}
value={registerForm.fullName}
onChange={(e) => setRegisterForm({...registerForm, fullName: e.target.value })}
/>
<FormInput
type={"email"}
name={"email"}
label={"Email"}
error={error.email}
value={registerForm.email}
onChange={(e) => setRegisterForm({...registerForm, email: e.target.value })}
/>
<FormInput
type={"password"}
name={"password"}
label={"Password"}
error={error.password}
value={registerForm.password}
onChange={(e) => setRegisterForm({...registerForm, password: e.target.value })}
/>
<Button title={"Create Account"} error={error.password} loading={loading} />
</form>
<footer>
<Link className="text-teal-700 hover:text-blue-900 text-sm float-right" to="/login">Already Have an account ?</Link>
</footer>
</div>
</div>
</section>
</>
)
}
export default SignUp;

View File

@ -0,0 +1,23 @@
module.exports = {
content: [
"./src/**/*.{js,jsx}",
],
theme: {
extend: {
keyframes: {
'fade-in-down': {
'0%': {
opacity: '0',
},
'100%': {
opacity: '1',
},
}
},
animation: {
'fade-in-down': 'fade-in-down 0.5s ease-out'
}
},
},
plugins: [],
}