parent
2af97d7f1f
commit
88eeb77272
@ -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; |
After Width: | Height: | Size: 793 B |
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> |
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; |
||||
}); |
Loading…
Reference in new issue