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