From 88eeb772725c2740765e768011ea751120ce641e Mon Sep 17 00:00:00 2001 From: drafish Date: Wed, 17 Jan 2024 13:59:10 +0800 Subject: [PATCH] add learneth plugin --- apps/learneth/project.json | 58 +++++ apps/learneth/src/App.css | 19 ++ apps/learneth/src/App.test.tsx | 15 ++ apps/learneth/src/App.tsx | 51 ++++ apps/learneth/src/assets/.gitkeep | 0 .../Font_Awesome_5_solid_book-reader.svg | 5 + apps/learneth/src/assets/logo-background.svg | 1 + .../src/components/BackButton/index.scss | 55 ++++ .../src/components/BackButton/index.tsx | 91 +++++++ .../src/components/LoadingScreen/index.css | 17 ++ .../src/components/LoadingScreen/index.tsx | 16 ++ .../src/components/RepoImporter/index.css | 4 + .../src/components/RepoImporter/index.tsx | 165 ++++++++++++ .../learneth/src/components/SlideIn/index.css | 21 ++ .../learneth/src/components/SlideIn/index.tsx | 18 ++ apps/learneth/src/index.css | 13 + apps/learneth/src/index.html | 18 ++ apps/learneth/src/logo.svg | 1 + apps/learneth/src/main.tsx | 15 ++ apps/learneth/src/pages/Home/index.css | 23 ++ apps/learneth/src/pages/Home/index.tsx | 136 ++++++++++ apps/learneth/src/pages/Logo/index.css | 5 + apps/learneth/src/pages/Logo/index.tsx | 26 ++ apps/learneth/src/pages/StepDetail/index.scss | 59 +++++ apps/learneth/src/pages/StepDetail/index.tsx | 246 ++++++++++++++++++ apps/learneth/src/pages/StepList/index.scss | 152 +++++++++++ apps/learneth/src/pages/StepList/index.tsx | 44 ++++ apps/learneth/src/polyfills.ts | 7 + apps/learneth/src/profile.json | 21 ++ apps/learneth/src/react-app-env.d.ts | 1 + apps/learneth/src/redux/hooks.ts | 5 + apps/learneth/src/redux/models/loading.ts | 14 + apps/learneth/src/redux/models/remixide.ts | 233 +++++++++++++++++ apps/learneth/src/redux/models/workshop.ts | 179 +++++++++++++ apps/learneth/src/redux/store.ts | 117 +++++++++ apps/learneth/src/remix-client.ts | 38 +++ apps/learneth/src/setupTests.ts | 5 + apps/learneth/tsconfig.app.json | 23 ++ apps/learneth/tsconfig.json | 16 ++ apps/learneth/webpack.config.js | 92 +++++++ apps/remix-ide/project.json | 2 +- package.json | 9 + yarn.lock | 241 ++++++++++++++++- 43 files changed, 2274 insertions(+), 3 deletions(-) create mode 100644 apps/learneth/project.json create mode 100644 apps/learneth/src/App.css create mode 100644 apps/learneth/src/App.test.tsx create mode 100644 apps/learneth/src/App.tsx create mode 100644 apps/learneth/src/assets/.gitkeep create mode 100644 apps/learneth/src/assets/Font_Awesome_5_solid_book-reader.svg create mode 100644 apps/learneth/src/assets/logo-background.svg create mode 100644 apps/learneth/src/components/BackButton/index.scss create mode 100644 apps/learneth/src/components/BackButton/index.tsx create mode 100644 apps/learneth/src/components/LoadingScreen/index.css create mode 100644 apps/learneth/src/components/LoadingScreen/index.tsx create mode 100644 apps/learneth/src/components/RepoImporter/index.css create mode 100644 apps/learneth/src/components/RepoImporter/index.tsx create mode 100644 apps/learneth/src/components/SlideIn/index.css create mode 100644 apps/learneth/src/components/SlideIn/index.tsx create mode 100644 apps/learneth/src/index.css create mode 100644 apps/learneth/src/index.html create mode 100644 apps/learneth/src/logo.svg create mode 100644 apps/learneth/src/main.tsx create mode 100644 apps/learneth/src/pages/Home/index.css create mode 100644 apps/learneth/src/pages/Home/index.tsx create mode 100644 apps/learneth/src/pages/Logo/index.css create mode 100644 apps/learneth/src/pages/Logo/index.tsx create mode 100644 apps/learneth/src/pages/StepDetail/index.scss create mode 100644 apps/learneth/src/pages/StepDetail/index.tsx create mode 100644 apps/learneth/src/pages/StepList/index.scss create mode 100644 apps/learneth/src/pages/StepList/index.tsx create mode 100644 apps/learneth/src/polyfills.ts create mode 100644 apps/learneth/src/profile.json create mode 100644 apps/learneth/src/react-app-env.d.ts create mode 100644 apps/learneth/src/redux/hooks.ts create mode 100644 apps/learneth/src/redux/models/loading.ts create mode 100644 apps/learneth/src/redux/models/remixide.ts create mode 100644 apps/learneth/src/redux/models/workshop.ts create mode 100644 apps/learneth/src/redux/store.ts create mode 100644 apps/learneth/src/remix-client.ts create mode 100644 apps/learneth/src/setupTests.ts create mode 100644 apps/learneth/tsconfig.app.json create mode 100644 apps/learneth/tsconfig.json create mode 100644 apps/learneth/webpack.config.js diff --git a/apps/learneth/project.json b/apps/learneth/project.json new file mode 100644 index 0000000000..4dfbec33e1 --- /dev/null +++ b/apps/learneth/project.json @@ -0,0 +1,58 @@ +{ + "name": "learneth", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/learneth/src", + "projectType": "application", + "implicitDependencies": [], + "targets": { + "build": { + "executor": "@nrwl/webpack:webpack", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "development", + "options": { + "compiler": "babel", + "outputPath": "dist/apps/learneth", + "index": "apps/learneth/src/index.html", + "baseHref": "./", + "main": "apps/learneth/src/main.tsx", + "polyfills": "apps/learneth/src/polyfills.ts", + "tsConfig": "apps/learneth/tsconfig.app.json", + "assets": ["apps/learneth/src/profile.json", "apps/learneth/src/assets/Font_Awesome_5_solid_book-reader.svg"], + "styles": ["apps/learneth/src/index.css"], + "scripts": [], + "webpackConfig": "apps/learneth/webpack.config.js" + }, + "configurations": { + "development": { + }, + "production": { + "fileReplacements": [ + { + "replace": "apps/learneth/src/environments/environment.ts", + "with": "apps/learneth/src/environments/environment.prod.ts" + } + ] + } + } + }, + "serve": { + "executor": "@nrwl/webpack:dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "learneth:build", + "hmr": true, + "baseHref": "/" + }, + "configurations": { + "development": { + "buildTarget": "learneth:build:development", + "port": 2023 + }, + "production": { + "buildTarget": "learneth:build:production" + } + } + } + }, + "tags": [] +} diff --git a/apps/learneth/src/App.css b/apps/learneth/src/App.css new file mode 100644 index 0000000000..bf3b1f7aa8 --- /dev/null +++ b/apps/learneth/src/App.css @@ -0,0 +1,19 @@ +/* You can add global styles to this file, and also import other style files */ + + +h1{ + font-size: 1.2rem !important; + font-weight: 700; +} +h2{ + font-size: 1rem !important; + font-weight: 700; +} +h3{ + font-size: 1rem !important; +} + +p { + font-size: 0.9rem; +} + diff --git a/apps/learneth/src/App.test.tsx b/apps/learneth/src/App.test.tsx new file mode 100644 index 0000000000..701a48420e --- /dev/null +++ b/apps/learneth/src/App.test.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { render, screen } from '@testing-library/react'; +import App from './App'; +import { store } from './redux/store'; + +test('renders learn react link', () => { + render( + + + , + ); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/apps/learneth/src/App.tsx b/apps/learneth/src/App.tsx new file mode 100644 index 0000000000..8582be5a75 --- /dev/null +++ b/apps/learneth/src/App.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { ToastContainer } from 'react-toastify'; +import LoadingScreen from './components/LoadingScreen'; +import LogoPage from './pages/Logo'; +import HomePage from './pages/Home'; +import StepListPage from './pages/StepList'; +import StepDetailPage from './pages/StepDetail'; +import 'react-toastify/dist/ReactToastify.css'; +import './App.css'; + +export const router = createBrowserRouter([ + { + path: '/', + element: , + }, + { + path: '/home', + element: , + }, + { + path: '/list', + element: , + }, + { + path: '/detail', + element: , + }, +]); + +function App(): JSX.Element { + return ( + <> + + + + + ); +} + +export default App; diff --git a/apps/learneth/src/assets/.gitkeep b/apps/learneth/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/learneth/src/assets/Font_Awesome_5_solid_book-reader.svg b/apps/learneth/src/assets/Font_Awesome_5_solid_book-reader.svg new file mode 100644 index 0000000000..2e31d1ca18 --- /dev/null +++ b/apps/learneth/src/assets/Font_Awesome_5_solid_book-reader.svg @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/apps/learneth/src/assets/logo-background.svg b/apps/learneth/src/assets/logo-background.svg new file mode 100644 index 0000000000..cbe3eaec20 --- /dev/null +++ b/apps/learneth/src/assets/logo-background.svg @@ -0,0 +1 @@ +remix_logo1 \ No newline at end of file diff --git a/apps/learneth/src/components/BackButton/index.scss b/apps/learneth/src/components/BackButton/index.scss new file mode 100644 index 0000000000..7f022076f7 --- /dev/null +++ b/apps/learneth/src/components/BackButton/index.scss @@ -0,0 +1,55 @@ +a { + + .arrow { + display: inline-block; + opacity: 0; + transform: scale(0.5); + transition: all 0.3s; + } + span { + display: inline-block; + padding-left: 5px; + transform: translateX(-0.875em); // size of icon + transition: transform 0.3s; + } + + + +} + +.workshoptitle{ + text-decoration: none; +} + +a:hover { + fa-icon { + opacity: 1; + transform: scale(1); + } + span { + transform: translateX(0); + } +} + +// .btn-close{ +// --bs-btn-close-color: #000; +// --bs-btn-close-bg: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3E%3C/svg%3E"); +// --bs-btn-close-opacity: 0.5; +// --bs-btn-close-hover-opacity: 0.75; +// --bs-btn-close-focus-shadow: 0 0 0 0.25rem #0d6efd40; +// --bs-btn-close-focus-opacity: 1; +// --bs-btn-close-disabled-opacity: 0.25; +// --bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%); +// background: #0000 var(--bs-btn-close-bg) center/1em auto no-repeat; +// border: 0; +// border-radius: .375rem; +// box-sizing: initial; +// height: 1em; +// opacity: var(--bs-btn-close-opacity); +// padding: .25em; +// width: 1em +// } + +// [data-bs-theme=dark] .btn-close { +// filter: var(--bs-btn-close-white-filter); +// } diff --git a/apps/learneth/src/components/BackButton/index.tsx b/apps/learneth/src/components/BackButton/index.tsx new file mode 100644 index 0000000000..22a2fbfff4 --- /dev/null +++ b/apps/learneth/src/components/BackButton/index.tsx @@ -0,0 +1,91 @@ +import React, {useState} from 'react' +import {Link, useNavigate} from 'react-router-dom' +import {Button, Modal, Tooltip, OverlayTrigger} from 'react-bootstrap' +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' +import {faHome, faBars, faChevronLeft, faChevronRight} from '@fortawesome/free-solid-svg-icons' +// import {useAppSelector} from '../../redux/hooks' +import './index.scss' + +function BackButton({entity}: any) { + const navigate = useNavigate() + const [show, setShow] = useState(false) + // const theme = useAppSelector((state) => state.remixide.theme) + const isDetailPage = location.pathname === '/detail' + const queryParams = new URLSearchParams(location.search) + const stepId = Number(queryParams.get('stepId')) + + return ( + + ) +} + +export default BackButton diff --git a/apps/learneth/src/components/LoadingScreen/index.css b/apps/learneth/src/components/LoadingScreen/index.css new file mode 100644 index 0000000000..88d2a5a3c7 --- /dev/null +++ b/apps/learneth/src/components/LoadingScreen/index.css @@ -0,0 +1,17 @@ +.spinnersOverlay { + background-color: rgba(51, 51, 51, 0.8); + z-index: 99; + opacity: 1; + height: 100%; + left: 0; + position: fixed; + top: 0; + width: 100%; +} +.spinnersLoading { + left: 50%; + margin: 0; + position: absolute; + top: 50%; + transform: translate(-50%,-50%); +} diff --git a/apps/learneth/src/components/LoadingScreen/index.tsx b/apps/learneth/src/components/LoadingScreen/index.tsx new file mode 100644 index 0000000000..43c27169ea --- /dev/null +++ b/apps/learneth/src/components/LoadingScreen/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import BounceLoader from 'react-spinners/BounceLoader'; +import './index.css'; +import { useAppSelector } from '../../redux/hooks'; + +const LoadingScreen: React.FC = () => { + const loading = useAppSelector((state) => state.loading.screen); + + return loading ? ( +
+ +
+ ) : null; +}; + +export default LoadingScreen; diff --git a/apps/learneth/src/components/RepoImporter/index.css b/apps/learneth/src/components/RepoImporter/index.css new file mode 100644 index 0000000000..e46d1a9675 --- /dev/null +++ b/apps/learneth/src/components/RepoImporter/index.css @@ -0,0 +1,4 @@ +.arrow-icon{ + width: 3px; + display: inline-block; +} diff --git a/apps/learneth/src/components/RepoImporter/index.tsx b/apps/learneth/src/components/RepoImporter/index.tsx new file mode 100644 index 0000000000..db00221fe8 --- /dev/null +++ b/apps/learneth/src/components/RepoImporter/index.tsx @@ -0,0 +1,165 @@ +import React, { useState, useEffect } from 'react'; +import { + Button, + Dropdown, + Form, + Tooltip, + OverlayTrigger, +} from 'react-bootstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faQuestionCircle, + faInfoCircle, + faChevronRight, + faChevronDown, +} from '@fortawesome/free-solid-svg-icons'; +import { useAppDispatch } from '../../redux/hooks'; +import './index.css'; + +function RepoImporter({ list, selectedRepo }: any): JSX.Element { + const [open, setOpen] = useState(false); + const [name, setName] = useState(''); + const [branch, setBranch] = useState(''); + const dispatch = useAppDispatch(); + + useEffect(() => { + setName(selectedRepo.name); + setBranch(selectedRepo.branch); + }, [selectedRepo]); + + const panelChange = () => { + setOpen(!open); + }; + + const selectRepo = (repo: { name: string; branch: string }) => { + dispatch({ type: 'workshop/loadRepo', payload: repo }); + }; + + const importRepo = (event: { preventDefault: () => void }) => { + event.preventDefault(); + dispatch({ type: 'workshop/loadRepo', payload: { name, branch } }); + }; + + const resetAll = () => { + dispatch({ type: 'workshop/resetAll' }); + setName(''); + setBranch(''); + }; + + return ( + <> + {selectedRepo.name && ( +
+ Tutorials from: +

{selectedRepo.name}

+ + Date modified:{' '} + {new Date(selectedRepo.datemodified).toLocaleString()} + +
+ )} + +
+
+ +
+
Import another tutorial repo
+
+ + {open && ( +
+ + + Select a repo + + + {list.map((item: any) => ( + { + selectRepo(item); + }} + > + {item.name}-{item.branch} + + ))} + + +
+ reset list +
+
+ )} + +
+ {open && ( +
+ + + REPO + + ie username/repository + } + > + + + { + setName(e.target.value); + }} + value={name} + /> + BRANCH + { + setBranch(e.target.value); + }} + value={branch} + /> + + + + how to setup your repo + +
+ )} +
+
+ + ); +} + +export default RepoImporter; diff --git a/apps/learneth/src/components/SlideIn/index.css b/apps/learneth/src/components/SlideIn/index.css new file mode 100644 index 0000000000..4f6e45324f --- /dev/null +++ b/apps/learneth/src/components/SlideIn/index.css @@ -0,0 +1,21 @@ +.slide-enter { + transform: translateY(100px); + opacity: 0; +} + +.slide-enter-active { + transform: translateY(0); + opacity: 1; + transition: opacity 400ms, transform 400ms cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.slide-exit { + transform: translateY(0); + opacity: 1; +} + +.slide-exit-active { + transform: translateY(100px); + opacity: 0; + transition: opacity 400ms, transform 400ms cubic-bezier(0.6, 0.04, 0.98, 0.335); +} diff --git a/apps/learneth/src/components/SlideIn/index.tsx b/apps/learneth/src/components/SlideIn/index.tsx new file mode 100644 index 0000000000..c4830b6688 --- /dev/null +++ b/apps/learneth/src/components/SlideIn/index.tsx @@ -0,0 +1,18 @@ +import React, { type ReactNode, useEffect, useState } from 'react'; +import { CSSTransition } from 'react-transition-group'; +import './index.css'; + +const SlideIn: React.FC<{ children: ReactNode }> = ({ children }) => { + const [show, setShow] = useState(false); + useEffect(() => { + setShow(true); + }, []); + + return ( + + {children} + + ); +}; + +export default SlideIn; diff --git a/apps/learneth/src/index.css b/apps/learneth/src/index.css new file mode 100644 index 0000000000..ec2585e8c0 --- /dev/null +++ b/apps/learneth/src/index.css @@ -0,0 +1,13 @@ +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; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/apps/learneth/src/index.html b/apps/learneth/src/index.html new file mode 100644 index 0000000000..21ecda7ec6 --- /dev/null +++ b/apps/learneth/src/index.html @@ -0,0 +1,18 @@ + + + + + Learn ETH + + + + + + + + +
+ + + diff --git a/apps/learneth/src/logo.svg b/apps/learneth/src/logo.svg new file mode 100644 index 0000000000..9dfc1c058c --- /dev/null +++ b/apps/learneth/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/learneth/src/main.tsx b/apps/learneth/src/main.tsx new file mode 100644 index 0000000000..eac81ff2eb --- /dev/null +++ b/apps/learneth/src/main.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { Provider } from 'react-redux'; +import './index.css'; +import App from './App'; +import { store } from './redux/store'; + +const root = ReactDOM.createRoot( + document.getElementById('root') as HTMLElement, +); +root.render( + + + , +); diff --git a/apps/learneth/src/pages/Home/index.css b/apps/learneth/src/pages/Home/index.css new file mode 100644 index 0000000000..2375651475 --- /dev/null +++ b/apps/learneth/src/pages/Home/index.css @@ -0,0 +1,23 @@ +.description-collapsed{ + height: 0px; + overflow: hidden; + word-wrap: break-word; + padding: 0px !important; + margin: 0px !important; +} + +.tag{ + display: inline; +} + +.arrow-icon{ + width: 12px; + display: inline-block; +} + +.workshop-link { + text-decoration: none; +} +.workshop-link:hover { + text-decoration: underline; +} diff --git a/apps/learneth/src/pages/Home/index.tsx b/apps/learneth/src/pages/Home/index.tsx new file mode 100644 index 0000000000..73ddb3e024 --- /dev/null +++ b/apps/learneth/src/pages/Home/index.tsx @@ -0,0 +1,136 @@ +import React, { useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faChevronRight, + faChevronDown, + faPlayCircle, +} from '@fortawesome/free-solid-svg-icons'; +import Markdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; +import { useAppDispatch, useAppSelector } from '../../redux/hooks'; +import RepoImporter from '../../components/RepoImporter'; +import './index.css'; + +function HomePage(): JSX.Element { + const [openKeys, setOpenKeys] = React.useState([]); + + const isOpen = (key: string) => openKeys.includes(key); + const handleClick = (key: string) => { + setOpenKeys( + isOpen(key) + ? openKeys.filter((item) => item !== key) + : [...openKeys, key], + ); + }; + + const dispatch = useAppDispatch(); + const { list, detail, selectedId } = useAppSelector( + (state) => state.workshop, + ); + + const selectedRepo = detail[selectedId]; + + const levelMap: any = { + 1: 'Beginner', + 2: 'Intermediate', + 3: 'Advanced', + }; + + useEffect(() => { + dispatch({ + type: 'workshop/init', + }); + }, []); + + return ( +
+ + {selectedRepo && ( +
+ {Object.keys(selectedRepo.group).map((level) => ( +
+
{levelMap[level]}:
+ {selectedRepo.group[level].map((item: any) => ( +
+ +
+ {levelMap[level] && ( +

+ {levelMap[level]} +

+ )} + + {selectedRepo.entities[item.id].metadata.data.tags?.map( + (tag: string) => ( +

+ {tag} +

+ ), + )} + + {selectedRepo.entities[item.id].steps && ( +
+ {selectedRepo.entities[item.id].steps.length} step(s) +
+ )} + +
+ + {selectedRepo.entities[item.id].description?.content} + +
+ +
+
+
+
+ ))} +
+ ))} +
+ )} +
+ ); +} + +export default HomePage; diff --git a/apps/learneth/src/pages/Logo/index.css b/apps/learneth/src/pages/Logo/index.css new file mode 100644 index 0000000000..7a827cacb8 --- /dev/null +++ b/apps/learneth/src/pages/Logo/index.css @@ -0,0 +1,5 @@ +.remixLogo { + position: absolute; + left: 0px; + right: 0px; +} diff --git a/apps/learneth/src/pages/Logo/index.tsx b/apps/learneth/src/pages/Logo/index.tsx new file mode 100644 index 0000000000..e62a814798 --- /dev/null +++ b/apps/learneth/src/pages/Logo/index.tsx @@ -0,0 +1,26 @@ +import React, {useEffect} from 'react' +// import remixClient from '../../remix-client'; +import {useAppDispatch} from '../../redux/hooks' +import logo from '../../assets/logo-background.svg' +import './index.css' + +const LogoPage: React.FC = () => { + const dispatch = useAppDispatch() + + useEffect(() => { + dispatch({type: 'remixide/connect'}) + // remixClient.on('theme', 'themeChanged', (theme: any) => { + // dispatch({ type: 'remixide/save', payload: { theme: theme.quality } }); + // }); + }, []) + + return ( +
+
+ +
+
+ ) +} + +export default LogoPage diff --git a/apps/learneth/src/pages/StepDetail/index.scss b/apps/learneth/src/pages/StepDetail/index.scss new file mode 100644 index 0000000000..e135826c8f --- /dev/null +++ b/apps/learneth/src/pages/StepDetail/index.scss @@ -0,0 +1,59 @@ +step-view { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + overflow: hidden; +} + +header, footer { + padding: 10px 5px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.menuspacer{ + // padding-top: 48px; + +} + +.errorloadingspacer{ + padding-top: 44px; + +} + +.title{ + pointer-events: none; +} + +h1 { + text-align: left; + font-size: 1.2rem !important; + word-break: break-word; +} + +markdown { + display: block; + flex: 1; + overflow: auto; + padding: 0px; + + h1 { + font-size: 1.2rem !important; + } + + h2 { + font-size: 1rem; + } + + h3 { + font-size: 1rem; + } + + h4 { + font-size: 1rem; + } +} + + diff --git a/apps/learneth/src/pages/StepDetail/index.tsx b/apps/learneth/src/pages/StepDetail/index.tsx new file mode 100644 index 0000000000..38ec41cec7 --- /dev/null +++ b/apps/learneth/src/pages/StepDetail/index.tsx @@ -0,0 +1,246 @@ +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import Markdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import BackButton from '../../components/BackButton'; +import { useAppSelector, useAppDispatch } from '../../redux/hooks'; +import './index.scss'; + +function StepDetailPage() { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const queryParams = new URLSearchParams(location.search); + const id = queryParams.get('id') as string; + const stepId = Number(queryParams.get('stepId')); + const { + workshop: { detail, selectedId }, + remixide: { errorLoadingFile, errors, success }, + } = useAppSelector((state: any) => state); + const entity = detail[selectedId].entities[id]; + const steps = entity.steps; + const step = steps[stepId]; + console.log(step); + + useEffect(() => { + dispatch({ + type: 'remixide/displayFile', + payload: step, + }); + dispatch({ + type: 'remixide/save', + payload: { errors: [], success: false }, + }); + window.scrollTo(0, 0); + }, [step]); + + useEffect(() => { + if (errors.length > 0 || success) { + window.scrollTo(0, document.documentElement.scrollHeight); + } + }, [errors, success]); + + return ( + <> +
+
+ +
+
+
+ {errorLoadingFile ? ( + <> +
+

{step.name}

+ +
+ + ) : ( + <> +
+

{step.name}

+ + )} +
+ + {step.markdown?.content} + +
+ {step.test?.content ? ( + <> + + {success && ( + + )} +
+ {success && ( +
+ Well done! No errors. +
+ )} + {errors.length > 0 && ( + <> + {!success && ( +
+ Errors +
+ )} + {errors.map((error: string, index: number) => ( +
+ {error} +
+ ))} + + )} +
+ + ) : ( + <> + + {stepId < steps.length - 1 && ( + + )} + {stepId === steps.length - 1 && ( + + )} + + )} + + ); +} + +export default StepDetailPage; diff --git a/apps/learneth/src/pages/StepList/index.scss b/apps/learneth/src/pages/StepList/index.scss new file mode 100644 index 0000000000..fcccaf4142 --- /dev/null +++ b/apps/learneth/src/pages/StepList/index.scss @@ -0,0 +1,152 @@ +:host { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +header { + padding: 10px 5px; +} + +.menuspacer{ + margin-top: 52px; + +} + +.steplink { + text-decoration: none; +} + +.title{ + pointer-events: none; +} + +h1 { + text-align: left; + font-size: 1.2rem !important; + word-break: break-word; +} +section { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + + + + + + + + .start { + padding: 5px 25px; + animation: jittery 2s 0.5s infinite; + box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.5); + color: white; + cursor: pointer; + } +} + +footer { + padding: 10px; + display: flex; + justify-content: space-between; + align-items: center; +} + +@keyframes jittery { + 5%, + 50% { + transform: scale(1); + box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.5); + } + 10% { + transform: scale(0.9); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); + } + 15% { + transform: scale(1.15); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); + } + 20% { + transform: scale(1.15) rotate(-5deg); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); + } + 25% { + transform: scale(1.15) rotate(5deg); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); + } + 30% { + transform: scale(1.15) rotate(-3deg); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); + } + 35% { + transform: scale(1.15) rotate(2deg); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); + } + 40% { + transform: scale(1.15) rotate(0); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); + } +} + +.slide-in { + animation: slideIn 0.5s forwards; + visibility: hidden; +} + +@keyframes slideIn { + 0% { + transform: translateY(-100%); + visibility: visible; + } + 100% { + transform: translateY(0); + visibility: visible; + } +} + +@-moz-keyframes slideIn { + 0% { + transform: translateY(-100%); + visibility: visible; + } + 100% { + transform: translateY(0); + visibility: visible; + } +} + +@-webkit-keyframes slideIn { + 0% { + transform: translateY(-100%); + visibility: visible; + } + 100% { + transform: translateY(0); + visibility: visible; + } +} + +@-o-keyframes slideIn { + 0% { + transform: translateY(-100%); + visibility: visible; + } + 100% { + transform: translateY(0); + visibility: visible; + } +} + +@-ms-keyframes slideIn { + 0% { + transform: translateY(-100%); + visibility: visible; + } + 100% { + transform: translateY(0); + visibility: visible; + } +} diff --git a/apps/learneth/src/pages/StepList/index.tsx b/apps/learneth/src/pages/StepList/index.tsx new file mode 100644 index 0000000000..cba5fc583d --- /dev/null +++ b/apps/learneth/src/pages/StepList/index.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import Markdown from 'react-markdown'; +import BackButton from '../../components/BackButton'; +import SlideIn from '../../components/SlideIn'; +import { useAppSelector } from '../../redux/hooks'; +import './index.scss'; + +function StepListPage(): JSX.Element { + const queryParams = new URLSearchParams(location.search); + const id = queryParams.get('id') as string; + const { detail, selectedId } = useAppSelector((state) => state.workshop); + const entity = detail[selectedId].entities[id]; + + return ( + <> +
+
+ +
+
+
+

{entity.name}

+
+ {entity.text} +
+ +
+ {entity.steps.map((step: any, i: number) => ( + + {step.name} ยป + + ))} +
+
+ + ); +} + +export default StepListPage; diff --git a/apps/learneth/src/polyfills.ts b/apps/learneth/src/polyfills.ts new file mode 100644 index 0000000000..53c485753e --- /dev/null +++ b/apps/learneth/src/polyfills.ts @@ -0,0 +1,7 @@ +/** + * Polyfill stable language features. These imports will be optimized by `@babel/preset-env`. + * + * See: https://github.com/zloirock/core-js#babel + */ +import 'core-js/stable' +import 'regenerator-runtime/runtime' diff --git a/apps/learneth/src/profile.json b/apps/learneth/src/profile.json new file mode 100644 index 0000000000..8f381750ef --- /dev/null +++ b/apps/learneth/src/profile.json @@ -0,0 +1,21 @@ +{ + "name": "LearnEth", + "displayName": "LearnEth", + "description": "Learn Ethereum with Remix!", + "documentation": "https://remix-learneth-plugin.readthedocs.io/en/latest/index.html", + "version": "0.1.0", + "methods": [ + "startTutorial", + "addRepository" + ], + "kind": "none", + "icon": "/plugins/learneth/assets/Font_Awesome_5_solid_book-reader.svg", + "location": "sidePanel", + "url": "/plugins/learneth", + "repo": "https://github.com/ethereum/remix-project/tree/master/apps/learneth", + "maintainedBy": "Remix", + "authorContact": "", + "targets": [ + "remix" + ] +} diff --git a/apps/learneth/src/react-app-env.d.ts b/apps/learneth/src/react-app-env.d.ts new file mode 100644 index 0000000000..af825776d5 --- /dev/null +++ b/apps/learneth/src/react-app-env.d.ts @@ -0,0 +1 @@ +import 'react-scripts'; diff --git a/apps/learneth/src/redux/hooks.ts b/apps/learneth/src/redux/hooks.ts new file mode 100644 index 0000000000..c786a3e7ea --- /dev/null +++ b/apps/learneth/src/redux/hooks.ts @@ -0,0 +1,5 @@ +import { useDispatch, type TypedUseSelectorHook, useSelector } from 'react-redux'; +import { type AppDispatch, type RootState } from './store'; + +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/apps/learneth/src/redux/models/loading.ts b/apps/learneth/src/redux/models/loading.ts new file mode 100644 index 0000000000..d553dcce8a --- /dev/null +++ b/apps/learneth/src/redux/models/loading.ts @@ -0,0 +1,14 @@ +import { type ModelType } from '../store'; + +const Model: ModelType = { + namespace: 'loading', + state: { screen: true }, + reducers: { + save(state, { payload }) { + return { ...state, ...payload }; + }, + }, + effects: {}, +}; + +export default Model; diff --git a/apps/learneth/src/redux/models/remixide.ts b/apps/learneth/src/redux/models/remixide.ts new file mode 100644 index 0000000000..bb9f6ba8f6 --- /dev/null +++ b/apps/learneth/src/redux/models/remixide.ts @@ -0,0 +1,233 @@ +import {toast} from 'react-toastify' +import {type ModelType} from '../store' +import remixClient from '../../remix-client' +import {router} from '../../App' + +function getFilePath(file: string): string { + const name = file.split('/') + return name.length > 1 ? `${name[name.length - 1]}` : '' +} + +const Model: ModelType = { + namespace: 'remixide', + state: { + errors: [], + success: false, + errorLoadingFile: false, + // theme: '', + }, + reducers: { + save(state, {payload}) { + return {...state, ...payload} + }, + }, + effects: { + *connect(_, {put}) { + toast.info('connecting to the REMIX IDE') + + yield put({ + type: 'loading/save', + payload: { + screen: true, + }, + }) + + yield remixClient.onload() + + // const theme = yield remixClient.call('theme', 'currentTheme'); + + // yield put({ type: 'remixide/save', payload: { theme: theme.quality } }); + + toast.dismiss() + + yield put({ + type: 'loading/save', + payload: { + screen: false, + }, + }) + + yield router.navigate('/home') + }, + *displayFile({payload: step}, {select, put}) { + let content = '' + let path = '' + if (step.solidity?.file) { + content = step.solidity.content + path = getFilePath(step.solidity.file) + } + if (step.js?.file) { + content = step.js.content + path = getFilePath(step.js.file) + } + if (step.vy?.file) { + content = step.vy.content + path = getFilePath(step.vy.file) + } + + if (!content) { + return + } + + toast.info(`loading ${path} into IDE`) + yield put({ + type: 'loading/save', + payload: { + screen: true, + }, + }) + + const {detail, selectedId} = yield select((state) => state.workshop) + + const workshop = detail[selectedId] + console.log('loading ', step, workshop) + + path = `.learneth/${workshop.name}/${step.name}/${path}` + try { + const isExist = yield remixClient.call('fileManager', 'exists' as any, path) + if (!isExist) { + yield remixClient.call('fileManager', 'setFile', path, content) + } + yield remixClient.call('fileManager', 'switchFile', `${path}`) + yield put({ + type: 'remixide/save', + payload: {errorLoadingFile: false}, + }) + toast.dismiss() + } catch (error) { + toast.dismiss() + toast.error('File could not be loaded. Please try again.') + yield put({ + type: 'remixide/save', + payload: {errorLoadingFile: true}, + }) + } + yield put({ + type: 'loading/save', + payload: { + screen: false, + }, + }) + }, + *testStep({payload: step}, {select, put}) { + yield put({ + type: 'loading/save', + payload: { + screen: true, + }, + }) + + try { + yield put({ + type: 'remixide/save', + payload: {success: false}, + }) + const {detail, selectedId} = yield select((state) => state.workshop) + + const workshop = detail[selectedId] + + let path: string + if (step.solidity.file) { + path = getFilePath(step.solidity.file) + path = `.learneth/${workshop.name}/${step.name}/${path}` + yield remixClient.call('fileManager', 'switchFile', `${path}`) + } + + console.log('testing ', step.test.content) + + path = getFilePath(step.test.file) + path = `.learneth/${workshop.name}/${step.name}/${path}` + yield remixClient.call('fileManager', 'setFile', path, step.test.content) + + const result = yield remixClient.call('solidityUnitTesting', 'testFromPath', path) + console.log('result ', result) + + if (!result) { + yield put({ + type: 'remixide/save', + payload: {errors: ['Compiler failed to test this file']}, + }) + } else { + const success = result.totalFailing === 0 + + if (success) { + yield put({ + type: 'remixide/save', + payload: {errors: [], success: true}, + }) + } else { + yield put({ + type: 'remixide/save', + payload: { + errors: result.errors.map((error: {message: any}) => error.message), + }, + }) + } + } + } catch (err) { + console.log('TESTING ERROR', err) + yield put({ + type: 'remixide/save', + payload: {errors: [String(err)]}, + }) + } + yield put({ + type: 'loading/save', + payload: { + screen: false, + }, + }) + }, + *showAnswer({payload: step}, {select, put}) { + yield put({ + type: 'loading/save', + payload: { + screen: true, + }, + }) + + toast.info('loading answer into IDE') + + try { + console.log('loading ', step) + const content = step.answer.content + let path = getFilePath(step.answer.file) + + const {detail, selectedId} = yield select((state) => state.workshop) + + const workshop = detail[selectedId] + path = `.learneth/${workshop.name}/${step.name}/${path}` + yield remixClient.call('fileManager', 'setFile', path, content) + yield remixClient.call('fileManager', 'switchFile', `${path}`) + } catch (err) { + yield put({ + type: 'remixide/save', + payload: {errors: [String(err)]}, + }) + } + + toast.dismiss() + yield put({ + type: 'loading/save', + payload: { + screen: false, + }, + }) + }, + *testSolidityCompiler(_, {put, select}) { + try { + yield remixClient.call('solidity', 'getCompilationResult') + } catch (err) { + const errors = yield select((state) => state.remixide.errors) + yield put({ + type: 'remixide/save', + payload: { + errors: [...errors, "The `Solidity Compiler` is not yet activated.
Please activate it using the `SOLIDITY` button in the `Featured Plugins` section of the homepage."], + }, + }) + } + }, + }, +} + +export default Model diff --git a/apps/learneth/src/redux/models/workshop.ts b/apps/learneth/src/redux/models/workshop.ts new file mode 100644 index 0000000000..eadd44e5e5 --- /dev/null +++ b/apps/learneth/src/redux/models/workshop.ts @@ -0,0 +1,179 @@ +import axios from 'axios'; +import { toast } from 'react-toastify'; +import groupBy from 'lodash/groupBy'; +import pick from 'lodash/pick'; +import { type ModelType } from '../store'; +import remixClient from '../../remix-client'; +import { router } from '../../App'; + +// const apiUrl = 'http://localhost:3001'; +const apiUrl = 'https://static.220.14.12.49.clients.your-server.de:3000'; + +const Model: ModelType = { + namespace: 'workshop', + state: { + list: [], + detail: {}, + selectedId: '', + }, + reducers: { + save(state, { payload }) { + return { ...state, ...payload }; + }, + }, + effects: { + *init(_, { put }) { + const cache = localStorage.getItem('workshop.state'); + + if (cache) { + const workshopState = JSON.parse(cache); + yield put({ + type: 'workshop/save', + payload: workshopState, + }); + } else { + yield put({ + type: 'workshop/loadRepo', + payload: { + name: 'ethereum/remix-workshops', + branch: 'master', + }, + }); + } + }, + *loadRepo({ payload }, { put, select }) { + toast.info(`loading ${payload.name}/${payload.branch}`); + + yield put({ + type: 'loading/save', + payload: { + screen: true, + }, + }); + + const { list, detail } = yield select((state) => state.workshop); + + const url = `${apiUrl}/clone/${encodeURIComponent(payload.name)}/${ + payload.branch + }?${Math.random()}`; + console.log('loading ', url); + const { data } = yield axios.get(url); + console.log(data); + const repoId = `${payload.name}-${payload.branch}`; + + for (let i = 0; i < data.ids.length; i++) { + const { + steps, + metadata: { + data: { steps: metadataSteps }, + }, + } = data.entities[data.ids[i]]; + + let newSteps = []; + + if (metadataSteps) { + newSteps = metadataSteps.map((step: any) => { + return { + ...steps.find((item: any) => item.name === step.path), + name: step.name, + }; + }); + } else { + newSteps = steps.map((step: any) => ({ + ...step, + name: step.name.replace('_', ' '), + })); + } + + const stepKeysWithFile = [ + 'markdown', + 'solidity', + 'test', + 'answer', + 'js', + 'vy', + ]; + + for (let j = 0; j < newSteps.length; j++) { + const step = newSteps[j]; + for (let k = 0; k < stepKeysWithFile.length; k++) { + const key = stepKeysWithFile[k]; + if (step[key]) { + try { + step[key].content = (yield remixClient.call( + 'contentImport', + 'resolve', + step[key].file, + )).content; + } catch (error) { + console.error(error); + } + } + } + } + data.entities[data.ids[i]].steps = newSteps; + } + + const workshopState = { + detail: { + ...detail, + [repoId]: { + ...data, + group: groupBy( + data.ids.map((id: string) => + pick(data.entities[id], ['level', 'id']), + ), + (item: any) => item.level, + ), + ...payload, + }, + }, + list: detail[repoId] ? list : [...list, payload], + selectedId: repoId, + }; + yield put({ + type: 'workshop/save', + payload: workshopState, + }); + localStorage.setItem('workshop.state', JSON.stringify(workshopState)); + + toast.dismiss(); + yield put({ + type: 'loading/save', + payload: { + screen: false, + }, + }); + + if (payload.id) { + const { detail, selectedId } = workshopState; + const { ids, entities } = detail[selectedId]; + for (let i = 0; i < ids.length; i++) { + const entity = entities[ids[i]]; + if (entity.metadata.data.id === payload.id || i + 1 === payload.id) { + yield router.navigate(`/list?id=${ids[i]}`); + break; + } + } + } + }, + *resetAll(_, { put }) { + yield put({ + type: 'workshop/save', + payload: { + list: [], + detail: {}, + selectedId: '', + }, + }); + + localStorage.removeItem('workshop.state'); + + yield put({ + type: 'workshop/init', + }); + }, + }, +}; + +export default Model; diff --git a/apps/learneth/src/redux/store.ts b/apps/learneth/src/redux/store.ts new file mode 100644 index 0000000000..5bd2929a61 --- /dev/null +++ b/apps/learneth/src/redux/store.ts @@ -0,0 +1,117 @@ +import { + configureStore, + createSlice, + type PayloadAction, + type Reducer, +} from '@reduxjs/toolkit'; +import createSagaMiddleware from 'redux-saga'; +import { + call, + put, + takeEvery, + delay, + select, + all, + fork, + type ForkEffect, +} from 'redux-saga/effects'; + +// @ts-expect-error +const context = require.context('./models', false, /\.ts$/); +const models = context.keys().map((key: any) => context(key).default); + +export type StateType = Record; +export interface ModelType { + namespace: string; + state: StateType; + reducers: Record< + string, + (state: StateType, action: PayloadAction) => StateType + >; + effects: Record< + string, + ( + action: PayloadAction, + effects: { + call: typeof call; + put: typeof put; + delay: typeof delay; + select: typeof select; + }, + ) => Generator + >; +} + +function createReducer(model: ModelType): Reducer { + const reducers = model.reducers; + Object.keys(model.effects).forEach((key) => { + reducers[key] = (state: StateType, action: PayloadAction) => state; + }); + const slice = createSlice({ + name: model.namespace, + initialState: model.state, + reducers, + }); + return slice.reducer; +} + +const rootReducer = models.reduce((prev: any, model: ModelType) => { + return { ...prev, [model.namespace]: createReducer(model) }; +}, {}); + +function watchEffects(model: ModelType): ForkEffect { + return fork(function* () { + for (const key in model.effects) { + const effect = model.effects[key]; + yield takeEvery( + `${model.namespace}/${key}`, + function* (action: PayloadAction) { + yield put({ + type: 'loading/save', + payload: { + [`${model.namespace}/${key}`]: true, + }, + }); + yield effect(action, { + call, + put, + delay, + select, + }); + yield put({ + type: 'loading/save', + payload: { + [`${model.namespace}/${key}`]: false, + }, + }); + }, + ); + } + }); +} + +function* rootSaga(): Generator { + yield all(models.map((model: ModelType) => watchEffects(model))); +} + +const configureAppStore = (initialState = {}) => { + const reduxSagaMonitorOptions = {}; + const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions); + + const middleware = [sagaMiddleware]; + + const store = configureStore({ + reducer: rootReducer, + middleware: (gDM) => gDM().concat([...middleware]), + preloadedState: initialState, + devTools: process.env.NODE_ENV !== 'production', + }); + + sagaMiddleware.run(rootSaga); + return store; +}; + +export const store = configureAppStore(); + +export type AppDispatch = typeof store.dispatch; +export type RootState = ReturnType; diff --git a/apps/learneth/src/remix-client.ts b/apps/learneth/src/remix-client.ts new file mode 100644 index 0000000000..fe25ef028a --- /dev/null +++ b/apps/learneth/src/remix-client.ts @@ -0,0 +1,38 @@ +import { PluginClient } from '@remixproject/plugin'; +import { createClient } from '@remixproject/plugin-webview'; +import { store } from './redux/store'; +import { router } from './App'; + +class RemixClient extends PluginClient { + constructor() { + super(); + createClient(this); + } + + startTutorial(name: any, branch: any, id: any): void { + console.log('start tutorial', name, branch, id); + void router.navigate('/home'); + store.dispatch({ + type: 'workshop/loadRepo', + payload: { + name, + branch, + id, + }, + }); + } + + addRepository(name: any, branch: any) { + console.log('add repo', name, branch); + void router.navigate('/home'); + store.dispatch({ + type: 'workshop/loadRepo', + payload: { + name, + branch, + }, + }); + } +} + +export default new RemixClient(); diff --git a/apps/learneth/src/setupTests.ts b/apps/learneth/src/setupTests.ts new file mode 100644 index 0000000000..8f2609b7b3 --- /dev/null +++ b/apps/learneth/src/setupTests.ts @@ -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'; diff --git a/apps/learneth/tsconfig.app.json b/apps/learneth/tsconfig.app.json new file mode 100644 index 0000000000..af84f21cfc --- /dev/null +++ b/apps/learneth/tsconfig.app.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "jest.config.ts", + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/apps/learneth/tsconfig.json b/apps/learneth/tsconfig.json new file mode 100644 index 0000000000..5aab5e7911 --- /dev/null +++ b/apps/learneth/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/apps/learneth/webpack.config.js b/apps/learneth/webpack.config.js new file mode 100644 index 0000000000..fecff4fa70 --- /dev/null +++ b/apps/learneth/webpack.config.js @@ -0,0 +1,92 @@ +const { composePlugins, withNx } = require('@nrwl/webpack') +const webpack = require('webpack') +const TerserPlugin = require("terser-webpack-plugin") +const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") + +// Nx plugins for webpack. +module.exports = composePlugins(withNx(), (config) => { + // Update the webpack config as needed here. + // e.g. `config.plugins.push(new MyPlugin())` + // add fallback for node modules + config.resolve.fallback = { + ...config.resolve.fallback, + "crypto": require.resolve("crypto-browserify"), + "stream": require.resolve("stream-browserify"), + "path": require.resolve("path-browserify"), + "http": require.resolve("stream-http"), + "https": require.resolve("https-browserify"), + "constants": require.resolve("constants-browserify"), + "os": false, //require.resolve("os-browserify/browser"), + "timers": false, // require.resolve("timers-browserify"), + "zlib": require.resolve("browserify-zlib"), + "fs": false, + "module": false, + "tls": false, + "net": false, + "readline": false, + "child_process": false, + "buffer": require.resolve("buffer/"), + "vm": require.resolve('vm-browserify'), + } + + + // add externals + config.externals = { + ...config.externals, + solc: 'solc', + } + + // add public path + config.output.publicPath = './' + + // add copy & provide plugin + config.plugins.push( + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + url: ['url', 'URL'], + process: 'process/browser', + }) + ) + + // set the define plugin to load the WALLET_CONNECT_PROJECT_ID + config.plugins.push( + new webpack.DefinePlugin({ + WALLET_CONNECT_PROJECT_ID: JSON.stringify(process.env.WALLET_CONNECT_PROJECT_ID), + }) + ) + + // souce-map loader + config.module.rules.push({ + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre" + }) + + config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings + + + // set minimizer + config.optimization.minimizer = [ + new TerserPlugin({ + parallel: true, + terserOptions: { + ecma: 2015, + compress: false, + mangle: false, + format: { + comments: false, + }, + }, + extractComments: false, + }), + new CssMinimizerPlugin(), + ]; + + config.watchOptions = { + ignored: /node_modules/ + } + + config.experiments.syncWebAssembly = true + + return config; +}); diff --git a/apps/remix-ide/project.json b/apps/remix-ide/project.json index 395b9f4a6f..67c0a39cd3 100644 --- a/apps/remix-ide/project.json +++ b/apps/remix-ide/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "apps/remix-ide/src", "projectType": "application", - "implicitDependencies": ["doc-gen", "doc-viewer", "etherscan", "vyper", "solhint", "walletconnect", "circuit-compiler"], + "implicitDependencies": ["doc-gen", "doc-viewer", "etherscan", "vyper", "solhint", "walletconnect", "circuit-compiler", "learneth"], "targets": { "build": { "executor": "@nrwl/webpack:webpack", diff --git a/package.json b/package.json index fdef679bf4..4da8e31e82 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "start": "nx start", "serve": "nx serve remix-ide --configuration=development", "serve:hot": "nx serve remix-ide --configuration=hot", + "serve:learneth": "nx serve learneth --configuration=development", "build": "nx build", "test": "nx test", "lint": "nx lint", @@ -136,11 +137,15 @@ "@ethereumjs/util": "^8.0.5", "@ethereumjs/vm": "^6.4.1", "@ethersphere/bee-js": "^3.2.0", + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/react-fontawesome": "^0.2.0", "@isomorphic-git/lightning-fs": "^4.4.1", "@microlink/react-json-view": "^1.23.0", "@openzeppelin/contracts": "^5.0.0", "@openzeppelin/upgrades-core": "^1.30.0", "@openzeppelin/wizard": "0.4.0", + "@reduxjs/toolkit": "^2.0.1", "@remixproject/engine": "0.3.42", "@remixproject/engine-electron": "0.3.42", "@remixproject/engine-web": "0.3.42", @@ -214,12 +219,16 @@ "react-markdown": "^8.0.5", "react-multi-carousel": "^2.8.2", "react-router-dom": "^6.16.0", + "react-spinners": "^0.13.8", "react-tabs": "^6.0.2", + "react-toastify": "^10.0.3", "react-virtualized": "^9.22.5", "react-virtuoso": "^4.6.2", "react-window": "^1.8.10", "react-zoom-pan-pinch": "^3.1.0", + "redux-saga": "^1.3.0", "regenerator-runtime": "0.13.7", + "rehype-raw": "^6.0.0", "remark-gfm": "^3.0.1", "rlp": "^3.0.0", "rss-parser": "^3.12.0", diff --git a/yarn.lock b/yarn.lock index ce81affdb8..a43b89d12f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2954,11 +2954,37 @@ intl-messageformat "10.1.0" tslib "2.4.0" +"@fortawesome/fontawesome-common-types@6.5.1": + version "6.5.1" + resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz#fdb1ec4952b689f5f7aa0bffe46180bb35490032" + integrity sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A== + "@fortawesome/fontawesome-free@^5.8.1": version "5.15.4" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5" integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg== +"@fortawesome/fontawesome-svg-core@^6.5.1": + version "6.5.1" + resolved "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz#9d56d46bddad78a7ebb2043a97957039fcebcf0a" + integrity sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ== + dependencies: + "@fortawesome/fontawesome-common-types" "6.5.1" + +"@fortawesome/free-solid-svg-icons@^6.5.1": + version "6.5.1" + resolved "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz#737b8d787debe88b400ab7528f47be333031274a" + integrity sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ== + dependencies: + "@fortawesome/fontawesome-common-types" "6.5.1" + +"@fortawesome/react-fontawesome@^0.2.0": + version "0.2.0" + resolved "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz#d90dd8a9211830b4e3c08e94b63a0ba7291ddcf4" + integrity sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw== + dependencies: + prop-types "^15.8.1" + "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -5342,6 +5368,59 @@ unbzip2-stream "1.4.3" yargs "17.7.1" +"@redux-saga/core@^1.3.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz#2ce08b73d407fc6ea9e7f7d83d2e97d981a3a8b8" + integrity sha512-L+i+qIGuyWn7CIg7k1MteHGfttKPmxwZR5E7OsGikCL2LzYA0RERlaUY00Y3P3ZV2EYgrsYlBrGs6cJP5OKKqA== + dependencies: + "@babel/runtime" "^7.6.3" + "@redux-saga/deferred" "^1.2.1" + "@redux-saga/delay-p" "^1.2.1" + "@redux-saga/is" "^1.1.3" + "@redux-saga/symbols" "^1.1.3" + "@redux-saga/types" "^1.2.1" + typescript-tuple "^2.2.1" + +"@redux-saga/deferred@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz#aca373a08ccafd6f3481037f2f7ee97f2c87c3ec" + integrity sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g== + +"@redux-saga/delay-p@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz#e72ac4731c5080a21f75b61bedc31cb639d9e446" + integrity sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w== + dependencies: + "@redux-saga/symbols" "^1.1.3" + +"@redux-saga/is@^1.1.3": + version "1.1.3" + resolved "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz#b333f31967e87e32b4e6b02c75b78d609dd4ad73" + integrity sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q== + dependencies: + "@redux-saga/symbols" "^1.1.3" + "@redux-saga/types" "^1.2.1" + +"@redux-saga/symbols@^1.1.3": + version "1.1.3" + resolved "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz#b731d56201719e96dc887dc3ae9016e761654367" + integrity sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg== + +"@redux-saga/types@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz#9403f51c17cae37edf870c6bc0c81c1ece5ccef8" + integrity sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA== + +"@reduxjs/toolkit@^2.0.1": + version "2.0.1" + resolved "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.1.tgz#0a5233c1e35c1941b03aece39cceade3467a1062" + integrity sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA== + dependencies: + immer "^10.0.3" + redux "^5.0.0" + redux-thunk "^3.1.0" + reselect "^5.0.1" + "@remix-run/router@1.14.0": version "1.14.0" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.14.0.tgz#9bc39a5a3a71b81bdb310eba6def5bc3966695b7" @@ -6411,6 +6490,11 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/parse5@^6.0.0": + version "6.0.3" + resolved "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" + integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== + "@types/pbkdf2@^3.0.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@types/pbkdf2/-/pbkdf2-3.1.0.tgz#039a0e9b67da0cdc4ee5dab865caa6b267bb66b1" @@ -6914,7 +6998,6 @@ version "2.11.1" resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.11.1.tgz#6e0174ec9026940eaadeedc53417e222eb45f5aa" integrity sha512-UfQH0ho24aa2M1xYmanbJv2ggQPebKmQytp2j20QEvURJ2R0v7YKWZ+0PfwOs6o6cuGw6gGxy/0WQXQRZSAsfg== - dependencies: "@walletconnect/jsonrpc-http-connection" "^1.0.7" "@walletconnect/jsonrpc-provider" "^1.0.13" "@walletconnect/jsonrpc-types" "^1.0.3" @@ -10790,6 +10873,11 @@ clsx@^2.0.0: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== +clsx@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" + integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== + cluster-key-slot@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" @@ -16120,11 +16208,71 @@ hasha@^3.0.0: dependencies: is-stream "^1.0.1" +hast-util-from-parse5@^7.0.0: + version "7.1.2" + resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz#aecfef73e3ceafdfa4550716443e4eb7b02e22b0" + integrity sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw== + dependencies: + "@types/hast" "^2.0.0" + "@types/unist" "^2.0.0" + hastscript "^7.0.0" + property-information "^6.0.0" + vfile "^5.0.0" + vfile-location "^4.0.0" + web-namespaces "^2.0.0" + +hast-util-parse-selector@^3.0.0: + version "3.1.1" + resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz#25ab00ae9e75cbc62cf7a901f68a247eade659e2" + integrity sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA== + dependencies: + "@types/hast" "^2.0.0" + +hast-util-raw@^7.2.0: + version "7.2.3" + resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz#dcb5b22a22073436dbdc4aa09660a644f4991d99" + integrity sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg== + dependencies: + "@types/hast" "^2.0.0" + "@types/parse5" "^6.0.0" + hast-util-from-parse5 "^7.0.0" + hast-util-to-parse5 "^7.0.0" + html-void-elements "^2.0.0" + parse5 "^6.0.0" + unist-util-position "^4.0.0" + unist-util-visit "^4.0.0" + vfile "^5.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-to-parse5@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz#c49391bf8f151973e0c9adcd116b561e8daf29f3" + integrity sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw== + dependencies: + "@types/hast" "^2.0.0" + comma-separated-tokens "^2.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + hast-util-whitespace@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== +hastscript@^7.0.0: + version "7.2.0" + resolved "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz#0eafb7afb153d047077fa2a833dc9b7ec604d10b" + integrity sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw== + dependencies: + "@types/hast" "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^3.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -16248,6 +16396,11 @@ html-react-parser@^3.0.4: react-property "2.0.0" style-to-js "1.1.1" +html-void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f" + integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A== + html2canvas@^1.0.0-rc.5: version "1.4.1" resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" @@ -16571,6 +16724,11 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= +immer@^10.0.3: + version "10.0.3" + resolved "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz#a8de42065e964aa3edf6afc282dfc7f7f34ae3c9" + integrity sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A== + immutable@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef" @@ -23072,6 +23230,11 @@ parse5@4.0.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== +parse5@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + parse5@^7.0.0: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -23837,7 +24000,6 @@ preact@^10.16.0: version "10.19.3" resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.3.tgz#7a7107ed2598a60676c943709ea3efb8aaafa899" integrity sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ== - prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" @@ -24624,6 +24786,11 @@ react-router@6.21.0: dependencies: "@remix-run/router" "1.14.0" +react-spinners@^0.13.8: + version "0.13.8" + resolved "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz#5262571be0f745d86bbd49a1e6b49f9f9cb19acc" + integrity sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA== + react-tabs@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-6.0.2.tgz#bc1065c3828561fee285a8fd045f22e0fcdde1eb" @@ -24641,6 +24808,13 @@ react-textarea-autosize@~8.3.2: use-composed-ref "^1.3.0" use-latest "^1.2.1" +react-toastify@^10.0.3: + version "10.0.3" + resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.3.tgz#1b948fabf63393464eb2f82119485de58b9a9b2f" + integrity sha512-PBJwXjFKKM73tgb6iSld4GMs9ShBWGUvc9zPHmdDgT4CdSr32iqSNh6y/fFN/tosvkTS6/tBLptDxXiXgcjvuw== + dependencies: + clsx "^2.1.0" + react-transition-group@^4.4.1: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" @@ -25015,6 +25189,18 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" +redux-saga@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/redux-saga/-/redux-saga-1.3.0.tgz#a59ada7c28010189355356b99738c9fcb7ade30e" + integrity sha512-J9RvCeAZXSTAibFY0kGw6Iy4EdyDNW7k6Q+liwX+bsck7QVsU78zz8vpBRweEfANxnnlG/xGGeOvf6r8UXzNJQ== + dependencies: + "@redux-saga/core" "^1.3.0" + +redux-thunk@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3" + integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== + redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" @@ -25034,6 +25220,11 @@ redux@^4.0.0, redux@^4.0.4: dependencies: "@babel/runtime" "^7.9.2" +redux@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== + regenerate-unicode-properties@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" @@ -25232,6 +25423,15 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" +rehype-raw@^6.0.0: + version "6.1.1" + resolved "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz#81bbef3793bd7abacc6bf8335879d1b6c868c9d4" + integrity sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ== + dependencies: + "@types/hast" "^2.0.0" + hast-util-raw "^7.2.0" + unified "^10.0.0" + release-zalgo@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" @@ -25405,6 +25605,11 @@ reselect@^4.0.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.7.tgz#56480d9ff3d3188970ee2b76527bd94a95567a42" integrity sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A== +reselect@^5.0.1: + version "5.1.0" + resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz#c479139ab9dd91be4d9c764a7f3868210ef8cd21" + integrity sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg== + reset@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/reset/-/reset-0.1.0.tgz#9fc7314171995ae6cb0b7e58b06ce7522af4bafb" @@ -28407,6 +28612,25 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript-compare@^0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz#7ee40a400a406c2ea0a7e551efd3309021d5f425" + integrity sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA== + dependencies: + typescript-logic "^0.0.0" + +typescript-logic@^0.0.0: + version "0.0.0" + resolved "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz#66ebd82a2548f2b444a43667bec120b496890196" + integrity sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q== + +typescript-tuple@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz#7d9813fb4b355f69ac55032e0363e8bb0f04dad2" + integrity sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q== + dependencies: + typescript-compare "^0.0.2" + typescript@^4.8.4: version "4.8.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" @@ -29157,6 +29381,14 @@ verror@1.3.6: dependencies: extsprintf "1.0.2" +vfile-location@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz#69df82fb9ef0a38d0d02b90dd84620e120050dd0" + integrity sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw== + dependencies: + "@types/unist" "^2.0.0" + vfile "^5.0.0" + vfile-message@^3.0.0: version "3.1.4" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" @@ -29340,6 +29572,11 @@ web-encoding@^1.0.2, web-encoding@^1.0.6: optionalDependencies: "@zxing/text-encoding" "0.9.0" +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + web-streams-polyfill@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"