├── .env ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── nodejs.yml │ └── tests.yml ├── .gitignore ├── .grenrc ├── .husky └── pre-commit ├── .nvmrc ├── .prettierrc.json ├── .stylelintignore ├── .stylelintrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.html ├── netlify.toml ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public ├── 404.html ├── img │ ├── icons │ │ ├── apple.png │ │ ├── cloud.png │ │ ├── earth.png │ │ ├── heart.png │ │ ├── hotdog.png │ │ ├── icecream.png │ │ ├── lightbulb.png │ │ ├── orange.png │ │ └── pear.png │ ├── sketch-thumbnails │ │ ├── Ant.svg │ │ ├── Badger.svg │ │ ├── Bear.svg │ │ ├── Beaver.svg │ │ ├── Bird.svg │ │ ├── Bug.svg │ │ ├── Bull.svg │ │ ├── Bumblebee.svg │ │ ├── Butterfly.svg │ │ ├── Cat.svg │ │ ├── Caterpillar.svg │ │ ├── Chicken.svg │ │ ├── Clown Fish.svg │ │ ├── Corgi.svg │ │ ├── Cow.svg │ │ ├── Crab.svg │ │ ├── Deer.svg │ │ ├── Dinosaur.svg │ │ ├── Dog.svg │ │ ├── Dolphin.svg │ │ ├── Dragonfly.svg │ │ ├── Duck.svg │ │ ├── Elephant.svg │ │ ├── Falcon.svg │ │ ├── Fish.svg │ │ ├── Fly.svg │ │ ├── Frog.svg │ │ ├── Giraffe.svg │ │ ├── Gorilla.svg │ │ ├── Grasshopper.svg │ │ ├── Horse.svg │ │ ├── Hummingbird.svg │ │ ├── Insect.svg │ │ ├── Kangaroo.svg │ │ ├── Kiwi Bird.svg │ │ ├── Ladybird.svg │ │ ├── Leopard.svg │ │ ├── Lion.svg │ │ ├── Llama.svg │ │ ├── Mite.svg │ │ ├── Mosquito.svg │ │ ├── Octopus.svg │ │ ├── Panda.svg │ │ ├── Pig With Lipstick.svg │ │ ├── Pig.svg │ │ ├── Prawn.svg │ │ ├── Puffin Bird.svg │ │ ├── Rabbit.svg │ │ ├── Rhinoceros.svg │ │ ├── Seahorse.svg │ │ ├── Shark.svg │ │ ├── Sheep.svg │ │ ├── Snail.svg │ │ ├── Spider.svg │ │ ├── Starfish.svg │ │ ├── Stork.svg │ │ ├── Turtle.svg │ │ ├── Unicorn.svg │ │ ├── Whale.svg │ │ └── Wolf.svg │ └── triangle.png └── tla-logo.png ├── src ├── actions │ ├── classesActions.js │ ├── outputActions.js │ ├── programsActions.js │ ├── uiActions.js │ └── userDataActions.js ├── components │ ├── Class │ │ ├── components │ │ │ ├── ClassInfoBox.tsx │ │ │ ├── ClassSketchList.jsx │ │ │ └── StudentListEntry.jsx │ │ ├── containers │ │ │ ├── ClassPageContainer.js │ │ │ └── CreateSketchModalContainer.js │ │ └── index.jsx │ ├── Classes │ │ ├── components │ │ │ ├── ClassBox.tsx │ │ │ ├── ConfirmLeaveModal.jsx │ │ │ ├── CreateClassModal.jsx │ │ │ └── JoinClassModal.jsx │ │ ├── containers │ │ │ ├── ClassesContainer.js │ │ │ ├── ConfirmLeaveModalContainer.js │ │ │ ├── CreateClassModalContainer.js │ │ │ └── JoinClassModalContainer.js │ │ └── index.jsx │ ├── EditorAndOutput │ │ └── EditorAndOutput.jsx │ ├── Error.tsx │ ├── Login.jsx │ ├── Login │ │ ├── CreateUserForm.jsx │ │ ├── LoginForm.tsx │ │ └── LoginInput.jsx │ ├── Main.jsx │ ├── Output │ │ ├── Output.jsx │ │ └── OutputContainer.js │ ├── PageNotFound.jsx │ ├── Sketches │ │ ├── components │ │ │ ├── ConfirmDeleteModal.jsx │ │ │ ├── CreateSketchModal.tsx │ │ │ └── EditSketchModal.jsx │ │ ├── constants │ │ │ └── index.js │ │ ├── containers │ │ │ ├── ConfirmDeleteModalContainer.js │ │ │ ├── CreateSketchModalContainer.js │ │ │ ├── EditSketchModalContainer.js │ │ │ └── SketchesContainer.js │ │ └── index.jsx │ ├── TextEditor │ │ ├── components │ │ │ ├── EditorRadio.jsx │ │ │ ├── ShareSketchModal.jsx │ │ │ └── TextEditor.tsx │ │ └── containers │ │ │ ├── DropdownButtonContainer.js │ │ │ └── TextEditorContainer.js │ ├── ViewOnly.jsx │ ├── app.jsx │ ├── common │ │ ├── DropdownButton.jsx │ │ ├── Footer.tsx │ │ ├── ImageSelector.jsx │ │ ├── LoadingPage.tsx │ │ ├── OpenPanelButton.tsx │ │ ├── ProfilePanel.jsx │ │ ├── Radio.tsx │ │ ├── SketchBox.tsx │ │ ├── Switch.tsx │ │ ├── ViewportAwareButton.tsx │ │ └── containers │ │ │ ├── OpenPanelButtonContainer.js │ │ │ └── ProfilePanelContainer.js │ └── containers │ │ ├── AppContainer.js │ │ ├── LoginContainer.js │ │ ├── MainContainer.js │ │ └── ViewOnlyContainer.js ├── constants │ └── index.ts ├── firebase │ └── index.js ├── history.js ├── img │ ├── background1.svg │ ├── blueguy.png │ ├── login1.svg │ ├── login2.svg │ ├── login3.svg │ ├── login4.svg │ ├── login5.svg │ ├── tla-logo-green.svg │ └── tla-logo.svg ├── index.tsx ├── lib │ ├── cookies.ts │ ├── fetch.ts │ ├── sketch.ts │ └── validate.ts ├── react-app-env.d.ts ├── reducers │ ├── classesReducer.js │ ├── index.ts │ ├── outputReducer.js │ ├── programsReducer.js │ ├── uiReducer.js │ └── userDataReducer.js ├── registerServiceWorker.js ├── setupTests.js ├── store.js ├── styles │ ├── ClassBox.scss │ ├── ClassPage.scss │ ├── Classes.scss │ ├── CustomCM.scss │ ├── Editor.scss │ ├── Footer.scss │ ├── ImageSelector.scss │ ├── Loading.scss │ ├── Login.scss │ ├── Main.scss │ ├── Modals.scss │ ├── Output.scss │ ├── Page.scss │ ├── Panel.scss │ ├── Radio.scss │ ├── Resizer.scss │ ├── SketchBox.scss │ ├── Sketches.scss │ ├── SketchesModal.scss │ ├── Switch.scss │ ├── app.scss │ ├── global.scss │ ├── triangle.png │ └── variables.scss └── util │ ├── classes.js │ └── languages │ ├── CodeDownloader.js │ ├── JsSourceDocLoggingScript.js │ ├── Processing.js │ ├── Python.js │ ├── React.js │ └── languages.js ├── tsconfig.json └── vite.config.ts /.env: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | DISABLE_ESLINT_PLUGIN=true 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # vite config 2 | vite.config.ts 3 | 4 | # dependencies 5 | /node_modules 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | = '' 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 0 9 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [16.x] 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm install 26 | - name: Run ESlint 27 | run: | 28 | git fetch origin master 29 | npm run lint-js-changes 30 | - name: Run Stylelint 31 | run: | 32 | git fetch origin master 33 | npm run lint-css-changes 34 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [20.x] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm install 25 | - run: npm run prod_build --if-present 26 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | test-coverage: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | node-version: [16.x] 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: npm install 18 | - name: Calc Coverage 19 | run: npm run test-coverage 20 | - name: Coveralls 21 | uses: coverallsapp/github-action@v1.1.2 22 | with: 23 | github-token: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | .eslintcache 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | yarn.lock 24 | -------------------------------------------------------------------------------- /.grenrc: -------------------------------------------------------------------------------- 1 | { 2 | "dataSource": "prs", 3 | "prefix": "Version ", 4 | "onlyMilestones": false, 5 | "groupBy": "label", 6 | "changelogFilename": "CHANGELOG.md" 7 | } -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm run pre-commit 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.12.0 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*", 5 | "options": { 6 | "trailingComma": "all", 7 | "bracketSpacing": true, 8 | "printWidth": 100, 9 | "singleQuote": true 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # production 8 | /build 9 | /dist 10 | 11 | # ignore ts and js files 12 | *.ts 13 | *.tsx 14 | *.js 15 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-standard-scss"], 3 | "rules": { 4 | "color-named": null, 5 | "selector-max-id": null, 6 | "selector-no-qualifying-type": null, 7 | "property-no-vendor-prefix": null, 8 | 9 | "scss/at-import-partial-extension-blacklist": null, 10 | "declaration-property-value-disallowed-list": null, 11 | "selector-class-pattern": null 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 UCLA Association for Computing Machinery 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | Teach LA Editor 20 | 21 | 50 | 51 | 52 | 53 | 56 |
57 | 58 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [context.production] 2 | command = "pnpm run prod_build" 3 | 4 | [context.deploy-preview] 5 | command = "pnpm run staging_build" 6 | 7 | [context.branch-deploy] 8 | command = "pnpm run staging_build" 9 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Teach LA | 404: Redirecting You... 6 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /public/img/icons/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/img/icons/apple.png -------------------------------------------------------------------------------- /public/img/icons/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/img/icons/cloud.png -------------------------------------------------------------------------------- /public/img/icons/earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/img/icons/earth.png -------------------------------------------------------------------------------- /public/img/icons/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/img/icons/heart.png -------------------------------------------------------------------------------- /public/img/icons/hotdog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/img/icons/hotdog.png -------------------------------------------------------------------------------- /public/img/icons/icecream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/img/icons/icecream.png -------------------------------------------------------------------------------- /public/img/icons/lightbulb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/img/icons/lightbulb.png -------------------------------------------------------------------------------- /public/img/icons/orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/img/icons/orange.png -------------------------------------------------------------------------------- /public/img/icons/pear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/img/icons/pear.png -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Badger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Bear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Beaver.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Bird.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Bug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Cat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Caterpillar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Chicken.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Corgi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Crab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Dog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Dolphin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Dragonfly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Duck.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Elephant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Frog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Giraffe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Grasshopper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Horse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Insect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Kangaroo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Ladybird.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Lion.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Mite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Panda.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Pig With Lipstick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Pig.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Puffin Bird.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Rabbit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Rhinoceros.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Shark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Spider.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Starfish.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Stork.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Turtle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Unicorn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Whale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/img/sketch-thumbnails/Wolf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/img/triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/img/triangle.png -------------------------------------------------------------------------------- /public/tla-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/public/tla-logo.png -------------------------------------------------------------------------------- /src/actions/classesActions.js: -------------------------------------------------------------------------------- 1 | export const ADD_STUDENT_CLASS = 'ADD_STUDENT_CLASS'; 2 | export function addStudentClass(cid, data) { 3 | return { type: ADD_STUDENT_CLASS, cid, data }; 4 | } 5 | 6 | export const REMOVE_STUDENT_CLASS = 'REMOVE_STUDENT_CLASS'; 7 | export function removeStudentClass(cid) { 8 | return { type: REMOVE_STUDENT_CLASS, cid }; 9 | } 10 | 11 | export const LOAD_STUDENT_CLASSES = 'LOAD_STUDENT_CLASSES'; 12 | export function loadStudentClasses(classes) { 13 | return { type: LOAD_STUDENT_CLASSES, classes }; 14 | } 15 | 16 | export const CLEAR_STUDENT_CLASSES = 'CLEAR_STUDENT_CLASSES'; 17 | export function clearStudentClasses() { 18 | return { type: CLEAR_STUDENT_CLASSES }; 19 | } 20 | 21 | export const ADD_INSTR_CLASS = 'ADD_INSTR_CLASS'; 22 | export function addInstrClass(cid, data) { 23 | return { type: ADD_INSTR_CLASS, cid, data }; 24 | } 25 | 26 | export const REMOVE_INSTR_CLASS = 'REMOVE_INSTR_CLASS'; 27 | export function removeInstrClass(cid) { 28 | return { type: REMOVE_INSTR_CLASS, cid }; 29 | } 30 | 31 | export const LOAD_INSTR_CLASSES = 'LOAD_INSTR_CLASSES'; 32 | export function loadInstrClasses(classes) { 33 | return { type: LOAD_INSTR_CLASSES, classes }; 34 | } 35 | 36 | export const CLEAR_INSTR_CLASSES = 'CLEAR_INSTR_CLASSES'; 37 | export function clearInstrClasses() { 38 | return { type: CLEAR_INSTR_CLASSES }; 39 | } 40 | 41 | export const CLEAR_CLASSES = 'CLEAR_CLASSES'; 42 | export function clearClasses() { 43 | return { type: CLEAR_CLASSES }; 44 | } 45 | -------------------------------------------------------------------------------- /src/actions/outputActions.js: -------------------------------------------------------------------------------- 1 | export const CLEAR_OUTPUT = 'CLEAR_OUTPUT'; 2 | export function clearOutput() { 3 | return { type: CLEAR_OUTPUT }; 4 | } 5 | 6 | export const SET_RUN_RESULT = 'SET_RUN_RESULT'; 7 | export function setRunResult(value) { 8 | return { type: SET_RUN_RESULT, value }; 9 | } 10 | 11 | export const SET_OUTPUT_LANGUAGE = 'SET_OUTPUT_LANGAUGE'; 12 | export function setOutputLanguage(value) { 13 | return { type: SET_OUTPUT_LANGUAGE, value }; 14 | } 15 | 16 | export const SET_OUTPUT = 'SET_OUTPUT'; 17 | export function setOutput({ output }) { 18 | // if output is not an object 19 | if (!output) { 20 | return { type: 'IGNORE' }; 21 | } 22 | return { type: SET_OUTPUT, runResult: output.code, language: output.language }; 23 | } 24 | -------------------------------------------------------------------------------- /src/actions/programsActions.js: -------------------------------------------------------------------------------- 1 | export const SET_PROGRAM_CODE = 'SET_PROGRAM_CODE'; 2 | export function setProgramCode(program, value) { 3 | return { type: SET_PROGRAM_CODE, program, value }; 4 | } 5 | 6 | export const SET_PROGRAM_LANGUAGE = 'SET_PROGRAM_LANGUAGE'; 7 | export function setProgramLanguage(program, value) { 8 | return { type: SET_PROGRAM_LANGUAGE, program, value }; 9 | } 10 | 11 | export const SET_PROGRAM_NAME = 'SET_PROGRAM_NAME'; 12 | export function setProgramName(program, value) { 13 | return { type: SET_PROGRAM_NAME, program, value }; 14 | } 15 | 16 | export const SET_PROGRAM_THUMBNAIL = 'SET_PROGRAM_THUMBNAIL'; 17 | export function setProgramThumbnail(program, value) { 18 | return { type: SET_PROGRAM_THUMBNAIL, program, value }; 19 | } 20 | 21 | export const DELETE_PROGRAM = 'DELETE_PROGRAM'; 22 | export function deleteProgram(program) { 23 | return { type: DELETE_PROGRAM, program }; 24 | } 25 | 26 | export const LOAD_PROGRAMS = 'LOAD_PROGRAMS'; 27 | export function loadPrograms(programs) { 28 | return { type: LOAD_PROGRAMS, programs }; 29 | } 30 | 31 | export const CLEAR_PROGRAMS = 'CLEAR_PROGRAMS'; 32 | export function clearPrograms() { 33 | return { type: CLEAR_PROGRAMS }; 34 | } 35 | 36 | export const SET_PROGRAM_DIRTY = 'SET_PROGRAM_DIRTY'; 37 | export function setProgramDirty(program, value) { 38 | return { type: SET_PROGRAM_DIRTY, program, value }; 39 | } 40 | 41 | export const ADD_PROGRAM = 'ADD_PROGRAM'; 42 | export function addProgram(program, data) { 43 | return { type: ADD_PROGRAM, program, data }; 44 | } 45 | -------------------------------------------------------------------------------- /src/actions/uiActions.js: -------------------------------------------------------------------------------- 1 | export const SCREEN_RESIZE = 'SCREEN_RESIZE'; 2 | export function screenResize(width, height) { 3 | return { type: SCREEN_RESIZE, width, height }; 4 | } 5 | 6 | export const TOGGLE_PANEL = 'TOGGLE_PANEL'; 7 | export function togglePanel() { 8 | return { type: TOGGLE_PANEL }; 9 | } 10 | 11 | export const SET_PANEL = 'SET_PANEL'; 12 | export function setPanel(value) { 13 | return { type: SET_PANEL, value }; 14 | } 15 | 16 | export const SET_THEME = 'SET_THEME'; 17 | export function setTheme(theme) { 18 | return { type: SET_THEME, theme }; 19 | } 20 | 21 | export const SET_CLASSES_LOADED = 'SET_CLASSES_LOADED'; 22 | export function setClassesLoaded(loaded) { 23 | return { type: SET_CLASSES_LOADED, value: loaded }; 24 | } 25 | 26 | export const SET_ON_INSTR_VIEW = 'SET_ON_INSTR_VIEW'; 27 | export function setOnInstrView(value) { 28 | return { type: SET_ON_INSTR_VIEW, value }; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Class/components/ClassInfoBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../../styles/ClassPage.scss'; 3 | 4 | interface ClassInfoBoxProps { 5 | title: string; 6 | children: React.ReactNode; 7 | } 8 | 9 | const ClassInfoBox = ({ 10 | title, 11 | children, 12 | } : ClassInfoBoxProps) => { 13 | return ( 14 |
15 | {title} 16 |
{children}
17 |
18 | ); 19 | }; 20 | 21 | export default ClassInfoBox; 22 | -------------------------------------------------------------------------------- /src/components/Class/components/ClassSketchList.jsx: -------------------------------------------------------------------------------- 1 | import { faPlus } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | 4 | import { ThumbnailArray } from '../../../constants'; 5 | import CodeDownloader from '../../../util/languages/CodeDownloader'; 6 | import SketchBox from '../../common/SketchBox'; 7 | 8 | import '../../../styles/SketchBox.scss'; 9 | 10 | const SKETCHES_ROW_PADDING = 100; 11 | const SKETCH_WIDTH = 220; 12 | 13 | function ClassSketchList({ 14 | calculatedWidth, isInstr, programData, setCreateSketchModalOpen, 15 | }) { 16 | const newList = programData?.concat([]) || []; 17 | newList.sort((a, b) => { 18 | if (a.name < b.name) return -1; 19 | if (a.name === b.name) return 0; 20 | return 1; 21 | }); 22 | 23 | const getThumbnailSrc = (val) => { 24 | if (val === undefined || val === '' || val >= ThumbnailArray.length || val < 0) { 25 | return ThumbnailArray[0]; 26 | } 27 | return ThumbnailArray[val]; 28 | }; 29 | 30 | const sketchList = newList.map(({ 31 | uid, name, language, thumbnail, code, 32 | }) => ( 33 | { 39 | CodeDownloader.download(name, language, code); 40 | }} 41 | pathname={isInstr ? `/editor/${uid}` : `/p/${uid}`} 42 | /> 43 | )); 44 | 45 | // Button for instructors to add a sketch to the class. 46 | if (isInstr) { 47 | sketchList.push( 48 | , 61 | ); 62 | } 63 | 64 | // TODO: This should be a flexbox, instead of this 65 | const numSketchesPerRow = Math.floor((calculatedWidth - SKETCHES_ROW_PADDING) / SKETCH_WIDTH); 66 | const rows = []; 67 | const originalLength = sketchList.length; 68 | for (let i = 0; i < originalLength / numSketchesPerRow; i++) { 69 | rows.push( 70 |
71 | {sketchList.splice(0, numSketchesPerRow)} 72 |
, 73 | ); 74 | } 75 | if (rows.length === 0) return 'No sketches found'; 76 | return
{rows}
; 77 | } 78 | 79 | export default ClassSketchList; 80 | -------------------------------------------------------------------------------- /src/components/Class/components/StudentListEntry.jsx: -------------------------------------------------------------------------------- 1 | import '../../../styles/ClassBox.scss'; 2 | 3 | const StudentListEntry = function (props) { 4 | const { name } = props; 5 | return ( 6 |
7 |
8 | {name} 9 |
{name}
10 |
11 |
12 | ); 13 | }; 14 | 15 | export default StudentListEntry; 16 | -------------------------------------------------------------------------------- /src/components/Class/containers/ClassPageContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { addInstrClass, addStudentClass } from '../../../actions/classesActions'; 3 | import { addProgram } from '../../../actions/programsActions'; 4 | import { togglePanel } from '../../../actions/uiActions'; 5 | import { setMostRecentProgram } from '../../../actions/userDataActions'; 6 | import { OPEN_PANEL_LEFT, CLOSED_PANEL_LEFT, PANEL_SIZE } from '../../../constants'; 7 | import ClassPage from '../index'; 8 | 9 | const mapStateToProps = (state) => { 10 | const left = (state.ui.panelOpen ? OPEN_PANEL_LEFT : CLOSED_PANEL_LEFT) + PANEL_SIZE; 11 | const calculatedWidth = state.ui.screenWidth - (left || 0); 12 | const cid = state.userData.currentClass; 13 | const blankClass = { 14 | name: '', 15 | creator: '', 16 | thumbnail: 0, 17 | programData: null, 18 | programs: null, 19 | wid: '', 20 | userData: {}, 21 | instructors: [], 22 | members: null, 23 | cid: '', 24 | }; 25 | 26 | return { 27 | calculatedWidth, 28 | left, 29 | screenHeight: state.ui.screenHeight, 30 | panelOpen: state.ui.panelOpen, 31 | uid: state.userData.uid, 32 | cid, 33 | classData: 34 | state.classes.getIn(['instrClasses', cid])?.toJS() 35 | || state.classes.getIn(['studClasses', cid])?.toJS() 36 | || blankClass, 37 | }; 38 | }; 39 | 40 | const mapDispatchToProps = (dispatch) => ({ 41 | addInstrClass: (cid, data) => dispatch(addInstrClass(cid, data)), 42 | addStudentClass: (cid, data) => dispatch(addStudentClass(cid, data)), 43 | addProgram: (program, value) => dispatch(addProgram(program, value)), 44 | setMostRecentProgram: (value) => dispatch(setMostRecentProgram(value)), 45 | togglePanel: () => dispatch(togglePanel()), 46 | }); 47 | 48 | const ClassPageContainer = connect(mapStateToProps, mapDispatchToProps)(ClassPage); 49 | 50 | export default ClassPageContainer; 51 | -------------------------------------------------------------------------------- /src/components/Class/containers/CreateSketchModalContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { addProgram } from '../../../actions/programsActions'; 3 | import { setMostRecentProgram } from '../../../actions/userDataActions'; 4 | import CreateSketchModal from '../../Sketches/components/CreateSketchModal'; 5 | 6 | const mapStateToProps = (state) => ({ 7 | uid: state.userData.uid, 8 | }); 9 | 10 | const mapDispatchToProps = (dispatch) => ({ 11 | addProgram: (program, data) => { 12 | dispatch(addProgram(program, data)); 13 | }, 14 | setMostRecentProgram: (value) => dispatch(setMostRecentProgram(value)), 15 | }); 16 | 17 | const CreateSketchModalContainer = connect(mapStateToProps, mapDispatchToProps)(CreateSketchModal); 18 | 19 | export default CreateSketchModalContainer; 20 | -------------------------------------------------------------------------------- /src/components/Classes/components/ClassBox.tsx: -------------------------------------------------------------------------------- 1 | import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { Link } from 'react-router-dom'; 4 | import '../../../styles/ClassBox.scss'; 5 | 6 | interface ClassBoxProps { 7 | deleteFunc: () => void; 8 | img: string; 9 | instructorString: string; 10 | name: string; 11 | redirFunc: () => void; 12 | showLeaveButton: JSX.Element; 13 | } 14 | 15 | const ClassBox = function ({ 16 | deleteFunc, img, instructorString, name, redirFunc, showLeaveButton, 17 | }: ClassBoxProps) { 18 | const leaveButton = showLeaveButton ? ( 19 |
26 | 27 |
28 | ) : ( 29 | '' 30 | ); 31 | 32 | return ( 33 |
34 | 35 | Class icon 40 |
41 | {name} 42 |
{instructorString}
43 |
44 | 45 | {leaveButton} 46 |
47 | ); 48 | }; 49 | 50 | export default ClassBox; 51 | -------------------------------------------------------------------------------- /src/components/Classes/components/ConfirmLeaveModal.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import ReactModal from 'react-modal'; 3 | import { 4 | Container, Row, Col, Button, 5 | } from 'reactstrap'; 6 | import * as fetch from '../../../lib/fetch'; 7 | 8 | const ConfirmLeaveModal = function (props) { 9 | const { 10 | isOpen, uid, cid, removeStudentClass, className, inClass, unsetClass, 11 | } = props; 12 | 13 | const [error, setError] = useState(''); 14 | 15 | const closeModal = () => { 16 | const { onClose } = props; 17 | if (onClose && {}.toString.call(onClose) === '[object Function]') { 18 | onClose(); 19 | } 20 | }; 21 | 22 | const onLeaveSubmit = () => { 23 | const data = { 24 | uid, 25 | cid, 26 | }; 27 | try { 28 | // Note: leaveClass doesn't currently return anything. 29 | fetch 30 | .leaveClass(data) 31 | .then((res) => { 32 | if (!res.ok) { 33 | setError(res.error || 'Failed to leave the class, please try again later'); 34 | return; 35 | } 36 | removeStudentClass(cid); 37 | if (inClass) { 38 | unsetClass(); 39 | } else { 40 | closeModal(); 41 | } 42 | }) 43 | .catch((err) => { 44 | setError('Failed to leave the class, please try again later'); 45 | console.error(err); 46 | }); 47 | } catch (err) { 48 | console.error(err); 49 | } 50 | 51 | // Testing stuff (do this instead of the try-catch block): 52 | // this.props.removeStudentClass(this.props.cid); 53 | // this.props.inClass ? this.props.unsetClass() : this.closeModal(); 54 | // end of test stuff 55 | 56 | setError(''); 57 | }; 58 | 59 | return ( 60 | 66 | 67 |

68 | Are you sure you want to permanently leave the class " 69 | {className} 70 | "? 71 |

72 |
73 |
{error ||
}
74 | 75 | 76 | 79 | 80 | 81 | 84 | 85 | 86 |
87 |
88 | ); 89 | }; 90 | 91 | export default ConfirmLeaveModal; 92 | -------------------------------------------------------------------------------- /src/components/Classes/containers/ClassesContainer.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { connect } from 'react-redux'; 3 | import { loadInstrClasses, loadStudentClasses } from '../../../actions/classesActions'; 4 | import { togglePanel, setClassesLoaded, setOnInstrView } from '../../../actions/uiActions'; 5 | import { setCurrentClass } from '../../../actions/userDataActions'; 6 | import { OPEN_PANEL_LEFT, CLOSED_PANEL_LEFT, PANEL_SIZE } from '../../../constants'; 7 | import Classes from '../index'; 8 | 9 | const mapStateToProps = (state) => { 10 | // TODO: Look over this part (instrClasses and studentClasses) 11 | const instrClasses = state.classes 12 | .get('instrClasses') 13 | .keySeq() 14 | .map((id) => { 15 | const temp = state.classes.get('instrClasses').get(id, Immutable.Map()).toJS(); 16 | temp.cid = id; 17 | return temp; 18 | }); 19 | const studentClasses = state.classes 20 | .get('studClasses') 21 | .keySeq() 22 | .map((id) => { 23 | const temp = state.classes.get('studClasses').get(id, Immutable.Map()).toJS(); 24 | temp.cid = id; 25 | return temp; 26 | }); 27 | 28 | const left = (state.ui.panelOpen ? OPEN_PANEL_LEFT : CLOSED_PANEL_LEFT) + PANEL_SIZE; 29 | const calculatedWidth = state.ui.screenWidth - (left || 0); 30 | 31 | return { 32 | studentClasses, 33 | instrClasses, 34 | calculatedWidth, 35 | left, 36 | screenHeight: state.ui.screenHeight, 37 | panelOpen: state.ui.panelOpen, 38 | classesLoaded: state.ui.classesLoaded, 39 | onInstrView: state.ui.onInstrView, 40 | classList: state.userData.classes, 41 | uid: state.userData.uid, 42 | username: state.userData.displayName, 43 | }; 44 | }; 45 | 46 | const mapDispatchToProps = (dispatch) => ({ 47 | setCurrentClass: (value) => dispatch(setCurrentClass(value)), 48 | togglePanel: () => dispatch(togglePanel()), 49 | loadInstrClasses: (classes) => dispatch(loadInstrClasses(classes)), 50 | loadStudentClasses: (classes) => dispatch(loadStudentClasses(classes)), 51 | setClassesLoaded: (value) => dispatch(setClassesLoaded(value)), 52 | setOnInstrView: (value) => dispatch(setOnInstrView(value)), 53 | }); 54 | 55 | const ClassesContainer = connect(mapStateToProps, mapDispatchToProps)(Classes); 56 | 57 | export default ClassesContainer; 58 | -------------------------------------------------------------------------------- /src/components/Classes/containers/ConfirmLeaveModalContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { removeStudentClass } from '../../../actions/classesActions'; 3 | import { setCurrentClass } from '../../../actions/userDataActions'; 4 | import ConfirmLeaveModal from '../components/ConfirmLeaveModal'; 5 | 6 | const mapStateToProps = (state) => ({ 7 | uid: state.userData.uid, 8 | }); 9 | 10 | const mapDispatchToProps = (dispatch) => ({ 11 | removeStudentClass: (cid) => dispatch(removeStudentClass(cid)), 12 | unsetClass: () => dispatch(setCurrentClass('')), 13 | }); 14 | 15 | const ConfirmLeaveModalContainer = connect(mapStateToProps, mapDispatchToProps)(ConfirmLeaveModal); 16 | 17 | export default ConfirmLeaveModalContainer; 18 | -------------------------------------------------------------------------------- /src/components/Classes/containers/CreateClassModalContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { addInstrClass } from '../../../actions/classesActions'; 3 | import { setCurrentClass } from '../../../actions/userDataActions'; 4 | import CreateClassModal from '../components/CreateClassModal'; 5 | 6 | const mapStateToProps = (state) => ({ 7 | uid: state.userData.uid, 8 | }); 9 | 10 | const mapDispatchToProps = (dispatch) => ({ 11 | addInstrClass: (cid, data) => dispatch(addInstrClass(cid, data)), 12 | setCurrentClass: (cid) => dispatch(setCurrentClass(cid)), 13 | }); 14 | 15 | const CreateClassModalContainer = connect(mapStateToProps, mapDispatchToProps)(CreateClassModal); 16 | 17 | export default CreateClassModalContainer; 18 | -------------------------------------------------------------------------------- /src/components/Classes/containers/JoinClassModalContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { addStudentClass } from '../../../actions/classesActions'; 3 | import JoinClassModal from '../components/JoinClassModal'; 4 | 5 | const mapStateToProps = (state) => ({ 6 | uid: state.userData.uid, 7 | }); 8 | 9 | const mapDispatchToProps = (dispatch) => ({ 10 | addStudentClass: (cid, data) => dispatch(addStudentClass(cid, data)), 11 | }); 12 | 13 | const JoinClassModalContainer = connect(mapStateToProps, mapDispatchToProps)(JoinClassModal); 14 | 15 | export default JoinClassModalContainer; 16 | -------------------------------------------------------------------------------- /src/components/Error.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { Button } from 'reactstrap'; 3 | 4 | import { GH_REPO_NAME } from '../constants'; 5 | import { signOut } from '../firebase'; 6 | import * as cookies from '../lib/cookies'; 7 | import Footer from './common/Footer'; 8 | import '../styles/Page.scss'; 9 | 10 | interface ErrorProps { 11 | errorMsg: string; 12 | isValidUser: boolean; 13 | returnTo: string; 14 | }; 15 | 16 | function Error({ errorMsg, returnTo = '/', isValidUser }: ErrorProps) { 17 | return ( 18 |
19 |
20 |

Uh oh, something went wrong!

21 |

22 | Error: 23 | {errorMsg} 24 |

25 |
26 | 29 |   30 | 31 | 34 | 35 |   36 | {isValidUser && ( 37 | 40 | )} 41 |
42 |
43 |
44 | ); 45 | } 46 | 47 | export default Error; 48 | -------------------------------------------------------------------------------- /src/components/Login/LoginInput.jsx: -------------------------------------------------------------------------------- 1 | import '../../styles/Login.scss'; 2 | 3 | /** -------Props------- 4 | * type: string, if password, hides the input with dots; also used as the header for the input 5 | * waiting: boolean that disables the input if true 6 | * data: value inside the input 7 | * onChange: function to be called when input changes 8 | */ 9 | 10 | const LoginInput = function ({ 11 | type, waiting, data, onChange, 12 | }) { 13 | return ( 14 |
15 |
{type}
16 | onChange(e.target.value)} 24 | /> 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default LoginInput; 31 | -------------------------------------------------------------------------------- /src/components/Output/OutputContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { getLanguageData } from '../../util/languages/languages'; 3 | import Output from './Output'; 4 | 5 | const mapStateToProps = (state) => { 6 | const { mostRecentProgram } = state.userData; 7 | return { 8 | mostRecentProgram, 9 | runResult: state.programs.getIn([mostRecentProgram, 'code']), 10 | language: getLanguageData(state.programs.getIn([mostRecentProgram, 'language'])), 11 | screenHeight: state.ui.screenHeight, 12 | screenWidth: state.ui.screenWidth, // probably will need this 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = () => ({ 17 | clearOutput: () => {}, 18 | }); 19 | 20 | const OutputContainer = connect(mapStateToProps, mapDispatchToProps)(Output); 21 | 22 | export default OutputContainer; 23 | -------------------------------------------------------------------------------- /src/components/PageNotFound.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { Button } from 'reactstrap'; 3 | import '../styles/Page.scss'; 4 | import { GH_REPO_NAME } from '../constants'; 5 | import * as cookies from '../lib/cookies'; 6 | import Footer from './common/Footer'; 7 | 8 | const PageNotFound = function () { 9 | return ( 10 |
11 |
12 | Error: 404 13 |

Uh oh, page not found!

14 |
15 | 16 | 19 | 20 |
21 |

22 | Getting this problem a lot? Let us know 23 | {' '} 24 | 25 | on GitHub 26 | 27 | . 28 |

29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export default PageNotFound; 36 | -------------------------------------------------------------------------------- /src/components/Sketches/components/ConfirmDeleteModal.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import ReactModal from 'react-modal'; 3 | import { 4 | Container, Row, Col, Button, 5 | } from 'reactstrap'; 6 | import * as fetch from '../../../lib/fetch'; 7 | 8 | const ConfirmDeleteModal = (props) => { 9 | const { 10 | onClose, isOpen, sketchName, uid, sketchKey, deleteProgram, 11 | } = props; 12 | 13 | const [_spinner, setSpinner] = useState(true); 14 | const [_error, setError] = useState(''); 15 | 16 | const closeModal = () => { 17 | if (onClose && {}.toString.call(onClose) === '[object Function]') { 18 | onClose(); 19 | } 20 | }; 21 | 22 | const onDeleteSubmit = () => { 23 | const data = { 24 | uid, 25 | docID: sketchKey, 26 | name: sketchKey, 27 | }; 28 | try { 29 | fetch 30 | .deleteSketch(data) 31 | .then((res) => { 32 | if (res.ok) { 33 | deleteProgram(sketchKey); 34 | closeModal(); 35 | } else { 36 | setSpinner(false); 37 | setError(res.text() || 'Failed to delete sketch, please try again later'); 38 | } 39 | }) 40 | .catch((err) => { 41 | setSpinner(false); 42 | setError('Failed to create sketch, please try again later'); 43 | console.error(err); 44 | }); 45 | } catch (err) { 46 | console.error(err); 47 | } 48 | setSpinner(true); 49 | setError(''); 50 | }; 51 | 52 | return ( 53 | 59 | 60 |

61 | Are you sure you want to delete " 62 | {sketchName} 63 | "? 64 |

65 |
66 | 67 | 68 | 71 | 72 | 73 | 76 | 77 | 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default ConfirmDeleteModal; 84 | -------------------------------------------------------------------------------- /src/components/Sketches/constants/index.js: -------------------------------------------------------------------------------- 1 | import { SUPPORTED_LANGUAGES } from '../../../util/languages/languages'; 2 | 3 | export const LanguageDropdownValues = SUPPORTED_LANGUAGES.map(({ value, display }) => ({ 4 | value, 5 | display, 6 | })); 7 | export const LanguageDropdownDefault = LanguageDropdownValues[0]; 8 | -------------------------------------------------------------------------------- /src/components/Sketches/containers/ConfirmDeleteModalContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { deleteProgram } from '../../../actions/programsActions'; 3 | import ConfirmDeleteModal from '../components/ConfirmDeleteModal'; 4 | 5 | const mapStateToProps = (state) => ({ uid: state.userData.uid }); 6 | 7 | const mapDispatchToProps = (dispatch) => ({ 8 | deleteProgram: (program, data) => dispatch(deleteProgram(program, data)), 9 | }); 10 | 11 | const ConfirmDeleteModalContainer = connect( 12 | mapStateToProps, 13 | mapDispatchToProps, 14 | )(ConfirmDeleteModal); 15 | 16 | export default ConfirmDeleteModalContainer; 17 | -------------------------------------------------------------------------------- /src/components/Sketches/containers/CreateSketchModalContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { addProgram } from '../../../actions/programsActions'; 3 | import { setMostRecentProgram } from '../../../actions/userDataActions'; 4 | import * as fetch from '../../../lib/fetch'; 5 | import CreateSketchModal from '../components/CreateSketchModal'; 6 | 7 | const mapStateToProps = (state) => ({ 8 | uid: state.userData.uid, 9 | }); 10 | 11 | const mapDispatchToProps = (dispatch) => ({ 12 | addProgram: (program, data) => dispatch(addProgram(program, data)), 13 | setMostRecentProgram: (value, uid) => { 14 | try { 15 | fetch.updateUserData(uid, { mostRecentProgram: value }).catch((err) => { 16 | console.error(err); 17 | }); 18 | } catch (err) { 19 | console.error(err); 20 | } 21 | dispatch(setMostRecentProgram(value)); 22 | }, 23 | }); 24 | 25 | const CreateSketchModalContainer = connect(mapStateToProps, mapDispatchToProps)(CreateSketchModal); 26 | 27 | export default CreateSketchModalContainer; 28 | -------------------------------------------------------------------------------- /src/components/Sketches/containers/EditSketchModalContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { 3 | setProgramLanguage, 4 | setProgramName, 5 | setProgramThumbnail, 6 | } from '../../../actions/programsActions'; 7 | import EditSketchModal from '../components/EditSketchModal'; 8 | 9 | const mapStateToProps = (state) => ({ 10 | uid: state.userData.uid, 11 | }); 12 | 13 | const mapDispatchToProps = (dispatch) => ({ 14 | setProgramLanguage: (program, value) => dispatch(setProgramLanguage(program, value)), 15 | setProgramName: (program, value) => dispatch(setProgramName(program, value)), 16 | setProgramThumbnail: (program, value) => dispatch(setProgramThumbnail(program, value)), 17 | }); 18 | 19 | const EditSketchModalContainer = connect( 20 | mapStateToProps, 21 | mapDispatchToProps, 22 | )(EditSketchModal); 23 | 24 | export default EditSketchModalContainer; 25 | -------------------------------------------------------------------------------- /src/components/Sketches/containers/SketchesContainer.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { connect } from 'react-redux'; 3 | import { togglePanel } from '../../../actions/uiActions'; 4 | import { setMostRecentProgram } from '../../../actions/userDataActions'; 5 | import { OPEN_PANEL_LEFT, CLOSED_PANEL_LEFT, PANEL_SIZE } from '../../../constants'; 6 | import * as fetch from '../../../lib/fetch'; 7 | import { getLanguageData } from '../../../util/languages/languages'; 8 | import Sketches from '../index'; 9 | 10 | const mapStateToProps = (state) => { 11 | const { mostRecentProgram } = state.userData; 12 | const programs = state.programs.keySeq().map((id) => { 13 | const temp = state.programs.get(id, Immutable.Map()).toJS(); 14 | temp.key = id; 15 | temp.language = getLanguageData(temp.language); 16 | return temp; 17 | }); 18 | 19 | const left = (state.ui.panelOpen ? OPEN_PANEL_LEFT : CLOSED_PANEL_LEFT) + PANEL_SIZE; 20 | const calculatedWidth = state.ui.screenWidth - (left || 0); 21 | 22 | return { 23 | mostRecentProgram, 24 | programs, 25 | calculatedWidth, 26 | left, 27 | screenHeight: state.ui.screenHeight, 28 | panelOpen: state.ui.panelOpen, 29 | uid: state.userData.uid, 30 | }; 31 | }; 32 | 33 | const mapDispatchToProps = (dispatch) => ({ 34 | setMostRecentProgram: (value, uid) => { 35 | try { 36 | fetch.updateUserData(uid, { mostRecentProgram: value }).catch((err) => { 37 | console.error(err); 38 | }); 39 | } catch (err) { 40 | console.error(err); 41 | } 42 | dispatch(setMostRecentProgram(value)); 43 | }, 44 | togglePanel: () => dispatch(togglePanel()), 45 | }); 46 | 47 | const SketchesContainer = connect(mapStateToProps, mapDispatchToProps)(Sketches); 48 | 49 | export default SketchesContainer; 50 | -------------------------------------------------------------------------------- /src/components/TextEditor/components/EditorRadio.jsx: -------------------------------------------------------------------------------- 1 | import { CODE_AND_OUTPUT, CODE_ONLY, OUTPUT_ONLY } from '../../../constants'; 2 | import Radio from '../../common/Radio'; 3 | 4 | const EditorRadio = function (props) { 5 | let options = []; 6 | if (!props.isSmall) options.push({ display: 'Both', value: CODE_AND_OUTPUT }); 7 | options = options.concat([ 8 | { display: 'Code', value: CODE_ONLY }, 9 | { display: 'Output', value: OUTPUT_ONLY }, 10 | ]); 11 | 12 | return ( 13 | 19 | ); 20 | }; 21 | 22 | export default EditorRadio; 23 | -------------------------------------------------------------------------------- /src/components/TextEditor/components/ShareSketchModal.jsx: -------------------------------------------------------------------------------- 1 | import { faCopy } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { useState } from 'react'; 4 | import ReactModal from 'react-modal'; 5 | 6 | import { Button, Input, InputGroup } from 'reactstrap'; 7 | 8 | import '../../../styles/Modals.scss'; 9 | 10 | /** 11 | * ShareSketchModal is a full-screen modal that displays 12 | * the current program's view-only route, which the user can manually 13 | * copy or use the "Copy To Clipboard" Button 14 | * @param {Boolean} showModal modal render state 15 | * @param {Function} toggleModal function that toggles the modal's render state 16 | * @param {String} shareUrl the URL to display/copy to clipboard 17 | * showModal modal render state 18 | * toggleModal function that toggles the modal's render state 19 | * shareUrl the URL to display/copy to clipboard 20 | */ 21 | 22 | function ShareSketchModal(props) { 23 | const { shareUrl, showModal, toggleModal } = props; 24 | const [copyStatus, setCopyStatus] = useState('Hit "Copy to Clipboard"!'); 25 | 26 | const initiateCopy = () => { 27 | navigator.clipboard.writeText(shareUrl).then( 28 | () => { 29 | // success 30 | setCopyStatus('Successfully copied!'); 31 | }, 32 | () => { 33 | // failed 34 | setCopyStatus('Copy failed. If this keeps on happening, let us know!'); 35 | }, 36 | ); 37 | }; 38 | 39 | return ( 40 | 47 |

Share This File

48 | 49 | 50 | 55 | 56 |
57 |

{copyStatus}

58 |
59 | ); 60 | } 61 | 62 | export default ShareSketchModal; 63 | -------------------------------------------------------------------------------- /src/components/TextEditor/containers/DropdownButtonContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { setMostRecentProgram } from '../../../actions/userDataActions'; 3 | import * as fetch from '../../../lib/fetch'; 4 | import { getLanguageData } from '../../../util/languages/languages'; 5 | import DropdownButton from '../../common/DropdownButton'; 6 | 7 | const mapStateToProps = (state) => { 8 | const { mostRecentProgram } = state.userData; 9 | 10 | const mostRecentLanguage = getLanguageData( 11 | state.programs.getIn([mostRecentProgram, 'language'], 'python'), 12 | ); 13 | 14 | const displayValue = ` ${state.programs.getIn([mostRecentProgram, 'name'], mostRecentProgram)}`; 15 | 16 | const programStateValues = state.programs.keySeq().map((id) => ({ 17 | display: state.programs.getIn([id, 'name'], id), 18 | value: id, 19 | icon: getLanguageData(state.programs.getIn([id, 'language'], 'python')).icon, 20 | })); 21 | 22 | const dirty = state.programs.getIn([mostRecentProgram, 'dirty'], false); 23 | 24 | const displayClass = 'editor'; 25 | 26 | const toggleProps = { 27 | className: 'btn-language-dropdown', 28 | color: '', 29 | size: '', 30 | }; 31 | 32 | const { icon } = mostRecentLanguage; 33 | 34 | return { 35 | displayValue, 36 | displayClass, 37 | dirty, 38 | DropdownItems: programStateValues, 39 | icon, 40 | toggleProps, 41 | uid: state.userData.uid, 42 | }; 43 | }; 44 | 45 | const mapDispatchToProps = (dispatch) => ({ 46 | onSelect: ({ 47 | _display, value, _dirty, uid, 48 | }) => { 49 | // TODO: Fix this dirty check 50 | // if (dirty) { 51 | // result = window.confirm('Are you sure you want to change programs? You have unsaved changes'); 52 | // } 53 | try { 54 | fetch.updateUserData(uid, { mostRecentProgram: value }).catch((err) => { 55 | console.error(err); 56 | }); 57 | } catch (err) { 58 | console.error(err); 59 | } 60 | dispatch(setMostRecentProgram(value)); 61 | }, 62 | }); 63 | 64 | const DropdownButtonContainer = connect(mapStateToProps, mapDispatchToProps)(DropdownButton); 65 | 66 | export default DropdownButtonContainer; 67 | -------------------------------------------------------------------------------- /src/components/TextEditor/containers/TextEditorContainer.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { connect } from 'react-redux'; 3 | import { addProgram, setProgramCode, setProgramDirty } from '../../../actions/programsActions'; 4 | 5 | import { setMostRecentProgram } from '../../../actions/userDataActions'; 6 | import { getLanguageData } from '../../../util/languages/languages'; 7 | import TextEditor from '../components/TextEditor'; 8 | 9 | const mapStateToProps = (state, ownProps) => { 10 | const { uid, mostRecentProgram } = state.userData; 11 | 12 | // program data should be an object representing the most recent program 13 | // should have 2 keys, code (which is the code) and langauge (which is the language the code is written it) 14 | // add key dirty 15 | const programData = state.programs.get(mostRecentProgram, Immutable.Map()).toJS(); 16 | return { 17 | ...programData, 18 | language: getLanguageData(programData.language), 19 | mostRecentProgram, 20 | theme: ownProps.theme, 21 | uid, 22 | }; 23 | }; 24 | 25 | const mapDispatchToProps = (dispatch, _ownProps) => ({ 26 | setProgramCode: (program, code) => { 27 | dispatch(setProgramCode(program, code)); 28 | }, 29 | dirtyCode: (program) => { 30 | dispatch(setProgramDirty(program, true)); 31 | }, 32 | addProgram: (program, data) => dispatch(addProgram(program, data)), 33 | setMostRecentProgram: (value) => dispatch(setMostRecentProgram(value)), 34 | }); 35 | 36 | const TextEditorContainer = connect(mapStateToProps, mapDispatchToProps)(TextEditor); 37 | 38 | export default TextEditorContainer; 39 | -------------------------------------------------------------------------------- /src/components/common/DropdownButton.jsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import { useState } from 'react'; 3 | import { 4 | Dropdown, DropdownToggle, DropdownMenu, DropdownItem, 5 | } from 'reactstrap'; 6 | 7 | /** --------Props--------------- 8 | * dropDownItems: array of data for each dropdown item 9 | * displayValue: string to be displayed as the placeholder for the dropdown 10 | * toggleProps: props that dictate the styling of dropDownToggle 11 | * icon: contains optional icon for editor dropDown 12 | * displayClass: contains different classes for editor and sketch dropdownbutton 13 | */ 14 | /** --------Optional props-------- 15 | * defaultOpen: boolean determining if the dropdown should start off open or closed 16 | */ 17 | 18 | function DropdownButton(props) { 19 | const { 20 | uid, 21 | icon, 22 | DropdownItems, 23 | defaultOpen, 24 | displayValue, 25 | displayClass, 26 | toggleProps, 27 | onSelect, 28 | dirty, 29 | } = props; 30 | 31 | const dropDownParentClass = `${displayClass}-language-dropdown`; 32 | const dropDownItemClass = `${displayClass}-language-dropdown-closed-content`; 33 | 34 | const [dropdownOpen, setdropdownOpen] = useState(defaultOpen || false); 35 | 36 | const toggleHandler = () => { 37 | setdropdownOpen(!dropdownOpen); 38 | }; 39 | 40 | const renderDropdownItems = () => DropdownItems.map(({ display, value, icon: itemIcon }) => ( 41 | onSelect({ 44 | display, 45 | value, 46 | dirty, 47 | uid, 48 | })} 49 | > 50 | 51 | {display} 52 | 53 | )); 54 | 55 | return ( 56 |
57 | toggleHandler(dropdownOpen)}> 58 | {/* eslint-disable-next-line react/jsx-props-no-spreading */} 59 | 60 |
61 | 62 | {displayValue} 63 |
64 |
65 | {renderDropdownItems()} 66 |
67 |
68 | ); 69 | } 70 | 71 | export default DropdownButton; 72 | -------------------------------------------------------------------------------- /src/components/common/Footer.tsx: -------------------------------------------------------------------------------- 1 | import TLALogo from '../../img/tla-logo.svg'; 2 | import '../../styles/Footer.scss'; 3 | 4 | const Footer = () => { 5 | return ( 6 | 15 | ); 16 | }; 17 | 18 | export default Footer; 19 | -------------------------------------------------------------------------------- /src/components/common/ImageSelector.jsx: -------------------------------------------------------------------------------- 1 | import ReactModal from 'react-modal'; 2 | import { Container } from 'reactstrap'; 3 | import '../../styles/ImageSelector.scss'; 4 | 5 | const ImageSelector = function (props) { 6 | // Extracting all data from props 7 | const { 8 | isOpen, closeModal, maxWidth, thumbnailPreview, icons, error, children, 9 | } = props; 10 | return ( 11 | 18 | 19 |
20 |

Choose a thumbnail

21 |
22 | {thumbnailPreview || null} 23 |
24 |
25 |
26 |
{icons}
27 |
28 |
{error ||
}
29 |
30 | {children} 31 | {' '} 32 | {/* Footer buttons as passed in as children */} 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default ImageSelector; 39 | -------------------------------------------------------------------------------- /src/components/common/LoadingPage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { RingLoader } from 'react-spinners'; 3 | import '../../styles/Loading.scss'; 4 | import { GH_REPO_NAME } from '../../constants'; 5 | 6 | interface LoadingProps { 7 | showHelpText: boolean; 8 | } 9 | 10 | const Loading = function ({ showHelpText = false } : LoadingProps) { 11 | const [hasHelpText, setHasHelpText] = useState(showHelpText); 12 | 13 | const timer = setTimeout( 14 | () => { 15 | setHasHelpText(true); 16 | }, 17 | 2000, 18 | ); 19 | 20 | useEffect(() => () => { 21 | clearTimeout(timer); 22 | }, []); 23 | 24 | return ( 25 |
26 |
Loading
27 | 28 | { hasHelpText && ( 29 |

30 | Looks like loading is taking a bit long! If it takes too long, submit an issue on 31 | {' '} 32 | 33 | 34 | Github 35 | 36 | . 37 |

38 | )} 39 |
40 | ); 41 | }; 42 | 43 | export default Loading; 44 | -------------------------------------------------------------------------------- /src/components/common/OpenPanelButton.tsx: -------------------------------------------------------------------------------- 1 | import { faBars } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | 4 | /** -------Props-------- 5 | * panelOpen: boolean, is profile panel visible 6 | * togglePanel: function to call when you want the Profile Panel to disappear/reappear 7 | */ 8 | 9 | interface OpenPanelButtonProps { 10 | panelOpen: boolean; 11 | togglePanel: () => void; 12 | } 13 | 14 | const OpenPanelButton = function ({ panelOpen, togglePanel }: OpenPanelButtonProps) { 15 | // if the left panel is closed, show nothing 16 | // otherwise show hamburger icon 17 | if (panelOpen) { 18 | return
; 19 | } 20 | return ( 21 | 24 | ); 25 | }; 26 | 27 | export default OpenPanelButton; 28 | -------------------------------------------------------------------------------- /src/components/common/SketchBox.tsx: -------------------------------------------------------------------------------- 1 | import '../../styles/SketchBox.scss'; 2 | import { faDownload, faEdit, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | 5 | import { Link } from 'react-router-dom'; 6 | import { Row, Col } from 'reactstrap'; 7 | 8 | interface SketchBoxProps { 9 | img: string; 10 | icon: any; 11 | name: string; 12 | deleteFunc: () => void; 13 | downloadFunc: () => void; 14 | editFunc: () => void; 15 | redirFunc: () => void; 16 | pathname: string; 17 | } 18 | 19 | function SketchBox({ 20 | img, icon, name, deleteFunc, downloadFunc, editFunc, redirFunc, pathname, 21 | }: SketchBoxProps) { 22 | const buttonData = [ 23 | { 24 | func: editFunc, 25 | icon: faEdit, 26 | }, 27 | { 28 | func: downloadFunc, 29 | icon: faDownload, 30 | }, 31 | { 32 | func: deleteFunc, 33 | icon: faTrashAlt, 34 | }, 35 | ]; 36 | 37 | return ( 38 |
39 | 44 | {"User's 49 |
50 | {name} 51 | 52 |
53 | 54 |
55 | 56 | {buttonData 57 | .map((button) => ( 58 | 59 | 60 | 61 | )) 62 | // put a thin divider between each button 63 | .flatMap((e, i) => [e,
]) 64 | .slice(0, -1)} 65 | 66 |
67 | ); 68 | } 69 | 70 | export default SketchBox; 71 | -------------------------------------------------------------------------------- /src/components/common/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import '../../styles/Switch.scss'; 3 | 4 | /** 5 | * Generic switch component. Props are as follows: 6 | * @param {boolean} [on] - whether the switch is set to the "on" position or not; optional prop, defaults to false 7 | * @param {function} onToggle - function to be called when switch is toggled. Has a single parameter 8 | * (bool) that holds whether the switch is in the "on" position (true) or not (false) 9 | * @param {JSX} onImg (optional): (JSX) element to be displayed on switch body when set to "on" 10 | * @param {JSX} offImg (optional): (JSX) element to be displayed on switch body when set to "off" 11 | */ 12 | interface SwitchProps { 13 | on: boolean; 14 | onToggle: (on:boolean) => void; 15 | onImg?: JSX.Element; 16 | offImg?: JSX.Element; 17 | 18 | } 19 | const Switch = function (props: SwitchProps) { 20 | const [on, setOn] = useState(!props.on ? false : props.on); 21 | 22 | useEffect(() => { 23 | if (props.on !== on) { 24 | setOn(props.on); 25 | } 26 | }); 27 | 28 | const onSwitchChange = () => { 29 | props.onToggle(!on); 30 | setOn(!on); 31 | }; 32 | 33 | const switchedClass = on ? ' switch-on' : ''; 34 | 35 | return ( 36 | 43 | ); 44 | }; 45 | 46 | export default Switch; 47 | -------------------------------------------------------------------------------- /src/components/common/ViewportAwareButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, ButtonProps } from 'reactstrap'; 3 | 4 | /** -------Props-------- 5 | * isSmall: dictates display of text field 6 | * icon: desired icon (i.e. ) 7 | * text: the text label for the button 8 | */ 9 | /** --------Optional props-------- 10 | * any props you would apply to a reactstrap Button. 11 | */ 12 | 13 | interface ViewportAwareButtonProps extends ButtonProps { 14 | icon: React.JSX.Element, 15 | text: string, 16 | isSmall: boolean, 17 | }; 18 | 19 | const ViewportAwareButton = function (props : ViewportAwareButtonProps) { 20 | // extract icon and text from props. 21 | const { 22 | icon, text, isSmall, ...remainder 23 | } = props; 24 | 25 | // render icon always, text only if not small. 26 | return ( 27 | 36 | ); 37 | }; 38 | 39 | export default ViewportAwareButton; 40 | -------------------------------------------------------------------------------- /src/components/common/containers/OpenPanelButtonContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { togglePanel } from '../../../actions/uiActions'; 3 | import OpenPanelButton from '../OpenPanelButton'; 4 | 5 | const mapStateToProps = (state) => ({ 6 | panelOpen: state.ui.panelOpen, 7 | }); 8 | 9 | const mapDispatchToProps = (dispatch) => ({ 10 | togglePanel: () => dispatch(togglePanel()), 11 | }); 12 | 13 | const OpenPanelButtonContainer = connect(mapStateToProps, mapDispatchToProps)(OpenPanelButton); 14 | 15 | export default OpenPanelButtonContainer; 16 | -------------------------------------------------------------------------------- /src/components/common/containers/ProfilePanelContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { togglePanel } from '../../../actions/uiActions'; 3 | import { setDisplayName, setPhotoName } from '../../../actions/userDataActions'; 4 | import { DEFAULT_PHOTO_NAME, CLOSED_PANEL_LEFT, OPEN_PANEL_LEFT } from '../../../constants'; 5 | import * as fetch from '../../../lib/fetch'; 6 | import ProfilePanel from '../ProfilePanel'; 7 | 8 | const mapStateToProps = (state, ownProps) => ({ 9 | displayName: state.userData.displayName, 10 | uid: state.userData.uid, 11 | photoName: state.userData.photoName || DEFAULT_PHOTO_NAME, 12 | screenHeight: state.ui.screenHeight, 13 | left: state.ui.panelOpen ? OPEN_PANEL_LEFT : CLOSED_PANEL_LEFT, 14 | theme: ownProps.theme, 15 | developerAcc: state.userData.developerAcc, 16 | }); 17 | 18 | const mapDispatchToProps = (dispatch, ownProps) => ({ 19 | onThemeChange: ownProps.onThemeChange, 20 | collectUserPhoto: () => {}, 21 | setDisplayName: (name, uid) => { 22 | try { 23 | fetch.updateUserData(uid, { displayName: name }).catch((err) => { 24 | console.error(err); 25 | }); 26 | } catch (err) { 27 | console.error(err); 28 | } 29 | dispatch(setDisplayName(name)); 30 | }, 31 | setPhotoName: (name, uid) => { 32 | try { 33 | fetch 34 | .updateUserData(uid, { photoName: name }) 35 | .then(() => { 36 | // TODO: if nothing went bad, keep the display name, 37 | // otherwise, change it back (or dont, depends how we wanna do it) 38 | }) 39 | .catch((err) => { 40 | console.error(err); 41 | }); 42 | } catch (err) { 43 | console.error(err); 44 | } 45 | dispatch(setPhotoName(name)); 46 | }, 47 | togglePanel: () => { 48 | dispatch(togglePanel()); 49 | }, 50 | }); 51 | 52 | const ProfilePanelContainer = connect(mapStateToProps, mapDispatchToProps)(ProfilePanel); 53 | 54 | export default ProfilePanelContainer; 55 | -------------------------------------------------------------------------------- /src/components/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | /* 2 | used to link state to App which contains the router, could link functions, 3 | but rather do that in each individual container 4 | */ 5 | import { connect } from 'react-redux'; 6 | import { clearClasses } from '../../actions/classesActions'; 7 | import { loadPrograms, clearPrograms } from '../../actions/programsActions'; 8 | import { screenResize } from '../../actions/uiActions'; 9 | import { loadUserData, clearUserData, loadFailure } from '../../actions/userDataActions'; 10 | import * as fetch from '../../lib/fetch'; 11 | import App from '../app'; 12 | 13 | const mapStateToProps = (state) => ({ 14 | uid: state.userData.uid, 15 | developerAcc: state.userData.developerAcc, 16 | errorMsg: state.userData.error, 17 | screenWidth: state.ui.screenWidth, 18 | screenHeight: state.ui.screenHeight, 19 | }); 20 | 21 | const mapDispatchToProps = (dispatch) => ({ 22 | loadUserData: async (uid, onFailure) => { 23 | const { ok, data, error } = await fetch.getUserData(uid, true); 24 | // if the request went fine, and there's a non empty userData 25 | if (ok && data && data.userData && Object.keys(data.userData).length) { 26 | if (data.programs) { 27 | dispatch(loadPrograms(data.programs)); 28 | } 29 | dispatch(loadUserData(uid, data.userData)); 30 | } else { 31 | console.error('Error getting user data:', error); 32 | onFailure('SERVER ERROR: Unable to get user data from server'); 33 | } 34 | }, 35 | clearUserData: () => { 36 | dispatch(clearUserData()); 37 | dispatch(clearPrograms()); 38 | dispatch(clearClasses()); 39 | }, 40 | loadFailure: (err) => { 41 | dispatch(loadFailure(err)); 42 | }, 43 | screenResize: (width, height) => dispatch(screenResize(width, height)), 44 | }); 45 | 46 | const Root = connect(mapStateToProps, mapDispatchToProps)(App); 47 | 48 | export default Root; 49 | -------------------------------------------------------------------------------- /src/components/containers/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { loadUserData, clearUserData, loadFailure } from '../../actions/userDataActions'; 3 | import Login from '../Login'; 4 | 5 | const mapStateToProps = (state) => ({ 6 | loggedIn: state.userData, 7 | }); 8 | 9 | const mapDispatchToProps = (dispatch) => ({ 10 | loadUserData: (user) => { 11 | dispatch(loadUserData(user)); 12 | }, 13 | clearUserData: () => { 14 | dispatch(clearUserData()); 15 | }, 16 | loadFailure: (err) => { 17 | dispatch(loadFailure(err)); 18 | }, 19 | }); 20 | 21 | const LoginPage = connect( 22 | mapStateToProps, 23 | mapDispatchToProps, 24 | )(Login); 25 | 26 | export default LoginPage; 27 | -------------------------------------------------------------------------------- /src/components/containers/MainContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { setOutput } from '../../actions/outputActions'; 3 | import { setProgramDirty } from '../../actions/programsActions'; 4 | import { togglePanel, setTheme } from '../../actions/uiActions'; 5 | 6 | import { setMostRecentProgram } from '../../actions/userDataActions'; 7 | import { CLOSED_PANEL_LEFT, OPEN_PANEL_LEFT, PANEL_SIZE } from '../../constants'; 8 | import { getLanguageData } from '../../util/languages/languages'; 9 | import Main from '../Main'; 10 | 11 | const mapStateToProps = (state) => { 12 | const { mostRecentProgram } = state.userData; 13 | 14 | // program data should be an object representing the most recent program 15 | // should have 2 keys, code (which is the code) and langauge (which is the language the code is written it) 16 | const code = state.programs.getIn([mostRecentProgram, 'code'], undefined); 17 | const dirty = state.programs.getIn([mostRecentProgram, 'dirty'], false); 18 | const name = state.programs.getIn([mostRecentProgram, 'name'], 'untitled'); 19 | const language = state.programs.getIn([mostRecentProgram, 'language'], 'txt'); 20 | 21 | const listOfPrograms = []; 22 | 23 | state.programs.keySeq().forEach((key) => listOfPrograms.push(key)); 24 | 25 | return { 26 | uid: state.userData.uid, 27 | mostRecentProgram, 28 | code, 29 | listOfPrograms, 30 | screenWidth: state.ui.screenWidth, 31 | screenHeight: state.ui.screenHeight, 32 | dirty, 33 | panelOpen: state.ui.panelOpen, 34 | left: (state.ui.panelOpen ? OPEN_PANEL_LEFT : CLOSED_PANEL_LEFT) + PANEL_SIZE, 35 | sketchName: name, 36 | language: getLanguageData(language), 37 | theme: state.ui.theme, 38 | }; 39 | }; 40 | 41 | const mapDispatchToProps = (dispatch) => ({ 42 | setMostRecentProgram: (value) => dispatch(setMostRecentProgram(value)), 43 | runCode: (code, language) => dispatch(setOutput(code, language)), 44 | cleanCode: (program) => dispatch(setProgramDirty(program, false)), 45 | togglePanel: () => dispatch(togglePanel()), 46 | setTheme: (theme) => dispatch(setTheme(theme)), 47 | }); 48 | 49 | const MainContainer = connect(mapStateToProps, mapDispatchToProps)(Main); 50 | 51 | export default MainContainer; 52 | -------------------------------------------------------------------------------- /src/components/containers/ViewOnlyContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { setOutput } from '../../actions/outputActions'; 3 | import { setProgramCode, setProgramLanguage } from '../../actions/programsActions'; 4 | import { togglePanel, setTheme } from '../../actions/uiActions'; 5 | 6 | import { CLOSED_PANEL_LEFT, OPEN_PANEL_LEFT, PANEL_SIZE } from '../../constants'; 7 | import ViewOnly from '../ViewOnly'; 8 | 9 | const mapStateToProps = (state) => { 10 | const { mostRecentProgram } = state.userData; 11 | return { 12 | uid: state.userData.uid, 13 | screenWidth: state.ui.screenWidth, 14 | screenHeight: state.ui.screenHeight, 15 | panelOpen: state.ui.panelOpen, 16 | left: (state.ui.panelOpen ? OPEN_PANEL_LEFT : CLOSED_PANEL_LEFT) + PANEL_SIZE, 17 | theme: state.ui.theme, 18 | mostRecentProgram, 19 | }; 20 | }; 21 | 22 | const mapDispatchToProps = (dispatch) => ({ 23 | runCode: (code, language) => dispatch(setOutput(code, language)), 24 | togglePanel: () => dispatch(togglePanel()), 25 | setProgramCode: (program, code) => { 26 | dispatch(setProgramCode(program, code)); 27 | }, 28 | setProgramLanguage: (program, lang) => { 29 | dispatch(setProgramLanguage(program, lang)); 30 | }, 31 | setTheme: (theme) => dispatch(setTheme(theme)), 32 | }); 33 | 34 | const ViewOnlyContainer = connect(mapStateToProps, mapDispatchToProps)(ViewOnly); 35 | 36 | export default ViewOnlyContainer; 37 | -------------------------------------------------------------------------------- /src/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | export default createBrowserHistory(); 4 | -------------------------------------------------------------------------------- /src/img/background1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/img/blueguy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/src/img/blueguy.png -------------------------------------------------------------------------------- /src/img/tla-logo-green.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/img/tla-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import ReactDOM from 'react-dom/client'; 3 | import 'bootstrap/dist/css/bootstrap.css'; 4 | import './styles/global.scss'; 5 | import { Provider } from 'react-redux'; 6 | import Root from './components/containers/AppContainer'; 7 | import registerServiceWorker from './registerServiceWorker'; 8 | import store from './store'; 9 | 10 | const root = ReactDOM.createRoot(document.getElementById('root')!); 11 | 12 | root.render( 13 | 14 | 15 | , 16 | ); 17 | 18 | registerServiceWorker(); 19 | -------------------------------------------------------------------------------- /src/lib/cookies.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * an internal helper function to get a cookie from the document. 3 | * almost word-for-word copied from https://www.w3schools.com/js/js_cookies.asp 4 | * @param {string} cname - the key value for the requested cookie 5 | * @return {string} the value of the requested key, "" for none set 6 | */ 7 | 8 | function getCookie(cname: string): string { 9 | const name = `${cname}=`; 10 | const decodedCookie = decodeURIComponent(document.cookie); 11 | const ca = decodedCookie.split(';'); 12 | for (let i = 0; i < ca.length; i++) { 13 | let c = ca[i]; 14 | while (c.charAt(0) === ' ') { 15 | c = c.substring(1); 16 | } 17 | if (c.indexOf(name) === 0) { 18 | return c.substring(name.length, c.length); 19 | } 20 | } 21 | return ''; 22 | } 23 | 24 | /** 25 | * an internal helpder function to set a cookie in the document 26 | * almost word-for-word copied from https://www.w3schools.com/js/js_cookies.asp 27 | * @param {string} cname - the key value for the requested cookie 28 | * @param {string} cvalue - the value to set the cookie to 29 | * @param {number} exdays - the number of days until the cookie expires 30 | */ 31 | 32 | function setCookie(cname: string, cvalue: string, exdays: number) { 33 | const d = new Date(); 34 | d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); 35 | const expires = `expires=${d.toUTCString()}`; 36 | document.cookie = `${cname}=${cvalue};${expires};path=/`; 37 | } 38 | 39 | /** 40 | * exported function that gets the current app theme from the theme cookie; 41 | * if none is set, returns dark and sets the cookie to dark 42 | * @returns {string} the current theme, or dark if the cookie doesn't exist 43 | */ 44 | 45 | export const getThemeFromCookie = (): string => { 46 | const theme = getCookie('theme'); 47 | if (theme !== '') { 48 | return theme; 49 | } 50 | setCookie('theme', 'dark', 365); 51 | return 'dark'; 52 | }; 53 | 54 | /** 55 | * exported function that sets the theme cookie to the input, with a one-year expiry date 56 | * @param {string} theme theme string to store in the cookie 57 | */ 58 | 59 | export const setThemeCookie = (theme: string) => { 60 | setCookie('theme', theme, 365); 61 | }; 62 | 63 | /** 64 | * exported function that toggles the theme cookie to flip-flop its value from light to dark 65 | * if none is set, assumes that the user is currently in dark and switches to light 66 | * @returns {string} returns the current value of the theme cookie 67 | */ 68 | 69 | export const toggleCookie = (): string => { 70 | if (getThemeFromCookie() === 'light') { 71 | setCookie('theme', 'dark', 365); 72 | return 'dark'; 73 | } 74 | setCookie('theme', 'light', 365); 75 | return 'light'; 76 | }; 77 | -------------------------------------------------------------------------------- /src/lib/sketch.ts: -------------------------------------------------------------------------------- 1 | import constants from '../constants'; 2 | 3 | /** 4 | * constructShareableSketchURL: given a program ID, generate 5 | * a shareable link to the view-only version of that program. 6 | * checks for the current hostname using window.location.hostname, 7 | * @param {String} programId the id of the program 8 | * @param {String} [domainRoot=window.location.hostname] domainRoot (e.g. editor.uclaacm.com). 9 | * defaults localhost to 8080 10 | */ 11 | 12 | export const constructShareableSketchURL = (programId: string, domainRoot: string = window.location.hostname) => { 13 | let root; 14 | if (domainRoot.includes('localhost')) { 15 | root = 'http://localhost:8080'; 16 | } else { 17 | root = `https://${domainRoot}`; 18 | } 19 | 20 | return `${root}${constants.ROUTER_BASE_NAME}p/${programId}`; 21 | }; 22 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reducers/classesReducer.js: -------------------------------------------------------------------------------- 1 | import Immutable, { Map } from 'immutable'; 2 | 3 | import { 4 | ADD_STUDENT_CLASS, 5 | REMOVE_STUDENT_CLASS, 6 | LOAD_STUDENT_CLASSES, 7 | CLEAR_STUDENT_CLASSES, 8 | ADD_INSTR_CLASS, 9 | REMOVE_INSTR_CLASS, 10 | LOAD_INSTR_CLASSES, 11 | CLEAR_INSTR_CLASSES, 12 | CLEAR_CLASSES, 13 | } from '../actions/classesActions'; 14 | 15 | const initialState = Map({ studClasses: Map(), instrClasses: Map() }); 16 | 17 | function classesReducer(state = initialState, action) { 18 | switch (action.type) { 19 | case ADD_STUDENT_CLASS: 20 | return state.setIn(['studClasses', action.cid], Immutable.fromJS(action.data)); 21 | case REMOVE_STUDENT_CLASS: 22 | return state.deleteIn(['studClasses', action.cid]); 23 | case LOAD_STUDENT_CLASSES: { 24 | // TODO: update this to make sure classes are indexed by class ID 25 | // Want to extract CIDs from action.classes and use them as the keys. 26 | const studClassesKeyed = {}; 27 | action.classes.forEach((classObj) => { 28 | studClassesKeyed[classObj.cid] = classObj; 29 | }); 30 | return state.set('studClasses', Immutable.fromJS(studClassesKeyed)); 31 | } 32 | case CLEAR_STUDENT_CLASSES: 33 | return state.set('studClasses', Map()); 34 | case ADD_INSTR_CLASS: 35 | return state.setIn(['instrClasses', action.cid], Immutable.fromJS(action.data)); 36 | case REMOVE_INSTR_CLASS: 37 | return state.deleteIn(['instrClasses', action.cid]); 38 | case LOAD_INSTR_CLASSES: { 39 | // TODO: update this to make sure classes are indexed by class ID 40 | const instrClassesKeyed = {}; 41 | action.classes.forEach((classObj) => { 42 | instrClassesKeyed[classObj.cid] = classObj; 43 | }); 44 | return state.set('instrClasses', Immutable.fromJS(instrClassesKeyed)); 45 | } 46 | case CLEAR_INSTR_CLASSES: 47 | return state.set('instrClasses', Map()); 48 | case CLEAR_CLASSES: 49 | return Map({ studClasses: Map(), instrClasses: Map() }); 50 | default: 51 | return state; 52 | } 53 | } 54 | 55 | export default classesReducer; 56 | -------------------------------------------------------------------------------- /src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import classesReducer from './classesReducer'; 3 | import outputReducer from './outputReducer'; 4 | import programsReducer from './programsReducer'; 5 | import uiReducer from './uiReducer'; 6 | import userDataReducer from './userDataReducer'; 7 | 8 | const appReducers = combineReducers({ 9 | userData: userDataReducer, 10 | output: outputReducer, 11 | programs: programsReducer, 12 | classes: classesReducer, 13 | ui: uiReducer, 14 | }); 15 | 16 | export default appReducers; 17 | -------------------------------------------------------------------------------- /src/reducers/outputReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | CLEAR_OUTPUT, 3 | SET_RUN_RESULT, 4 | SET_OUTPUT_LANGUAGE, 5 | SET_OUTPUT, 6 | } from '../actions/outputActions'; 7 | import { getLanguageData } from '../util/languages/languages'; 8 | 9 | const initialState = { 10 | runResult: '', 11 | language: getLanguageData('python'), 12 | }; 13 | 14 | function outputReducer(state = initialState, action) { 15 | switch (action.type) { 16 | case CLEAR_OUTPUT: 17 | return initialState; 18 | case SET_RUN_RESULT: 19 | return { ...state, runResult: action.value }; 20 | case SET_OUTPUT_LANGUAGE: 21 | return { ...state, language: action.value }; 22 | case SET_OUTPUT: 23 | return { ...state, runResult: action.runResult, language: action.language }; 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | export default outputReducer; 30 | -------------------------------------------------------------------------------- /src/reducers/programsReducer.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { 3 | SET_PROGRAM_CODE, 4 | SET_PROGRAM_LANGUAGE, 5 | SET_PROGRAM_NAME, 6 | SET_PROGRAM_THUMBNAIL, 7 | DELETE_PROGRAM, 8 | LOAD_PROGRAMS, 9 | CLEAR_PROGRAMS, 10 | SET_PROGRAM_DIRTY, 11 | ADD_PROGRAM, 12 | } from '../actions/programsActions'; 13 | 14 | const initialState = Immutable.Map(); 15 | 16 | function programsReducer(state = initialState, action) { 17 | switch (action.type) { 18 | case LOAD_PROGRAMS: 19 | return Immutable.fromJS(action.programs); 20 | case SET_PROGRAM_CODE: 21 | return state.setIn([action.program, 'code'], action.value); 22 | case SET_PROGRAM_LANGUAGE: 23 | return state.setIn([action.program, 'language'], action.value); 24 | case SET_PROGRAM_DIRTY: 25 | return state.setIn([action.program, 'dirty'], action.value); 26 | case SET_PROGRAM_NAME: 27 | return state.setIn([action.program, 'name'], action.value); 28 | case SET_PROGRAM_THUMBNAIL: 29 | return state.setIn([action.program, 'thumbnail'], action.value); 30 | case ADD_PROGRAM: 31 | return state.set(action.program, Immutable.fromJS(action.data)); 32 | case DELETE_PROGRAM: 33 | return state.delete(action.program); 34 | case CLEAR_PROGRAMS: 35 | return Immutable.Map(); 36 | default: 37 | return state; 38 | } 39 | } 40 | 41 | export default programsReducer; 42 | -------------------------------------------------------------------------------- /src/reducers/uiReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SCREEN_RESIZE, 3 | TOGGLE_PANEL, 4 | SET_PANEL, 5 | SET_THEME, 6 | SET_CLASSES_LOADED, 7 | SET_ON_INSTR_VIEW, 8 | } from '../actions/uiActions'; 9 | 10 | const initialState = { 11 | screenWidth: typeof window === 'object' ? window.innerWidth : null, 12 | screenHeight: typeof window === 'object' ? window.innerHeight : null, 13 | panelOpen: false, 14 | theme: 'dark', 15 | classesLoaded: false, 16 | onInstrView: false, 17 | }; 18 | 19 | function uiReducer(state = initialState, action) { 20 | switch (action.type) { 21 | case SCREEN_RESIZE: 22 | return { ...state, screenWidth: action.width, screenHeight: action.height }; 23 | case TOGGLE_PANEL: 24 | return { ...state, panelOpen: !state.panelOpen }; 25 | case SET_PANEL: 26 | return { ...state, panelOpen: action.value }; 27 | case SET_THEME: 28 | return { ...state, theme: action.theme }; 29 | case SET_CLASSES_LOADED: 30 | return { ...state, classesLoaded: action.value }; 31 | case SET_ON_INSTR_VIEW: 32 | return { ...state, onInstrView: action.value }; 33 | default: 34 | return state; 35 | } 36 | } 37 | 38 | export default uiReducer; 39 | -------------------------------------------------------------------------------- /src/reducers/userDataReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOAD_USER_DATA, 3 | CLEAR_USER_DATA, 4 | LOAD_FAILURE, 5 | SET_DISPLAY_NAME, 6 | SET_MOST_RECENT_PROGRAM, 7 | SET_PHOTO_NAME, 8 | SET_CURRENT_CLASS, 9 | } from '../actions/userDataActions'; 10 | 11 | const initialState = { 12 | error: '', 13 | displayName: '', 14 | uid: '', 15 | mostRecentProgram: '', 16 | photoName: '', 17 | currentClass: '', 18 | }; 19 | 20 | // eslint-disable-next-line default-param-last 21 | function userDataReducer(state = initialState, action) { 22 | switch (action.type) { 23 | case LOAD_USER_DATA: 24 | // pull all values we want to pay attention to out of the object 25 | return { ...state, ...action.userData }; 26 | case CLEAR_USER_DATA: 27 | return initialState; 28 | case LOAD_FAILURE: 29 | return { ...state, error: action.message }; 30 | case SET_DISPLAY_NAME: 31 | return { ...state, displayName: action.value }; 32 | case SET_PHOTO_NAME: 33 | return { ...state, photoName: action.photoName }; 34 | case SET_MOST_RECENT_PROGRAM: 35 | return { ...state, mostRecentProgram: action.value }; 36 | case SET_CURRENT_CLASS: 37 | return { ...state, currentClass: action.value }; 38 | default: 39 | return state; 40 | } 41 | } 42 | export default userDataReducer; 43 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // test-setup.js 2 | import * as enzyme from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | enzyme.configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { thunk } from 'redux-thunk'; 3 | import appReducers from './reducers'; 4 | 5 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 6 | const store = createStore(appReducers, composeEnhancers(applyMiddleware(thunk))); 7 | 8 | export default store; 9 | -------------------------------------------------------------------------------- /src/styles/ClassBox.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | 4 | .class-thumbnail { 5 | display: block; 6 | width: 80px; 7 | height: 80px; 8 | margin-left: 0px; 9 | } 10 | 11 | .class-box { 12 | display: flex; 13 | flex-direction: row; 14 | justify-content: flex-start; 15 | align-items: center; 16 | height: 9em; 17 | width: 95%; 18 | border-radius: 5px; 19 | margin: 10px; 20 | color: $theme-dark; 21 | cursor: pointer; 22 | flex: 0 0 auto; 23 | @include themify($themes) { 24 | color: themed("cardColor"); 25 | background-color: themed("cardBackground"); 26 | border: themed("cardBorder") 27 | } 28 | } 29 | 30 | .class-box.join-class { 31 | height: 5em; 32 | padding-left: 20px; 33 | padding-right: 20px; 34 | } 35 | 36 | .class-box-body { 37 | color: inherit; 38 | display: inherit; 39 | width: 90%; 40 | margin-left: 20px; 41 | margin-right: 20px; 42 | } 43 | 44 | .class-box-body:hover { 45 | color: inherit; 46 | text-decoration: none; 47 | } 48 | 49 | .class-info { 50 | margin-left: 20px; 51 | padding-top: 10px; 52 | } 53 | 54 | .instructor-name { 55 | margin-top: 15px; 56 | } 57 | 58 | .sketch-divider { 59 | width: 100%; 60 | margin: 0; 61 | padding: 0; 62 | @include themify($themes){ 63 | border-top: 1px solid themed("cardDivider"); 64 | } 65 | } 66 | 67 | .sketch-button-divider { 68 | width: 1px; 69 | @include themify($themes){ 70 | border: 0.5px solid themed("cardDivider"); 71 | } 72 | } 73 | 74 | .sketch-name { 75 | width: 85%; 76 | text-overflow: ellipsis; 77 | white-space: nowrap; 78 | overflow: hidden; 79 | } 80 | 81 | .sketch-icon { 82 | width: 85%; 83 | text-overflow: ellipsis; 84 | white-space: nowrap; 85 | overflow: hidden; 86 | } 87 | 88 | .sketch-metadata{ 89 | display: flex; 90 | width: 100%; 91 | flex-direction: row; 92 | justify-content: space-between; 93 | align-items: center; 94 | } 95 | -------------------------------------------------------------------------------- /src/styles/ClassPage.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .add-sketch-box { 4 | height: 153px; 5 | } 6 | 7 | .add-sketch-box-body { 8 | align-items: center; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | .add-sketch-plus { 14 | margin: 40px 0 19px; 15 | } 16 | 17 | .back-btn { 18 | margin-left: 10px; 19 | } 20 | 21 | .class-content-container { 22 | display: flex; 23 | flex-direction: column; 24 | justify-content: flex-start; 25 | align-items: center; 26 | margin: 20px 50px; 27 | overflow: auto; 28 | } 29 | 30 | .section-header { 31 | font-size: 1.5rem; 32 | margin-bottom: 35px; 33 | } 34 | 35 | .section-content { 36 | font-size: 1.25rem; 37 | } 38 | 39 | .class-info-box { 40 | width: 100%; 41 | margin: 5px 10px 25px; 42 | } 43 | 44 | .class-sketches-grid { 45 | display: flex; 46 | flex-direction: column; 47 | justify-content: flex-start; 48 | align-items: flex-start; 49 | width: 100%; 50 | overflow-y: auto; 51 | overflow-x: hidden; 52 | } 53 | 54 | .class-sketches-grid-row { 55 | display: flex; 56 | flex-direction: row; 57 | justify-content: flex-start; 58 | align-items: center; 59 | width: 100%; 60 | padding: 20px 0; 61 | } 62 | -------------------------------------------------------------------------------- /src/styles/Classes.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .classes-container { 4 | flex-direction: column; 5 | align-items: flex-start; 6 | @include themify($themes) { 7 | background-color: themed("backgroundColor"); 8 | color: themed("color"); 9 | } 10 | justify-content: flex-start; 11 | position: absolute; 12 | transition: all 0.5s ease; 13 | overflow: hidden; 14 | } 15 | 16 | .classes-content-container { 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: flex-start; 20 | align-items: center; 21 | height: 90%; 22 | width: 100%; 23 | overflow: auto; 24 | } 25 | 26 | .classes-grid { 27 | display: flex; 28 | flex-direction: column; 29 | justify-content: flex-start; 30 | align-items: center; 31 | height: 90%; 32 | width: 100%; 33 | overflow: auto; 34 | } 35 | 36 | // a copy of .sketches-header 37 | .classes-header { 38 | height: 67px; 39 | width: 100%; 40 | display: flex; 41 | flex-direction: row; 42 | justify-content: flex-start; 43 | align-items: center; 44 | @include themify($themes) { 45 | border-bottom: 1px solid themed("color"); 46 | } 47 | } 48 | 49 | .classes-header-text { 50 | font-weight: bold; 51 | padding: 1rem; 52 | cursor: pointer; 53 | flex: 0 1 auto; 54 | font-size: 45px; 55 | } 56 | 57 | .join-class-text { 58 | margin-left: 20px; 59 | } 60 | 61 | .join-class-input { 62 | font-size: 1.7em; 63 | line-height: normal; 64 | padding: 0.25rem 0.1rem 0px 0.1rem; 65 | border: 0px; 66 | @include themify($themes) { 67 | background-color: themed("backgroundColor"); 68 | color: themed("color"); 69 | } 70 | border-bottom: 2px solid #dddcdf; 71 | outline: 0; 72 | width: 50%; 73 | min-width: 300px; 74 | max-width: 500px; 75 | transition: all 0.5s ease; 76 | // color: #dddcdf; 77 | display: inline-block; 78 | } 79 | 80 | .join-class-button { 81 | display: inline-block; 82 | font-family: "Josefin Slab", sans-serif; 83 | // color: #dddcdf; 84 | color: white; 85 | // background-color: #857e8f; 86 | @include themify($themes) { 87 | background-color: themed("accentBackground"); 88 | } 89 | cursor: pointer; 90 | overflow: visible; 91 | text-align: center; 92 | border: 0; 93 | border-radius: 3px; 94 | padding: 8px 10px 8px 10px; 95 | margin-left: 1rem; 96 | font-size: 1.7em; 97 | line-height: 1.4em; 98 | transition: all 0.5s ease; 99 | } 100 | 101 | .join-class-button:hover { 102 | /* border: 2px solid white; 103 | border-radius: 10px; 104 | /* box-shadow: inset 0 0 0 100px rgba(0, 0, 0, 0.2); */ 105 | box-shadow: inset 0 0 0 1px white; 106 | } 107 | 108 | .join-class-button:disabled { 109 | opacity: 0.3; 110 | box-shadow: none; 111 | cursor: default; 112 | } 113 | 114 | .join-class-plus { 115 | display: block; 116 | width: 80px; 117 | margin-left: 0px; 118 | } 119 | 120 | .no-classes-container { 121 | margin-top: 3em; 122 | text-align: center; 123 | width: 100%; 124 | height: 100%; 125 | padding: 1em; 126 | @include themify($themes) { 127 | color: themed("color"); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/styles/Footer.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .footer { 4 | align-items: center; 5 | bottom: 0; 6 | height: 8vh; /* calc used to do arithmetic */ 7 | position: absolute; 8 | text-align: center; 9 | width: 100%; 10 | 11 | @include themify($themes) { 12 | background-color: themed('accentBackground'); 13 | } 14 | } 15 | 16 | .footer-image { 17 | height: 100%; 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/ImageSelector.scss: -------------------------------------------------------------------------------- 1 | .image-selector-modal { 2 | background-color: rgb(255, 255, 255); 3 | border-radius: 20px; 4 | left: 50%; 5 | min-height: 470px; 6 | padding: 40px; 7 | position: fixed; 8 | top: 50%; 9 | transform: translate(-50%, -50%); 10 | } 11 | 12 | .image-selector-modal-header { 13 | align-items: flex-start; 14 | display: flex; 15 | flex-direction: row; 16 | justify-content: space-between; 17 | text-align: left; 18 | } 19 | 20 | .image-selector-modal-header-thumbnail-container { 21 | flex: 0 0; 22 | height: 100px; 23 | width: 100px; 24 | } 25 | 26 | .image-selector-gallery { 27 | display: flex; 28 | flex-wrap: wrap; 29 | height: 250px; 30 | justify-content: space-evenly; 31 | margin-bottom: 10px; 32 | margin-left: 10px; 33 | overflow-y: auto; 34 | width: 100%; 35 | } 36 | -------------------------------------------------------------------------------- /src/styles/Loading.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .Loading { 4 | align-items: center; 5 | background-color: $theme-light; 6 | display: flex; 7 | flex-direction: column; 8 | height: 100%; 9 | justify-content: center; 10 | width: 100%; 11 | } 12 | 13 | .Loading-title { 14 | color: $off-white; 15 | font-family: $font-family-heading; 16 | font-size: 5rem; 17 | margin: 0 0 2rem; 18 | } 19 | 20 | .Loading-page-text { 21 | color: $off-white; 22 | margin-left: 2rem; 23 | margin-right: 2rem; 24 | margin-top: 3rem; 25 | } 26 | 27 | .Loading-link-text { 28 | color: $off-white; 29 | text-decoration: underline; 30 | } 31 | 32 | .Loading-link-text:hover { 33 | color: $teachla-primary; 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/Main.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .main { 4 | align-items: flex-start; 5 | background-color: $theme-dark; 6 | display: flex; 7 | flex-direction: row; 8 | justify-content: flex-start; 9 | position: relative; 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/Modals.scss: -------------------------------------------------------------------------------- 1 | .modal-sm { 2 | background-color: rgb(255, 255, 255); 3 | border-radius: 20px; 4 | left: 50%; 5 | min-width: 400px; 6 | padding: 40px; 7 | position: fixed; 8 | top: 50%; 9 | transform: translate(-50%, -50%); 10 | width: 50%; 11 | } 12 | 13 | .modal-md { 14 | background-color: rgb(255, 255, 255); 15 | border-radius: 20px; 16 | left: 50%; 17 | min-width: 450px; 18 | padding: 40px; 19 | position: fixed; 20 | top: 50%; 21 | transform: translate(-50%, -50%); 22 | width: 50%; 23 | } 24 | 25 | .modal-overlay { 26 | background-color: rgba(59, 59, 59, 0.514); 27 | bottom: 0; 28 | left: 0; 29 | position: fixed; 30 | right: 0; 31 | top: 0; 32 | z-index: 80; 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/Output.scss: -------------------------------------------------------------------------------- 1 | .editor-output-iframe div { 2 | box-sizing: border-box; 3 | } 4 | 5 | .editor-output-iframe { 6 | display: flex; 7 | flex-flow: column; 8 | padding: 10px; 9 | } 10 | 11 | #my-canvas { 12 | background-color: #c5c5c5; 13 | border: 1px solid black; 14 | height: 40%; 15 | margin: 10px 0; 16 | } 17 | 18 | #inner { 19 | background-color: #222; 20 | border: 0; 21 | color: #ddd; 22 | font-family: monospace; 23 | height: 40%; 24 | margin: 0 auto; 25 | overflow: auto; 26 | padding: 10px 35px 10px 10px; 27 | position: relative; 28 | resize: vertical; 29 | width: 100%; 30 | word-wrap: break-word; 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/Page.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .full-container { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | .page-container { 9 | align-items: center; 10 | display: flex; 11 | flex-direction: column; 12 | height: 100%; 13 | justify-content: center; 14 | 15 | @include themify($themes) { 16 | background-color: themed('backgroundColor'); 17 | color: themed('color'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/Radio.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .radio-selector { 4 | display: flex; 5 | font-size: 1rem; 6 | height: 60px; 7 | } 8 | 9 | .radio-option { 10 | background-color: $theme-light; 11 | color: #eee; 12 | cursor: pointer; 13 | max-width: 70px; 14 | padding: 8px; 15 | } 16 | 17 | .radio-option:hover { 18 | box-shadow: inset 0 0 0 50px rgba(0, 0, 0, 0.2); 19 | } 20 | 21 | .radio-option-selected { 22 | background-color: $radio-purple; 23 | border: none !important; 24 | color: #fff; 25 | cursor: pointer; 26 | padding: 8px; 27 | } 28 | 29 | .radio-option-selected:hover { 30 | box-shadow: inset 0 0 0 50px rgba(0, 0, 0, 0.2); 31 | } 32 | 33 | #radio-left { 34 | border-radius: 0.3rem 0 0 0.3rem; 35 | border-right: 2px solid rgba(255, 255, 255, 0.1); 36 | } 37 | 38 | #radio-right { 39 | border-left: 2px solid rgba(255, 255, 255, 0.1); 40 | border-radius: 0 0.3rem 0.3rem 0; 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/Resizer.scss: -------------------------------------------------------------------------------- 1 | // CSS from react-split-pane on GitHub: https://github.com/tomkp/react-split-pane#example-styling 2 | 3 | .Resizer { 4 | background: #000; 5 | -moz-background-clip: padding; 6 | -webkit-background-clip: padding; 7 | background-clip: padding-box; 8 | -moz-box-sizing: border-box; 9 | -webkit-box-sizing: border-box; 10 | box-sizing: border-box; 11 | opacity: 0.2; 12 | z-index: 1; 13 | } 14 | 15 | .Resizer:hover { 16 | transition: all 2s ease; 17 | } 18 | 19 | .Resizer.horizontal { 20 | border-bottom: 5px solid rgba(255, 255, 255, 0); 21 | border-top: 5px solid rgba(255, 255, 255, 0); 22 | cursor: w-resize; 23 | height: 11px; 24 | margin: -5px 0; 25 | width: 100%; 26 | } 27 | 28 | .Resizer.horizontal:hover { 29 | border-bottom: 5px solid rgba(0, 0, 0, 0.5); 30 | border-top: 5px solid rgba(0, 0, 0, 0.5); 31 | } 32 | 33 | .Resizer.vertical { 34 | border-left: 5px solid rgba(255, 255, 255, 0); 35 | border-right: 5px solid rgba(255, 255, 255, 0); 36 | cursor: w-resize; 37 | margin: 0 -5px; 38 | width: 11px; 39 | } 40 | 41 | .Resizer.vertical:hover { 42 | border-left: 5px solid rgba(0, 0, 0, 0.5); 43 | border-right: 5px solid rgba(0, 0, 0, 0.5); 44 | } 45 | 46 | .Resizer.disabled { 47 | cursor: not-allowed; 48 | } 49 | 50 | .Resizer.disabled:hover { 51 | border-color: transparent; 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/SketchBox.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .sketch-thumbnail { 4 | display: block; 5 | height: 80px; 6 | margin-left: auto; 7 | margin-right: auto; 8 | width: 80px; 9 | } 10 | 11 | .sketch-box { 12 | align-items: center; 13 | border-radius: 5px; 14 | color: $theme-dark; 15 | cursor: pointer; 16 | display: flex; 17 | flex: 0 0 auto; 18 | flex-direction: column; 19 | justify-content: flex-start; 20 | margin: 0 10px; 21 | width: 200px; 22 | 23 | @include themify($themes) { 24 | background-color: themed('cardBackground'); 25 | border: themed('cardBorder'); 26 | color: themed('cardColor'); 27 | } 28 | } 29 | 30 | .sketch-box-body { 31 | color: inherit; 32 | margin-left: auto; 33 | margin-right: auto; 34 | width: 90%; 35 | } 36 | 37 | .sketch-box-body:hover { 38 | color: inherit; 39 | text-decoration: none; 40 | } 41 | 42 | .sketch-divider { 43 | margin: 0; 44 | padding: 0; 45 | width: 100%; 46 | 47 | @include themify($themes) { 48 | border-top: 1px solid themed('cardDivider'); 49 | } 50 | } 51 | 52 | .sketch-button-divider { 53 | width: 1px; 54 | padding: 0; 55 | 56 | @include themify($themes) { 57 | border: 0.5px solid themed('cardDivider'); 58 | } 59 | } 60 | 61 | .sketch-name { 62 | overflow: hidden; 63 | text-overflow: ellipsis; 64 | white-space: nowrap; 65 | width: 85%; 66 | } 67 | 68 | .sketch-icon { 69 | overflow: hidden; 70 | text-overflow: ellipsis; 71 | white-space: nowrap; 72 | width: 85%; 73 | } 74 | 75 | .sketch-metadata { 76 | align-items: center; 77 | display: flex; 78 | flex-direction: row; 79 | justify-content: space-between; 80 | width: 100%; 81 | } 82 | -------------------------------------------------------------------------------- /src/styles/Sketches.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .sketches-container { 4 | align-items: flex-start; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | overflow: hidden; 8 | position: absolute; 9 | transition: all 0.5s ease; 10 | 11 | @include themify($themes) { 12 | background-color: themed('backgroundColor'); 13 | color: themed('color'); 14 | } 15 | } 16 | 17 | .sketches-header-text { 18 | cursor: pointer; 19 | flex: 0 1 auto; 20 | font-size: 45px; 21 | font-weight: bold; 22 | padding: 1rem; 23 | } 24 | 25 | .sketches-header { 26 | align-items: center; 27 | display: flex; 28 | flex-direction: row; 29 | height: 67px; 30 | justify-content: flex-start; 31 | width: 100%; 32 | 33 | @include themify($themes) { 34 | border-bottom: 1px solid themed('color'); 35 | } 36 | } 37 | 38 | .sketches-grid { 39 | align-items: flex-start; 40 | display: flex; 41 | flex-direction: column; 42 | justify-content: flex-start; 43 | overflow-x: hidden; 44 | overflow-y: auto; 45 | width: 100%; 46 | } 47 | 48 | .sketches-grid-row { 49 | align-items: center; 50 | display: flex; 51 | flex-direction: row; 52 | justify-content: flex-start; 53 | padding: 20px 50px; 54 | width: 100%; 55 | } 56 | 57 | .no-sketches-container { 58 | height: 100%; 59 | margin-top: 3em; 60 | padding: 1em; 61 | text-align: center; 62 | width: 100%; 63 | 64 | @include themify($themes) { 65 | color: themed('color'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/styles/SketchesModal.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .sketches-language-dropdown { 4 | color: #0d0d0e; 5 | flex: 0 1 auto; 6 | } 7 | 8 | .sketches-language-dropdown-closed-content { 9 | display: inline-block; 10 | } 11 | 12 | .sketches-gallery-img { 13 | border: 3px white solid; 14 | border-radius: 100%; 15 | height: 80px; 16 | object-fit: cover; 17 | padding: 2px; 18 | width: 80px; 19 | } 20 | 21 | .sketches-gallery-img-selected { 22 | border: 3px $theme-highlight solid; 23 | border-radius: 100%; 24 | height: 80px; 25 | object-fit: cover; 26 | padding: 2px; 27 | width: 80px; 28 | } 29 | 30 | .sketches-gallery-img:hover { 31 | border: 3px $theme-light solid; 32 | border-radius: 100%; 33 | height: 80px; 34 | object-fit: cover; 35 | padding: 2px; 36 | width: 80px; 37 | } 38 | 39 | .sketches-gallery::-webkit-scrollbar { 40 | width: 10px; 41 | } 42 | 43 | /* Track */ 44 | .sketches-gallery::-webkit-scrollbar-track { 45 | background: #f1f1f1; 46 | } 47 | 48 | /* Handle */ 49 | .sketches-gallery::-webkit-scrollbar-thumb { 50 | background: #37343d; 51 | } 52 | 53 | /* Handle on hover */ 54 | .sketches-gallery::-webkit-scrollbar-thumb:hover { 55 | background: #2f2d33; 56 | } 57 | 58 | .sketches-modal-header { 59 | align-items: flex-start; 60 | display: flex; 61 | flex-direction: row; 62 | justify-content: space-between; 63 | text-align: left; 64 | } 65 | 66 | .sketches-modal-header-thumbnail-container { 67 | flex: 0 0; 68 | height: 100px; 69 | width: 100px; 70 | } 71 | 72 | .sketches-modal-header-thumbnail { 73 | flex: 0 0; 74 | height: 100px; 75 | width: 100px; 76 | } 77 | 78 | .sketches-modal { 79 | background-color: rgb(255, 255, 255); 80 | border-radius: 20px; 81 | left: 50%; 82 | min-width: 400px; 83 | padding: 40px; 84 | position: fixed; 85 | top: 50%; 86 | transform: translate(-50%, -50%); 87 | width: 50%; 88 | } 89 | 90 | .sketches-form-spinner { 91 | align-items: center; 92 | display: flex; 93 | flex-direction: row; 94 | justify-content: center; 95 | width: 59.02px; 96 | } 97 | -------------------------------------------------------------------------------- /src/styles/Switch.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .switch { 4 | border-radius: 6px; 5 | box-sizing: content-box; 6 | cursor: pointer; 7 | display: block; 8 | font-family: inherit; 9 | height: 30px; 10 | padding: 0.5rem 1rem; 11 | position: relative; 12 | width: 100px; 13 | } 14 | 15 | .switch-input { 16 | box-sizing: content-box; 17 | left: 0; 18 | opacity: 0; 19 | padding: 0; 20 | position: absolute; 21 | top: 0; 22 | } 23 | 24 | .switch-body { 25 | align-items: center; 26 | background: $white; 27 | border-radius: inherit; 28 | display: flex; 29 | font-size: 1rem; 30 | height: inherit; 31 | justify-content: flex-end; 32 | padding: 10px; 33 | position: relative; 34 | transition: all $switch-transition-time; 35 | 36 | &.switch-on { 37 | background: $theme-dark; 38 | justify-content: flex-start !important; 39 | } 40 | } 41 | 42 | .switch-handle { 43 | background: $theme-dark; 44 | border-radius: 6px; 45 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2); 46 | height: 30px; 47 | position: absolute; 48 | top: 8px; 49 | transition: all $switch-transition-time; 50 | width: 30px; 51 | 52 | &.switch-on { 53 | background: white; 54 | margin-left: 70px; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | html, 4 | body, 5 | #root { 6 | /* without this the background color doesn't affect the whole window */ 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | body { 12 | box-sizing: border-box; 13 | font-family: $font-family-body; 14 | overflow: hidden; 15 | } 16 | 17 | h1, 18 | h2, 19 | h3, 20 | h4, 21 | h5, 22 | h6 { 23 | font-family: $font-family-heading; 24 | } 25 | 26 | * { 27 | /* needed for the modal's button to stay within the window */ 28 | box-sizing: border-box; 29 | } 30 | 31 | .force-no-wrap { 32 | white-space: nowrap; 33 | } 34 | 35 | .teachla-green { 36 | background-clip: text; 37 | //color: #92D32B; 38 | background-color: $teachla-primary; 39 | background-image: $teachla-gradient; 40 | background-size: 100%; 41 | -webkit-text-fill-color: transparent; 42 | -moz-text-fill-color: transparent; 43 | } 44 | 45 | .black-divider { 46 | border: 0; 47 | border-top: 1px solid $theme-dark; 48 | height: 1px; 49 | margin: 0; 50 | margin-top: 1em; 51 | } 52 | -------------------------------------------------------------------------------- /src/styles/triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uclaacm/TeachLAFrontend/fe30777a0ea6956a081985ad585eb507c02ac429/src/styles/triangle.png -------------------------------------------------------------------------------- /src/util/classes.js: -------------------------------------------------------------------------------- 1 | const getInstructorString = (instructors, userData, def = '') => { 2 | let instString = ''; 3 | if (!userData) return def; 4 | 5 | switch (instructors.length) { 6 | case 0: 7 | break; 8 | case 1: 9 | instString = (userData[instructors[0]] || {}).displayName; 10 | break; 11 | case 2: 12 | instString = instructors.map((id) => (userData[id] || {}).displayName).join(' and '); 13 | break; 14 | default: 15 | instString = instructors.map((id) => (userData[id] || {}).displayName).join(', '); 16 | } 17 | 18 | return instString; 19 | }; 20 | 21 | export { getInstructorString }; 22 | -------------------------------------------------------------------------------- /src/util/languages/CodeDownloader.js: -------------------------------------------------------------------------------- 1 | export default class CodeDownloader { 2 | static download = (name, language, code) => { 3 | const extension = `.${language.extension || 'txt'}`; 4 | 5 | // taken from this: https://stackoverflow.com/questions/44656610/download-a-string-as-txt-file-in-react 6 | const element = document.createElement('a'); 7 | const file = new Blob([language.renderDownload(code, true)], { type: 'text/plain' }); 8 | element.href = URL.createObjectURL(file); 9 | element.download = name + extension; 10 | document.body.appendChild(element); // Required for this to work in FireFox 11 | element.click(); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/util/languages/JsSourceDocLoggingScript.js: -------------------------------------------------------------------------------- 1 | export default function getJsSrcDocLoggingScript() { 2 | return ` 3 | 37 | `; 38 | } 39 | -------------------------------------------------------------------------------- /src/util/languages/Processing.js: -------------------------------------------------------------------------------- 1 | import getJsSrcDocLoggingScript from './JsSourceDocLoggingScript'; 2 | 3 | const getUserScript = (code) => ` 4 | 7 | `; 8 | 9 | const getProcessingSrcDocBody = (code, showConsole) => ` 10 | 11 | ${ 12 | showConsole 13 | ? '
' 14 | : '' 15 | } 16 | ${getJsSrcDocLoggingScript()} 17 | ${getUserScript(code)} 18 | 19 | `; 20 | 21 | const getProcessingSrcDocHead = () => ` 22 | 23 | 24 | 25 | 26 | 27 | 46 | 47 | `; 48 | 49 | export default function CreateProcessingDoc(code, showConsole) { 50 | return ` ${getProcessingSrcDocHead()} ${getProcessingSrcDocBody(code, showConsole)}`; 51 | } 52 | -------------------------------------------------------------------------------- /src/util/languages/Python.js: -------------------------------------------------------------------------------- 1 | import Sk from 'skulpt'; 2 | 3 | export default function CreatePythonDoc(prog) { 4 | function outf(text) { 5 | const mypre = document.getElementById('inner'); 6 | let received; 7 | if (text !== '\\n') { 8 | received = true; 9 | } else if (received) { 10 | received = false; 11 | } else { 12 | received = true; 13 | } 14 | if (received) { 15 | mypre.value += `> ${text}`; 16 | } 17 | if (mypre.scrollTop >= mypre.scrollHeight - mypre.offsetHeight - mypre.offsetHeight) { 18 | mypre.scrollTop = mypre.scrollHeight; 19 | } 20 | } 21 | 22 | function builtinRead(x) { 23 | if (Sk.builtinFiles === undefined || Sk.builtinFiles.files[x] === undefined) { 24 | throw Error(`File not found: '${x}'`); 25 | } 26 | return Sk.builtinFiles.files[x]; 27 | } 28 | Sk.pre = 'output'; 29 | Sk.configure({ output: outf, read: builtinRead, __future__: Sk.python3 }); 30 | (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = 'my-canvas'; 31 | Sk.misceval 32 | .asyncToPromise(() => Sk.importMainWithBody('', false, prog, true)) 33 | .catch((err) => { 34 | const b = document.getElementById('output'); 35 | if (b) { 36 | b.style.display = 'block'; 37 | } 38 | const a = document.getElementById('inner'); 39 | a.value += `\nERROR: ${err.toString()}`; 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/util/languages/React.js: -------------------------------------------------------------------------------- 1 | import getJsSrcDocLoggingScript from './JsSourceDocLoggingScript'; 2 | 3 | const getReactSrcDocHead = () => ` 4 | 5 | 6 | 7 | 8 | 9 | 10 | 35 | 36 | `; 37 | 38 | const getUserScript = (code) => ` 39 | 46 | `; 47 | 48 | const getReactSrcDocBody = (code, showConsole) => ` 49 | 50 | ${ 51 | showConsole 52 | ? '
' 53 | : '' 54 | } 55 | ${getJsSrcDocLoggingScript()} 56 | ${getUserScript(code)} 57 |
58 | 59 | `; 60 | 61 | export default function CreateReactDoc(code, showConsole) { 62 | return ` ${getReactSrcDocHead()} ${getReactSrcDocBody(code, showConsole)}`; 63 | } 64 | -------------------------------------------------------------------------------- /src/util/languages/languages.js: -------------------------------------------------------------------------------- 1 | import { faPython, faHtml5, faReact } from '@fortawesome/free-brands-svg-icons'; 2 | import { faCogs } from '@fortawesome/free-solid-svg-icons'; 3 | import CreateProcessingDoc from './Processing'; 4 | import CreatePythonDoc from './Python'; 5 | import CreateReactDoc from './React'; 6 | 7 | export const SUPPORTED_LANGUAGES = [ 8 | { 9 | value: 'python', 10 | display: 'Python', 11 | icon: faPython, 12 | codemirror: 'python', 13 | extension: 'py', 14 | render: CreatePythonDoc, 15 | }, 16 | // { 17 | // value: "javascript", 18 | // display: "JavaScript", 19 | // icon: faJs, 20 | // codemirror: "javascript", 21 | // extension: "js", 22 | // }, 23 | { 24 | value: 'html', 25 | display: 'HTML', 26 | icon: faHtml5, 27 | codemirror: 'htmlmixed', 28 | extension: 'html', 29 | }, 30 | { 31 | value: 'processing', 32 | display: 'Processing', 33 | icon: faCogs, 34 | codemirror: 'javascript', 35 | extension: 'html', 36 | render: CreateProcessingDoc, 37 | renderDownload: CreateProcessingDoc, 38 | }, 39 | { 40 | value: 'react', 41 | display: 'React.JS', 42 | icon: faReact, 43 | codemirror: 'jsx', 44 | extension: 'html', 45 | render: CreateReactDoc, 46 | renderDownload: CreateReactDoc, 47 | }, 48 | ]; 49 | 50 | const DEFAULT_LANGUAGE = { 51 | identifier: 'txt', 52 | display: 'Text', 53 | codemirror: undefined, 54 | extension: 'txt', 55 | render: (code) => code, 56 | renderDownload: (code) => code, 57 | }; 58 | 59 | export const getLanguageData = (lang) => ({ 60 | ...DEFAULT_LANGUAGE, 61 | ...SUPPORTED_LANGUAGES.find((data) => data.value === lang), 62 | }); 63 | 64 | export const enrichWithLanguageData = (arr) => arr.map((sketch) => ({ 65 | ...sketch, 66 | language: getLanguageData(sketch.language), 67 | })); 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "types": ["vite/client"], 5 | "jsx": "react-jsx", 6 | "module": "esnext", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "lib": [ 12 | "dom", 13 | "dom.iterable", 14 | "esnext" 15 | ], 16 | "allowJs": true, 17 | "allowSyntheticDefaultImports": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": true 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig(() => { 5 | return { 6 | build: { 7 | outDir: 'build', 8 | }, 9 | plugins: [react()], 10 | }; 11 | }); 12 | --------------------------------------------------------------------------------