Merge branch 'master' of https://github.com/ethereum/remix-project into git4
commit
4ff34b68fd
File diff suppressed because one or more lines are too long
@ -0,0 +1,134 @@ |
||||
# Remix LearnEth Plugin |
||||
|
||||
## Available Scripts |
||||
|
||||
In the project directory, you can run: |
||||
|
||||
### `npm run serve:plugin --plugin=learneth` |
||||
|
||||
Runs the app in the development mode.\ |
||||
Open [http://localhost:2024](http://localhost:2024) to view it in the browser. |
||||
|
||||
The page will reload if you make edits.\ |
||||
You will also see any lint errors in the console. |
||||
|
||||
### `npm run build:plugin --plugin=learneth` |
||||
|
||||
Builds the app for production to the `dist/apps/learneth` folder.\ |
||||
It correctly bundles React in production mode and optimizes the build for the best performance. |
||||
|
||||
The build is minified and the filenames include the hashes.\ |
||||
Your app is ready to be deployed! |
||||
|
||||
## Loading the plugin in remix |
||||
|
||||
When testing with localhost you should use the HTTP version of either REMIX or REMIX ALPHA. Click on the plugin manager icon and |
||||
add the plugin 'Connect to a local plugin'. Your plugin will be at http://localhost:2024/. |
||||
|
||||
## Setting up the REMIX IDE for working with the plugin |
||||
|
||||
The plugin only works when a compiler environment is loaded as well, for example on the home screen of the IDE you select 'Solidity' or 'Vyper'. Without this the plugin |
||||
cannot compile and test files in the workshops. |
||||
|
||||
## Setting up your Github workshops repo |
||||
|
||||
You can create your own workshops that can be imported in the plugin. |
||||
When importing a github repo the plugin will look for a directory structure describing the workshops. |
||||
For example: https://github.com/ethereum/remix-workshops |
||||
|
||||
### Root directories |
||||
|
||||
Root directories are individual workshops, the name used will be the name of the workshop unless you override this with the name property in the config.yml. |
||||
|
||||
### README.md |
||||
|
||||
The readme in each directry contains an explanation of what the workshop is about. If an additional summary property is provided in the config.yml that will be used in the overview section of the plugin. |
||||
|
||||
### config.yml |
||||
|
||||
This config file contains meta data describing some properties of your workshop, for example |
||||
|
||||
``` |
||||
--- |
||||
id: someid |
||||
name: my workshop name |
||||
summary: something about this workshop |
||||
level: 4 |
||||
tags: |
||||
- solidity |
||||
- beginner |
||||
``` |
||||
|
||||
Level: a level of difficulty indicator ( 1 - 5 ) |
||||
|
||||
Tags: an array of tags |
||||
|
||||
id: this is used by the system to let REMIX call startTutorial(repo,branch,id). See below for more instructions. |
||||
|
||||
### Steps |
||||
|
||||
Each workshop contains what we call steps. |
||||
Each step is a directory containing: |
||||
|
||||
- a readme describing the step, what to do. |
||||
- sol files: |
||||
- these can be sol files and test sol files. The test files should be name yoursolname_test.sol |
||||
- ANSWER files: these are named yoursolname_answer.sol and can be used to show the solution or the correct answer. The plugin will load the |
||||
file in the IDE when a user clicks on 'Show Answer' |
||||
- js files |
||||
- vyper files |
||||
|
||||
## Functions to call the plugin from the IDE |
||||
|
||||
### Add a repository: |
||||
|
||||
``` |
||||
addRepository(repoName, branch) |
||||
``` |
||||
|
||||
### Start a tutorial |
||||
|
||||
``` |
||||
startTutorial(repoName,branch,id) |
||||
``` |
||||
|
||||
You don't need to add a seperate addRepository before calling startTutorial, this call will also add the repo. |
||||
|
||||
_Parameters_ |
||||
|
||||
id: this can be two things: |
||||
|
||||
- type of number, it specifies the n-th tutorial in the list |
||||
- type of string, this refers to the ID parameter in the config.yml file in the tutorial |
||||
for example: |
||||
|
||||
``` |
||||
--- |
||||
id: basics |
||||
name: 1 Basics of Solidity |
||||
summary: Some basic functions explained |
||||
level: 4 |
||||
tags: |
||||
- solidity |
||||
``` |
||||
|
||||
### How to call these functions in the REMIX IDE |
||||
|
||||
``` |
||||
(function () { |
||||
try { |
||||
// You don't need to add a seperate addRepository before calling startTutorial, this is just an example |
||||
remix.call('LearnEth', 'addRepository', "ethereum/remix-workshops", "master") |
||||
remix.call('LearnEth', 'startTutorial', "ethereum/remix-workshops", "master", "basics") |
||||
remix.call('LearnEth', 'startTutorial', "ethereum/remix-workshops", "master", 2) |
||||
} catch (e) { |
||||
console.log(e.message) |
||||
} |
||||
})() |
||||
``` |
||||
|
||||
Then call this in the REMIX console |
||||
|
||||
``` |
||||
remix.exeCurrent() |
||||
``` |
@ -0,0 +1,58 @@ |
||||
{ |
||||
"name": "learneth", |
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json", |
||||
"sourceRoot": "apps/learneth/src", |
||||
"projectType": "application", |
||||
"implicitDependencies": [], |
||||
"targets": { |
||||
"build": { |
||||
"executor": "@nrwl/webpack:webpack", |
||||
"outputs": ["{options.outputPath}"], |
||||
"defaultConfiguration": "development", |
||||
"options": { |
||||
"compiler": "babel", |
||||
"outputPath": "dist/apps/learneth", |
||||
"index": "apps/learneth/src/index.html", |
||||
"baseHref": "./", |
||||
"main": "apps/learneth/src/main.tsx", |
||||
"polyfills": "apps/learneth/src/polyfills.ts", |
||||
"tsConfig": "apps/learneth/tsconfig.app.json", |
||||
"assets": ["apps/learneth/src/profile.json"], |
||||
"styles": ["apps/learneth/src/index.css"], |
||||
"scripts": [], |
||||
"webpackConfig": "apps/learneth/webpack.config.js" |
||||
}, |
||||
"configurations": { |
||||
"development": { |
||||
}, |
||||
"production": { |
||||
"fileReplacements": [ |
||||
{ |
||||
"replace": "apps/learneth/src/environments/environment.ts", |
||||
"with": "apps/learneth/src/environments/environment.prod.ts" |
||||
} |
||||
] |
||||
} |
||||
} |
||||
}, |
||||
"serve": { |
||||
"executor": "@nrwl/webpack:dev-server", |
||||
"defaultConfiguration": "development", |
||||
"options": { |
||||
"buildTarget": "learneth:build", |
||||
"hmr": true, |
||||
"baseHref": "/" |
||||
}, |
||||
"configurations": { |
||||
"development": { |
||||
"buildTarget": "learneth:build:development", |
||||
"port": 2024 |
||||
}, |
||||
"production": { |
||||
"buildTarget": "learneth:build:production" |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
"tags": [] |
||||
} |
@ -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,41 @@ |
||||
import React from 'react' |
||||
import {createHashRouter, RouterProvider} from 'react-router-dom' |
||||
import {ToastContainer} from 'react-toastify' |
||||
import LoadingScreen from './components/LoadingScreen' |
||||
import LogoPage from './pages/Logo' |
||||
import HomePage from './pages/Home' |
||||
import StepListPage from './pages/StepList' |
||||
import StepDetailPage from './pages/StepDetail' |
||||
import 'react-toastify/dist/ReactToastify.css' |
||||
import './App.css' |
||||
|
||||
export const router = createHashRouter([ |
||||
{ |
||||
path: '/', |
||||
element: <LogoPage />, |
||||
}, |
||||
{ |
||||
path: '/home', |
||||
element: <HomePage />, |
||||
}, |
||||
{ |
||||
path: '/list', |
||||
element: <StepListPage />, |
||||
}, |
||||
{ |
||||
path: '/detail', |
||||
element: <StepDetailPage />, |
||||
}, |
||||
]) |
||||
|
||||
function App(): JSX.Element { |
||||
return ( |
||||
<> |
||||
<RouterProvider router={router} /> |
||||
<LoadingScreen /> |
||||
<ToastContainer position="bottom-right" newestOnTop closeOnClick rtl={false} pauseOnFocusLoss draggable pauseOnHover autoClose={false} theme="colored" /> |
||||
</> |
||||
) |
||||
} |
||||
|
||||
export default App |
@ -0,0 +1,28 @@ |
||||
a { |
||||
.arrow { |
||||
display: inline-block; |
||||
opacity: 0; |
||||
transform: scale(0.5); |
||||
transition: all 0.3s; |
||||
} |
||||
span { |
||||
display: inline-block; |
||||
padding-left: 5px; |
||||
transform: translateX(-0.875em); // size of icon |
||||
transition: transform 0.3s; |
||||
} |
||||
} |
||||
|
||||
.workshoptitle{ |
||||
text-decoration: none; |
||||
} |
||||
|
||||
a:hover { |
||||
fa-icon { |
||||
opacity: 1; |
||||
transform: scale(1); |
||||
} |
||||
span { |
||||
transform: translateX(0); |
||||
} |
||||
} |
@ -0,0 +1,87 @@ |
||||
import React, {useState} from 'react' |
||||
import {Link, useLocation, useNavigate} from 'react-router-dom' |
||||
import {Button, Modal, Tooltip, OverlayTrigger} from 'react-bootstrap' |
||||
import './index.scss' |
||||
|
||||
function BackButton({entity}: any) { |
||||
const navigate = useNavigate() |
||||
const location = useLocation() |
||||
const [show, setShow] = useState(false) |
||||
const isDetailPage = location.pathname === '/detail' |
||||
const queryParams = new URLSearchParams(location.search) |
||||
const stepId = Number(queryParams.get('stepId')) |
||||
|
||||
return ( |
||||
<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>}> |
||||
<i className="fas fa-home pl-1" /> |
||||
</OverlayTrigger> |
||||
</div> |
||||
</li> |
||||
{isDetailPage && ( |
||||
<li className="nav-item"> |
||||
<Link className="btn" to={`/list?id=${entity.id}`} title="Tutorial menu"> |
||||
<i className="fas fa-bars" /> |
||||
</Link> |
||||
</li> |
||||
)} |
||||
</ul> |
||||
{isDetailPage && ( |
||||
<form className="form-inline"> |
||||
{stepId > 0 && ( |
||||
<Link to={`/detail?id=${entity.id}&stepId=${stepId - 1}`}> |
||||
<i className="fas fa-chevron-left pr-1" /> |
||||
</Link> |
||||
)} |
||||
{stepId + 1}/{entity && <div className="">{entity.steps.length}</div>} |
||||
{stepId < entity.steps.length - 1 && ( |
||||
<Link to={`/detail?id=${entity.id}&stepId=${stepId + 1}`}> |
||||
<i className="fas fa-chevron-right pl-1" /> |
||||
</Link> |
||||
)} |
||||
</form> |
||||
)} |
||||
<Modal |
||||
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,120 @@ |
||||
import React, {useState, useEffect} from 'react' |
||||
import {Button, Dropdown, Form, Tooltip, OverlayTrigger} from 'react-bootstrap' |
||||
import {useAppDispatch} from '../../redux/hooks' |
||||
import './index.css' |
||||
|
||||
function RepoImporter({list, selectedRepo}: any): JSX.Element { |
||||
const [open, setOpen] = useState(false) |
||||
const [name, setName] = useState('') |
||||
const [branch, setBranch] = useState('') |
||||
const dispatch = useAppDispatch() |
||||
|
||||
useEffect(() => { |
||||
setName(selectedRepo.name) |
||||
setBranch(selectedRepo.branch) |
||||
}, [selectedRepo]) |
||||
|
||||
const panelChange = () => { |
||||
setOpen(!open) |
||||
} |
||||
|
||||
const selectRepo = (repo: {name: string; branch: string}) => { |
||||
dispatch({type: 'workshop/loadRepo', payload: repo}) |
||||
} |
||||
|
||||
const importRepo = (event: {preventDefault: () => void}) => { |
||||
event.preventDefault() |
||||
dispatch({type: 'workshop/loadRepo', payload: {name, branch}}) |
||||
} |
||||
|
||||
const resetAll = () => { |
||||
dispatch({type: 'workshop/resetAll'}) |
||||
setName('') |
||||
setBranch('') |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
{selectedRepo.name && ( |
||||
<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"> |
||||
<i className={`arrow-icon pt-1 fas fa-xs ${open ? 'fa-chevron-down' : 'fa-chevron-right'}`}></i> |
||||
</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>}> |
||||
<i className="fas fa-question-circle" /> |
||||
</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"> |
||||
<i className="fas fa-info-circle" /> how to setup your repo |
||||
</a> |
||||
</Form> |
||||
)} |
||||
<hr /> |
||||
</div> |
||||
</> |
||||
) |
||||
} |
||||
|
||||
export default RepoImporter |
@ -0,0 +1,21 @@ |
||||
.slide-enter { |
||||
transform: translateY(100px); |
||||
opacity: 0; |
||||
} |
||||
|
||||
.slide-enter-active { |
||||
transform: translateY(0); |
||||
opacity: 1; |
||||
transition: opacity 400ms, transform 400ms cubic-bezier(0.175, 0.885, 0.32, 1.275); |
||||
} |
||||
|
||||
.slide-exit { |
||||
transform: translateY(0); |
||||
opacity: 1; |
||||
} |
||||
|
||||
.slide-exit-active { |
||||
transform: translateY(100px); |
||||
opacity: 0; |
||||
transition: opacity 400ms, transform 400ms cubic-bezier(0.6, 0.04, 0.98, 0.335); |
||||
} |
@ -0,0 +1,18 @@ |
||||
import React, {type ReactNode, useEffect, useState} from 'react' |
||||
import {CSSTransition} from 'react-transition-group' |
||||
import './index.css' |
||||
|
||||
const SlideIn: React.FC<{children: ReactNode}> = ({children}) => { |
||||
const [show, setShow] = useState(false) |
||||
useEffect(() => { |
||||
setShow(true) |
||||
}, []) |
||||
|
||||
return ( |
||||
<CSSTransition in={show} timeout={400} classNames="slide" unmountOnExit> |
||||
{children} |
||||
</CSSTransition> |
||||
) |
||||
} |
||||
|
||||
export default SlideIn |
@ -0,0 +1,13 @@ |
||||
body { |
||||
margin: 0; |
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', |
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', |
||||
sans-serif; |
||||
-webkit-font-smoothing: antialiased; |
||||
-moz-osx-font-smoothing: grayscale; |
||||
} |
||||
|
||||
code { |
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', |
||||
monospace; |
||||
} |
@ -0,0 +1,18 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="utf-8" /> |
||||
<title>Learn ETH</title> |
||||
<base href="./" /> |
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||
<!-- <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous"/> --> |
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> --> |
||||
<link rel="stylesheet" integrity="ha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" |
||||
crossorigin="anonymous" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css"> |
||||
</head> |
||||
<body> |
||||
<div id="root"></div> |
||||
<script src="https://kit.fontawesome.com/41dd021e94.js" crossorigin="anonymous"></script> |
||||
</body> |
||||
</html> |
@ -0,0 +1,13 @@ |
||||
import React from 'react' |
||||
import ReactDOM from 'react-dom/client' |
||||
import {Provider} from 'react-redux' |
||||
import './index.css' |
||||
import App from './App' |
||||
import {store} from './redux/store' |
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) |
||||
root.render( |
||||
<Provider store={store}> |
||||
<App /> |
||||
</Provider> |
||||
) |
@ -0,0 +1,24 @@ |
||||
.description-collapsed{ |
||||
height: 0px; |
||||
overflow: hidden; |
||||
word-wrap: break-word; |
||||
padding: 0px !important; |
||||
margin: 0px !important; |
||||
} |
||||
|
||||
.tag{ |
||||
display: inline; |
||||
} |
||||
|
||||
.arrow-icon{ |
||||
width: 12px; |
||||
display: inline-block; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.workshop-link { |
||||
cursor: pointer; |
||||
} |
||||
.workshop-link:hover { |
||||
text-decoration: underline; |
||||
} |
@ -0,0 +1,96 @@ |
||||
import React, {useEffect} from 'react' |
||||
import {Link} from 'react-router-dom' |
||||
import Markdown from 'react-markdown' |
||||
import rehypeRaw from 'rehype-raw' |
||||
import remarkGfm from 'remark-gfm' |
||||
import {useAppDispatch, useAppSelector} from '../../redux/hooks' |
||||
import RepoImporter from '../../components/RepoImporter' |
||||
import './index.css' |
||||
|
||||
function HomePage(): JSX.Element { |
||||
const [openKeys, setOpenKeys] = React.useState<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> |
||||
<span |
||||
className="arrow-icon" |
||||
onClick={() => { |
||||
handleClick(item.id) |
||||
}} |
||||
> |
||||
<i className={`fas fa-xs ${isOpen(item.id) ? 'fa-chevron-down' : 'fa-chevron-right'}`} /> |
||||
</span> |
||||
<span |
||||
className="workshop-link" |
||||
onClick={() => { |
||||
handleClick(item.id) |
||||
}} |
||||
> |
||||
{selectedRepo.entities[item.id].name} |
||||
</span> |
||||
<Link to={`/list?id=${item.id}`} className="text-decoration-none float-right"> |
||||
<i className="fas fa-play-circle fa-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,20 @@ |
||||
import React, {useEffect} from 'react' |
||||
import {useAppDispatch} from '../../redux/hooks' |
||||
|
||||
const LogoPage: React.FC = () => { |
||||
const dispatch = useAppDispatch() |
||||
|
||||
useEffect(() => { |
||||
dispatch({type: 'remixide/connect'}) |
||||
}, []) |
||||
|
||||
return ( |
||||
<div> |
||||
<div> |
||||
<img className="w-100" src="https://remix.ethereum.org/assets/img/remixLogo.webp" /> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
export default LogoPage |
@ -0,0 +1,54 @@ |
||||
step-view { |
||||
height: 100%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: space-between; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
header, footer { |
||||
padding: 10px 5px; |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
|
||||
.errorloadingspacer{ |
||||
padding-top: 44px; |
||||
|
||||
} |
||||
|
||||
.title{ |
||||
pointer-events: none; |
||||
} |
||||
|
||||
h1 { |
||||
text-align: left; |
||||
font-size: 1.2rem !important; |
||||
word-break: break-word; |
||||
} |
||||
|
||||
markdown { |
||||
display: block; |
||||
flex: 1; |
||||
overflow: auto; |
||||
padding: 0px; |
||||
|
||||
h1 { |
||||
font-size: 1.2rem !important; |
||||
} |
||||
|
||||
h2 { |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
h3 { |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
h4 { |
||||
font-size: 1rem; |
||||
} |
||||
} |
||||
|
||||
|
@ -0,0 +1,228 @@ |
||||
import React, {useEffect} from 'react' |
||||
import {useLocation, useNavigate} from 'react-router-dom' |
||||
import Markdown from 'react-markdown' |
||||
import rehypeRaw from 'rehype-raw' |
||||
import BackButton from '../../components/BackButton' |
||||
import {useAppSelector, useAppDispatch} from '../../redux/hooks' |
||||
import './index.scss' |
||||
|
||||
function StepDetailPage() { |
||||
const navigate = useNavigate() |
||||
const location = useLocation() |
||||
const dispatch = useAppDispatch() |
||||
const queryParams = new URLSearchParams(location.search) |
||||
const id = queryParams.get('id') as string |
||||
const stepId = Number(queryParams.get('stepId')) |
||||
const { |
||||
workshop: {detail, selectedId}, |
||||
remixide: {errorLoadingFile, errors, success}, |
||||
} = useAppSelector((state: any) => state) |
||||
const entity = detail[selectedId].entities[id] |
||||
const steps = entity.steps |
||||
const step = steps[stepId] |
||||
console.log(step) |
||||
|
||||
useEffect(() => { |
||||
dispatch({ |
||||
type: 'remixide/displayFile', |
||||
payload: step, |
||||
}) |
||||
dispatch({ |
||||
type: 'remixide/save', |
||||
payload: {errors: [], success: false}, |
||||
}) |
||||
window.scrollTo(0, 0) |
||||
}, [step]) |
||||
|
||||
useEffect(() => { |
||||
if (errors.length > 0 || success) { |
||||
window.scrollTo(0, document.documentElement.scrollHeight) |
||||
} |
||||
}, [errors, success]) |
||||
|
||||
return ( |
||||
<> |
||||
<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,144 @@ |
||||
:host { |
||||
height: 100%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: space-between; |
||||
} |
||||
|
||||
header { |
||||
padding: 10px 5px; |
||||
} |
||||
|
||||
.menuspacer{ |
||||
margin-top: 52px; |
||||
} |
||||
|
||||
.steplink { |
||||
text-decoration: none; |
||||
} |
||||
|
||||
.title{ |
||||
pointer-events: none; |
||||
} |
||||
|
||||
h1 { |
||||
text-align: left; |
||||
font-size: 1.2rem !important; |
||||
word-break: break-word; |
||||
} |
||||
section { |
||||
flex: 1; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
.start { |
||||
padding: 5px 25px; |
||||
animation: jittery 2s 0.5s infinite; |
||||
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.5); |
||||
color: white; |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
|
||||
footer { |
||||
padding: 10px; |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
|
||||
@keyframes jittery { |
||||
5%, |
||||
50% { |
||||
transform: scale(1); |
||||
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.5); |
||||
} |
||||
10% { |
||||
transform: scale(0.9); |
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); |
||||
} |
||||
15% { |
||||
transform: scale(1.15); |
||||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); |
||||
} |
||||
20% { |
||||
transform: scale(1.15) rotate(-5deg); |
||||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); |
||||
} |
||||
25% { |
||||
transform: scale(1.15) rotate(5deg); |
||||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); |
||||
} |
||||
30% { |
||||
transform: scale(1.15) rotate(-3deg); |
||||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); |
||||
} |
||||
35% { |
||||
transform: scale(1.15) rotate(2deg); |
||||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); |
||||
} |
||||
40% { |
||||
transform: scale(1.15) rotate(0); |
||||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); |
||||
} |
||||
} |
||||
|
||||
.slide-in { |
||||
animation: slideIn 0.5s forwards; |
||||
visibility: hidden; |
||||
} |
||||
|
||||
@keyframes slideIn { |
||||
0% { |
||||
transform: translateY(-100%); |
||||
visibility: visible; |
||||
} |
||||
100% { |
||||
transform: translateY(0); |
||||
visibility: visible; |
||||
} |
||||
} |
||||
|
||||
@-moz-keyframes slideIn { |
||||
0% { |
||||
transform: translateY(-100%); |
||||
visibility: visible; |
||||
} |
||||
100% { |
||||
transform: translateY(0); |
||||
visibility: visible; |
||||
} |
||||
} |
||||
|
||||
@-webkit-keyframes slideIn { |
||||
0% { |
||||
transform: translateY(-100%); |
||||
visibility: visible; |
||||
} |
||||
100% { |
||||
transform: translateY(0); |
||||
visibility: visible; |
||||
} |
||||
} |
||||
|
||||
@-o-keyframes slideIn { |
||||
0% { |
||||
transform: translateY(-100%); |
||||
visibility: visible; |
||||
} |
||||
100% { |
||||
transform: translateY(0); |
||||
visibility: visible; |
||||
} |
||||
} |
||||
|
||||
@-ms-keyframes slideIn { |
||||
0% { |
||||
transform: translateY(-100%); |
||||
visibility: visible; |
||||
} |
||||
100% { |
||||
transform: translateY(0); |
||||
visibility: visible; |
||||
} |
||||
} |
@ -0,0 +1,41 @@ |
||||
import React from 'react' |
||||
import {Link, useLocation} from 'react-router-dom' |
||||
import Markdown from 'react-markdown' |
||||
import BackButton from '../../components/BackButton' |
||||
import SlideIn from '../../components/SlideIn' |
||||
import {useAppSelector} from '../../redux/hooks' |
||||
import './index.scss' |
||||
|
||||
function StepListPage(): JSX.Element { |
||||
const location = useLocation() |
||||
const queryParams = new URLSearchParams(location.search) |
||||
const id = queryParams.get('id') as string |
||||
const {detail, selectedId} = useAppSelector((state) => state.workshop) |
||||
const entity = detail[selectedId].entities[id] |
||||
|
||||
return ( |
||||
<> |
||||
<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": "assets/img/learnEthLogo.webp", |
||||
"location": "sidePanel", |
||||
"url": "plugins/learneth/index.html", |
||||
"repo": "https://github.com/ethereum/remix-project/tree/master/apps/learneth", |
||||
"maintainedBy": "Remix", |
||||
"authorContact": "", |
||||
"targets": [ |
||||
"remix" |
||||
] |
||||
} |
@ -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,229 @@ |
||||
import {toast} from 'react-toastify' |
||||
import {type ModelType} from '../store' |
||||
import remixClient from '../../remix-client' |
||||
import {router} from '../../App' |
||||
|
||||
function getFilePath(file: string): string { |
||||
const name = file.split('/') |
||||
return name.length > 1 ? `${name[name.length - 1]}` : '' |
||||
} |
||||
|
||||
const Model: ModelType = { |
||||
namespace: 'remixide', |
||||
state: { |
||||
errors: [], |
||||
success: false, |
||||
errorLoadingFile: false, |
||||
// theme: '',
|
||||
}, |
||||
reducers: { |
||||
save(state, {payload}) { |
||||
return {...state, ...payload} |
||||
}, |
||||
}, |
||||
effects: { |
||||
*connect(_, {put}) { |
||||
toast.info('connecting to the REMIX IDE') |
||||
|
||||
yield put({ |
||||
type: 'loading/save', |
||||
payload: { |
||||
screen: true, |
||||
}, |
||||
}) |
||||
|
||||
yield remixClient.onload() |
||||
|
||||
toast.dismiss() |
||||
|
||||
yield put({ |
||||
type: 'loading/save', |
||||
payload: { |
||||
screen: false, |
||||
}, |
||||
}) |
||||
|
||||
yield router.navigate('/home') |
||||
}, |
||||
*displayFile({payload: step}, {select, put}) { |
||||
let content = '' |
||||
let path = '' |
||||
if (step.solidity?.file) { |
||||
content = step.solidity.content |
||||
path = getFilePath(step.solidity.file) |
||||
} |
||||
if (step.js?.file) { |
||||
content = step.js.content |
||||
path = getFilePath(step.js.file) |
||||
} |
||||
if (step.vy?.file) { |
||||
content = step.vy.content |
||||
path = getFilePath(step.vy.file) |
||||
} |
||||
|
||||
if (!content) { |
||||
return |
||||
} |
||||
|
||||
toast.info(`loading ${path} into IDE`) |
||||
yield put({ |
||||
type: 'loading/save', |
||||
payload: { |
||||
screen: true, |
||||
}, |
||||
}) |
||||
|
||||
const {detail, selectedId} = yield select((state) => state.workshop) |
||||
|
||||
const workshop = detail[selectedId] |
||||
console.log('loading ', step, workshop) |
||||
|
||||
path = `.learneth/${workshop.name}/${step.name}/${path}` |
||||
try { |
||||
const isExist = yield remixClient.call('fileManager', 'exists' as any, path) |
||||
if (!isExist) { |
||||
yield remixClient.call('fileManager', 'setFile', path, content) |
||||
} |
||||
yield remixClient.call('fileManager', 'switchFile', `${path}`) |
||||
yield put({ |
||||
type: 'remixide/save', |
||||
payload: {errorLoadingFile: false}, |
||||
}) |
||||
toast.dismiss() |
||||
} catch (error) { |
||||
toast.dismiss() |
||||
toast.error('File could not be loaded. Please try again.') |
||||
yield put({ |
||||
type: 'remixide/save', |
||||
payload: {errorLoadingFile: true}, |
||||
}) |
||||
} |
||||
yield put({ |
||||
type: 'loading/save', |
||||
payload: { |
||||
screen: false, |
||||
}, |
||||
}) |
||||
}, |
||||
*testStep({payload: step}, {select, put}) { |
||||
yield put({ |
||||
type: 'loading/save', |
||||
payload: { |
||||
screen: true, |
||||
}, |
||||
}) |
||||
|
||||
try { |
||||
yield put({ |
||||
type: 'remixide/save', |
||||
payload: {success: false}, |
||||
}) |
||||
const {detail, selectedId} = yield select((state) => state.workshop) |
||||
|
||||
const workshop = detail[selectedId] |
||||
|
||||
let path: string |
||||
if (step.solidity.file) { |
||||
path = getFilePath(step.solidity.file) |
||||
path = `.learneth/${workshop.name}/${step.name}/${path}` |
||||
yield remixClient.call('fileManager', 'switchFile', `${path}`) |
||||
} |
||||
|
||||
console.log('testing ', step.test.content) |
||||
|
||||
path = getFilePath(step.test.file) |
||||
path = `.learneth/${workshop.name}/${step.name}/${path}` |
||||
yield remixClient.call('fileManager', 'setFile', path, step.test.content) |
||||
|
||||
const result = yield remixClient.call('solidityUnitTesting', 'testFromPath', path) |
||||
console.log('result ', result) |
||||
|
||||
if (!result) { |
||||
yield put({ |
||||
type: 'remixide/save', |
||||
payload: {errors: ['Compiler failed to test this file']}, |
||||
}) |
||||
} else { |
||||
const success = result.totalFailing === 0 |
||||
|
||||
if (success) { |
||||
yield put({ |
||||
type: 'remixide/save', |
||||
payload: {errors: [], success: true}, |
||||
}) |
||||
} else { |
||||
yield put({ |
||||
type: 'remixide/save', |
||||
payload: { |
||||
errors: result.errors.map((error: {message: any}) => error.message), |
||||
}, |
||||
}) |
||||
} |
||||
} |
||||
} catch (err) { |
||||
console.log('TESTING ERROR', err) |
||||
yield put({ |
||||
type: 'remixide/save', |
||||
payload: {errors: [String(err)]}, |
||||
}) |
||||
} |
||||
yield put({ |
||||
type: 'loading/save', |
||||
payload: { |
||||
screen: false, |
||||
}, |
||||
}) |
||||
}, |
||||
*showAnswer({payload: step}, {select, put}) { |
||||
yield put({ |
||||
type: 'loading/save', |
||||
payload: { |
||||
screen: true, |
||||
}, |
||||
}) |
||||
|
||||
toast.info('loading answer into IDE') |
||||
|
||||
try { |
||||
console.log('loading ', step) |
||||
const content = step.answer.content |
||||
let path = getFilePath(step.answer.file) |
||||
|
||||
const {detail, selectedId} = yield select((state) => state.workshop) |
||||
|
||||
const workshop = detail[selectedId] |
||||
path = `.learneth/${workshop.name}/${step.name}/${path}` |
||||
yield remixClient.call('fileManager', 'setFile', path, content) |
||||
yield remixClient.call('fileManager', 'switchFile', `${path}`) |
||||
} catch (err) { |
||||
yield put({ |
||||
type: 'remixide/save', |
||||
payload: {errors: [String(err)]}, |
||||
}) |
||||
} |
||||
|
||||
toast.dismiss() |
||||
yield put({ |
||||
type: 'loading/save', |
||||
payload: { |
||||
screen: false, |
||||
}, |
||||
}) |
||||
}, |
||||
*testSolidityCompiler(_, {put, select}) { |
||||
try { |
||||
yield remixClient.call('solidity', 'getCompilationResult') |
||||
} catch (err) { |
||||
const errors = yield select((state) => state.remixide.errors) |
||||
yield put({ |
||||
type: 'remixide/save', |
||||
payload: { |
||||
errors: [...errors, "The `Solidity Compiler` is not yet activated.<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,164 @@ |
||||
import axios from 'axios' |
||||
import {toast} from 'react-toastify' |
||||
import groupBy from 'lodash/groupBy' |
||||
import pick from 'lodash/pick' |
||||
import {type ModelType} from '../store' |
||||
import remixClient from '../../remix-client' |
||||
import {router} from '../../App' |
||||
|
||||
// const apiUrl = 'http://localhost:3001';
|
||||
const apiUrl = 'https://static.220.14.12.49.clients.your-server.de:3000' |
||||
|
||||
const Model: ModelType = { |
||||
namespace: 'workshop', |
||||
state: { |
||||
list: [], |
||||
detail: {}, |
||||
selectedId: '', |
||||
}, |
||||
reducers: { |
||||
save(state, {payload}) { |
||||
return {...state, ...payload} |
||||
}, |
||||
}, |
||||
effects: { |
||||
*init(_, {put}) { |
||||
const cache = localStorage.getItem('workshop.state') |
||||
|
||||
if (cache) { |
||||
const workshopState = JSON.parse(cache) |
||||
yield put({ |
||||
type: 'workshop/save', |
||||
payload: workshopState, |
||||
}) |
||||
} else { |
||||
yield put({ |
||||
type: 'workshop/loadRepo', |
||||
payload: { |
||||
name: 'ethereum/remix-workshops', |
||||
branch: 'master', |
||||
}, |
||||
}) |
||||
} |
||||
}, |
||||
*loadRepo({payload}, {put, select}) { |
||||
toast.info(`loading ${payload.name}/${payload.branch}`) |
||||
|
||||
yield put({ |
||||
type: 'loading/save', |
||||
payload: { |
||||
screen: true, |
||||
}, |
||||
}) |
||||
|
||||
const {list, detail} = yield select((state) => state.workshop) |
||||
|
||||
const url = `${apiUrl}/clone/${encodeURIComponent(payload.name)}/${payload.branch}?${Math.random()}` |
||||
console.log('loading ', url) |
||||
const {data} = yield axios.get(url) |
||||
console.log(data) |
||||
const repoId = `${payload.name}-${payload.branch}` |
||||
|
||||
for (let i = 0; i < data.ids.length; i++) { |
||||
const { |
||||
steps, |
||||
metadata: { |
||||
data: {steps: metadataSteps}, |
||||
}, |
||||
} = data.entities[data.ids[i]] |
||||
|
||||
let newSteps = [] |
||||
|
||||
if (metadataSteps) { |
||||
newSteps = metadataSteps.map((step: any) => { |
||||
return { |
||||
...steps.find((item: any) => item.name === step.path), |
||||
name: step.name, |
||||
} |
||||
}) |
||||
} else { |
||||
newSteps = steps.map((step: any) => ({ |
||||
...step, |
||||
name: step.name.replace('_', ' '), |
||||
})) |
||||
} |
||||
|
||||
const stepKeysWithFile = ['markdown', 'solidity', 'test', 'answer', 'js', 'vy'] |
||||
|
||||
for (let j = 0; j < newSteps.length; j++) { |
||||
const step = newSteps[j] |
||||
for (let k = 0; k < stepKeysWithFile.length; k++) { |
||||
const key = stepKeysWithFile[k] |
||||
if (step[key]) { |
||||
try { |
||||
step[key].content = (yield remixClient.call('contentImport', 'resolve', step[key].file)).content |
||||
} catch (error) { |
||||
console.error(error) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
data.entities[data.ids[i]].steps = newSteps |
||||
} |
||||
|
||||
const workshopState = { |
||||
detail: { |
||||
...detail, |
||||
[repoId]: { |
||||
...data, |
||||
group: groupBy( |
||||
data.ids.map((id: string) => pick(data.entities[id], ['level', 'id'])), |
||||
(item: any) => item.level |
||||
), |
||||
...payload, |
||||
}, |
||||
}, |
||||
list: detail[repoId] ? list : [...list, payload], |
||||
selectedId: repoId, |
||||
} |
||||
yield put({ |
||||
type: 'workshop/save', |
||||
payload: workshopState, |
||||
}) |
||||
localStorage.setItem('workshop.state', JSON.stringify(workshopState)) |
||||
|
||||
toast.dismiss() |
||||
yield put({ |
||||
type: 'loading/save', |
||||
payload: { |
||||
screen: false, |
||||
}, |
||||
}) |
||||
|
||||
if (payload.id) { |
||||
const {detail, selectedId} = workshopState |
||||
const {ids, entities} = detail[selectedId] |
||||
for (let i = 0; i < ids.length; i++) { |
||||
const entity = entities[ids[i]] |
||||
if (entity.metadata.data.id === payload.id || i + 1 === payload.id) { |
||||
yield router.navigate(`/list?id=${ids[i]}`) |
||||
break |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
*resetAll(_, {put}) { |
||||
yield put({ |
||||
type: 'workshop/save', |
||||
payload: { |
||||
list: [], |
||||
detail: {}, |
||||
selectedId: '', |
||||
}, |
||||
}) |
||||
|
||||
localStorage.removeItem('workshop.state') |
||||
|
||||
yield put({ |
||||
type: 'workshop/init', |
||||
}) |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
export default Model |
@ -0,0 +1,97 @@ |
||||
import {configureStore, createSlice, type PayloadAction, type Reducer} from '@reduxjs/toolkit' |
||||
import createSagaMiddleware from 'redux-saga' |
||||
import {call, put, takeEvery, delay, select, all, fork, type ForkEffect} from 'redux-saga/effects' |
||||
|
||||
// @ts-expect-error
|
||||
const context = require.context('./models', false, /\.ts$/) |
||||
const models = context.keys().map((key: any) => context(key).default) |
||||
|
||||
export type StateType = Record<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,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,90 @@ |
||||
const {composePlugins, withNx} = require('@nrwl/webpack') |
||||
const webpack = require('webpack') |
||||
const TerserPlugin = require('terser-webpack-plugin') |
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') |
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(withNx(), (config) => { |
||||
// Update the webpack config as needed here.
|
||||
// e.g. `config.plugins.push(new MyPlugin())`
|
||||
// add fallback for node modules
|
||||
config.resolve.fallback = { |
||||
...config.resolve.fallback, |
||||
crypto: require.resolve('crypto-browserify'), |
||||
stream: require.resolve('stream-browserify'), |
||||
path: require.resolve('path-browserify'), |
||||
http: require.resolve('stream-http'), |
||||
https: require.resolve('https-browserify'), |
||||
constants: require.resolve('constants-browserify'), |
||||
os: false, //require.resolve("os-browserify/browser"),
|
||||
timers: false, // require.resolve("timers-browserify"),
|
||||
zlib: require.resolve('browserify-zlib'), |
||||
fs: false, |
||||
module: false, |
||||
tls: false, |
||||
net: false, |
||||
readline: false, |
||||
child_process: false, |
||||
buffer: require.resolve('buffer/'), |
||||
vm: require.resolve('vm-browserify'), |
||||
} |
||||
|
||||
// add externals
|
||||
config.externals = { |
||||
...config.externals, |
||||
solc: 'solc', |
||||
} |
||||
|
||||
// add public path
|
||||
config.output.publicPath = './' |
||||
|
||||
// add copy & provide plugin
|
||||
config.plugins.push( |
||||
new webpack.ProvidePlugin({ |
||||
Buffer: ['buffer', 'Buffer'], |
||||
url: ['url', 'URL'], |
||||
process: 'process/browser', |
||||
}) |
||||
) |
||||
|
||||
// set the define plugin to load the WALLET_CONNECT_PROJECT_ID
|
||||
config.plugins.push( |
||||
new webpack.DefinePlugin({ |
||||
WALLET_CONNECT_PROJECT_ID: JSON.stringify(process.env.WALLET_CONNECT_PROJECT_ID), |
||||
}) |
||||
) |
||||
|
||||
// souce-map loader
|
||||
config.module.rules.push({ |
||||
test: /\.js$/, |
||||
use: ['source-map-loader'], |
||||
enforce: 'pre', |
||||
}) |
||||
|
||||
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
|
||||
|
||||
// set minimizer
|
||||
config.optimization.minimizer = [ |
||||
new TerserPlugin({ |
||||
parallel: true, |
||||
terserOptions: { |
||||
ecma: 2015, |
||||
compress: false, |
||||
mangle: false, |
||||
format: { |
||||
comments: false, |
||||
}, |
||||
}, |
||||
extractComments: false, |
||||
}), |
||||
new CssMinimizerPlugin(), |
||||
] |
||||
|
||||
config.watchOptions = { |
||||
ignored: /node_modules/, |
||||
} |
||||
|
||||
config.experiments.syncWebAssembly = true |
||||
|
||||
return config |
||||
}) |
@ -0,0 +1,33 @@ |
||||
{ |
||||
"editor.keyboardShortcuts": "Keyboard Shortcuts", |
||||
"editor.keyboardShortcuts.text1": "Compile the current contract", |
||||
"editor.keyboardShortcuts.text2": "Open the File Explorer", |
||||
"editor.keyboardShortcuts.text3": "Open the Plugin Manager", |
||||
"editor.keyboardShortcuts.text4": "Compile the current contract & Run an associated script", |
||||
"editor.editorKeyboardShortcuts": "Editor Keyboard Shortcuts", |
||||
"editor.editorKeyboardShortcuts.text1": "Format the code in the current file", |
||||
"editor.importantLinks": "Important Links", |
||||
"editor.importantLinks.text1": "Official website about the Remix Project", |
||||
"editor.importantLinks.text2": "Official documentation", |
||||
"editor.title1": "Pasted Code Alert", |
||||
"editor.title1.message1": "You have just pasted a code snippet or contract in the editor.", |
||||
"editor.title1.message2": "Make sure you fully understand this code before deploying or interacting with it. Don't get scammed!", |
||||
"editor.title1.message3": "Running untrusted code can put your wallet <span> at risk </span>. In a worst-case scenario, you could <span>lose all your money</span>.", |
||||
"editor.title1.message4": "If you don't fully understand it, please don't run this code.", |
||||
"editor.title1.message5": "If you are not a smart contract developer, ask someone you trust who has the skills to determine if this code is safe to use.", |
||||
"editor.title1.message6": "See <a> these recommendations </a> for more information.", |
||||
"editor.zoomIn": "Zoom In", |
||||
"editor.zoomOut": "Zoom Out", |
||||
"editor.formatCode": "Format Code", |
||||
"editor.generateDocumentation": "Generate documentation for this function", |
||||
"editor.generateDocumentation2": "Generate documentation for the function \"{name}\"", |
||||
"editor.generateDocumentationByAI": "solidity code: {content}\n Generate the documentation for the function {currentFunction} using the Doxygen style syntax", |
||||
"editor.explainFunction": "Explain this function", |
||||
"editor.explainFunction2": "Explain the function \"{name}\"", |
||||
"editor.explainFunctionByAI": "solidity code: {content}\n Explain the function {currentFunction}", |
||||
"editor.executeFreeFunction": "Run a free function", |
||||
"editor.executeFreeFunction2": "Run the free function \"{name}\"", |
||||
"editor.toastText1": "This can only execute free function", |
||||
"editor.toastText2": "Please go to Remix settings and activate the code editor features or wait that the current editor context is loaded.", |
||||
"editor.text": "The file is opened in <b>read-only</b> mode." |
||||
} |
@ -1,39 +1,13 @@ |
||||
import debuggerJson from './debugger.json'; |
||||
import filePanelJson from './filePanel.json'; |
||||
import homeJson from './home.json'; |
||||
import panelJson from './panel.json'; |
||||
import pluginManagerJson from './pluginManager.json'; |
||||
import searchJson from './search.json'; |
||||
import settingsJson from './settings.json'; |
||||
import solidityJson from './solidity.json'; |
||||
import terminalJson from './terminal.json'; |
||||
import udappJson from './udapp.json'; |
||||
import solidityUnitTestingJson from './solidityUnitTesting.json'; |
||||
import permissionHandlerJson from './permissionHandler.json'; |
||||
import electronJson from './electron.json'; |
||||
import solUmlGenJson from './solUmlGen.json' |
||||
import remixAppJson from './remixApp.json' |
||||
import remixUiTabsJson from './remixUiTabs.json' |
||||
import circuitJson from './circuit.json'; |
||||
import gitJson from './git.json' |
||||
function readAndCombineJsonFiles() { |
||||
const dataContext = require.context('./', true, /\.json$/) |
||||
|
||||
export default { |
||||
...debuggerJson, |
||||
...filePanelJson, |
||||
...homeJson, |
||||
...panelJson, |
||||
...pluginManagerJson, |
||||
...searchJson, |
||||
...settingsJson, |
||||
...solidityJson, |
||||
...terminalJson, |
||||
...udappJson, |
||||
...solidityUnitTestingJson, |
||||
...permissionHandlerJson, |
||||
...electronJson, |
||||
...solUmlGenJson, |
||||
...remixAppJson, |
||||
...remixUiTabsJson, |
||||
...circuitJson, |
||||
...gitJson |
||||
let combinedData = {} |
||||
dataContext.keys().forEach((key) => { |
||||
const jsonData = dataContext(key) |
||||
combinedData = {...combinedData, ...jsonData} |
||||
}) |
||||
|
||||
return combinedData |
||||
} |
||||
|
||||
export default readAndCombineJsonFiles() |
||||
|
@ -0,0 +1,19 @@ |
||||
{ |
||||
"publishToStorage.title1": "Publish To Storage", |
||||
"publishToStorage.title1.message": "This contract may be abstract, it may not implement an abstract parent's methods completely or it may not invoke an inherited contract's constructor correctly.", |
||||
"publishToStorage.title2": "Published {name}'s Metadata and Sources", |
||||
"publishToStorage.title2.message": "Metadata and sources of \"{name}\" were published successfully.", |
||||
"publishToStorage.title3": "Swarm Publish Failed", |
||||
"publishToStorage.title4": "IPFS Settings", |
||||
"publishToStorage.title4.message1": "You have not set your own custom IPFS settings.", |
||||
"publishToStorage.title4.message2": "We won’t be providing a public endpoint anymore for publishing your contracts to IPFS.", |
||||
"publishToStorage.title4.message3": "Instead of that, 4 options are now available:", |
||||
"publishToStorage.title4.message4": "DEFAULT OPTION: Use the public INFURA node. This will not guarantee your data will persist.", |
||||
"publishToStorage.title4.message5": "Use your own INFURA IPFS node. This requires a subscription. <a>Learn more</a>", |
||||
"publishToStorage.title4.message6": "Use any external IPFS which doesn’t require any authentication.", |
||||
"publishToStorage.title4.message7": "Use your own local ipfs node (which usually runs under http://localhost:5001)", |
||||
"publishToStorage.title4.message8": "You can update your IPFS settings in the SETTINGS tab.", |
||||
"publishToStorage.title4.message9": "Now the default option will be used.", |
||||
"publishToStorage.title5": "IPFS Publish Failed", |
||||
"publishToStorage.title5.message": "Failed to publish metadata file and sources to {storage}, please check the {storage} gateways is available." |
||||
} |
@ -1,3 +1,20 @@ |
||||
{ |
||||
"remixApp.scrollToSeeAllTabs": "Scroll to see all tabs" |
||||
"remixApp.scrollToSeeAllTabs": "Scroll to see all tabs", |
||||
"remixApp.alert": "Alert", |
||||
"remixApp.ok": "OK", |
||||
"remixApp.enterText1": "Welcome to Remix IDE", |
||||
"remixApp.enterText2": "In order to understand your needs better, we would like to know how you typically use Remix", |
||||
"remixApp.enterText3": "Learning - discovering web3 development", |
||||
"remixApp.enterText4": "Prototyping - trying out concepts and techniques", |
||||
"remixApp.enterText5": "Developing projects - Remix as your main dev tool", |
||||
"remixApp.enterText6": "Production - only deployments", |
||||
"remixApp.matomoText1": "An Opt-in version of <a>Matomo</a>, an open source data analytics platform is being used to improve Remix IDE.", |
||||
"remixApp.matomoText2": "We realize that our users have sensitive information in their code and that their privacy - your privacy - must be protected.", |
||||
"remixApp.matomoText3": "All data collected through Matomo is stored on our own server - no data is ever given to third parties.", |
||||
"remixApp.matomoText4": "We do not collect nor store any personally identifiable information (PII).", |
||||
"remixApp.matomoText5": "For more info, see: <a>Matomo Analytics on Remix iDE</a>.", |
||||
"remixApp.matomoText6": "You can change your choice in the Settings panel anytime.", |
||||
"remixApp.matomoTitle": "Help us to improve Remix IDE", |
||||
"remixApp.accept": "Accept", |
||||
"remixApp.decline": "Decline" |
||||
} |
||||
|
@ -1,7 +1,7 @@ |
||||
{ |
||||
"remixUiTabs.tooltipText1": "Run script (CTRL + SHIFT + S)", |
||||
"remixUiTabs.tooltipText2": "Compile CTRL + S", |
||||
"remixUiTabs.tooltipText3": "Select .sol or .yul file to compile or a .ts or .js file and run it", |
||||
"remixUiTabs.tooltipText3": "Select .sol, .vy or .yul file to compile or a .ts or .js file and run it", |
||||
"remixUiTabs.zoomOut": "Zoom out", |
||||
"remixUiTabs.zoomIn": "Zoom in" |
||||
} |
||||
|
@ -0,0 +1,16 @@ |
||||
{ |
||||
"remixd.connectionAlert1": "Cannot connect to the remixd daemon. Please make sure you have the remixd running in the background.", |
||||
"remixd.connectionAlert2": "Connection to remixd terminated. Please make sure remixd is still running in the background.", |
||||
"remixd.remixdConnect": "Access file system using remixd", |
||||
"remixd.connect": "Connect", |
||||
"remixd.cancel": "Cancel", |
||||
"remixd.text1": "Access your local file system from Remix IDE using <a>Remixd NPM package</a>.", |
||||
"remixd.text2": "Remixd <a>documentation</a>.", |
||||
"remixd.text3": "The remixd command is:", |
||||
"remixd.text4": "The remixd command without options uses the terminal's current directory as the shared directory and the shared Remix domain can only be https://remix.ethereum.org, https://remix-alpha.ethereum.org, or https://remix-beta.ethereum.org", |
||||
"remixd.text5": "Example command with flags:", |
||||
"remixd.text6": "For info about ports, see <a>Remixd ports usage</a>", |
||||
"remixd.text7": "This feature is still in Alpha. We recommend to keep a backup of the shared folder.", |
||||
"remixd.text8": "Before using, make sure remixd version is latest i.e.", |
||||
"remixd.text9": "Read here how to update it" |
||||
} |
@ -1,36 +1,17 @@ |
||||
import debuggerJson from './debugger.json'; |
||||
import filePanelJson from './filePanel.json'; |
||||
import homeJson from './home.json'; |
||||
import panelJson from './panel.json'; |
||||
import pluginManagerJson from './pluginManager.json'; |
||||
import searchJson from './search.json'; |
||||
import settingsJson from './settings.json'; |
||||
import solidityJson from './solidity.json'; |
||||
import terminalJson from './terminal.json'; |
||||
import udappJson from './udapp.json'; |
||||
import solidityUnitTestingJson from './solidityUnitTesting.json'; |
||||
import permissionHandlerJson from './permissionHandler.json'; |
||||
import solUmlGenJson from './solUmlGen.json' |
||||
import remixAppJson from './remixApp.json' |
||||
import remixUiTabsJson from './remixUiTabs.json' |
||||
import enJson from '../en'; |
||||
import enJson from '../en' |
||||
|
||||
function readAndCombineJsonFiles() { |
||||
const dataContext = require.context('./', true, /\.json$/) |
||||
|
||||
let combinedData = {} |
||||
dataContext.keys().forEach((key) => { |
||||
const jsonData = dataContext(key) |
||||
combinedData = {...combinedData, ...jsonData} |
||||
}) |
||||
|
||||
return combinedData |
||||
} |
||||
|
||||
// There may have some un-translated content. Always fill in the gaps with EN JSON.
|
||||
// No need for a defaultMessage prop when render a FormattedMessage component.
|
||||
export default Object.assign({}, enJson, { |
||||
...debuggerJson, |
||||
...filePanelJson, |
||||
...homeJson, |
||||
...panelJson, |
||||
...pluginManagerJson, |
||||
...searchJson, |
||||
...settingsJson, |
||||
...solidityJson, |
||||
...terminalJson, |
||||
...udappJson, |
||||
...solidityUnitTestingJson, |
||||
...permissionHandlerJson, |
||||
...solUmlGenJson, |
||||
...remixAppJson, |
||||
...remixUiTabsJson, |
||||
}) |
||||
export default Object.assign({}, enJson, readAndCombineJsonFiles()) |
||||
|
@ -1,36 +1,17 @@ |
||||
import debuggerJson from './debugger.json'; |
||||
import filePanelJson from './filePanel.json'; |
||||
import homeJson from './home.json'; |
||||
import panelJson from './panel.json'; |
||||
import pluginManagerJson from './pluginManager.json'; |
||||
import searchJson from './search.json'; |
||||
import settingsJson from './settings.json'; |
||||
import solidityJson from './solidity.json'; |
||||
import terminalJson from './terminal.json'; |
||||
import udappJson from './udapp.json'; |
||||
import solidityUnitTestingJson from './solidityUnitTesting.json'; |
||||
import permissionHandlerJson from './permissionHandler.json'; |
||||
import solUmlGenJson from './solUmlGen.json' |
||||
import remixAppJson from './remixApp.json' |
||||
import remixUiTabsJson from './remixUiTabs.json' |
||||
import enJson from '../en'; |
||||
import enJson from '../en' |
||||
|
||||
function readAndCombineJsonFiles() { |
||||
const dataContext = require.context('./', true, /\.json$/) |
||||
|
||||
let combinedData = {} |
||||
dataContext.keys().forEach((key) => { |
||||
const jsonData = dataContext(key) |
||||
combinedData = {...combinedData, ...jsonData} |
||||
}) |
||||
|
||||
return combinedData |
||||
} |
||||
|
||||
// There may have some un-translated content. Always fill in the gaps with EN JSON.
|
||||
// No need for a defaultMessage prop when render a FormattedMessage component.
|
||||
export default Object.assign({}, enJson, { |
||||
...debuggerJson, |
||||
...filePanelJson, |
||||
...homeJson, |
||||
...panelJson, |
||||
...pluginManagerJson, |
||||
...searchJson, |
||||
...settingsJson, |
||||
...solidityJson, |
||||
...terminalJson, |
||||
...udappJson, |
||||
...solidityUnitTestingJson, |
||||
...permissionHandlerJson, |
||||
...solUmlGenJson, |
||||
...remixAppJson, |
||||
...remixUiTabsJson, |
||||
}) |
||||
export default Object.assign({}, enJson, readAndCombineJsonFiles()) |
||||
|
@ -1,36 +1,17 @@ |
||||
import debuggerJson from './debugger.json'; |
||||
import filePanelJson from './filePanel.json'; |
||||
import homeJson from './home.json'; |
||||
import panelJson from './panel.json'; |
||||
import pluginManagerJson from './pluginManager.json'; |
||||
import searchJson from './search.json'; |
||||
import settingsJson from './settings.json'; |
||||
import solidityJson from './solidity.json'; |
||||
import terminalJson from './terminal.json'; |
||||
import udappJson from './udapp.json'; |
||||
import solidityUnitTestingJson from './solidityUnitTesting.json'; |
||||
import permissionHandlerJson from './permissionHandler.json'; |
||||
import solUmlGenJson from './solUmlGen.json' |
||||
import remixAppJson from './remixApp.json' |
||||
import remixUiTabsJson from './remixUiTabs.json' |
||||
import enJson from '../en'; |
||||
import enJson from '../en' |
||||
|
||||
function readAndCombineJsonFiles() { |
||||
const dataContext = require.context('./', true, /\.json$/) |
||||
|
||||
let combinedData = {} |
||||
dataContext.keys().forEach((key) => { |
||||
const jsonData = dataContext(key) |
||||
combinedData = {...combinedData, ...jsonData} |
||||
}) |
||||
|
||||
return combinedData |
||||
} |
||||
|
||||
// There may have some un-translated content. Always fill in the gaps with EN JSON.
|
||||
// No need for a defaultMessage prop when render a FormattedMessage component.
|
||||
export default Object.assign({}, enJson, { |
||||
...debuggerJson, |
||||
...filePanelJson, |
||||
...homeJson, |
||||
...panelJson, |
||||
...pluginManagerJson, |
||||
...searchJson, |
||||
...settingsJson, |
||||
...solidityJson, |
||||
...terminalJson, |
||||
...udappJson, |
||||
...solidityUnitTestingJson, |
||||
...permissionHandlerJson, |
||||
...solUmlGenJson, |
||||
...remixAppJson, |
||||
...remixUiTabsJson, |
||||
}) |
||||
export default Object.assign({}, enJson, readAndCombineJsonFiles()) |
||||
|
@ -1,38 +1,17 @@ |
||||
import debuggerJson from './debugger.json'; |
||||
import filePanelJson from './filePanel.json'; |
||||
import homeJson from './home.json'; |
||||
import panelJson from './panel.json'; |
||||
import pluginManagerJson from './pluginManager.json'; |
||||
import searchJson from './search.json'; |
||||
import settingsJson from './settings.json'; |
||||
import solidityJson from './solidity.json'; |
||||
import terminalJson from './terminal.json'; |
||||
import udappJson from './udapp.json'; |
||||
import solidityUnitTestingJson from './solidityUnitTesting.json'; |
||||
import permissionHandlerJson from './permissionHandler.json'; |
||||
import solUmlGenJson from './solUmlGen.json' |
||||
import remixAppJson from './remixApp.json' |
||||
import remixUiTabsJson from './remixUiTabs.json' |
||||
import enJson from '../en'; |
||||
import circuitJson from './circuit.json'; |
||||
import enJson from '../en' |
||||
|
||||
function readAndCombineJsonFiles() { |
||||
const dataContext = require.context('./', true, /\.json$/) |
||||
|
||||
let combinedData = {} |
||||
dataContext.keys().forEach((key) => { |
||||
const jsonData = dataContext(key) |
||||
combinedData = {...combinedData, ...jsonData} |
||||
}) |
||||
|
||||
return combinedData |
||||
} |
||||
|
||||
// There may have some un-translated content. Always fill in the gaps with EN JSON.
|
||||
// No need for a defaultMessage prop when render a FormattedMessage component.
|
||||
export default Object.assign({}, enJson, { |
||||
...debuggerJson, |
||||
...filePanelJson, |
||||
...homeJson, |
||||
...panelJson, |
||||
...pluginManagerJson, |
||||
...searchJson, |
||||
...settingsJson, |
||||
...solidityJson, |
||||
...terminalJson, |
||||
...udappJson, |
||||
...solidityUnitTestingJson, |
||||
...permissionHandlerJson, |
||||
...solUmlGenJson, |
||||
...remixAppJson, |
||||
...remixUiTabsJson, |
||||
...circuitJson |
||||
}) |
||||
export default Object.assign({}, enJson, readAndCombineJsonFiles()) |
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue