├── .npmrc ├── README.md ├── .gitignore ├── src ├── types.d.ts ├── netlify.toml └── client │ ├── util │ ├── sleep.ts │ ├── metaStarter.ts │ ├── prettyLog.ts │ ├── chooseGrid.ts │ └── algoChoices.ts │ ├── index.html │ ├── components │ ├── Button.tsx │ ├── varTracking │ │ ├── VarContainer.tsx │ │ ├── LogContainer.tsx │ │ └── VarCard.tsx │ ├── Row.tsx │ ├── Cell.tsx │ ├── ControlButtons.tsx │ ├── CodeSpace.tsx │ ├── PanelOptions.tsx │ └── App.tsx │ ├── index.tsx │ └── styles │ └── index.scss ├── .babelrc ├── nginx.conf ├── Dockerrun.aws.json ├── tsconfig.json ├── .prettierrc ├── Dockerfile ├── .github └── workflows │ └── deploy.yaml ├── scripts └── deploy.sh ├── package.json └── webpack.config.js /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graph-traverse 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | package-lock.json 4 | z_* -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Object { 2 | [index: string]: any; 3 | } 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } -------------------------------------------------------------------------------- /src/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "build/" 4 | functions = "functions/" -------------------------------------------------------------------------------- /src/client/util/sleep.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (ms: number): Promise => { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | }; 4 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html; 8 | try_files $uri $uri/ /index.html; 9 | } 10 | } -------------------------------------------------------------------------------- /src/client/util/metaStarter.ts: -------------------------------------------------------------------------------- 1 | export const metaStarter = (): any => { 2 | return { 3 | current: ``, 4 | potential: ``, 5 | visited: new Set(), 6 | discovered: new Set(), 7 | varObj: {}, 8 | logArr: [], 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /Dockerrun.aws.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSEBDockerrunVersion": "1", 3 | "Image": { 4 | "Name": "888780614544.dkr.ecr.us-east-2.amazonaws.com/graphsy:", 5 | "Update": "true" 6 | }, 7 | "Ports": [ 8 | { 9 | "ContainerPort": "80" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "es6", 5 | "jsx": "react", 6 | "esModuleInterop": true, 7 | "noImplicitAny": false, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "lib": ["ES2021", "DOM"] 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules", "**/*.spec.ts", "dist"] 14 | } 15 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Graphsy 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/client/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import '../styles/index.scss'; 4 | 5 | interface IbuttonProps { 6 | fullGrid: any; 7 | runAlgo: any; 8 | } 9 | 10 | const Button = (props: IbuttonProps) => { 11 | return ( 12 | 15 | ); 16 | }; 17 | export default Button; 18 | -------------------------------------------------------------------------------- /src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { HashRouter } from 'react-router-dom'; 4 | import App from './components/App'; 5 | 6 | const root = createRoot(document.getElementById('root')!); 7 | 8 | console.log('CICD Worked!'); 9 | 10 | root.render( 11 | // 12 | 13 | 14 | 15 | // 16 | ); 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxBracketSameLine": false, 8 | "jsxSingleQuote": true, 9 | "printWidth": 80, 10 | "proseWrap": "preserve", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": true, 14 | "singleQuote": true, 15 | "tabWidth": 2, 16 | "trailingComma": "es5", 17 | "useTabs": false 18 | } 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | FROM node:16.13 AS frontend 3 | WORKDIR /usr/src/app 4 | COPY . /usr/src/app 5 | RUN npm install 6 | RUN npm run build 7 | # delete everything except dist folder 8 | # RUN rm -rf !("dist") 9 | # move dist, delete everything, then copy it back in 10 | # RUN mv dist ../dist 11 | # RUN rm -rf ./* 12 | # RUN mv ../dist dist 13 | 14 | ### Nginx ### 15 | FROM nginx:alpine 16 | # Remove the default Nginx configuration 17 | RUN rm -rf /etc/nginx/conf.d/* 18 | # Copy the custom Nginx configuration 19 | COPY nginx.conf /etc/nginx/conf.d/ 20 | # Copy dist from build 21 | COPY --from=frontend /usr/src/app/dist /usr/share/nginx/html 22 | # Expose port 80 23 | EXPOSE 80 24 | # Start Nginx when the container starts 25 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /src/client/components/varTracking/VarContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import VarCard from './VarCard'; 3 | 4 | interface Card { 5 | keyName: string; 6 | value: any; 7 | type: string; 8 | } 9 | interface IvarContainerProps { 10 | [keyName: string]: Card; 11 | } 12 | 13 | const VarContainer = ({ vars }: IvarContainerProps) => { 14 | // loop through vars, creating cards 15 | const varsArr = []; 16 | for (const keyName in vars) { 17 | const value = vars[keyName]; 18 | varsArr.push( 19 | 20 | ); 21 | } 22 | 23 | // display cards in return 24 | 25 | return
{varsArr}
; 26 | }; 27 | export default VarContainer; 28 | -------------------------------------------------------------------------------- /src/client/components/Row.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Cell from './Cell'; 3 | 4 | import '../styles/index.scss'; 5 | 6 | interface IrowProps { 7 | size: number; 8 | row: number; 9 | cells: any; 10 | meta: any; 11 | updateGrid: any; 12 | } 13 | 14 | const Row = (props: IrowProps) => { 15 | const cells = props.cells.map((cell: any, i: number) => { 16 | return ( 17 | 25 | ); 26 | }); 27 | 28 | return ( 29 |
33 | {cells} 34 |
35 | ); 36 | }; 37 | export default Row; 38 | -------------------------------------------------------------------------------- /src/client/components/varTracking/LogContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | interface iLogContainerProps { 4 | logs: [string, any][]; 5 | [i: number]: number; 6 | } 7 | 8 | const LogContainer = ({ logs }: iLogContainerProps) => { 9 | // loop through logs, creating cards 10 | const logsArr = []; 11 | for (let i = logs.length - 1; i >= 0; i--) { 12 | const log = logs[i]; 13 | logsArr.push( 14 | log.length > 1 ? ( 15 |
16 | 17 |

{JSON.stringify(log[1])}

18 |
19 | ) : ( 20 |
21 |

{log[0]}

22 |
23 | ) 24 | ); 25 | } 26 | 27 | // display cards in return 28 | 29 | return
{logsArr}
; 30 | }; 31 | export default LogContainer; 32 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 10 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 11 | GITHUB_SHA: ${{ github.sha }} 12 | 13 | jobs: 14 | deploy: 15 | name: Deploy 16 | runs-on: ubuntu-latest 17 | environment: production 18 | 19 | steps: 20 | - name: Checkout Repo 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Python 24 | uses: actions/setup-python@v4 25 | with: 26 | version: '3.x' 27 | 28 | - name: Run Py Script 29 | run: | 30 | python3 -m pip install --upgrade pip 31 | python3 -m pip install --user awscli 32 | python3 -m pip install --user awsebcli 33 | 34 | - name: Run Deploy Script 35 | run: sh ./scripts/deploy.sh 36 | -------------------------------------------------------------------------------- /src/client/components/Cell.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import '../styles/index.scss'; 4 | 5 | interface IcellProps { 6 | row: number; 7 | cell: number; 8 | value: any; 9 | meta: any; 10 | updateGrid: any; 11 | } 12 | 13 | const Cell = (props: IcellProps) => { 14 | let status = ''; 15 | if (props.value === 'C') status += ' carrot'; 16 | else if (props.meta.current === `${props.row}.${props.cell}`) 17 | status += ' active'; 18 | else if (props.meta.potential === `${props.row}.${props.cell}`) 19 | status += ' potential'; 20 | 21 | if (props.value === 'W') status += ' wall'; 22 | else if (props.meta.discovered.has(`${props.row}.${props.cell}`)) 23 | status += ' discovered'; 24 | else if (props.meta.visited.has(`${props.row}.${props.cell}`)) 25 | status += ' visited'; 26 | else ''; 27 | 28 | return ( 29 |
30 | { 34 | props.updateGrid(e, props.row, props.cell); 35 | }} 36 | value={props.value} 37 | /> 38 |
{`r${props.row}, c${props.cell}`}
39 |
40 | ); 41 | }; 42 | export default Cell; 43 | -------------------------------------------------------------------------------- /src/client/components/ControlButtons.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler } from 'react'; 2 | 3 | interface IbuttonProps { 4 | algoRunning: boolean; 5 | pausedState: boolean; 6 | handleStart: MouseEventHandler; 7 | handleAbort: MouseEventHandler; 8 | handlePause: MouseEventHandler; 9 | } 10 | 11 | const ControlButtons = ({ 12 | algoRunning, 13 | pausedState, 14 | handleStart, 15 | handleAbort, 16 | handlePause, 17 | }: IbuttonProps) => { 18 | return !algoRunning ? ( 19 |
20 | 27 |
28 | ) : ( 29 |
30 | 37 | 44 |
45 | ); 46 | }; 47 | export default ControlButtons; 48 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | echo "Processing deploy.sh" 2 | # Set EB BUCKET as env variable 3 | EB_BUCKET=elasticbeanstalk-us-east-2-888780614544 4 | # Set the default region for aws cli 5 | aws configure set default.region us-east-2 6 | # Log in to ECR 7 | eval $(aws ecr get-login --no-include-email --region us-east-2) 8 | # Build docker image based on our production Dockerfile 9 | docker build -t 888780614544/graphsy . 10 | # tag the image with the GitHub SHA 11 | docker tag 888780614544/graphsy:latest 888780614544.dkr.ecr.us-east-2.amazonaws.com/graphsy:$GITHUB_SHA 12 | # Push built image to ECS 13 | docker push 888780614544.dkr.ecr.us-east-2.amazonaws.com/graphsy:$GITHUB_SHA 14 | # Use the linux sed command to replace the text '' in our Dockerrun file with the GitHub SHA key 15 | sed -i='' "s//$GITHUB_SHA/" Dockerrun.aws.json 16 | # Zip up our codebase, along with modified Dockerrun and our .ebextensions directory 17 | zip -r graphsy-prod-deploy.zip Dockerrun.aws.json .ebextensions 18 | # Upload zip file to s3 bucket 19 | aws s3 cp graphsy-prod-deploy.zip s3://$EB_BUCKET/graphsy-prod-deploy.zip 20 | # Create a new application version with new Dockerrun 21 | aws elasticbeanstalk create-application-version --application-name g5 --version-label $GITHUB_SHA --source-bundle S3Bucket=$EB_BUCKET,S3Key=graphsy-prod-deploy.zip 22 | # Update environment to use new version number 23 | aws elasticbeanstalk update-environment --environment-name G5-env --version-label $GITHUB_SHA -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graph-traverse", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/client/index.tsx", 6 | "scripts": { 7 | "tsnode": "node --loader ts-node/esm --no-warnings", 8 | "build": "NODE_ENV=production webpack", 9 | "dev": "NODE_ENV=development webpack-dev-server" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/kmccracko/graphsy" 14 | }, 15 | "author": "Kyle McCracken", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/kmccracko/graphsy/issues" 19 | }, 20 | "homepage": "https://github.com/kmccracko/graphsy#readme", 21 | "devDependencies": { 22 | "@babel/core": "^7.16.7", 23 | "@babel/preset-env": "^7.16.7", 24 | "@babel/preset-react": "^7.16.7", 25 | "@types/express": "^4.17.14", 26 | "@types/node": "^17.0.45", 27 | "@types/react": "^18.0.18", 28 | "@types/react-dom": "^18.0.6", 29 | "@types/react-infinite-scroller": "^1.2.3", 30 | "@types/webpack-env": "^1.18.0", 31 | "babel-loader": "^8.2.3", 32 | "copy-webpack-plugin": "^10.2.0", 33 | "css-loader": "^6.7.1", 34 | "file-loader": "^6.2.0", 35 | "html-webpack-plugin": "^5.5.0", 36 | "mini-css-extract-plugin": "^2.6.1", 37 | "node-sass": "^7.0.3", 38 | "nodemon": "^2.0.19", 39 | "sass-loader": "^13.0.2", 40 | "style-loader": "^3.3.1", 41 | "ts-loader": "^9.3.1", 42 | "typescript": "^4.7.4", 43 | "webpack": "^5.65.0", 44 | "webpack-bundle-analyzer": "^4.6.1", 45 | "webpack-cli": "^4.9.1", 46 | "webpack-dev-server": "^4.7.2" 47 | }, 48 | "dependencies": { 49 | "ace-builds": "^1.13.1", 50 | "axios": "^0.27.2", 51 | "cors": "^2.8.5", 52 | "dotenv-webpack": "^8.0.1", 53 | "express": "^4.17.2", 54 | "node-cache": "^5.1.2", 55 | "pg": "^8.8.0", 56 | "react": "^18.3.0-next-f0efa1164-20220901", 57 | "react-ace": "^10.1.0", 58 | "react-client": "^1.0.1", 59 | "react-dom": "^18.3.0-next-f0efa1164-20220901", 60 | "react-router": "^6.3.0", 61 | "react-router-dom": "^6.3.0", 62 | "sass": "^1.54.9", 63 | "ts-node": "^10.9.1" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const Dotenv = require('dotenv-webpack'); // required for accessing .env from front-end. used in plugins. 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const BundleAnalyzerPlugin = 5 | require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 6 | 7 | module.exports = { 8 | mode: process.env.NODE_ENV, 9 | entry: ['./src/client/index.tsx'], 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | publicPath: '/', 13 | filename: 'bundle.js', 14 | }, 15 | devtool: 'eval-source-map', 16 | resolve: { 17 | extensions: ['.jsx', '.js', '.ts', '.tsx'], 18 | }, 19 | // devtool: 'inline-source-map', 20 | devServer: { 21 | static: { 22 | publicPath: '/', 23 | directory: path.resolve(__dirname, 'dist'), 24 | }, 25 | port: 9090, 26 | historyApiFallback: true, 27 | headers: { 'Access-Control-Allow-Origin': '*' }, 28 | // compress: true, 29 | hot: true, 30 | proxy: { 31 | '*': { 32 | target: 'http://localhost:3003', 33 | secure: false, 34 | // changeOrigin: true, 35 | }, 36 | }, 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /\.(s(a|c)ss)$/, 42 | use: ['style-loader', 'css-loader', 'sass-loader'], 43 | }, 44 | { 45 | test: /\.(js|jsx|ts|tsx)$/, 46 | exclude: /node_modules/, 47 | use: ['babel-loader', 'ts-loader'], 48 | // options: { 49 | // presets: ['@babel/preset-env', '@babel/preset-react'], 50 | // }, 51 | }, 52 | { 53 | test: /\.(png|svg|jpeg|jpg|jpeg|gif)$/, 54 | use: [ 55 | { 56 | // loads files as base64 encoded data url if image file is less than set limit 57 | loader: 'file-loader', 58 | options: { 59 | name: '[name].[ext]', 60 | }, 61 | }, 62 | ], 63 | }, 64 | ], 65 | }, 66 | plugins: [ 67 | new HtmlWebpackPlugin({ 68 | template: './src/client/index.html', 69 | }), 70 | // new Dotenv({ systemvars: true }), 71 | // new BundleAnalyzerPlugin(), 72 | ], 73 | }; 74 | -------------------------------------------------------------------------------- /src/client/util/prettyLog.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Console logs a label. If provided, any additional values will be logged out unless blank 3 | * @param {string} label The heading of your log. Typically a variable name. ex: "Count" | "Users Object" 4 | * @param {string} type Informs the formatting of the log. 5 | * * header 6 | * * label 7 | * * warn 8 | * * instructions 9 | * * spacesmall 10 | * * spacebig 11 | * @param {...any} values Any extra values will be console logged individually. 12 | */ 13 | const prettyLog = (label = '', type: any = undefined, ...values: any) => { 14 | let style = `font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;`; 15 | switch (type) { 16 | case 'header': 17 | style += ` 18 | background-image: linear-gradient(43deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%); 19 | background-color: #245047; 20 | padding: 20px; 21 | color: black; 22 | border: 7px solid black; 23 | border-radius: 20px; 24 | font-size: x-large; 25 | font-weight: bold;`; 26 | break; 27 | case 'label': 28 | style += ` 29 | color: white; 30 | font-size: larger; 31 | background-color: #245047; 32 | padding: 3px 15px; 33 | border-radius: 7px; 34 | `; 35 | break; 36 | case 'warn': 37 | style += ` 38 | color: white; 39 | background-color: darkred; 40 | padding: 10px; 41 | font-weight: bold; 42 | `; 43 | break; 44 | case 'instructions': 45 | style += ` 46 | font-size: small; 47 | padding: 10px; 48 | border-width: 2px; 49 | border-style: solid; 50 | `; 51 | break; 52 | case 'spacebig': 53 | style += ` 54 | padding: 0px 100%; 55 | margin: 20px 0px; 56 | color: rgb(0,0,0,0); 57 | background-image: linear-gradient(43deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%);`; 58 | break; 59 | default: 60 | style = ''; 61 | break; 62 | } 63 | console.log('%c' + label, style); 64 | for (let el of values) if (el !== '') console.log(el); 65 | }; 66 | 67 | export { prettyLog }; 68 | -------------------------------------------------------------------------------- /src/client/components/varTracking/VarCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | interface IvarCardProps { 4 | keyName: string; 5 | value: any; 6 | type?: string; 7 | } 8 | 9 | const VarCard = ({ keyName, value, type }: IvarCardProps) => { 10 | // Represent the data 11 | let valueRepresentation: any = 'NO MATCH'; 12 | // Number 13 | if (typeof value === 'number') { 14 | valueRepresentation = {value}; 15 | } 16 | // String 17 | else if (typeof value === 'string') { 18 | valueRepresentation = `${'"' + value + '"'}`; 19 | } 20 | // Boolean 21 | else if (typeof value == 'boolean') { 22 | valueRepresentation = value; 23 | } 24 | // Array 25 | else if (Array.isArray(value) || value.has) { 26 | const elArr = []; 27 | let first = true; 28 | elArr.push({'['}); 29 | for (const el of value) { 30 | let content; 31 | if (!first) { 32 | content = `, ${JSON.stringify(el)}`; 33 | } else { 34 | content = `${JSON.stringify(el)}`; 35 | first = false; 36 | } 37 | elArr.push( 38 | 39 | {content} 40 | 41 | ); 42 | } 43 | elArr.push({']'}); 44 | valueRepresentation = elArr; 45 | } 46 | // Object 47 | else { 48 | const elArr = []; 49 | let first = true; 50 | elArr.push({'{'}); 51 | elArr.push(
); 52 | for (const el in value) { 53 | if (!first) { 54 | elArr.push({`, `}); 55 | elArr.push(
); 56 | } else { 57 | first = false; 58 | } 59 | elArr.push( 60 | 61 | {`${JSON.stringify(el)} : ${JSON.stringify(value[el])}`} 62 | 63 | ); 64 | } 65 | elArr.push(
); 66 | elArr.push({'}'}); 67 | valueRepresentation = elArr; 68 | } 69 | 70 | return ( 71 |
72 |
{keyName}
73 |
{valueRepresentation}
74 |
75 | ); 76 | }; 77 | export default VarCard; 78 | -------------------------------------------------------------------------------- /src/client/components/CodeSpace.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ace from 'ace-builds/src-noconflict/ace'; 3 | import AceEditor from 'react-ace'; 4 | 5 | // import 'ace-builds/webpack-resolver'; 6 | import 'ace-builds/src-noconflict/mode-javascript'; 7 | import 'ace-builds/src-noconflict/theme-tomorrow_night'; 8 | import 'ace-builds/src-noconflict/ext-language_tools'; 9 | import { addCompleter } from 'ace-builds/src-noconflict/ext-language_tools'; 10 | const snippetManager = ace.require('ace/snippets').snippetManager; 11 | 12 | const addCompleters = (...arr) => { 13 | for (let el of arr) { 14 | let { name, value, caption, meta } = el; 15 | addCompleter({ 16 | getCompletions: function (editor, session, pos, prefix, callback) { 17 | callback(null, [ 18 | { 19 | name, 20 | value, 21 | caption, 22 | meta, 23 | score: 1000, 24 | }, 25 | ]); 26 | }, 27 | }); 28 | } 29 | }; 30 | 31 | snippetManager.register( 32 | [ 33 | { 34 | name: 'updateScreen', 35 | scope: 'javascript', 36 | tabTrigger: 'updateScreen', 37 | trigger: 'updateScreen', 38 | content: 'await updateScreen({${1:key}:`${${2:r}}.${${3:c}}`})', 39 | }, 40 | ], 41 | 'javascript' 42 | ); 43 | 44 | addCompleters( 45 | { 46 | name: 'discovered', 47 | value: 'discovered', 48 | caption: 'discovered', 49 | meta: 'green fill (set)', 50 | score: 1000, 51 | }, 52 | { 53 | name: 'visited', 54 | value: 'visited', 55 | caption: 'visited', 56 | meta: 'gray fill (set)', 57 | score: 1000, 58 | }, 59 | { 60 | name: 'potential', 61 | value: 'potential', 62 | caption: 'potential', 63 | meta: 'yellow outline (str)', 64 | score: 1000, 65 | }, 66 | { 67 | name: 'current', 68 | value: 'current', 69 | caption: 'current', 70 | meta: 'red outline (str)', 71 | score: 1000, 72 | } 73 | ); 74 | 75 | interface iCodeEditorProps { 76 | code: string; 77 | onChangeCode: any; 78 | } 79 | 80 | const CodeEditor = (props: iCodeEditorProps) => { 81 | return ( 82 |
83 | 110 |
111 | ); 112 | }; 113 | 114 | export default CodeEditor; 115 | -------------------------------------------------------------------------------- /src/client/util/chooseGrid.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param gridName The name of the grid 4 | * @returns A 2D grid 5 | */ 6 | function chooseGrid(gridName: string): any[][] { 7 | switch (gridName) { 8 | case 'anchor': 9 | return [ 10 | [0, 0, 1, 0, 0], 11 | [0, 0, 1, 0, 0], 12 | [1, 0, 1, 0, 1], 13 | [1, 1, 1, 1, 1], 14 | ]; 15 | case 'grid1': 16 | return [ 17 | [1, 1, 0, 0, 1], 18 | [1, 1, 0, 0, 0], 19 | [0, 0, 1, 0, 0], 20 | [1, 0, 0, 1, 1], 21 | ]; 22 | case 'bigEmpty': 23 | return [ 24 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 25 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 26 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 27 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 28 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 29 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 30 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 31 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 32 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 33 | ]; 34 | case 'maze': 35 | return [ 36 | ['', 'W', '', '', '', '', '', '', '', ''], 37 | ['', 'W', '', 'W', '', 'W', 'W', 'W', 'W', ''], 38 | ['', '', '', 'W', '', 'W', '', '', '', ''], 39 | ['', 'W', '', '', 'W', '', '', 'W', '', 'W'], 40 | ['', 'W', 'W', 'W', '', 'W', '', 'W', 'W', 'W'], 41 | ['', '', '', '', '', 'W', '', '', '', ''], 42 | ]; 43 | case 'bigmaze': 44 | return [ 45 | [' ',' ',' ',' ',' ',' ',' ','W',' ',' ',' ',' ',' ',' ',' ','W',' ',' ',' ',' ',' ',' ',' ',' ',' ','W',' '], 46 | ['W',' ',' ','W',' ','W',' ','W','W',' ','W','W','W',' ','W','W','W',' ','W','W','W','W','W','W',' ','W',' '], 47 | [' ',' ','W','W',' ','W',' ',' ','W',' ',' ',' ','W',' ','W',' ',' ',' ','W',' ',' ',' ',' ','W',' ','W',' '], 48 | [' ','W',' ','W',' ','W',' ',' ',' ','W',' ',' ','W',' ','W',' ','W','W','W',' ',' ','W',' ','W',' ',' ',' '], 49 | [' ','W',' ',' ',' ',' ',' ','W','W','W',' ',' ','W',' ',' ',' ',' ',' ',' ',' ','W',' ',' ','W',' ','W','W'], 50 | [' ','W',' ','W','W',' ','W',' ',' ',' ',' ','W','W','W','W','W',' ','W','W','W','W',' ',' ',' ','W','W',' '], 51 | [' ','W',' ','W',' ',' ','W',' ',' ','W',' ',' ',' ',' ',' ','W',' ',' ',' ',' ',' ','W',' ',' ',' ',' ',' '], 52 | [' ','W',' ','W',' ',' ','W','W','W',' ',' ','W',' ','W','W','W','W',' ','W','W',' ',' ','W','W','W',' ',' '], 53 | [' ',' ',' ','W',' ','W',' ',' ',' ',' ',' ','W',' ',' ',' ',' ',' ',' ',' ',' ','W',' ',' ',' ',' ',' ','W'], 54 | [' ','W','W','W','W',' ',' ','W',' ',' ',' ','W','W','W','W',' ','W','W','W',' ',' ','W','W','W',' ','W',' '], 55 | [' ','W',' ',' ','W',' ',' ','W',' ',' ',' ','W',' ',' ',' ',' ',' ',' ',' ','W','W',' ',' ',' ','W',' ',' '], 56 | [' ','W',' ',' ','W',' ',' ','W',' ','W','W',' ',' ','W','W','W','W','W',' ',' ','W',' ','W',' ','W',' ',' '], 57 | [' ','W',' ','W','W',' ',' ','W',' ',' ',' ',' ',' ','W',' ',' ',' ',' ','W',' ',' ',' ','W',' ',' ','W',' '], 58 | [' ','W',' ','W',' ',' ',' ','W','W','W',' ',' ','W',' ',' ','W','W',' ',' ','W',' ',' ',' ','W',' ',' ',' '], 59 | [' ','W',' ',' ',' ','W','W',' ',' ',' ',' ','W',' ',' ','W','E',' ','W',' ',' ','W','W',' ',' ','W','W','W'], 60 | [' ','W',' ','W',' ',' ','W',' ','W','W',' ',' ','W',' ',' ','W',' ',' ',' ','W','W',' ','W',' ',' ',' ',' '], 61 | [' ','W',' ','W',' ',' ','W',' ','W',' ',' ',' ',' ','W',' ','W','W','W','W',' ',' ',' ',' ','W','W','W',' '], 62 | [' ','W',' ','W',' ',' ','W',' ','W',' ','W','W','W','W',' ',' ',' ',' ',' ',' ','W','W',' ','W',' ','W',' '], 63 | [' ',' ',' ','W',' ',' ',' ',' ','W',' ',' ',' ',' ',' ',' ','W',' ','W',' ','W',' ',' ',' ',' ',' ',' ',' '], 64 | ] 65 | case 'partitions': 66 | return [ 67 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 68 | [0, 'W', 'W', 'W', 'W', 'W', 'W', 'W', 0], 69 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 70 | [0, 'W', 'W', 'W', 'W', 'W', 'W', 'W', 0], 71 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 72 | [0, 'W', 'W', 'W', 'W', 'W', 'W', 'W', 0], 73 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 74 | [0, 'W', 'W', 'W', 'W', 'W', 'W', 'W', 0], 75 | [0, 0, 0, 0, 0, 0, 0, 0, 0], 76 | ]; 77 | } 78 | } 79 | 80 | export default chooseGrid; 81 | -------------------------------------------------------------------------------- /src/client/components/PanelOptions.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler, ChangeEventHandler } from 'react'; 2 | 3 | import CodeEditor from './CodeSpace'; 4 | 5 | interface IbuttonProps { 6 | delay: Number; 7 | startPoint: Number[]; 8 | endPoint: Number[]; 9 | gridChoice: string; 10 | algoChoice: string; 11 | algoChoices: any; 12 | algoDesc: string; 13 | customAlgoString: string; 14 | customGridString: string; 15 | currentGrid: any[][]; 16 | defaultFill: string; 17 | handleDelayChange: ChangeEventHandler; 18 | handlePointChange: Function; 19 | handleGridSelect: ChangeEventHandler; 20 | handleAlgoSelect: ChangeEventHandler; 21 | handleCustomFuncChange: Function; 22 | handleCustomGridChange: Function; 23 | handleCreateGrid: MouseEventHandler; 24 | handleGridResize: Function; 25 | handleDefaultFillChange: ChangeEventHandler; 26 | } 27 | 28 | const PanelOptions = ({ 29 | delay, 30 | startPoint, 31 | endPoint, 32 | gridChoice, 33 | algoChoice, 34 | algoChoices, 35 | algoDesc, 36 | customAlgoString, 37 | customGridString, 38 | currentGrid, 39 | defaultFill, 40 | handleDelayChange, 41 | handlePointChange, 42 | handleGridSelect, 43 | handleAlgoSelect, 44 | handleCustomFuncChange, 45 | handleCustomGridChange, 46 | handleCreateGrid, 47 | handleGridResize, 48 | handleDefaultFillChange, 49 | }: IbuttonProps) => { 50 | return ( 51 |
52 | 53 | 63 | 64 |
65 | 66 |
67 | 68 | handlePointChange(0, e.target.value)} 72 | > 73 |
74 | 75 |
76 | 77 | handlePointChange(1, e.target.value)} 81 | > 82 |
83 |
84 | 85 | { 86 |
87 | 88 |
89 | 90 | handlePointChange(0, e.target.value, 'e')} 94 | > 95 |
96 | 97 |
98 | 99 | handlePointChange(1, e.target.value, 'e')} 103 | > 104 |
105 |
106 | } 107 | 108 |
109 |
110 | 111 | 120 |
121 | 122 |
123 | 124 | 129 |
130 |
131 | 132 | 133 | {algoDesc} 134 | 135 | 139 | 140 | 141 | JSON or JS code. Must generate a nested array. 142 | 143 | 147 | 148 | 149 | 150 |
151 |
152 | 153 |
154 |
155 | 163 | 169 |
170 |
171 | 179 | 185 |
186 |
187 |
188 | 189 |
190 | 191 | 197 |
198 |
199 |
200 | ); 201 | }; 202 | export default PanelOptions; 203 | -------------------------------------------------------------------------------- /src/client/util/algoChoices.ts: -------------------------------------------------------------------------------- 1 | const nestedForLoop = { 2 | func: ` 3 | for (let i = start[0]; i < grid.length; i++) { 4 | for (let j = start[1]; j < grid[i].length; j++) { 5 | if ( 6 | await updateScreen({ 7 | current: \`\${i}.\${j}\`, 8 | }) 9 | ) 10 | return; 11 | } 12 | }`, 13 | desc: 'Uses nested for loops to iterate through array. Ignores walls ("W").', 14 | }; 15 | 16 | const nestedForLoopWithVars = { 17 | func: ` 18 | const arr = [] 19 | let counter = 0 20 | 21 | // set order of vars in panel (if we want) 22 | updateVars({counter}) 23 | updateVars({arr}) 24 | 25 | for (let r = 0; r < grid.length; r++) { 26 | for (let c = 0; c < grid[r].length; c++) { 27 | 28 | // update vars in code 29 | counter++ 30 | arr.push(\`\${r}.\${c}\`) 31 | 32 | // update tracked vars in panel 33 | updateVars({arr}) 34 | updateVars({counter}) 35 | 36 | // update screen visuals, update grid value 37 | await updateScreen({discovered:\`\${r}.\${c}\`}) 38 | 39 | // log something to panel 40 | if (counter % 5 === 0) await log(\`\${counter} divisible by 5 :)\`) 41 | if (counter % 14 === 0) await log('Div by 14', \`\${counter} divisible by 14 :)\`) 42 | } 43 | }`, 44 | desc: 'Uses nested for loops to iterate through array. Ignores walls ("W"). Tracks variables of interest and writes logs.', 45 | }; 46 | 47 | const countIslands = { 48 | func: ` 49 | const discovered = new Set(); 50 | let islandCount = 0; 51 | 52 | const findMore = async (i, j) => { 53 | if ( 54 | await updateScreen({ 55 | potential: \`\${i}.\${j}\`, 56 | }) 57 | ) 58 | return 'abort'; 59 | // if 1 and not yet seen and coordinate is valid, discover it and continue 60 | if (!discovered.has(\`\${i}.\${j}\`) && grid[i] && grid[i][j]) { 61 | await updateScreen({ 62 | current: \`\${i}.\${j}\`, 63 | discovered: \`\${i}.\${j}\`, 64 | }); 65 | // add location to saved 66 | discovered.add(\`\${i}.\${j}\`); 67 | // try up, down, left, right 68 | if ((await findMore(i - 1, j)) === 'abort') return; 69 | if ((await findMore(i + 1, j)) === 'abort') return; 70 | if ((await findMore(i, j - 1)) === 'abort') return; 71 | if ((await findMore(i, j + 1)) === 'abort') return; 72 | await updateScreen({ 73 | potential: \`\${i}.\${j}\`, 74 | }); 75 | } 76 | }; 77 | 78 | for (let i = start[0]; i < grid.length; i++) { 79 | for (let j = start[1]; j < grid[i].length; j++) { 80 | if ( 81 | await updateScreen({ 82 | current: \`\${i}.\${j}\`, 83 | potential: \`\`, 84 | visited: \`\${i}.\${j}\`, 85 | }) 86 | ) 87 | return; 88 | 89 | // if 1s, check if we've seen it 90 | if (grid[i][j]) { 91 | // if undiscovered, launch findMore recursion 92 | if (!discovered.has(\`\${i}.\${j}\`)) { 93 | islandCount++; 94 | await findMore(i, j); 95 | } 96 | } 97 | await updateScreen({ 98 | current: \`\${i}.\${j}\`, 99 | visited: \`\${i}.\${j}\`, 100 | }); 101 | } 102 | } 103 | alert(islandCount);`, 104 | desc: 'Uses depth-first recursion to traverse islands (groups of consecutive 1s) entirely when found. Ignores walls ("W").', 105 | }; 106 | 107 | const BFS_breadthFirst = { 108 | func: ` 109 | // start point 110 | let [r, c] = start; 111 | // queue 112 | const queue = [\`\${r}.\${c}\`]; 113 | const visited = new Set(); 114 | 115 | // while queue not empty 116 | while (queue.length) { 117 | // n = dequeue 118 | let [row, col] = queue.pop().split('.'); 119 | r = Number(row); 120 | c = Number(col); 121 | 122 | if ( 123 | await updateScreen({ 124 | current: \`\${r}.\${c}\`, 125 | discovered: \`\${r}.\${c}\`, 126 | }) 127 | ) 128 | return; 129 | 130 | // loop through coords 131 | for (let coordinate of [ 132 | \`\${r - 1}.\${c}\`, 133 | \`\${r + 1}.\${c}\`, 134 | \`\${r}.\${c - 1}\`, 135 | \`\${r}.\${c + 1}\`, 136 | ]) { 137 | // extract r, c 138 | let r = Number(coordinate.split('.')[0]); 139 | let c = Number(coordinate.split('.')[1]); 140 | 141 | if (grid[r] && grid[r][c] !== undefined && !visited.has(\`\${r}.\${c}\`)) { 142 | // 143 | await updateScreen({ 144 | visited: \`\${r}.\${c}\`, 145 | }); 146 | // add to visited 147 | visited.add(\`\${r}.\${c}\`); 148 | // unshift to queue 149 | queue.unshift(\`\${r}.\${c}\`); 150 | } 151 | } 152 | }`, 153 | desc: 'Breadth-first Search. Uses a queue. Traverses everything, ignores walls ("W").', 154 | }; 155 | 156 | const DFS_depthFirst = { 157 | func: ` 158 | const discovered = new Set(); 159 | 160 | const findMore = async (i, j) => { 161 | if ( 162 | await updateScreen({ 163 | potential: \`\${i}.\${j}\`, 164 | }) 165 | ) 166 | return 'abort'; 167 | // if 1 and not yet seen and coordinate is valid, discover it and continue 168 | if (!discovered.has(\`\${i}.\${j}\`) && grid[i] && grid[i][j] !== undefined) { 169 | await updateScreen({ 170 | current: \`\${i}.\${j}\`, 171 | discovered: \`\${i}.\${j}\`, 172 | }); 173 | // add location to saved 174 | discovered.add(\`\${i}.\${j}\`); 175 | // try up, down, left, right 176 | if ((await findMore(i - 1, j)) === 'abort') return 'abort'; 177 | if ((await findMore(i, j + 1)) === 'abort') return 'abort'; 178 | if ((await findMore(i + 1, j)) === 'abort') return 'abort'; 179 | if ((await findMore(i, j - 1)) === 'abort') return 'abort'; 180 | await updateScreen({ 181 | potential: \`\${i}.\${j}\`, 182 | }); 183 | } 184 | }; 185 | await findMore(start[0], start[1]);`, 186 | desc: 'Depth-first Search. Uses recursion stacks. Traverses everything, ignores walls ("W").', 187 | }; 188 | 189 | const solveMazeBFS = { 190 | func: ` 191 | // start point 192 | let [r, c] = start; 193 | // queue 194 | const queue = [\`\${r}.\${c}\`]; 195 | const visited = new Set(); 196 | 197 | // while queue not empty 198 | while (queue.length) { 199 | // n = dequeue 200 | let [row, col] = queue.pop().split('.'); 201 | r = Number(row); 202 | c = Number(col); 203 | 204 | if ( 205 | await updateScreen({ 206 | current: \`\${r}.\${c}\`, 207 | discovered: \`\${r}.\${c}\`, 208 | }) 209 | ) 210 | return; 211 | 212 | if (r === end[0] && c === end[1]) break; 213 | 214 | // loop through coords 215 | for (let coordinate of [ 216 | \`\${r - 1}.\${c}\`, 217 | \`\${r + 1}.\${c}\`, 218 | \`\${r}.\${c - 1}\`, 219 | \`\${r}.\${c + 1}\`, 220 | ]) { 221 | // extract r, c 222 | let r = Number(coordinate.split('.')[0]); 223 | let c = Number(coordinate.split('.')[1]); 224 | 225 | if (grid[r] && grid[r][c] !== undefined && !visited.has(\`\${r}.\${c}\`)) { 226 | // 227 | if (grid[r][c] === 'W') { 228 | visited.add(\`\${r}.\${c}\`); 229 | continue; 230 | } 231 | 232 | await updateScreen({ 233 | visited: \`\${r}.\${c}\`, 234 | }); 235 | // add to visited 236 | visited.add(\`\${r}.\${c}\`); 237 | // unshift to queue 238 | queue.unshift(\`\${r}.\${c}\`); 239 | } 240 | } 241 | }`, 242 | desc: 'Solves a maze using naive Breadth-first Search. Endpoint required. Walls ("W") matter.', 243 | }; 244 | 245 | const solveMazeDFS = { 246 | func: ` 247 | const discovered = new Set(); 248 | 249 | const findMore = async (i, j) => { 250 | if ( 251 | await updateScreen({ 252 | potential: \`\${i}.\${j}\`, 253 | }) 254 | ) 255 | return 'done'; 256 | // if 1 and not yet seen and coordinate is valid, discover it and continue 257 | if ( 258 | !discovered.has(\`\${i}.\${j}\`) && 259 | grid[i] && 260 | grid[i][j] !== undefined && 261 | grid[i][j] !== 'W' 262 | ) { 263 | await updateScreen({ 264 | current: \`\${i}.\${j}\`, 265 | discovered: \`\${i}.\${j}\`, 266 | }); 267 | 268 | if (i === end[0] && j === end[1]) return 'done'; 269 | 270 | // add location to saved 271 | discovered.add(\`\${i}.\${j}\`); 272 | // try up, down, left, right 273 | if ((await findMore(i - 1, j)) === 'done') return 'done'; 274 | if ((await findMore(i, j + 1)) === 'done') return 'done'; 275 | if ((await findMore(i + 1, j)) === 'done') return 'done'; 276 | if ((await findMore(i, j - 1)) === 'done') return 'done'; 277 | await updateScreen({ 278 | potential: \`\${i}.\${j}\`, 279 | }); 280 | } 281 | }; 282 | await findMore(start[0], start[1]);`, 283 | desc: 'Solves a maze using naive Depth-first Search. Endpoint required. Walls ("W") matter.', 284 | }; 285 | 286 | const closestCarrot = { 287 | func: ` 288 | // start point 289 | let [r, c] = start; 290 | // queue 291 | const queue = [{ coord: [r, c], level: 0 }]; 292 | const visited = new Set(); 293 | let shortestPath = -1; 294 | 295 | // while queue not empty 296 | while (queue.length) { 297 | // n = dequeue 298 | let currentCoord = queue.pop(); 299 | [r, c] = currentCoord.coord; 300 | let currentLevel = currentCoord.level; 301 | 302 | if ( 303 | await updateScreen({ 304 | current: \`\${r}.\${c}\`, 305 | discovered: \`\${r}.\${c}\`, 306 | }) 307 | ) 308 | return; 309 | 310 | if (grid[r][c] === 'C') { 311 | shortestPath = currentLevel; 312 | break; 313 | } 314 | 315 | // loop through coords 316 | for (let coordinate of [ 317 | [r - 1, c], 318 | [r + 1, c], 319 | [r, c - 1], 320 | [r, c + 1], 321 | ]) { 322 | // extract r, c 323 | let [r, c] = coordinate; 324 | 325 | if (grid[r] && grid[r][c] !== undefined && !visited.has(\`\${r}.\${c}\`)) { 326 | if (grid[r][c] === 'W') { 327 | visited.add(\`\${r}.\${c}\`); 328 | continue; 329 | } 330 | // 331 | await updateScreen({ 332 | visited: \`\${r}.\${c}\`, 333 | }); 334 | // add to visited 335 | visited.add(\`\${r}.\${c}\`); 336 | // unshift to queue 337 | queue.unshift({ coord: [r, c], level: currentLevel + 1 }); 338 | } 339 | } 340 | } 341 | alert(shortestPath);`, 342 | desc: 'Finds the closest "C" using Breadth-first Search and returns shortest path to it. Walls ("W") matter.', 343 | }; 344 | 345 | interface algoChoice { 346 | func: string; 347 | desc: string; 348 | } 349 | interface algoChoices { 350 | [key: string]: algoChoice; 351 | } 352 | 353 | export const algoChoices: algoChoices = { 354 | nestedForLoop, 355 | nestedForLoopWithVars, 356 | countIslands, 357 | BFS_breadthFirst, 358 | DFS_depthFirst, 359 | solveMazeBFS, 360 | solveMazeDFS, 361 | closestCarrot, 362 | }; 363 | -------------------------------------------------------------------------------- /src/client/styles/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | width: 100%; 4 | margin: 0; 5 | background-color: rgb(49, 49, 49); 6 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 7 | Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 8 | color: white; 9 | } 10 | h1 { 11 | text-align: center; 12 | } 13 | button { 14 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 15 | Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 16 | align-self: center; 17 | width: 250px; 18 | margin: 10px; 19 | padding: 10px; 20 | border-radius: 20px; 21 | background-color: rgb(0, 0, 0, 0.2); 22 | border: 3px solid rgb(255, 255, 255, 0.9); 23 | color: rgb(255, 255, 255, 0.9); 24 | font-size: larger; 25 | cursor: pointer; 26 | &:hover { 27 | background-color: rgb(255, 255, 255, 0.1) !important; 28 | } 29 | &:active { 30 | transform: scale(0.95, 0.95); 31 | } 32 | transition: background-color 300ms, transform 60ms; 33 | } 34 | #root { 35 | // height: 100%; 36 | #Main { 37 | // height: 100%; 38 | #main2 { 39 | display: flex; 40 | align-items: flex-start; 41 | } 42 | } 43 | } 44 | #panel { 45 | margin: 20px 0px 0px 20px; 46 | height: 91vh; 47 | overflow-y: auto; 48 | display: flex; 49 | flex-direction: column; 50 | padding: 0px 20px 20px 20px; 51 | padding-bottom: 30px; 52 | background-color: rgb(60, 91, 107); 53 | border: 1px solid rgb(152, 207, 218); 54 | border-radius: 20px; 55 | // max-width: 50vw; 56 | min-width: 20vw; 57 | resize: horizontal; 58 | 59 | #start-stop-buttons { 60 | display: flex; 61 | justify-content: space-evenly; 62 | button { 63 | width: 40%; 64 | } 65 | } 66 | #options { 67 | & > * { 68 | margin-left: 20px; 69 | margin-right: 20px; 70 | } 71 | .text-editor { 72 | margin: 0px; 73 | } 74 | margin-top: 10px; 75 | border: 5px solid black; 76 | // max-width: 50vw; 77 | min-width: 18vw; 78 | overflow-y: auto; 79 | display: flex; 80 | flex-direction: column; 81 | padding-bottom: 25px; 82 | 83 | &::-webkit-scrollbar { 84 | width: 25px; 85 | } 86 | &::-webkit-scrollbar-track { 87 | // margin: 15px; 88 | background-color: rgb(99, 133, 143); 89 | border-radius: 10px; 90 | } 91 | &::-webkit-scrollbar-thumb { 92 | background: rgb(147, 193, 207); 93 | // border: 2px solid rgb(126, 109, 0); 94 | border-radius: 10px; 95 | &:hover { 96 | background: #88a8af; 97 | } 98 | } 99 | #select-container-outer { 100 | display: flex; 101 | justify-content: space-around; 102 | .select-container-inner { 103 | width: 45%; 104 | display: flex; 105 | flex-direction: column; 106 | } 107 | } 108 | input, 109 | select, 110 | textarea { 111 | font-size: larger; 112 | color: white; 113 | background-color: rgb(27, 42, 52, 0.9); 114 | padding: 5px; 115 | border-radius: 7px; 116 | } 117 | select { 118 | margin-top: 5px; 119 | } 120 | textarea { 121 | font-size: large; 122 | font-weight: 100; 123 | min-height: 100px; 124 | // margin-bottom: 50px; 125 | } 126 | .coordinate-set { 127 | margin-top: 10px; 128 | // margin-bottom: 10px; 129 | display: flex; 130 | justify-content: flex-start; 131 | align-items: center; 132 | .coordinate { 133 | display: flex; 134 | flex-direction: column; 135 | align-items: center; 136 | & > * { 137 | margin-top: 0px; 138 | margin-right: 15px; 139 | margin-left: 15px; 140 | text-align: center; 141 | width: 80px; 142 | } 143 | } 144 | } 145 | .elaboration { 146 | margin-top: 5px; 147 | font-size: large; 148 | color: rgb(255, 255, 255, 0.6); 149 | font-style: italic; 150 | margin-bottom: 15px; 151 | } 152 | } 153 | #panel-alt { 154 | height: 100%; 155 | display: flex; 156 | flex-direction: column; 157 | justify-content: space-between; 158 | overflow-y: auto; 159 | #var-container { 160 | margin-top: 10px; 161 | // border: 5px solid black; 162 | // max-width: 50vw; 163 | // background-color: #8b8b8d; 164 | min-width: 18vw; 165 | overflow-y: auto; 166 | display: flex; 167 | justify-content: space-evenly; 168 | flex-wrap: wrap; 169 | max-height: 80%; 170 | padding: 10px 0px; 171 | 172 | .var-card { 173 | width: 100%; 174 | margin: 0px 10px 0px 0px; 175 | display: flex; 176 | flex-direction: column; 177 | // align-items: flex-start; 178 | .var-card-title { 179 | font-size: larger; 180 | // font-weight: bold; 181 | // background-color: rgb(0, 0, 0); 182 | color: rgb(255, 255, 255); 183 | padding: 20px 5px 5px 5px; 184 | align-self: stretch; 185 | } 186 | .var-card-value { 187 | // display: flex; 188 | // flex-wrap: wrap; 189 | font-size: large; 190 | background-color: rgb(255, 255, 255, 0.8); 191 | color: rgb(0, 0, 0); 192 | padding: 7px 15px; 193 | font-family: monospace; 194 | // width: 95%; 195 | // align-self: center; 196 | .var-number { 197 | font-weight: bold; 198 | font-size: x-large; 199 | } 200 | } 201 | &:has(.var-card-value .var-number) { 202 | width: unset; 203 | .var-card-title { 204 | text-align: center; 205 | } 206 | .var-card-value { 207 | margin: 0px 5px; 208 | text-align: center; 209 | } 210 | } 211 | } 212 | } 213 | #log-container { 214 | max-height: 40%; 215 | overflow-y: scroll; 216 | background-color: rgb(33, 33, 33); 217 | rotate: 180deg; 218 | direction: rtl; 219 | 220 | &::-webkit-scrollbar { 221 | width: 15px; 222 | } 223 | &::-webkit-scrollbar-track { 224 | // margin: 15px; 225 | background-color: rgb(137, 137, 137); 226 | // border-radius: 10px; 227 | } 228 | &::-webkit-scrollbar-thumb { 229 | background: rgb(205, 205, 205); 230 | // border: 2px solid rgb(126, 109, 0); 231 | // border-radius: 10px; 232 | &:hover { 233 | background: #88a8af; 234 | } 235 | } 236 | 237 | div { 238 | rotate: 180deg; 239 | direction: ltr; 240 | padding: 10px; 241 | border-bottom: 1px solid #575757; 242 | } 243 | label { 244 | letter-spacing: unset; 245 | font-size: unset; 246 | color: rgb(157, 208, 255); 247 | } 248 | & p { 249 | margin: 4px 0px; 250 | } 251 | } 252 | } 253 | } 254 | 255 | #grid { 256 | align-self: center; 257 | margin: 1% 5%; 258 | display: grid; 259 | grid-template-columns: 1fr; 260 | justify-content: center; 261 | align-content: center; 262 | height: 85vh; 263 | // width: 100%; 264 | border-radius: 30px; 265 | .row { 266 | display: grid; 267 | justify-content: center; 268 | display: flex; 269 | .cell { 270 | aspect-ratio: 1/1; 271 | position: relative; // relative for tooltip 272 | display: flex; 273 | input { 274 | width: 100%; 275 | border: 5px solid black; 276 | border-radius: 10px; 277 | background-color: gray; 278 | color: white; 279 | font-size: 1.2rem; 280 | text-align: center; 281 | padding: 0px; 282 | // transition: scale 600ms; 283 | // transition-delay: 1000ms; 284 | &:focus { 285 | z-index: 3; 286 | } 287 | } 288 | .carrot { 289 | border: 3px solid rgb(255, 169, 72); 290 | color: rgb(0, 0, 0, 0); 291 | &:focus { 292 | color: white; 293 | border-width: 5px; 294 | } 295 | z-index: 2; 296 | } 297 | .visited { 298 | background-color: rgb(53, 70, 79); 299 | } 300 | .discovered { 301 | background-color: rgb(67, 127, 71); 302 | } 303 | .potential { 304 | border: 5px solid rgb(190, 179, 21); 305 | z-index: 2; 306 | } 307 | .active { 308 | border: 5px solid rgb(190, 21, 21); 309 | z-index: 2; 310 | // scale: 1.1; 311 | // transition: scale 100ms; 312 | } 313 | .wall { 314 | color: darkgray; 315 | background-color: rgb(31, 31, 31); 316 | } 317 | &:has(.carrot) { 318 | &::after { 319 | content: '🥕'; 320 | pointer-events: none; 321 | position: absolute; 322 | width: 70%; 323 | height: 70%; 324 | left: 14%; 325 | top: 14%; 326 | font-size: 2vw; 327 | text-align: center; 328 | z-index: 2; 329 | } 330 | } 331 | &::before { 332 | // this is to give black background in between cells 333 | content: ''; 334 | position: absolute; 335 | margin-left: -5%; 336 | margin-top: -5%; 337 | width: 110%; 338 | height: 115%; 339 | z-index: -100; 340 | background-color: rgb(0, 0, 0); 341 | } 342 | } 343 | } 344 | } 345 | 346 | .cell .overlay { 347 | opacity: 0; 348 | z-index: -111; 349 | background-color: rgb(255, 255, 255); 350 | color: rgb(0, 0, 0); 351 | font-size: 110%; 352 | font-weight: bolder; 353 | text-align: center; 354 | border-radius: 6px; 355 | padding: 5px 5px; 356 | position: absolute; 357 | translate: 0px 30px; 358 | left: 50%; 359 | transition: opacity 100ms, translate 100ms, z-index 100ms step-end; // applied on exit 360 | // the following are just to center the tooltip 361 | width: 70px; 362 | // margin left formula is -(width/2 + padding left) 363 | margin-left: -40px; 364 | } 365 | 366 | .cell:hover .overlay { 367 | z-index: 100; 368 | opacity: 1; 369 | translate: 0px -10px; 370 | transition: opacity 300ms, translate 200ms, z-index 1ms; // applied on entry 371 | transition-delay: 500ms; 372 | } 373 | .mono { 374 | font-family: 'Courier New', Courier, monospace; 375 | } 376 | .inactive { 377 | opacity: 0.5; 378 | pointer-events: none; 379 | } 380 | .social-link { 381 | font-size: large; 382 | margin-top: 10px; 383 | text-align: center; 384 | color: rgb(255, 225, 125); 385 | :visited { 386 | all: unset; 387 | } 388 | } 389 | .fl-row { 390 | display: flex; 391 | } 392 | .fl-col { 393 | display: flex; 394 | flex-direction: column; 395 | } 396 | .fl-between-h { 397 | justify-content: space-between; 398 | } 399 | .fl-center-h { 400 | justify-content: center; 401 | align-items: center; 402 | } 403 | .grid-adj { 404 | margin: 0px 2%; 405 | margin-top: 10px; 406 | display: flex; 407 | // display: grid; 408 | // grid-template-columns: 1fr 1fr 1fr; 409 | // grid-template-rows: 70px 70px 70px; 410 | justify-content: center; 411 | // align-items: center; 412 | .btn-small { 413 | margin: 5px 5px; 414 | padding: 6px 6px 9px 6px; 415 | width: 80px; 416 | // background-color: rgba(75, 75, 75, 0.7); 417 | border-width: 2px; 418 | &.minus { 419 | // border: 3px solid rgb(255, 124, 124); 420 | } 421 | &.plus { 422 | // border: 3px solid rgb(37, 171, 53); 423 | } 424 | } 425 | } 426 | label { 427 | letter-spacing: 2px; 428 | margin-top: 20px; 429 | font-size: larger; 430 | font-weight: 600; 431 | } 432 | .sublabel { 433 | font-weight: 400; 434 | font-size: large; 435 | text-align: center; 436 | margin: 15px 0px 5px 0px; 437 | } 438 | #fill-cell { 439 | width: 300px; 440 | } 441 | -------------------------------------------------------------------------------- /src/client/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | import Row from './Row'; 4 | 5 | import '../styles/index.scss'; 6 | import { metaStarter } from '../util/metaStarter'; 7 | import { sleep } from '../util/sleep'; 8 | import { prettyLog } from '../util/prettyLog'; 9 | import { algoChoices } from '../util/algoChoices'; 10 | import chooseGrid from '../util/chooseGrid'; 11 | import VarContainer from './varTracking/VarContainer'; 12 | import ControlButtons from './ControlButtons'; 13 | import PanelOptions from './PanelOptions'; 14 | import LogContainer from './varTracking/LogContainer'; 15 | 16 | // key for use in resetting meta sets. ensures no accidental resets 17 | const resetKey = Symbol('RESET'); 18 | 19 | const App = () => { 20 | // set vars 21 | const [customAlgoString, setCustomAlgoString] = useState(''); 22 | const [customGridString, setCustomGridString] = useState( 23 | '[[1,2,3],[4,5,6],[7,8,9]]' 24 | ); 25 | const [defaultFill, setDefaultFill] = useState(''); 26 | 27 | const [algoChoice, setAlgoChoice] = useState('nestedForLoop'); 28 | const [algoDesc, setAlgoDesc] = useState(''); 29 | const [gridChoice, setGridChoice] = useState('maze'); 30 | const [currentGrid, setCurrentGrid] = useState([[]]); 31 | const [algoRunning, setAlgoRunning] = useState(false); 32 | const [startPoint, setStartPoint] = useState([0, 0]); 33 | const [endPoint, setEndPoint] = useState([0, 0]); 34 | const [delay, setDelay] = useState(10); 35 | const [meta, setMeta] = useState(metaStarter()); 36 | const [pausedState, setPausedState] = useState(false); 37 | 38 | let stopStatus = useRef({ isPaused: 1, isAborted: 1 }); 39 | 40 | useEffect(() => { 41 | if (gridChoice !== 'custom') setCurrentGrid(chooseGrid(gridChoice)); 42 | }, [gridChoice]); 43 | 44 | useEffect(() => { 45 | let { func, desc } = algoChoices[algoChoice]; 46 | setCustomAlgoString(func); 47 | setAlgoDesc(desc); 48 | }, [algoChoice]); 49 | 50 | useEffect(() => { 51 | setEndPoint([currentGrid.length - 1, currentGrid[0].length - 1]); 52 | if (!algoRunning) setMeta(() => metaStarter()); 53 | }, [currentGrid]); 54 | 55 | useEffect(() => { 56 | console.log('algoRunning changed! is now', algoRunning); 57 | if (algoRunning && algoChoice) { 58 | stopStatus.current.isAborted = 0; 59 | stopStatus.current.isPaused = 0; 60 | runAlgo(); 61 | } else { 62 | stopStatus.current.isAborted = 1; 63 | stopStatus.current.isPaused = 1; 64 | setAlgoRunning(false); 65 | } 66 | }, [algoRunning]); 67 | 68 | /** 69 | * Updates the grid's value at a given coordinate 70 | * 71 | * @param e event object, e.target.value is new value 72 | * @param row row of grid (number) 73 | * @param col column of grid (number) 74 | */ 75 | const updateGrid = async (e: any, row: number, col: number) => { 76 | const newValue = 77 | Number(e.target.value) !== Number(e.target.value) || 78 | e.target.value !== '0' 79 | ? e.target.value 80 | : Number(e.target.value); 81 | setCurrentGrid((grid: any) => { 82 | const gridClone = [...grid]; 83 | console.log(gridClone); 84 | gridClone[row][col] = newValue; 85 | return [...gridClone]; 86 | }); 87 | }; 88 | 89 | const checkForAbort = () => { 90 | if (stopStatus.current.isAborted) { 91 | setAlgoRunning(false); 92 | return true; 93 | } 94 | }; 95 | 96 | const checkForPause = async () => { 97 | while (stopStatus.current.isPaused) { 98 | if (stopStatus.current.isPaused) { 99 | setPausedState(true); 100 | if (checkForAbort()) return true; 101 | console.log('paused...'); 102 | await sleep(200); 103 | } 104 | } 105 | setPausedState(false); 106 | }; 107 | 108 | /** 109 | * 110 | * @param newMeta object with new values for meta state. Sets will use the .add method. 111 | * @param ms Milliseconds to delay after updating screen. 112 | */ 113 | const updateScreen = async (newMeta: any, ms: number = delay) => { 114 | // check if we should abort, if yes returns 1 115 | // if "await updateScreen({...})" ever returns 1, 116 | // the executing function should try to complete itself however it can 117 | if (checkForAbort()) return true; 118 | await checkForPause(); 119 | 120 | // updates meta object to inform classes 121 | setMeta((meta: any) => { 122 | const tempObj = { ...meta }; 123 | 124 | // update visited if key exists. if resetKey passed, reset visited 125 | // if resetKey, shape should be key: [resetKey, newSetValue] where newSetValue must be a set 126 | if (newMeta.visited !== undefined) { 127 | if (newMeta.visited.length && newMeta.visited[0] === resetKey) 128 | tempObj.visited = newMeta.visited[1]; 129 | else tempObj.visited = meta.visited.add(newMeta.visited); 130 | } 131 | 132 | // update discovered if key exists. if resetKey passed, reset discovered 133 | // if resetKey, shape should be key: [resetKey, newSetValue] where newSetValue must be a set 134 | if (newMeta.discovered !== undefined) { 135 | if (newMeta.discovered.length && newMeta.discovered[0] === resetKey) 136 | tempObj.discovered = newMeta.discovered[1]; 137 | else tempObj.discovered = meta.discovered.add(newMeta.discovered); 138 | } 139 | 140 | // update potential if key exists 141 | if (newMeta.potential !== undefined) 142 | tempObj.potential = newMeta.potential; 143 | 144 | // update current if key exists 145 | if (newMeta.current !== undefined) tempObj.current = newMeta.current; 146 | return tempObj; 147 | }); 148 | 149 | // updates grid values if passed 150 | if (newMeta.grid !== undefined) { 151 | console.log(newMeta.grid); 152 | await updateGrid( 153 | { target: { value: newMeta.grid[2] } }, 154 | newMeta.grid[0], 155 | newMeta.grid[1] 156 | ); 157 | } 158 | await sleep(ms); 159 | }; 160 | 161 | /** 162 | * 163 | * @param newVarObj Object with key of var name and var value 164 | * @param ms Delay time. Global delay unless specified 165 | * @returns 166 | */ 167 | const updateVars = async (newVarObj, ms: number = delay) => { 168 | if (checkForAbort()) return true; 169 | await checkForPause(); 170 | 171 | // updates var object to inform var container 172 | setMeta((meta: any) => { 173 | const [varKey, newValue] = Object.entries(newVarObj)[0]; 174 | const tempMeta = { ...meta }; 175 | const tempVarObj = { ...tempMeta.varObj }; 176 | 177 | tempVarObj[varKey] = newValue; 178 | 179 | // if a key exists, it will be updated 180 | // else it will be created 181 | tempMeta.varObj = tempVarObj; 182 | return tempMeta; 183 | }); 184 | await sleep(ms); 185 | }; 186 | 187 | const updateLogs = async (...args) => { 188 | if (checkForAbort()) return true; 189 | await checkForPause(); 190 | 191 | const [label, content] = args; 192 | 193 | // updates var object to inform var container 194 | setMeta((meta: any) => { 195 | const tempMeta = { ...meta }; 196 | const tempLogArr = [...tempMeta.logArr]; 197 | 198 | const newLog = []; 199 | newLog.push(label); 200 | if (args.length > 1) newLog.push(content); 201 | tempLogArr.push(newLog); 202 | 203 | tempMeta.logArr = tempLogArr; 204 | return tempMeta; 205 | }); 206 | await sleep(delay); 207 | }; 208 | 209 | /** 210 | * Invoke currentAlgo function and provide it with params from current state 211 | */ 212 | const runAlgo = async () => { 213 | const func = buildAlgo(customAlgoString); 214 | 215 | setMeta(() => metaStarter()); 216 | await sleep(100); 217 | 218 | if (!func) return setAlgoRunning(false); 219 | func( 220 | currentGrid, 221 | startPoint.map((n) => Number(n)), 222 | endPoint.map((n) => Number(n)), 223 | updateScreen, 224 | updateVars, 225 | updateLogs, 226 | resetKey 227 | ); 228 | }; 229 | 230 | /** 231 | * 232 | * Updates currentAlgo with a func generated from user input. 233 | * 234 | * Takes user's input and wraps it in a function that returns an async function. 235 | * 236 | * We generate the outer function, then invoke it and pass the returned async function as the new currentAlgo. 237 | */ 238 | const buildAlgo = (algoString: string) => { 239 | console.log(algoString); 240 | let newFunc; 241 | try { 242 | newFunc = new Function( 243 | 'grid, start=[0,0], end, updateScreen, updateVars, log, resetKey', 244 | ` 245 | return async (grid, start, end, updateScreen, updateVars, log, resetKey) => { 246 | ${algoString} 247 | }` 248 | ); 249 | } catch (err) { 250 | console.log(err); 251 | alert( 252 | 'Could not generate algorithm. Aborting. Check console for details' 253 | ); 254 | return newFunc; 255 | } 256 | const asyncFunc = newFunc(); 257 | return asyncFunc; 258 | }; 259 | 260 | // control button functions 261 | const handleStart = () => setAlgoRunning(true); 262 | const handleAbort = () => { 263 | stopStatus.current.isAborted ^= 1; 264 | setAlgoRunning(false); 265 | }; 266 | const handlePause = () => (stopStatus.current.isPaused ^= 1); 267 | 268 | // options functions 269 | const handleDelayChange = (e) => setDelay(Number(e.target.value)); 270 | const handlePointChange = (pos, value, type) => { 271 | let [point, setter] = [startPoint, setStartPoint]; 272 | if (type === 'e') [point, setter] = [endPoint, setEndPoint]; 273 | const newArr = [...point]; 274 | newArr[pos] = value; 275 | setter(newArr); 276 | }; 277 | const handleAlgoSelect = (e: any) => { 278 | setAlgoChoice(e.target.value); 279 | handleCustomFuncChange(algoChoices[e.target.value].func); 280 | }; 281 | const handleGridSelect = (e: any) => { 282 | setGridChoice(e.target.value); 283 | }; 284 | const handleDefaultFillChange = (e: any) => { 285 | setDefaultFill(e.target.value); 286 | }; 287 | const handleCustomFuncChange = (newValue: string) => { 288 | setCustomAlgoString(newValue); 289 | }; 290 | const handleCustomGridChange = (newValue: string) => { 291 | const cleanStr = newValue.replaceAll("'", '"'); 292 | console.log(cleanStr); 293 | setCustomGridString(cleanStr); 294 | }; 295 | const handleCreateGrid = () => { 296 | if (gridChoice !== 'custom') return; 297 | 298 | let parsedGrid; 299 | 300 | // try to parse json, then try to eval 301 | try { 302 | parsedGrid = JSON.parse(customGridString); 303 | } catch (err) { 304 | // if not json, try to use eval 305 | try { 306 | parsedGrid = eval(customGridString); 307 | } catch (err) { 308 | // if eval fails, give give both errors? 309 | console.log(err); 310 | return alert('Could not generate grid. Check console for details.'); 311 | } 312 | } 313 | // verify this is an array 314 | if (!Array.isArray(parsedGrid)) { 315 | alert('Input is not an array. Aborting.'); 316 | return; 317 | } 318 | // verify row lengths all match 319 | let firstLen = parsedGrid[0].length; 320 | for (let row of parsedGrid) { 321 | if (row.length !== firstLen) 322 | alert('Warning: This grid has rows of inequal length.'); 323 | } 324 | setCurrentGrid(parsedGrid); 325 | }; 326 | const handleGridResize = (adjustments: [number, number]) => { 327 | let [rowChange, colChange] = adjustments; 328 | 329 | let newGrid; 330 | if (rowChange) { 331 | if (rowChange > 0) { 332 | newGrid = [ 333 | ...currentGrid, 334 | Array(currentGrid[0].length).fill(defaultFill), 335 | ]; 336 | } else if (currentGrid.length > 1) { 337 | newGrid = currentGrid.slice(0, -1); 338 | } else return; 339 | } else { 340 | if (colChange < 0 && currentGrid[0].length < 2) return; 341 | newGrid = currentGrid.reduce((acc, cur) => { 342 | if (colChange > 0) return [...acc, [...cur, defaultFill]]; 343 | else return [...acc, cur.slice(0, -1)]; 344 | }, []); 345 | } 346 | setCurrentGrid(newGrid); 347 | }; 348 | 349 | return ( 350 |
351 | 352 | 356 |
357 |

Graphsy

358 | 365 | {!algoRunning ? ( 366 | 388 | ) : ( 389 |
390 | 391 | 392 |
393 | )} 394 | 399 | Check out this project on Github 400 | 401 |
402 |
currentGrid[0].length 407 | ? Math.round(100 / currentGrid.length) 408 | : Math.round(100 / currentGrid[0].length) 409 | }%)`, 410 | }} 411 | > 412 | {currentGrid.map((el: any, i: number) => { 413 | return ( 414 | currentGrid[0].length 417 | ? Math.round(100 / currentGrid.length) 418 | : Math.round(100 / currentGrid[0].length) 419 | } 420 | key={i} 421 | row={i} 422 | cells={el} 423 | meta={meta} 424 | updateGrid={updateGrid} 425 | > 426 | ); 427 | })} 428 |
429 |
430 | } 431 | /> 432 | 433 | 434 | ); 435 | }; 436 | export default App; 437 | --------------------------------------------------------------------------------