diff --git a/apps/learneth/README.md b/apps/learneth/README.md
new file mode 100644
index 0000000000..a8e9909931
--- /dev/null
+++ b/apps/learneth/README.md
@@ -0,0 +1,134 @@
+# Remix LearnEth Plugin
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm run serve:plugin --plugin=learneth`
+
+Runs the app in the development mode.\
+Open [http://localhost:2024](http://localhost:2024) to view it in the browser.
+
+The page will reload if you make edits.\
+You will also see any lint errors in the console.
+
+### `npm run build:plugin --plugin=learneth`
+
+Builds the app for production to the `dist/apps/learneth` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+## Loading the plugin in remix
+
+When testing with localhost you should use the HTTP version of either REMIX or REMIX ALPHA. Click on the plugin manager icon and
+add the plugin 'Connect to a local plugin'. Your plugin will be at http://localhost:2024/.
+
+## Setting up the REMIX IDE for working with the plugin
+
+The plugin only works when a compiler environment is loaded as well, for example on the home screen of the IDE you select 'Solidity' or 'Vyper'. Without this the plugin
+cannot compile and test files in the workshops.
+
+## Setting up your Github workshops repo
+
+You can create your own workshops that can be imported in the plugin.
+When importing a github repo the plugin will look for a directory structure describing the workshops.
+For example: https://github.com/ethereum/remix-workshops
+
+### Root directories
+
+Root directories are individual workshops, the name used will be the name of the workshop unless you override this with the name property in the config.yml.
+
+### README.md
+
+The readme in each directry contains an explanation of what the workshop is about. If an additional summary property is provided in the config.yml that will be used in the overview section of the plugin.
+
+### config.yml
+
+This config file contains meta data describing some properties of your workshop, for example
+
+```
+---
+id: someid
+name: my workshop name
+summary: something about this workshop
+level: 4
+tags:
+ - solidity
+ - beginner
+```
+
+Level: a level of difficulty indicator ( 1 - 5 )
+
+Tags: an array of tags
+
+id: this is used by the system to let REMIX call startTutorial(repo,branch,id). See below for more instructions.
+
+### Steps
+
+Each workshop contains what we call steps.
+Each step is a directory containing:
+
+- a readme describing the step, what to do.
+- sol files:
+ - these can be sol files and test sol files. The test files should be name yoursolname_test.sol
+ - ANSWER files: these are named yoursolname_answer.sol and can be used to show the solution or the correct answer. The plugin will load the
+ file in the IDE when a user clicks on 'Show Answer'
+- js files
+- vyper files
+
+## Functions to call the plugin from the IDE
+
+### Add a repository:
+
+```
+addRepository(repoName, branch)
+```
+
+### Start a tutorial
+
+```
+startTutorial(repoName,branch,id)
+```
+
+You don't need to add a seperate addRepository before calling startTutorial, this call will also add the repo.
+
+_Parameters_
+
+id: this can be two things:
+
+- type of number, it specifies the n-th tutorial in the list
+- type of string, this refers to the ID parameter in the config.yml file in the tutorial
+ for example:
+
+```
+---
+id: basics
+name: 1 Basics of Solidity
+summary: Some basic functions explained
+level: 4
+tags:
+ - solidity
+```
+
+### How to call these functions in the REMIX IDE
+
+```
+(function () {
+try {
+ // You don't need to add a seperate addRepository before calling startTutorial, this is just an example
+ remix.call('LearnEth', 'addRepository', "ethereum/remix-workshops", "master")
+ remix.call('LearnEth', 'startTutorial', "ethereum/remix-workshops", "master", "basics")
+ remix.call('LearnEth', 'startTutorial', "ethereum/remix-workshops", "master", 2)
+} catch (e) {
+ console.log(e.message)
+}
+})()
+```
+
+Then call this in the REMIX console
+
+```
+remix.exeCurrent()
+```
diff --git a/apps/learneth/project.json b/apps/learneth/project.json
new file mode 100644
index 0000000000..a4db7488a7
--- /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"],
+ "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": 2024
+ },
+ "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.tsx b/apps/learneth/src/App.tsx
new file mode 100644
index 0000000000..e61fbf7b74
--- /dev/null
+++ b/apps/learneth/src/App.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+import {createHashRouter, 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 = createHashRouter([
+ {
+ 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/components/BackButton/index.scss b/apps/learneth/src/components/BackButton/index.scss
new file mode 100644
index 0000000000..53b73037d5
--- /dev/null
+++ b/apps/learneth/src/components/BackButton/index.scss
@@ -0,0 +1,28 @@
+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);
+ }
+}
diff --git a/apps/learneth/src/components/BackButton/index.tsx b/apps/learneth/src/components/BackButton/index.tsx
new file mode 100644
index 0000000000..14f8112d4f
--- /dev/null
+++ b/apps/learneth/src/components/BackButton/index.tsx
@@ -0,0 +1,87 @@
+import React, {useState} from 'react'
+import {Link, useLocation, useNavigate} from 'react-router-dom'
+import {Button, Modal, Tooltip, OverlayTrigger} from 'react-bootstrap'
+import './index.scss'
+
+function BackButton({entity}: any) {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const [show, setShow] = useState(false)
+ const isDetailPage = location.pathname === '/detail'
+ const queryParams = new URLSearchParams(location.search)
+ const stepId = Number(queryParams.get('stepId'))
+
+ return (
+
+
+ {isDetailPage && (
+
+ )}
+ {
+ setShow(false)
+ }}
+ >
+
+ Leave tutorial
+
+ Are you sure you want to leave the tutorial?
+
+ {
+ setShow(false)
+ }}
+ >
+ No
+
+ {
+ setShow(false)
+ navigate('/home')
+ }}
+ >
+ Yes
+
+
+
+
+ )
+}
+
+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..06eb7e4fe7
--- /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..47c1f73705
--- /dev/null
+++ b/apps/learneth/src/components/RepoImporter/index.tsx
@@ -0,0 +1,120 @@
+import React, {useState, useEffect} from 'react'
+import {Button, Dropdown, Form, Tooltip, OverlayTrigger} from 'react-bootstrap'
+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}
+ />
+
+
+ Import {name}
+
+
+ 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..4b034183c3
--- /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/main.tsx b/apps/learneth/src/main.tsx
new file mode 100644
index 0000000000..f710e14a4d
--- /dev/null
+++ b/apps/learneth/src/main.tsx
@@ -0,0 +1,13 @@
+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..70c6426ea4
--- /dev/null
+++ b/apps/learneth/src/pages/Home/index.css
@@ -0,0 +1,24 @@
+.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;
+ cursor: pointer;
+}
+
+.workshop-link {
+ cursor: pointer;
+}
+.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..6835901900
--- /dev/null
+++ b/apps/learneth/src/pages/Home/index.tsx
@@ -0,0 +1,96 @@
+import React, {useEffect} from 'react'
+import {Link} from 'react-router-dom'
+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) => (
+
+
+ {
+ handleClick(item.id)
+ }}
+ >
+
+
+ {
+ handleClick(item.id)
+ }}
+ >
+ {selectedRepo.entities[item.id].name}
+
+
+
+
+
+
+ {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.tsx b/apps/learneth/src/pages/Logo/index.tsx
new file mode 100644
index 0000000000..6fcb816633
--- /dev/null
+++ b/apps/learneth/src/pages/Logo/index.tsx
@@ -0,0 +1,20 @@
+import React, {useEffect} from 'react'
+import {useAppDispatch} from '../../redux/hooks'
+
+const LogoPage: React.FC = () => {
+ const dispatch = useAppDispatch()
+
+ useEffect(() => {
+ dispatch({type: 'remixide/connect'})
+ }, [])
+
+ 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..5b59a87727
--- /dev/null
+++ b/apps/learneth/src/pages/StepDetail/index.scss
@@ -0,0 +1,54 @@
+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;
+}
+
+.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..2df3b9efe6
--- /dev/null
+++ b/apps/learneth/src/pages/StepDetail/index.tsx
@@ -0,0 +1,228 @@
+import React, {useEffect} from 'react'
+import {useLocation, 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 location = useLocation()
+ 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}
+ {
+ dispatch({
+ type: 'remixide/displayFile',
+ payload: step,
+ })
+ }}
+ >
+ Load the file
+
+
+ >
+ ) : (
+ <>
+
+ {step.name}
+ >
+ )}
+
+ {step.markdown?.content}
+
+ {step.test?.content ? (
+ <>
+
+ {errorLoadingFile ? (
+ {
+ dispatch({
+ type: 'remixide/displayFile',
+ payload: step,
+ })
+ }}
+ >
+ Load the file
+
+ ) : (
+ <>
+ {!errorLoadingFile ? (
+ <>
+ {
+ dispatch({
+ type: 'remixide/testStep',
+ payload: step,
+ })
+ }}
+ >
+ Check Answer
+
+ {step.answer?.content && (
+ {
+ dispatch({
+ type: 'remixide/showAnswer',
+ payload: step,
+ })
+ }}
+ >
+ Show answer
+
+ )}
+ >
+ ) : (
+ <>
+ {!errorLoadingFile && (
+ <>
+ {
+ navigate(stepId === steps.length - 1 ? `/list?id=${id}` : `/detail?id=${id}&stepId=${stepId + 1}`)
+ }}
+ >
+ Next
+
+ {step.answer?.content && (
+ {
+ dispatch({
+ type: 'remixide/showAnswer',
+ payload: step,
+ })
+ }}
+ >
+ Show answer
+
+ )}
+ >
+ )}
+ >
+ )}
+ >
+ )}
+
+ {success && (
+ {
+ navigate(stepId === steps.length - 1 ? `/list?id=${id}` : `/detail?id=${id}&stepId=${stepId + 1}`)
+ }}
+ >
+ Next
+
+ )}
+
+ {success && (
+
+ Well done! No errors.
+
+ )}
+ {errors.length > 0 && (
+ <>
+ {!success && (
+
+ Errors
+
+ )}
+ {errors.map((error: string, index: number) => (
+
+ {error}
+
+ ))}
+ >
+ )}
+
+ >
+ ) : (
+ <>
+
+ {!errorLoadingFile && step.answer?.content && (
+ {
+ dispatch({
+ type: 'remixide/showAnswer',
+ payload: step,
+ })
+ }}
+ >
+ Show answer
+
+ )}
+
+ {stepId < steps.length - 1 && (
+ {
+ navigate(`/detail?id=${id}&stepId=${stepId + 1}`)
+ }}
+ >
+ Next
+
+ )}
+ {stepId === steps.length - 1 && (
+ {
+ navigate(`/list?id=${id}`)
+ }}
+ >
+ Finish tutorial
+
+ )}
+ >
+ )}
+ >
+ )
+}
+
+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..eb56db2b90
--- /dev/null
+++ b/apps/learneth/src/pages/StepList/index.scss
@@ -0,0 +1,144 @@
+: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..1c05930e24
--- /dev/null
+++ b/apps/learneth/src/pages/StepList/index.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+import {Link, useLocation} 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 location = useLocation()
+ 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..2eade4d42d
--- /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": "assets/img/learnEthLogo.webp",
+ "location": "sidePanel",
+ "url": "plugins/learneth/index.html",
+ "repo": "https://github.com/ethereum/remix-project/tree/master/apps/learneth",
+ "maintainedBy": "Remix",
+ "authorContact": "",
+ "targets": [
+ "remix"
+ ]
+}
diff --git a/apps/learneth/src/redux/hooks.ts b/apps/learneth/src/redux/hooks.ts
new file mode 100644
index 0000000000..256734f44f
--- /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..38e09ef7c2
--- /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..2dea0d96f4
--- /dev/null
+++ b/apps/learneth/src/redux/models/remixide.ts
@@ -0,0 +1,229 @@
+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()
+
+ 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..daf41b5ff9
--- /dev/null
+++ b/apps/learneth/src/redux/models/workshop.ts
@@ -0,0 +1,164 @@
+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..5092828cc6
--- /dev/null
+++ b/apps/learneth/src/redux/store.ts
@@ -0,0 +1,97 @@
+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) => 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..4d4ab9844f
--- /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/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..4db6b9fc00
--- /dev/null
+++ b/apps/learneth/webpack.config.js
@@ -0,0 +1,90 @@
+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/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js
index 8fa3fb15dd..c642e44e3c 100644
--- a/apps/remix-ide/src/remixAppManager.js
+++ b/apps/remix-ide/src/remixAppManager.js
@@ -89,7 +89,7 @@ let requiredModules = [ // services + layout views + system views
// dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd)
const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither']
-const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'etherscan', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler']
+const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'etherscan', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler', 'learneth']
const sensitiveCalls = {
fileManager: ['writeFile', 'copyFile', 'rename', 'copyDir'],
diff --git a/package.json b/package.json
index fdef679bf4..80aa2b75a0 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,8 @@
"start": "nx start",
"serve": "nx serve remix-ide --configuration=development",
"serve:hot": "nx serve remix-ide --configuration=hot",
+ "serve:plugin": "nx serve ${npm_config_plugin} --configuration=development",
+ "build:plugin": "NODE_ENV=production nx build ${npm_config_plugin} --configuration=production --skip-nx-cache",
"build": "nx build",
"test": "nx test",
"lint": "nx lint",
@@ -141,6 +143,7 @@
"@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 +217,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..93a208eb2c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5342,6 +5342,59 @@
unbzip2-stream "1.4.3"
yargs "17.7.1"
+"@redux-saga/core@^1.3.0":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@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.yarnpkg.com/@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.yarnpkg.com/@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.yarnpkg.com/@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.yarnpkg.com/@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.yarnpkg.com/@redux-saga/types/-/types-1.2.1.tgz#9403f51c17cae37edf870c6bc0c81c1ece5ccef8"
+ integrity sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==
+
+"@reduxjs/toolkit@^2.0.1":
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.2.0.tgz#69b7d7933ea3e7f0cbbe182caa4850e09ea643ae"
+ integrity sha512-ZvPYKfu4kDnAqPhJ1bsis8QFbiQRz3Q2HxW3tw9tVGusPzYKRG7ju1FA+34PGcwCoemjGGv+f/7fEygcRZIwmA==
+ dependencies:
+ immer "^10.0.3"
+ redux "^5.0.1"
+ 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 +6464,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.yarnpkg.com/@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"
@@ -10790,6 +10848,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.yarnpkg.com/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 +16183,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.yarnpkg.com/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.yarnpkg.com/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.yarnpkg.com/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.yarnpkg.com/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.yarnpkg.com/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 +16371,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.yarnpkg.com/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 +16699,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.yarnpkg.com/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 +23205,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.yarnpkg.com/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"
@@ -24624,6 +24762,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.yarnpkg.com/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 +24784,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.4"
+ resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.4.tgz#6ecdbbf923a07fc45850e69b0566efc7bf733283"
+ integrity sha512-etR3RgueY8pe88SA67wLm8rJmL1h+CLqUGHuAoNsseW35oTGJEri6eBTyaXnFKNQ80v/eO10hBYLgz036XRGgA==
+ 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"
@@ -25027,6 +25177,18 @@ redis-parser@^3.0.0:
dependencies:
redis-errors "^1.0.0"
+redux-saga@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/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.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
+ integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==
+
redux@^4.0.0, redux@^4.0.4:
version "4.1.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
@@ -25034,6 +25196,11 @@ redux@^4.0.0, redux@^4.0.4:
dependencies:
"@babel/runtime" "^7.9.2"
+redux@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/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 +25399,15 @@ regjsparser@^0.9.1:
dependencies:
jsesc "~0.5.0"
+rehype-raw@^6.0.0:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/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 +25581,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.yarnpkg.com/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 +28588,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.yarnpkg.com/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.yarnpkg.com/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.yarnpkg.com/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 +29357,14 @@ verror@1.3.6:
dependencies:
extsprintf "1.0.2"
+vfile-location@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/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 +29548,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.yarnpkg.com/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"