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 @@
+
\ 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"