add learneth plugin

pull/4466/head
drafish 10 months ago committed by yann300
parent 2af97d7f1f
commit 88eeb77272
  1. 58
      apps/learneth/project.json
  2. 19
      apps/learneth/src/App.css
  3. 15
      apps/learneth/src/App.test.tsx
  4. 51
      apps/learneth/src/App.tsx
  5. 0
      apps/learneth/src/assets/.gitkeep
  6. 5
      apps/learneth/src/assets/Font_Awesome_5_solid_book-reader.svg
  7. 1
      apps/learneth/src/assets/logo-background.svg
  8. 55
      apps/learneth/src/components/BackButton/index.scss
  9. 91
      apps/learneth/src/components/BackButton/index.tsx
  10. 17
      apps/learneth/src/components/LoadingScreen/index.css
  11. 16
      apps/learneth/src/components/LoadingScreen/index.tsx
  12. 4
      apps/learneth/src/components/RepoImporter/index.css
  13. 165
      apps/learneth/src/components/RepoImporter/index.tsx
  14. 21
      apps/learneth/src/components/SlideIn/index.css
  15. 18
      apps/learneth/src/components/SlideIn/index.tsx
  16. 13
      apps/learneth/src/index.css
  17. 18
      apps/learneth/src/index.html
  18. 1
      apps/learneth/src/logo.svg
  19. 15
      apps/learneth/src/main.tsx
  20. 23
      apps/learneth/src/pages/Home/index.css
  21. 136
      apps/learneth/src/pages/Home/index.tsx
  22. 5
      apps/learneth/src/pages/Logo/index.css
  23. 26
      apps/learneth/src/pages/Logo/index.tsx
  24. 59
      apps/learneth/src/pages/StepDetail/index.scss
  25. 246
      apps/learneth/src/pages/StepDetail/index.tsx
  26. 152
      apps/learneth/src/pages/StepList/index.scss
  27. 44
      apps/learneth/src/pages/StepList/index.tsx
  28. 7
      apps/learneth/src/polyfills.ts
  29. 21
      apps/learneth/src/profile.json
  30. 1
      apps/learneth/src/react-app-env.d.ts
  31. 5
      apps/learneth/src/redux/hooks.ts
  32. 14
      apps/learneth/src/redux/models/loading.ts
  33. 233
      apps/learneth/src/redux/models/remixide.ts
  34. 179
      apps/learneth/src/redux/models/workshop.ts
  35. 117
      apps/learneth/src/redux/store.ts
  36. 38
      apps/learneth/src/remix-client.ts
  37. 5
      apps/learneth/src/setupTests.ts
  38. 23
      apps/learneth/tsconfig.app.json
  39. 16
      apps/learneth/tsconfig.json
  40. 92
      apps/learneth/webpack.config.js
  41. 2
      apps/remix-ide/project.json
  42. 9
      package.json
  43. 241
      yarn.lock

@ -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": []
}

@ -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;
}

@ -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(
<Provider store={store}>
<App />
</Provider>,
);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

@ -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: <LogoPage />,
},
{
path: '/home',
element: <HomePage />,
},
{
path: '/list',
element: <StepListPage />,
},
{
path: '/detail',
element: <StepDetailPage />,
},
]);
function App(): JSX.Element {
return (
<>
<RouterProvider router={router} />
<LoadingScreen />
<ToastContainer
position="bottom-right"
newestOnTop
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
autoClose={false}
theme="colored"
/>
</>
);
}
export default App;

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M352 96c0-53.02-42.98-96-96-96s-96 42.98-96 96 42.98 96 96 96 96-42.98 96-96zM233.59 241.1c-59.33-36.32-155.43-46.3-203.79-49.05C13.55 191.13 0 203.51 0 219.14v222.8c0 14.33 11.59 26.28 26.49 27.05 43.66 2.29 131.99 10.68 193.04 41.43 9.37 4.72 20.48-1.71 20.48-11.87V252.56c-.01-4.67-2.32-8.95-6.42-11.46zm248.61-49.05c-48.35 2.74-144.46 12.73-203.78 49.05-4.1 2.51-6.41 6.96-6.41 11.63v245.79c0 10.19 11.14 16.63 20.54 11.9 61.04-30.72 149.32-39.11 192.97-41.4 14.9-.78 26.49-12.73 26.49-27.06V219.14c-.01-15.63-13.56-28.01-29.81-27.09z"/></svg>
<!--
Font Awesome Free 5.2.0 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
-->

After

Width:  |  Height:  |  Size: 793 B

@ -0,0 +1 @@
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 105 100"><title>remix_logo1</title><path fill="#393e5e" d="M91.84,35a.09.09,0,0,1-.1-.07,41,41,0,0,0-79.48,0,.09.09,0,0,1-.1.07C9.45,35,1,35.35,1,42.53c0,8.56,1,16,6,20.32,2.16,1.85,5.81,2.3,9.27,2.22a44.4,44.4,0,0,0,6.45-.68.09.09,0,0,0,.06-.15A34.81,34.81,0,0,1,17,45c0-.1,0-.21,0-.31a35,35,0,0,1,70,0c0,.1,0,.21,0,.31a34.81,34.81,0,0,1-5.78,19.24.09.09,0,0,0,.06.15,44.4,44.4,0,0,0,6.45.68c3.46.08,7.11-.37,9.27-2.22,5-4.27,6-11.76,6-20.32C103,35.35,94.55,35,91.84,35Z"/><path fill="#393e5e" d="M52,74,25.4,65.13a.1.1,0,0,0-.1.17L51.93,91.93a.1.1,0,0,0,.14,0L78.7,65.3a.1.1,0,0,0-.1-.17L52,74A.06.06,0,0,1,52,74Z"/><path fill="#393e5e" d="M75.68,46.9,82,45a.09.09,0,0,0,.08-.09,29.91,29.91,0,0,0-.87-6.94.11.11,0,0,0-.09-.08l-6.43-.58a.1.1,0,0,1-.06-.18l4.78-4.18a.13.13,0,0,0,0-.12,30.19,30.19,0,0,0-3.65-6.07.09.09,0,0,0-.11,0l-5.91,2a.1.1,0,0,1-.12-.14L72.19,23a.11.11,0,0,0,0-.12,29.86,29.86,0,0,0-5.84-4.13.09.09,0,0,0-.11,0l-4.47,4.13a.1.1,0,0,1-.17-.07l.09-6a.1.1,0,0,0-.07-.1,30.54,30.54,0,0,0-7-1.47.1.1,0,0,0-.1.07l-2.38,5.54a.1.1,0,0,1-.18,0l-2.37-5.54a.11.11,0,0,0-.11-.06,30,30,0,0,0-7,1.48.12.12,0,0,0-.07.1l.08,6.05a.09.09,0,0,1-.16.07L37.8,18.76a.11.11,0,0,0-.12,0,29.75,29.75,0,0,0-5.83,4.13.11.11,0,0,0,0,.12l2.59,5.6a.11.11,0,0,1-.13.14l-5.9-2a.11.11,0,0,0-.12,0,30.23,30.23,0,0,0-3.62,6.08.11.11,0,0,0,0,.12l4.79,4.19a.1.1,0,0,1-.06.17L23,37.91a.1.1,0,0,0-.09.07A29.9,29.9,0,0,0,22,44.92a.1.1,0,0,0,.07.1L28.4,47a.1.1,0,0,1,0,.18l-5.84,3.26a.16.16,0,0,0,0,.11,30.17,30.17,0,0,0,2.1,6.76c.32.71.67,1.4,1,2.08a.1.1,0,0,0,.06,0L52,68.16H52l26.34-8.78a.1.1,0,0,0,.06-.05,30.48,30.48,0,0,0,3.11-8.88.1.1,0,0,0-.05-.11l-5.83-3.26A.1.1,0,0,1,75.68,46.9Z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -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);
// }

@ -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 (
<nav className="navbar navbar-light bg-light justify-content-between pt-1 pb-1 pl-1">
<ul className="nav mr-auto">
<li className="nav-item">
<div
className="btn back"
onClick={() => {
setShow(true)
}}
role="button"
>
<OverlayTrigger placement="right" overlay={<Tooltip id="tooltip-right">Leave tutorial</Tooltip>}>
<FontAwesomeIcon className="pl-1" icon={faHome} />
</OverlayTrigger>
</div>
</li>
{isDetailPage && (
<li className="nav-item">
<Link className="btn" to={`/list?id=${entity.id}`} title="Tutorial menu">
<FontAwesomeIcon icon={faBars} />
</Link>
</li>
)}
</ul>
{isDetailPage && (
<form className="form-inline">
{stepId > 0 && (
<Link to={`/detail?id=${entity.id}&stepId=${stepId - 1}`}>
<FontAwesomeIcon className="pr-1" icon={faChevronLeft} />
</Link>
)}
{stepId + 1}/{entity && <div className="">{entity.steps.length}</div>}
{stepId < entity.steps.length - 1 && (
<Link to={`/detail?id=${entity.id}&stepId=${stepId + 1}`}>
<FontAwesomeIcon className="pl-1" icon={faChevronRight} />
</Link>
)}
</form>
)}
<Modal
// data-bs-theme={theme}
show={show}
onHide={() => {
setShow(false)
}}
>
<Modal.Header placeholder={''} closeButton>
<Modal.Title>Leave tutorial</Modal.Title>
</Modal.Header>
<Modal.Body>Are you sure you want to leave the tutorial?</Modal.Body>
<Modal.Footer>
<Button
variant="secondary"
onClick={() => {
setShow(false)
}}
>
No
</Button>
<Button
variant="success"
onClick={() => {
setShow(false)
navigate('/home')
}}
>
Yes
</Button>
</Modal.Footer>
</Modal>
</nav>
)
}
export default BackButton

@ -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%);
}

@ -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 ? (
<div className="spinnersOverlay">
<BounceLoader color="#a7b0ae" size={100} className="spinnersLoading" />
</div>
) : null;
};
export default LoadingScreen;

@ -0,0 +1,4 @@
.arrow-icon{
width: 3px;
display: inline-block;
}

@ -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 && (
<div className="container-fluid mb-3 small mt-3">
Tutorials from:
<h4 className="mb-1">{selectedRepo.name}</h4>
<span className="">
Date modified:{' '}
{new Date(selectedRepo.datemodified).toLocaleString()}
</span>
</div>
)}
<div
onClick={panelChange}
style={{ cursor: 'pointer' }}
className="container-fluid d-flex mb-3 small"
>
<div className="d-flex pr-2 pl-2">
<FontAwesomeIcon
className="arrow-icon pt-1"
size="xs"
icon={open ? faChevronDown : faChevronRight}
/>
</div>
<div className="d-flex">Import another tutorial repo</div>
</div>
{open && (
<div className="container-fluid">
<Dropdown className="w-100">
<Dropdown.Toggle
className="btn btn-secondary w-100"
id="dropdownBasic1"
>
Select a repo
</Dropdown.Toggle>
<Dropdown.Menu className="w-100">
{list.map((item: any) => (
<Dropdown.Item
key={`${item.name}/${item.branch}`}
onClick={() => {
selectRepo(item);
}}
>
{item.name}-{item.branch}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
<div
onClick={resetAll}
className="small mb-3"
style={{ cursor: 'pointer' }}
>
reset list
</div>
</div>
)}
<div className="container-fluid mt-3">
{open && (
<Form onSubmit={importRepo}>
<Form.Group className="form-group">
<Form.Label className="mr-2" htmlFor="name">
REPO
</Form.Label>
<OverlayTrigger
placement="right"
overlay={
<Tooltip id="tooltip-right">ie username/repository</Tooltip>
}
>
<FontAwesomeIcon icon={faQuestionCircle} />
</OverlayTrigger>
<Form.Control
id="name"
required
onChange={(e) => {
setName(e.target.value);
}}
value={name}
/>
<Form.Label htmlFor="branch">BRANCH</Form.Label>
<Form.Control
id="branch"
required
onChange={(e) => {
setBranch(e.target.value);
}}
value={branch}
/>
</Form.Group>
<Button
className="btn btn-success start w-100"
type="submit"
disabled={!name || !branch}
>
Import {name}
</Button>
<a
href="https://github.com/bunsenstraat/remix-learneth-plugin/blob/master/README.md"
className="d-none"
target="_blank"
rel="noreferrer"
>
<FontAwesomeIcon icon={faInfoCircle} /> how to setup your repo
</a>
</Form>
)}
<hr />
</div>
</>
);
}
export default RepoImporter;

@ -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);
}

@ -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 (
<CSSTransition in={show} timeout={400} classNames="slide" unmountOnExit>
{children}
</CSSTransition>
);
};
export default SlideIn;

@ -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;
}

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Learn ETH</title>
<base href="./" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous"/> -->
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> -->
<link rel="stylesheet" integrity="ha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf"
crossorigin="anonymous" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css">
</head>
<body>
<div id="root"></div>
<script src="https://kit.fontawesome.com/41dd021e94.js" crossorigin="anonymous"></script>
</body>
</html>

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -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(
<Provider store={store}>
<App />
</Provider>,
);

@ -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;
}

@ -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<string[]>([]);
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 (
<div className="App">
<RepoImporter list={list} selectedRepo={selectedRepo || {}} />
{selectedRepo && (
<div className="container-fluid">
{Object.keys(selectedRepo.group).map((level) => (
<div key={level}>
<div className="mb-2 border-bottom small">{levelMap[level]}:</div>
{selectedRepo.group[level].map((item: any) => (
<div key={item.id}>
<div>
<a
href="#"
className="arrow-icon"
onClick={() => {
handleClick(item.id);
}}
>
<FontAwesomeIcon
size="xs"
icon={isOpen(item.id) ? faChevronDown : faChevronRight}
/>
</a>
<a
href="#"
className="workshop-link"
onClick={() => {
handleClick(item.id);
}}
>
{selectedRepo.entities[item.id].name}
</a>
<Link
to={`/list?id=${item.id}`}
className="text-decoration-none float-right"
>
<FontAwesomeIcon icon={faPlayCircle} size="lg" />
</Link>
</div>
<div
className={`container-fluid bg-light pt-3 mt-2 ${
isOpen(item.id) ? '' : 'description-collapsed'
}`}
>
{levelMap[level] && (
<p className="tag pt-2 pr-1 font-weight-bold small text-uppercase">
{levelMap[level]}
</p>
)}
{selectedRepo.entities[item.id].metadata.data.tags?.map(
(tag: string) => (
<p
key={tag}
className="tag pr-1 font-weight-bold small text-uppercase"
>
{tag}
</p>
),
)}
{selectedRepo.entities[item.id].steps && (
<div className="d-none">
{selectedRepo.entities[item.id].steps.length} step(s)
</div>
)}
<div className="workshop-list_description pb-3 pt-3">
<Markdown
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
>
{selectedRepo.entities[item.id].description?.content}
</Markdown>
</div>
<div className="actions"></div>
</div>
<div className="mb-3"></div>
</div>
))}
</div>
))}
</div>
)}
</div>
);
}
export default HomePage;

@ -0,0 +1,5 @@
.remixLogo {
position: absolute;
left: 0px;
right: 0px;
}

@ -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 (
<div>
<div className="remixLogo">
<img src={logo} />
</div>
</div>
)
}
export default LogoPage

@ -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;
}
}

@ -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 (
<>
<div className="fixed-top">
<div className="bg-light">
<BackButton entity={entity} />
</div>
</div>
<div id="top"></div>
{errorLoadingFile ? (
<>
<div className="errorloadingspacer"></div>
<h1 className="pl-3 pr-3 pt-3 pb-1">{step.name}</h1>
<button
className="w-100nav-item rounded-0 nav-link btn btn-success test"
onClick={() => {
dispatch({
type: 'remixide/displayFile',
payload: step,
});
}}
>
Load the file
</button>
<div className="mb-4"></div>
</>
) : (
<>
<div className="menuspacer"></div>
<h1 className="pr-3 pl-3 pt-3 pb-1">{step.name}</h1>
</>
)}
<div className="container-fluid">
<Markdown rehypePlugins={[rehypeRaw]}>
{step.markdown?.content}
</Markdown>
</div>
{step.test?.content ? (
<>
<nav className="nav nav-pills nav-fill">
{errorLoadingFile ? (
<button
className="nav-item rounded-0 nav-link btn btn-warning test"
onClick={() => {
dispatch({
type: 'remixide/displayFile',
payload: step,
});
}}
>
Load the file
</button>
) : (
<>
{!errorLoadingFile ? (
<>
<button
className="nav-item rounded-0 nav-link btn btn-info test"
onClick={() => {
dispatch({
type: 'remixide/testStep',
payload: step,
});
}}
>
Check Answer
</button>
{step.answer?.content && (
<button
className="nav-item rounded-0 nav-link btn btn-warning test"
onClick={() => {
dispatch({
type: 'remixide/showAnswer',
payload: step,
});
}}
>
Show answer
</button>
)}
</>
) : (
<>
{!errorLoadingFile && (
<>
<button
className="nav-item rounded-0 nav-link btn btn-success test"
onClick={() => {
navigate(
stepId === steps.length - 1
? `/list?id=${id}`
: `/detail?id=${id}&stepId=${stepId + 1}`,
);
}}
>
Next
</button>
{step.answer?.content && (
<button
className="nav-item rounded-0 nav-link btn btn-warning test"
onClick={() => {
dispatch({
type: 'remixide/showAnswer',
payload: step,
});
}}
>
Show answer
</button>
)}
</>
)}
</>
)}
</>
)}
</nav>
{success && (
<button
className="w-100 rounded-0 nav-item nav-link btn btn-success"
onClick={() => {
navigate(
stepId === steps.length - 1
? `/list?id=${id}`
: `/detail?id=${id}&stepId=${stepId + 1}`,
);
}}
>
Next
</button>
)}
<div id="errors">
{success && (
<div
className="alert rounded-0 alert-success mb-0 mt-0"
role="alert"
>
Well done! No errors.
</div>
)}
{errors.length > 0 && (
<>
{!success && (
<div
className="alert rounded-0 alert-danger mb-0 mt-0"
role="alert"
>
Errors
</div>
)}
{errors.map((error: string, index: number) => (
<div
key={index}
className="alert rounded-0 alert-warning mb-0 mt-0"
>
{error}
</div>
))}
</>
)}
</div>
</>
) : (
<>
<nav className="nav nav-pills nav-fill">
{!errorLoadingFile && step.answer?.content && (
<button
className="nav-item rounded-0 nav-link btn btn-warning test"
onClick={() => {
dispatch({
type: 'remixide/showAnswer',
payload: step,
});
}}
>
Show answer
</button>
)}
</nav>
{stepId < steps.length - 1 && (
<button
className="w-100 btn btn-success"
onClick={() => {
navigate(`/detail?id=${id}&stepId=${stepId + 1}`);
}}
>
Next
</button>
)}
{stepId === steps.length - 1 && (
<button
className="w-100 btn btn-success"
onClick={() => {
navigate(`/list?id=${id}`);
}}
>
Finish tutorial
</button>
)}
</>
)}
</>
);
}
export default StepDetailPage;

@ -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;
}
}

@ -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 (
<>
<div className="fixed-top">
<div className="bg-light">
<BackButton />
</div>
</div>
<div id="top"></div>
<h1 className="pl-3 pr-3 pt-2 pb-1 menuspacer">{entity.name}</h1>
<div className="container-fluid">
<Markdown>{entity.text}</Markdown>
</div>
<SlideIn>
<article className="list-group m-3">
{entity.steps.map((step: any, i: number) => (
<Link
key={i}
to={`/detail?id=${id}&stepId=${i}`}
className="rounded-0 btn btn-light border-bottom text-left steplink"
>
{step.name} »
</Link>
))}
</article>
</SlideIn>
</>
);
}
export default StepListPage;

@ -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'

@ -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"
]
}

@ -0,0 +1 @@
import 'react-scripts';

@ -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<RootState> = useSelector;

@ -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;

@ -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.<br>Please activate it using the `SOLIDITY` button in the `Featured Plugins` section of the homepage.<img class='img-thumbnail mt-3' src='assets/activatesolidity.png'>"],
},
})
}
},
},
}
export default Model

@ -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;

@ -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<string, any>;
export interface ModelType {
namespace: string;
state: StateType;
reducers: Record<
string,
(state: StateType, action: PayloadAction<any>) => StateType
>;
effects: Record<
string,
(
action: PayloadAction<any>,
effects: {
call: typeof call;
put: typeof put;
delay: typeof delay;
select: typeof select;
},
) => Generator<any, void, any>
>;
}
function createReducer(model: ModelType): Reducer {
const reducers = model.reducers;
Object.keys(model.effects).forEach((key) => {
reducers[key] = (state: StateType, action: PayloadAction<any>) => 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<typeof store.getState>;

@ -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();

@ -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';

@ -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"]
}

@ -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"
}
]
}

@ -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;
});

@ -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",

@ -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",

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

Loading…
Cancel
Save